Skip to content

Commit 87f5a50

Browse files
Follows updated keyboard interaction rules (#292)
* Follows updated keyboard interaction rules * Test updates * Change to headless test * Open feature popup on space * New interactions * Adds tests and new keyboard interaction Co-authored-by: Peter Rushforth <[email protected]>
1 parent aef5a80 commit 87f5a50

File tree

6 files changed

+94
-43
lines changed

6 files changed

+94
-43
lines changed

src/mapml.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,9 @@ summary {
391391
}
392392

393393
.mapml-crosshair {
394-
margin: -18px 0 0 -18px;
395-
width: 36px;
396-
height: 36px;
394+
margin: -36px 0 0 -36px;
395+
width: 72px;
396+
height: 72px;
397397
left: 50%;
398398
top: 50%;
399399
content: '';

src/mapml/handlers/QueryHandler.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ export var QueryHandler = L.Handler.extend({
2222
}
2323
},
2424
_queryTopLayerAtMapCenter: function (event) {
25-
if (event.originalEvent.key === " ") {
25+
setTimeout(() => {
26+
if (this._map.isFocused && !this._map._popupClosed && (event.originalEvent.key === " " || +event.originalEvent.keyCode === 13)) {
2627
this._map.fire('click', {
2728
latlng: this._map.getCenter(),
2829
layerPoint: this._map.latLngToLayerPoint(this._map.getCenter()),
2930
containerPoint: this._map.latLngToContainerPoint(this._map.getCenter())
3031
});
32+
} else {
33+
delete this._map._popupClosed;
3134
}
35+
}, 0);
3236
},
3337
_queryTopLayer: function(event) {
3438
var layer = this._getTopQueryableLayer();

src/mapml/layers/Crosshair.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,22 @@ export var Crosshair = L.Layer.extend({
3030

3131
this._container = L.DomUtil.create("div", "mapml-crosshair", map._container);
3232
this._container.innerHTML = svgInnerHTML;
33-
this._mapFocused = false;
33+
map.isFocused = false;
3434
this._isQueryable = false;
3535

3636
map.on("layerchange layeradd layerremove overlayremove", this._toggleEvents, this);
37-
L.DomEvent.on(map._container, "keydown keyup mousedown", this._onKey, this);
38-
37+
map.on("popupopen", this._isMapFocused, this);
38+
L.DomEvent.on(map._container, "keydown keyup mousedown", this._isMapFocused, this);
3939

4040
this._addOrRemoveCrosshair();
4141
},
4242

43+
onRemove: function (map) {
44+
map.off("layerchange layeradd layerremove overlayremove", this._toggleEvents);
45+
map.off("popupopen", this._isMapFocused);
46+
L.DomEvent.off(map._container, "keydown keyup mousedown", this._isMapFocused);
47+
},
48+
4349
_toggleEvents: function () {
4450
if (this._hasQueryableLayer()) {
4551
this._map.on("viewreset move moveend", this._addOrRemoveCrosshair, this);
@@ -59,7 +65,7 @@ export var Crosshair = L.Layer.extend({
5965

6066
_hasQueryableLayer: function () {
6167
let layers = this._map.options.mapEl.layers;
62-
if (this._mapFocused) {
68+
if (this._map.isFocused) {
6369
for (let layer of layers) {
6470
if (layer.checked && layer._layer.queryable) {
6571
return true;
@@ -69,20 +75,14 @@ export var Crosshair = L.Layer.extend({
6975
return false;
7076
},
7177

72-
_onKey: function (e) {
73-
//set mapFocused = true if arrow buttons are used
74-
if (["keydown", "keyup"].includes(e.type) && e.target.classList.contains("leaflet-container") && [32, 37, 38, 39, 40, 187, 189].includes(+e.keyCode)) {
75-
this._mapFocused = true;
76-
//set mapFocused = true if map is focued using tab
77-
} else if (e.type === "keyup" && e.target.classList.contains("leaflet-container") && +e.keyCode === 9) {
78-
this._mapFocused = true;
79-
// set mapFocused = false and close all popups if tab or escape is used
80-
} else if((e.type === "keyup" && e.target.classList.contains("leaflet-interactive") && +e.keyCode === 9) || +e.keyCode === 27){
81-
this._mapFocused = false;
82-
this._map.closePopup();
83-
// set mapFocused = false if any other key is pressed
78+
_isMapFocused: function (e) {
79+
//set this._map.isFocused = true if arrow buttons are used
80+
if (this._map._container.parentNode.activeElement.classList.contains("leaflet-container") && ["keydown"].includes(e.type) && (e.shiftKey && e.keyCode === 9)) {
81+
this._map.isFocused = false;
82+
} else if (this._map._container.parentNode.activeElement.classList.contains("leaflet-container") && ["keyup", "keydown"].includes(e.type)) {
83+
this._map.isFocused = true;
8484
} else {
85-
this._mapFocused = false;
85+
this._map.isFocused = false;
8686
}
8787
this._addOrRemoveCrosshair();
8888
},

src/mapml/layers/FeatureLayer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ export var MapMLFeatures = L.FeatureGroup.extend({
281281

282282
if (options.onEachFeature) {
283283
options.onEachFeature(layer.properties, layer);
284+
layer._events.keypress.push({
285+
"ctx": layer,
286+
"fn": this._onSpacePress,
287+
});
284288
}
285289
if(this._staticFeature){
286290
let featureZoom = mapml.getAttribute('zoom') || nativeZoom;
@@ -324,6 +328,11 @@ export var MapMLFeatures = L.FeatureGroup.extend({
324328
for(let i = 0; i < toDelete.length;i++){
325329
this._container.removeChild(toDelete[i]);
326330
}
331+
},
332+
_onSpacePress: function(e){
333+
if(e.originalEvent.keyCode === 32){
334+
this._openPopup(e);
335+
}
327336
},
328337
geometryToLayer: function (mapml, pointToLayer, coordsToLatLng, vectorOptions, nativeCS, zoom) {
329338
var geometry = mapml.tagName.toUpperCase() === 'FEATURE' ? mapml.getElementsByTagName('geometry')[0] : mapml,

src/mapml/layers/MapLayer.js

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export var MapMLLayer = L.Layer.extend({
9595
var c = document.createElement('div');
9696
c.classList.add("mapml-popup-content");
9797
c.insertAdjacentHTML('afterbegin', properties.innerHTML);
98-
geometry.bindPopup(c, {autoPan:false, closeButton: false, minWidth: 108});
98+
geometry.bindPopup(c, {autoPan:false, minWidth: 108});
9999
}
100100
}
101101
});
@@ -125,7 +125,7 @@ export var MapMLLayer = L.Layer.extend({
125125
var c = document.createElement('div');
126126
c.classList.add("mapml-popup-content");
127127
c.insertAdjacentHTML('afterbegin', properties.innerHTML);
128-
geometry.bindPopup(c, {autoPan:false, closeButton: false, minWidth: 108});
128+
geometry.bindPopup(c, {autoPan:false, minWidth: 108});
129129
}
130130
}
131131
}).addTo(map);
@@ -1162,8 +1162,7 @@ export var MapMLLayer = L.Layer.extend({
11621162
content = popup._container.getElementsByClassName("mapml-popup-content")[0];
11631163

11641164
content.setAttribute("tabindex", "-1");
1165-
content.focus();
1166-
popup._count = 0;
1165+
popup._count = 0; // used for feature pagination
11671166

11681167
if(popup._source._eventParents){ // check if the popup is for a feature or query
11691168
layer = popup._source._eventParents[Object.keys(popup._source._eventParents)[0]]; // get first parent of feature, there should only be one
@@ -1232,7 +1231,11 @@ export var MapMLLayer = L.Layer.extend({
12321231
L.DomEvent.on(controlFocusButton, 'click', L.DomEvent.stop);
12331232
L.DomEvent.on(controlFocusButton, 'click', (e) => {
12341233
map.closePopup();
1235-
map._controlContainer.focus();
1234+
if(map._controlContainer.firstElementChild.firstElementChild.firstElementChild){
1235+
map._controlContainer.firstElementChild.firstElementChild.firstElementChild.focus();
1236+
} else {
1237+
map._controlContainer.focus();
1238+
}
12361239
}, popup);
12371240

12381241
let divider = L.DomUtil.create("hr");
@@ -1241,31 +1244,60 @@ export var MapMLLayer = L.Layer.extend({
12411244
popup._navigationBar = div;
12421245
popup._content.appendChild(divider);
12431246
popup._content.appendChild(div);
1244-
1247+
1248+
content.focus();
12451249

12461250
if(path) {
12471251
// e.target = this._map
12481252
// Looks for keydown, more specifically tab and shift tab
12491253
map.on("keydown", focusFeature);
1250-
// if popup closes then the focusFeature handler can be removed
1251-
map.on("popupclose", removeHandlers);
1254+
} else {
1255+
map.on("keydown", focusMap);
12521256
}
12531257
// When popup is open, what gets focused with tab needs to be done using JS as the DOM order is not in an accessibility friendly manner
12541258
function focusFeature(focusEvent){
1255-
if(focusEvent.originalEvent.path[0].title==="Focus Controls" && +focusEvent.originalEvent.keyCode === 9){
1259+
let isTab = focusEvent.originalEvent.keyCode === 9,
1260+
shiftPressed = focusEvent.originalEvent.shiftKey;
1261+
if((focusEvent.originalEvent.path[0].classList.contains("leaflet-popup-close-button") && isTab && !shiftPressed) || focusEvent.originalEvent.keyCode === 27){
12561262
L.DomEvent.stop(focusEvent);
1257-
path.focus();
1258-
} else if(focusEvent.originalEvent.shiftKey && +focusEvent.originalEvent.keyCode === 9){
12591263
map.closePopup(popup);
1260-
L.DomEvent.stop(focusEvent);
12611264
path.focus();
1265+
} else if ((focusEvent.originalEvent.path[0].title==="Focus Map" || focusEvent.originalEvent.path[0].classList.contains("mapml-popup-content")) && isTab && shiftPressed){
1266+
setTimeout(() => { //timeout needed so focus of the feature is done even after the keypressup event occurs
1267+
L.DomEvent.stop(focusEvent);
1268+
map.closePopup(popup);
1269+
path.focus();
1270+
}, 0);
1271+
}
1272+
}
1273+
1274+
function focusMap(focusEvent){
1275+
let isTab = focusEvent.originalEvent.keyCode === 9,
1276+
shiftPressed = focusEvent.originalEvent.shiftKey;
1277+
1278+
if((focusEvent.originalEvent.keyCode === 13 && focusEvent.originalEvent.path[0].classList.contains("leaflet-popup-close-button")) || focusEvent.originalEvent.keyCode === 27 ){
1279+
L.DomEvent.stopPropagation(focusEvent);
1280+
map._container.focus();
1281+
map.closePopup(popup);
1282+
if(focusEvent.originalEvent.keyCode !== 27)map._popupClosed = true;
1283+
} else if (isTab && focusEvent.originalEvent.path[0].classList.contains("leaflet-popup-close-button")){
1284+
map.closePopup(popup);
1285+
} else if ((focusEvent.originalEvent.path[0].title==="Focus Map" || focusEvent.originalEvent.path[0].classList.contains("mapml-popup-content")) && isTab && shiftPressed){
1286+
setTimeout(() => { //timeout needed so focus of the feature is done even after the keypressup event occurs
1287+
L.DomEvent.stop(focusEvent);
1288+
map.closePopup(popup);
1289+
map._container.focus();
1290+
}, 0);
12621291
}
12631292
}
12641293

1294+
// if popup closes then the focusFeature handler can be removed
1295+
map.on("popupclose", removeHandlers);
12651296
function removeHandlers(removeEvent){
12661297
if (removeEvent.popup === popup){
12671298
map.off("keydown", focusFeature);
1268-
map.off("popupclose", removeHandlers);
1299+
map.off("keydown", focusMap);
1300+
map.off('popupclose', removeHandlers);
12691301
}
12701302
}
12711303
},

test/e2e/core/keyboardInteraction.test.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jest.setTimeout(50000);
3030
expect(afterTab).toEqual("");
3131
});
3232

33-
test("[" + browserType + "]" + " Crosshair remains on map move with arrow keys + space", async () => {
33+
test("[" + browserType + "]" + " Crosshair remains on map move with arrow keys", async () => {
3434
await page.keyboard.press("ArrowUp");
3535
await page.waitForTimeout(500);
3636
await page.keyboard.press("ArrowDown");
@@ -39,13 +39,11 @@ jest.setTimeout(50000);
3939
await page.waitForTimeout(500);
4040
await page.keyboard.press("ArrowRight");
4141
await page.waitForTimeout(500);
42-
await page.keyboard.press(" ")
43-
await page.waitForTimeout(500);
4442
const afterMove = await page.$eval("div > div.mapml-crosshair", (div) => div.style.visibility);
4543
expect(afterMove).toEqual("");
4644
});
4745

48-
test("[" + browserType + "]" + " Crosshair hidden on esc + tab out", async () => {
46+
test("[" + browserType + "]" + " Crosshair shows on esc but hidden on tab out", async () => {
4947
await page.keyboard.press("Escape");
5048
const afterEsc = await page.$eval("div > div.mapml-crosshair", (div) => div.style.visibility);
5149
await page.click("body");
@@ -55,7 +53,7 @@ jest.setTimeout(50000);
5553
await page.keyboard.press("Tab");
5654
const afterTab = await page.$eval("div > div.mapml-crosshair", (div) => div.style.visibility);
5755

58-
expect(afterEsc).toEqual("hidden");
56+
expect(afterEsc).toEqual("");
5957
expect(afterTab).toEqual("hidden");
6058
});
6159

@@ -164,16 +162,24 @@ jest.setTimeout(50000);
164162
const rh6 = await page.evaluateHandle(root => root.activeElement, nh6);
165163
const f6 = await (await page.evaluateHandle(elem => elem.title, rh6)).jsonValue();
166164

165+
await page.keyboard.press("Tab");
166+
const h7 = await page.evaluateHandle(() => document.querySelector("mapml-viewer"));
167+
const nh7 = await page.evaluateHandle(doc => doc.shadowRoot, h7);
168+
const rh7 = await page.evaluateHandle(root => root.activeElement, nh7);
169+
const f7 = await (await page.evaluateHandle(elem => elem.className, rh7)).jsonValue();
170+
167171
expect(f).toEqual("mapml-popup-content");
168-
expect(f2).toEqual("A");
172+
expect(f2.toUpperCase()).toEqual("A");
169173
expect(f3).toEqual("Focus Map");
170174
expect(f4).toEqual("Previous Feature");
171175
expect(f5).toEqual("Next Feature");
172176
expect(f6).toEqual("Focus Controls");
177+
expect(f7).toEqual("leaflet-popup-close-button");
173178
});
174179

175180
test("[" + browserType + "]" + " Tab to next feature after tabbing out of popup", async () => {
176181
await page.keyboard.press("Tab");
182+
177183
const h = await page.evaluateHandle(() => document.querySelector("mapml-viewer"));
178184
const nh = await page.evaluateHandle(doc => doc.shadowRoot, h);
179185
const rh = await page.evaluateHandle(root => root.activeElement, nh);
@@ -182,19 +188,19 @@ jest.setTimeout(50000);
182188
expect(f).toEqual("M-53 451L153 508L113 146L-53 191z");
183189
});
184190

185-
test("[" + browserType + "]" + " Shift + Tab to previous feature while popup open", async () => {
191+
test("[" + browserType + "]" + " Shift + Tab to current feature while popup open", async () => {
186192
await page.keyboard.press("Enter");
187193
await page.keyboard.press("Shift+Tab");
194+
188195
const h = await page.evaluateHandle(() => document.querySelector("mapml-viewer"));
189196
const nh = await page.evaluateHandle(doc => doc.shadowRoot, h);
190197
const rh = await page.evaluateHandle(root => root.activeElement, nh);
191198
const f = await (await page.evaluateHandle(elem => elem.getAttribute("d"), rh)).jsonValue();
192199

193-
expect(f).toEqual("M330 83L553 83L553 339L330 339z");
200+
expect(f).toEqual("M-53 451L153 508L113 146L-53 191z");
194201
});
195202

196203
test("[" + browserType + "]" + " Previous feature button focuses previous feature", async () => {
197-
await page.keyboard.press("Tab");
198204
await page.keyboard.press("Enter");
199205
await page.keyboard.press("Tab");
200206
await page.keyboard.press("Tab");

0 commit comments

Comments
 (0)