diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 15410d286..eb2001a78 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -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 diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 397413ea8..dc22fb5b5 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -211,6 +211,7 @@ export class MapViewer extends HTMLElement { this._attributionControl = this._map.attributionControl.setPrefix('Maps4HTML | Leaflet'); 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). diff --git a/src/mapml.css b/src/mapml.css index af8e90376..37fada4c1 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -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; +} \ No newline at end of file diff --git a/src/mapml/handlers/QueryHandler.js b/src/mapml/handlers/QueryHandler.js index bbcd15653..356a86750 100644 --- a/src/mapml/handlers/QueryHandler.js +++ b/src/mapml/handlers/QueryHandler.js @@ -151,14 +151,15 @@ 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); }); @@ -166,11 +167,13 @@ export var QueryHandler = L.Handler.extend({ } 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); }); } } diff --git a/src/mapml/index.js b/src/mapml/index.js index 1853e3f5b..e703becac 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -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) { @@ -627,4 +628,7 @@ M.mapMLStaticTileLayer = mapMLStaticTileLayer; M.DebugOverlay = DebugOverlay; M.debugOverlay = debugOverlay; +M.Crosshair = Crosshair; +M.crosshair = crosshair; + }(window, document)); diff --git a/src/mapml/layers/Crosshair.js b/src/mapml/layers/Crosshair.js new file mode 100644 index 000000000..ad622adb1 --- /dev/null +++ b/src/mapml/layers/Crosshair.js @@ -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 = ` + `; + + 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); +}; \ No newline at end of file diff --git a/src/mapml/layers/FeatureLayer.js b/src/mapml/layers/FeatureLayer.js index 0083cc18e..1ddf8dfc0 100644 --- a/src/mapml/layers/FeatureLayer.js +++ b/src/mapml/layers/FeatureLayer.js @@ -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 { @@ -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 = '|❮'; + 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 = "❮"; + 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 = "❯"; + 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 = '❯|'; + 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){ @@ -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 diff --git a/src/mapml/layers/MapLayer.js b/src/mapml/layers/MapLayer.js index 162a1a218..d78987052 100644 --- a/src/mapml/layers/MapLayer.js +++ b/src/mapml/layers/MapLayer.js @@ -93,8 +93,9 @@ export var MapMLLayer = L.Layer.extend({ // need to parse as HTML to preserve semantics and styles if (properties) { var c = document.createElement('div'); + c.classList.add("mapml-popup-content"); c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, {autoPan:false}); + geometry.bindPopup(c, {autoPan:false, closeButton: false, minWidth: 108}); } } }); @@ -122,8 +123,9 @@ export var MapMLLayer = L.Layer.extend({ // need to parse as HTML to preserve semantics and styles if (properties) { var c = document.createElement('div'); + c.classList.add("mapml-popup-content"); c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, {autoPan:false}); + geometry.bindPopup(c, {autoPan:false, closeButton: false, minWidth: 108}); } } }).addTo(map); @@ -189,6 +191,7 @@ export var MapMLLayer = L.Layer.extend({ setTimeout(() => { map.fire('checkdisabled'); }, 0); + map.on("popupopen", this._focusPopup, this); }, _validProjection : function(map){ @@ -1152,7 +1155,12 @@ export var MapMLLayer = L.Layer.extend({ if (this._templatedLayer && this._templatedLayer._queries) { return this._templatedLayer._queries; } - } + }, + _focusPopup: function(e){ + let content = e.popup._container.getElementsByClassName("mapml-popup-content")[0]; + content.setAttribute("tabindex", "-1"); + content.focus(); + }, }); export var mapMLLayer = function (url, node, options) { if (!url && !node) return null; diff --git a/src/mapml/layers/TemplatedFeaturesLayer.js b/src/mapml/layers/TemplatedFeaturesLayer.js index 5fee6ed43..21a0c5794 100644 --- a/src/mapml/layers/TemplatedFeaturesLayer.js +++ b/src/mapml/layers/TemplatedFeaturesLayer.js @@ -39,7 +39,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ // need to parse as HTML to preserve semantics and styles var c = document.createElement('div'); c.insertAdjacentHTML('afterbegin', properties.innerHTML); - geometry.bindPopup(c, {autoPan:false}); + geometry.bindPopup(c, {autoPan:false, closeButton: false}); } }); } @@ -87,6 +87,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ parser = new DOMParser(), features = this._features, map = this._map, + context = this, MAX_PAGES = 10, _pullFeatureFeed = function (url, limit) { return (fetch (url,{redirect: 'follow',headers: headers}) @@ -111,6 +112,7 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ _pullFeatureFeed(this._getfeaturesUrl(), MAX_PAGES) .then(function() { map.addLayer(features); + M.TemplatedFeaturesLayer.prototype._updateTabIndex(context); }) .catch(function (error) { console.log(error);}); }, @@ -119,6 +121,24 @@ export var TemplatedFeaturesLayer = L.Layer.extend({ this._updateZIndex(); return this; }, + _updateTabIndex: function(context){ + let c = context || this; + for(let layerNum in c._features._layers){ + let layer = c._features._layers[layerNum]; + if(layer._path){ + if(layer._path.getAttribute("d") !== "M0 0"){ + layer._path.setAttribute("tabindex", 0); + } else { + layer._path.removeAttribute("tabindex"); + } + if(layer._path.childElementCount === 0) { + let title = document.createElement("title"); + title.innerText = "Feature"; + layer._path.appendChild(title); + } + } + } + }, _updateZIndex: function () { if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) { this._container.style.zIndex = this.options.zIndex; diff --git a/src/web-map.js b/src/web-map.js index b48a09796..0e9c414b3 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -224,6 +224,7 @@ export class WebMap extends HTMLMapElement { this._attributionControl = this._map.attributionControl.setPrefix('Maps4HTML | Leaflet'); this.setControls(false,false,true); + this._crosshair = M.crosshair().addTo(this._map); if (this.hasAttribute('name')) { var name = this.getAttribute('name'); if (name) { diff --git a/test/e2e/core/keyboardInteraction.html b/test/e2e/core/keyboardInteraction.html new file mode 100644 index 000000000..fbe8add98 --- /dev/null +++ b/test/e2e/core/keyboardInteraction.html @@ -0,0 +1,86 @@ + + +
+