Merge pull request #223 from camptocamp/access_panelbutton

make it possible to create accessible panel buttons
This commit is contained in:
Éric Lemoine
2012-02-29 04:39:57 -08:00
6 changed files with 343 additions and 25 deletions

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Custom and accessible panel</title>
<link rel="stylesheet" href="../theme/default/style.css" type="text/css">
<link rel="stylesheet" href="style.css" type="text/css">
<style type="text/css">
.olControlPanel button {
position: relative;
display: block;
margin: 2px;
border: 1px solid;
padding: 0 5px;
border-radius: 4px;
height: 35px;
background-color: white;
float: left;
overflow: visible; /* needed to remove padding from buttons in IE */
}
.olControlPanel button span {
padding-left: 25px;
}
.olControlPanel button span:first-child {
padding-left: 0;
display: block;
position: absolute;
left: 2px;
}
.olControlPanel .olControlDrawFeatureItemActive span:first-child {
background-image: url("../theme/default/img/draw_line_on.png");
height: 22px;
width: 24px;
top: 5px;
}
.olControlPanel .olControlDrawFeatureItemInactive span:first-child {
background-image: url("../theme/default/img/draw_line_off.png");
height: 22px;
width: 24px;
top: 5px;
}
.olControlPanel .olControlZoomBoxItemInactive span:first-child {
background-image: url("../img/drag-rectangle-off.png");
height: 29px;
width: 29px;
top: 2px;
}
.olControlPanel .olControlZoomBoxItemActive span:first-child {
background-image: url("../img/drag-rectangle-on.png");
height: 29px;
width: 29px;
top: 2px;
}
.olControlPanel .olControlZoomToMaxExtentItemInactive span:first-child {
background-image: url("../img/zoom-world-mini.png");
height: 18px;
width: 18px;
top: 8px;
}
.olControlPanel .navHistory span:first-child {
background-image: url("../theme/default/img/navigation_history.png");
height: 24px;
width: 24px;
top: 4px;
}
.olControlPanel .navHistoryPreviousItemActive span:first-child {
background-position: 0 0;
}
.olControlPanel .navHistoryPreviousItemInactive span:first-child {
background-position: 0 -24px;
}
.olControlPanel .navHistoryNextItemActive span:first-child {
background-position: -24px 0;
}
.olControlPanel .navHistoryNextItemInactive span:first-child {
background-position: -24px -24px;
}
</style>
<script src="../lib/OpenLayers.js"></script>
<script src="accessible-panel.js"></script>
</head>
<body onload="init()">
<h1 id="title">Custom and accessible panel</h1>
<div id="tags">
panels, CSS, style, accessibility, button
</div>
<p id="shortdesc">
Create a custom and accessible panel, styled entirely with
CSS.
</p>
<div id="panel"></div>
<div id="map" class="smallmap"></div>
<div id="docs">
<p>An accessible panel:
<ul>
<li>The buttons are actual HTML buttons. You can therefore
use the TAB key to give the focus to the panel's buttons, and the "ENTER"
key to activate or trigger the corresponding control.</li>
<li>The buttons include text and titles (displayed when a button
is hovered).</li>
<li>If you remove colors from the page (for example using FireFox's <a
href="https://addons.mozilla.org/en-US/firefox/addon/no-color/">No
Color extension</a>) the buttons are still visible, and
accessible using the keyboard.</li>
</ul>
</p>
<p>By default a panel creates buttons as divs. In this example the
<code>createControlMarkup</code> panel function is overridden to create
a more accessible markup for the buttons. See the <a
href="accessible-panel.js" target="_blank"> accessible-panel.js
source</a> to see how this is done.</p>
<p>Note: in IE 8, when a button is pressed its content shifts by 1 pixel.
This is a <a
href="http://labs.findsubstance.com/2009/05/21/ie8-form-button-with-background-image-on-click-css-bug/">known
IE8 bug</a>, with known workarounds. No workaround is applied in this
example though.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,64 @@
var lon = 5;
var lat = 40;
var zoom = 5;
var map, layer;
function init() {
map = new OpenLayers.Map( 'map', { controls: [] } );
layer = new OpenLayers.Layer.WMS( "OpenLayers WMS",
"http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic'} );
map.addLayer(layer);
vlayer = new OpenLayers.Layer.Vector( "Editable" );
map.addLayer(vlayer);
zb = new OpenLayers.Control.ZoomBox({
title: "Zoom box: zoom clicking and dragging",
text: "Zoom"
});
var panel = new OpenLayers.Control.Panel({
defaultControl: zb,
createControlMarkup: function(control) {
var button = document.createElement('button'),
iconSpan = document.createElement('span'),
textSpan = document.createElement('span');
iconSpan.innerHTML = '&nbsp;';
button.appendChild(iconSpan);
if (control.text) {
textSpan.innerHTML = control.text;
}
button.appendChild(textSpan);
return button;
}
});
panel.addControls([
zb,
new OpenLayers.Control.DrawFeature(vlayer, OpenLayers.Handler.Path,
{title:'Draw a feature', text: 'Draw'}),
new OpenLayers.Control.ZoomToMaxExtent({
title:"Zoom to the max extent",
text: "World"
})
]);
nav = new OpenLayers.Control.NavigationHistory({
previousOptions: {
title: "Go to previous map position",
text: "Prev"
},
nextOptions: {
title: "Go to next map position",
text: "Next"
},
displayClass: "navHistory"
});
// parent control must be added to the map
map.addControl(nav);
panel.addControls([nav.next, nav.previous]);
map.addControl(panel);
map.setCenter(new OpenLayers.LonLat(lon, lat), zoom);
}

View File

@@ -245,26 +245,58 @@ OpenLayers.Control.Panel = OpenLayers.Class(OpenLayers.Control, {
controls = [controls];
}
this.controls = this.controls.concat(controls);
// Give each control a panel_div which will be used later.
// Access to this div is via the panel_div attribute of the
// control added to the panel.
// Also, stop mousedowns and clicks, but don't stop mouseup,
// since they need to pass through.
for (var i=0, len=controls.length; i<len; i++) {
var element = document.createElement("div");
element.className = controls[i].displayClass + "ItemInactive olButton";
controls[i].panel_div = element;
if (controls[i].title != "") {
controls[i].panel_div.title = controls[i].title;
var control = controls[i],
element = this.createControlMarkup(control);
OpenLayers.Element.addClass(element,
control.displayClass + "ItemInactive");
OpenLayers.Element.addClass(element, "olButton");
if (control.title != "" && !element.title) {
element.title = control.title;
}
}
control.panel_div = element;
}
if (this.map) { // map.addControl() has already been called on the panel
this.addControlsToMap(controls);
this.redraw();
}
},
/**
* APIMethod: createControlMarkup
* This function just creates a div for the control. If specific HTML
* markup is needed this function can be overridden in specific classes,
* or at panel instantiation time:
*
* Example:
* (code)
* var panel = new OpenLayers.Control.Panel({
* defaultControl: control,
* // ovverride createControlMarkup to create actual buttons
* // including texts wrapped into span elements.
* createControlMarkup: function(control) {
* var button = document.createElement('button'),
* span = document.createElement('span');
* if (control.text) {
* span.innerHTML = control.text;
* }
* return button;
* }
* });
* (end)
*
* Parameters:
* control - {<OpenLayers.Control>} The control to create the HTML
* markup for.
*
* Returns:
* {DOMElement} The markup.
*/
createControlMarkup: function(control) {
return document.createElement("div");
},
/**
* Method: addControlsToMap

View File

@@ -20,6 +20,12 @@ OpenLayers.Event = {
* element._eventCacheID
*/
observers: false,
/**
* Constant: KEY_SPACE
* {int}
*/
KEY_SPACE: 32,
/**
* Constant: KEY_BACKSPACE
@@ -388,7 +394,8 @@ OpenLayers.Events = OpenLayers.Class({
"mousedown", "mouseup", "mousemove",
"click", "dblclick", "rightclick", "dblrightclick",
"resize", "focus", "blur",
"touchstart", "touchmove", "touchend"
"touchstart", "touchmove", "touchend",
"keydown"
],
/**

View File

@@ -39,7 +39,7 @@ OpenLayers.Events.buttonclick = OpenLayers.Class({
*/
events: [
'mousedown', 'mouseup', 'click', 'dblclick',
'touchstart', 'touchmove', 'touchend'
'touchstart', 'touchmove', 'touchend', 'keydown'
],
/**
@@ -97,6 +97,31 @@ OpenLayers.Events.buttonclick = OpenLayers.Class({
delete this.target;
},
/**
* Method: getPressedButton
* Get the pressed button, if any. Returns undefined if no button
* was pressed.
*
* Arguments:
* element - {DOMElement} The event target.
*
* Returns:
* {DOMElement} The button element, or undefined.
*/
getPressedButton: function(element) {
var depth = 3, // limit the search depth
button;
do {
if(OpenLayers.Element.hasClass(element, "olButton")) {
// hit!
button = element;
break;
}
element = element.parentNode;
} while(--depth > 0 && element);
return button;
},
/**
* Method: buttonClick
* Check if a button was clicked, and fire the buttonclick event
@@ -108,15 +133,25 @@ OpenLayers.Events.buttonclick = OpenLayers.Class({
var propagate = true,
element = OpenLayers.Event.element(evt);
if (element && (OpenLayers.Event.isLeftClick(evt) || !~evt.type.indexOf("mouse"))) {
if (element.nodeType === 3 || OpenLayers.Element.hasClass(element, "olAlphaImg")) {
element = element.parentNode;
}
if (OpenLayers.Element.hasClass(element, "olButton")) {
if (this.startEvt) {
if (this.completeRegEx.test(evt.type)) {
var pos = OpenLayers.Util.pagePosition(element);
// was a button pressed?
var button = this.getPressedButton(element);
if (button) {
if (evt.type === "keydown") {
switch (evt.keyCode) {
case OpenLayers.Event.KEY_RETURN:
case OpenLayers.Event.KEY_SPACE:
this.target.triggerEvent("buttonclick", {
buttonElement: element,
buttonElement: button
});
OpenLayers.Event.stop(evt);
propagate = false;
break;
}
} else if (this.startEvt) {
if (this.completeRegEx.test(evt.type)) {
var pos = OpenLayers.Util.pagePosition(button);
this.target.triggerEvent("buttonclick", {
buttonElement: button,
buttonXY: {
x: this.startEvt.clientX - pos[0],
y: this.startEvt.clientY - pos[1]

View File

@@ -27,9 +27,44 @@
buttonClick.destroy();
events.destroy();
}
function test_getPressedButton(t) {
t.plan(4);
// set up
events = new OpenLayers.Events({}, element);
buttonClick = new OpenLayers.Events.buttonclick(events);
var button = document.createElement('button'),
span1 = document.createElement('span'),
span2 = document.createElement('span'),
span3 = document.createElement('span');
button.className = 'olButton';
button.appendChild(span1);
span1.appendChild(span2);
span2.appendChild(span3);
t.ok(buttonClick.getPressedButton(button) === button,
'getPressedButton returns button when element is button');
t.ok(buttonClick.getPressedButton(span1) === button,
'getPressedButton returns button when element is button descendant level 1');
t.ok(buttonClick.getPressedButton(span2) === button,
'getPressedButton returns button when element is button descendant level 2');
t.eq(buttonClick.getPressedButton(span3), undefined,
'getPressedButton returns undefined when element is button descendant level 3');
// test
// tear down
buttonClick.destroy();
events.destroy();
}
function test_ButtonClick_buttonClick(t) {
t.plan(23);
t.plan(27);
events = new OpenLayers.Events({}, element);
events.on({
"buttonclick": logEvent,
@@ -38,7 +73,8 @@
"click": logEvent,
"dblclick": logEvent,
"touchstart": logEvent,
"touchend": logEvent
"touchend": logEvent,
"keydown": logEvent
});
buttonClick = events.extensions["buttonclick"];
@@ -111,12 +147,26 @@
t.eq(log[1].type, "buttonclick", "buttonclick for 2nd click IE");
// rightclick
log = []
log = [];
trigger({type: "mousedown", button: 2});
trigger({type: "mouseup", button: 2});
t.eq(log.length, 2, "two events fired for rightclick");
t.eq(log[0].type, "mousedown", "mousedown from rightclick goes through");
t.eq(log[1].type, "mouseup", "mouseup from rightclick goes through");
// keydown RETURN
log = [];
trigger({type: "keydown", keyCode: OpenLayers.Event.KEY_RETURN});
trigger({type: "click"});
t.eq(log.length, 1, "one event fired for RETURN keydown");
t.eq(log[0].type, "buttonclick", "buttonclick for RETURN keydown");
// keydown SPACE
log = [];
trigger({type: "keydown", keyCode: OpenLayers.Event.KEY_SPACE});
trigger({type: "click"});
t.eq(log.length, 1, "one event fired for SPACE keydown");
t.eq(log[0].type, "buttonclick", "buttonclick for SPACE keydown");
}
</script>
</head>