Skip to content

Commit

Permalink
Adding tabbing navigation and accessibility to map (#270)
Browse files Browse the repository at this point in the history
* Allow features to be focused

* Adds crosshair, shows on keyboard movement

* Shows crosshair on tab

* Only run on moveend if there are queryable layers

* Revert listener change

* Adds tests for keyboard interaction

* Remove close button from feature popup

* Add bypass navigation

* Query popup fix

* Add feature count, move controls to bottom

* Test update to consider skip buttons

* Test update

* Adds next and previous focus buttons

* Add tests for keyboard interaction

* Rename variables and add comments

* Remove handlers on close

Co-authored-by: Peter Rushforth <peter.rushforth@canada.ca>
  • Loading branch information
ahmadayubi and prushforth authored Feb 10, 2021
1 parent cf8d242 commit 4f8ff65
Show file tree
Hide file tree
Showing 14 changed files with 637 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ jobs:
destination_folder: 'dist'
user_email: aayub041@uottawa.ca
user_name: 'ahmadayubi'
commit_msg: 'Sync MapML Build'
commit_msg: '[AUTO] Sync MapML Build'
destination_branch: main
1 change: 1 addition & 0 deletions src/mapml-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export class MapViewer extends HTMLElement {
this._attributionControl = this._map.attributionControl.setPrefix('<a href="https://www.w3.org/community/maps4html/" title="W3C Maps for HTML Community Group">Maps4HTML</a> | <a href="https://leafletjs.com" title="A JS library for interactive maps">Leaflet</a>');

this.setControls(false,false,true);
this._crosshair = M.crosshair().addTo(this._map);

// Make the Leaflet container element programmatically identifiable
// (https://github.com/Leaflet/Leaflet/issues/7193).
Expand Down
38 changes: 38 additions & 0 deletions src/mapml.css
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,41 @@ summary {
.leaflet-container .leaflet-control-container {
visibility: unset!important;
}

.mapml-crosshair {
margin: -18px 0 0 -18px;
width: 36px;
height: 36px;
left: 50%;
top: 50%;
content: '';
display: block;
position: absolute;
z-index: 10000;
}

.mapml-popup-button {
padding: 0 4px 0 4px;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
white-space: nowrap;
}

.mapml-focus-buttons {
white-space: nowrap;
display: inline;
}

.mapml-feature-count {
display:inline;
white-space: nowrap;
text-align: center;
padding: 2px;
}
13 changes: 8 additions & 5 deletions src/mapml/handlers/QueryHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,26 +151,29 @@ export var QueryHandler = L.Handler.extend({
});
f.addTo(map);

var c = document.createElement('iframe');
let div = L.DomUtil.create("div", "mapml-popup-content"),
c = L.DomUtil.create("iframe");
c.csp = "script-src 'none'";
c.style = "border: none";
c.srcdoc = mapmldoc.querySelector('feature properties').innerHTML;

div.appendChild(c);
// passing a latlng to the popup is necessary for when there is no
// geometry / null geometry
layer.bindPopup(c, popupOptions).openPopup(loc);
layer.bindPopup(div, popupOptions).openPopup(loc);
layer.on('popupclose', function() {
map.removeLayer(f);
});
});
}
function handleOtherResponse(response, layer, loc) {
return response.text().then(text => {
var c = document.createElement('iframe');
let div = L.DomUtil.create("div", "mapml-popup-content"),
c = L.DomUtil.create("iframe");
c.csp = "script-src 'none'";
c.style = "border: none";
c.srcdoc = text;
layer.bindPopup(c, popupOptions).openPopup(loc);
div.appendChild(c);
layer.bindPopup(div, popupOptions).openPopup(loc);
});
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/mapml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { QueryHandler } from './handlers/QueryHandler';
import { ContextMenu } from './handlers/ContextMenu';
import { Util } from './utils/Util';
import { ReloadButton, reloadButton } from './control/ReloadButton';
import { Crosshair, crosshair } from "./layers/Crosshair";

/* global L, Node */
(function (window, document, undefined) {
Expand Down Expand Up @@ -627,4 +628,7 @@ M.mapMLStaticTileLayer = mapMLStaticTileLayer;
M.DebugOverlay = DebugOverlay;
M.debugOverlay = debugOverlay;

M.Crosshair = Crosshair;
M.crosshair = crosshair;

}(window, document));
95 changes: 95 additions & 0 deletions src/mapml/layers/Crosshair.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export var Crosshair = L.Layer.extend({
onAdd: function (map) {

//SVG crosshair design from https://github.com/xguaita/Leaflet.MapCenterCoord/blob/master/src/icons/MapCenterCoordIcon1.svg?short_path=81a5c76
let svgInnerHTML = `<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 99.999998 99.999998"
xml:space="preserve">
<g><circle
r="3.9234731"
cy="50.21946"
cx="50.027821"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /><path
d="m 4.9734042,54.423642 31.7671398,0 c 2.322349,0 4.204185,-1.881836 4.204185,-4.204185 0,-2.322349 -1.881836,-4.204184 -4.204185,-4.204184 l -31.7671398,0 c -2.3223489,-2.82e-4 -4.20418433,1.881554 -4.20418433,4.204184 0,2.322631 1.88183543,4.204185 4.20418433,4.204185 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /><path
d="m 54.232003,5.1650429 c 0,-2.3223489 -1.881836,-4.20418433 -4.204184,-4.20418433 -2.322349,0 -4.204185,1.88183543 -4.204185,4.20418433 l 0,31.7671401 c 0,2.322349 1.881836,4.204184 4.204185,4.204184 2.322348,0 4.204184,-1.881835 4.204184,-4.204184 l 0,-31.7671401 z"
style="fill:#000000;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1" /><path
d="m 99.287826,50.219457 c 0,-2.322349 -1.881835,-4.204184 -4.204184,-4.204184 l -31.76714,0 c -2.322349,0 -4.204184,1.881835 -4.204184,4.204184 0,2.322349 1.881835,4.204185 4.204184,4.204185 l 31.76714,0 c 2.320658,0 4.204184,-1.881836 4.204184,-4.204185 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /><path
d="m 45.823352,95.27359 c 0,2.322349 1.881836,4.204184 4.204185,4.204184 2.322349,0 4.204184,-1.881835 4.204184,-4.204184 l 0,-31.76714 c 0,-2.322349 -1.881835,-4.204185 -4.204184,-4.204185 -2.322349,0 -4.204185,1.881836 -4.204185,4.204185 l 0,31.76714 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /></g></svg>
`;

this._container = L.DomUtil.create("div", "mapml-crosshair", map._container);
this._container.innerHTML = svgInnerHTML;
this._mapFocused = false;
this._isQueryable = false;

map.on("layerchange layeradd layerremove overlayremove", this._toggleEvents, this);
L.DomEvent.on(map._container, "keydown keyup mousedown", this._onKey, this);


this._addOrRemoveCrosshair();
},

_toggleEvents: function () {
if (this._hasQueryableLayer()) {
this._map.on("viewreset move moveend", this._addOrRemoveCrosshair, this);
} else {
this._map.off("viewreset move moveend", this._addOrRemoveCrosshair, this);
}
this._addOrRemoveCrosshair();
},

_addOrRemoveCrosshair: function (e) {
if (this._hasQueryableLayer()) {
this._container.style.visibility = null;
} else {
this._container.style.visibility = "hidden";
}
},

_hasQueryableLayer: function () {
let layers = this._map.options.mapEl.layers;
if (this._mapFocused) {
for (let layer of layers) {
if (layer.checked && layer._layer.queryable) {
return true;
}
}
}
return false;
},

_onKey: function (e) {
//set mapFocused = true if arrow buttons are used
if (["keydown", "keyup"].includes(e.type) && e.target.classList.contains("leaflet-container") && [32, 37, 38, 39, 40, 187, 189].includes(+e.keyCode)) {
this._mapFocused = true;
//set mapFocused = true if map is focued using tab
} else if (e.type === "keyup" && e.target.classList.contains("leaflet-container") && +e.keyCode === 9) {
this._mapFocused = true;
// set mapFocused = false and close all popups if tab or escape is used
} else if((e.type === "keyup" && e.target.classList.contains("leaflet-interactive") && +e.keyCode === 9) || +e.keyCode === 27){
this._mapFocused = false;
this._map.closePopup();
// set mapFocused = false if any other key is pressed
} else {
this._mapFocused = false;
}
this._addOrRemoveCrosshair();
},

});


export var crosshair = function (options) {
return new Crosshair(options);
};
142 changes: 142 additions & 0 deletions src/mapml/layers/FeatureLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export var MapMLFeatures = L.FeatureGroup.extend({
}
},

onAdd: function(map){
L.FeatureGroup.prototype.onAdd.call(this, map);
map.on("popupopen", this._attachSkipButtons, this);
this._updateTabIndex();
},

getEvents: function(){
if(this._staticFeature){
return {
Expand All @@ -47,6 +53,141 @@ export var MapMLFeatures = L.FeatureGroup.extend({
};
},

_updateTabIndex: function(){
for(let feature in this._features){
for(let path of this._features[feature]){
if(path._path){
if(path._path.getAttribute("d") !== "M0 0"){
path._path.setAttribute("tabindex", 0);
} else {
path._path.removeAttribute("tabindex");
}
if(path._path.childElementCount === 0) {
let title = document.createElement("title");
title.innerText = "Feature";
path._path.appendChild(title);
}
}
}
}
},

_attachSkipButtons: function(e){
if(!e.popup._source._path) return;
if(!e.popup._container.querySelector('div[class="mapml-focus-buttons"]')){
//add when popopen event happens instead
let div = L.DomUtil.create("div", "mapml-focus-buttons");

// creates |< button, focuses map
let mapFocusButton = L.DomUtil.create('a',"mapml-popup-button", div);
mapFocusButton.href = '#';
mapFocusButton.role = "button";
mapFocusButton.title = "Focus Map";
mapFocusButton.innerHTML = '|&#10094;';
L.DomEvent.disableClickPropagation(mapFocusButton);
L.DomEvent.on(mapFocusButton, 'click', L.DomEvent.stop);
L.DomEvent.on(mapFocusButton, 'click', this._skipBackward, this);

// creates < button, focuses previous feature, if none exists focuses the current feature
let previousButton = L.DomUtil.create('a', "mapml-popup-button", div);
previousButton.href = '#';
previousButton.role = "button";
previousButton.title = "Previous Feature";
previousButton.innerHTML = "&#10094;";
L.DomEvent.disableClickPropagation(previousButton);
L.DomEvent.on(previousButton, 'click', L.DomEvent.stop);
L.DomEvent.on(previousButton, 'click', this._previousFeature, e.popup);

// static feature counter that 1/1
let featureCount = L.DomUtil.create("p", "mapml-feature-count", div), currentFeature = 1;
featureCount.innerText = currentFeature+"/1";
//for(let feature of e.popup._source._path.parentNode.children){
// if(feature === e.popup._source._path)break;
// currentFeature++;
//}
//featureCount.innerText = currentFeature+"/"+e.popup._source._path.parentNode.childElementCount;

// creates > button, focuses next feature, if none exists focuses the current feature
let nextButton = L.DomUtil.create('a', "mapml-popup-button", div);
nextButton.href = '#';
nextButton.role = "button";
nextButton.title = "Next Feature";
nextButton.innerHTML = "&#10095;";
L.DomEvent.disableClickPropagation(nextButton);
L.DomEvent.on(nextButton, 'click', L.DomEvent.stop);
L.DomEvent.on(nextButton, 'click', this._nextFeature, e.popup);

// creates >| button, focuses map controls
let controlFocusButton = L.DomUtil.create('a',"mapml-popup-button", div);
controlFocusButton.href = '#';
controlFocusButton.role = "button";
controlFocusButton.title = "Focus Controls";
controlFocusButton.innerHTML = '&#10095;|';
L.DomEvent.disableClickPropagation(controlFocusButton);
L.DomEvent.on(controlFocusButton, 'click', L.DomEvent.stop);
L.DomEvent.on(controlFocusButton, 'click', this._skipForward, this);

let divider = L.DomUtil.create("hr");
divider.style.borderTop = "1px solid #bbb";

e.popup._content.appendChild(divider);
e.popup._content.appendChild(div);
}

// 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
function focusFeature(focusEvent){
if(focusEvent.originalEvent.path[0].title==="Focus Controls" && +focusEvent.originalEvent.keyCode === 9){
L.DomEvent.stop(focusEvent);
e.popup._source._path.focus();
} else if(focusEvent.originalEvent.shiftKey && +focusEvent.originalEvent.keyCode === 9){
e.target.closePopup(e.popup);
L.DomEvent.stop(focusEvent);
e.popup._source._path.focus();
}
}

function removeHandlers(removeEvent){
if (removeEvent.popup === e.popup){
e.target.off("keydown", focusFeature);
e.target.off("popupclose", removeHandlers);
}
}
// e.target = this._map
// Looks for keydown, more specifically tab and shift tab
e.target.on("keydown", focusFeature);

// if popup closes then the focusFeature handler can be removed
e.target.on("popupclose", removeHandlers);
},

_skipBackward: function(e){
this._map.closePopup();
this._map._container.focus();
},

_previousFeature: function(e){
this._map.closePopup();
if(this._source._path.previousSibling){
this._source._path.previousSibling.focus();
} else {
this._source._path.focus();
}
},

_nextFeature: function(e){
this._map.closePopup();
if(this._source._path.nextSibling){
this._source._path.nextSibling.focus();
} else {
this._source._path.focus();
}
},

_skipForward: function(e){
this._map.closePopup();
this._map._controlContainer.focus();
},

_handleMoveEnd : function(){
let mapZoom = this._map.getZoom();
if(mapZoom > this.zoomBounds.maxZoom || mapZoom < this.zoomBounds.minZoom){
Expand All @@ -62,6 +203,7 @@ export var MapMLFeatures = L.FeatureGroup.extend({
this._map.getPixelBounds(),
mapZoom,this._map.options.projection));
this._removeCSS();
this._updateTabIndex();
},

//sets default if any are missing, better to only replace ones that are missing
Expand Down
Loading

0 comments on commit 4f8ff65

Please sign in to comment.