From 0a377e321dd57d02415be7400949c15465c23317 Mon Sep 17 00:00:00 2001 From: ben-lu-uw Date: Fri, 1 Oct 2021 14:05:28 -0400 Subject: [PATCH 1/6] Announce move and zoom and implement bounce back at bounds Create util function for zoom/movement screen reader support Fix setTimeout Focus on the map Focus on the map Fix focus on the map Add announceMoveAndZoom functionality to all layer types Use aria-label for announce zoom and move Use output element for announcing zoom and move Add/Fix zoom and pan bounds Add dragging bounds Create new handler to listen for move events Add bounds Implement combined bounds to handle multiple layers bound check [work in progress] Implement combined bounds to handle multiple layers bound check [work in progress] Fix total bounds and bounds check Add total bounds rectangle to debug layer Change output element and total layer bounds rectangle position in dom to satisfy tests Disable bounds check when no bounds are present Refactor output element Set initial bounds to center of first bounds instead of [0,0] Clean up code Refactor output element class name Resolve indexing issues [work in progress] Make deselected layers not considered for the total bounds Use layeradd/layerremove instead of checkdisable for timing reasons Announce location on focus Fix max/min zoom announcements Fix dragged out of bounds condition Fix dragged out of bounds condition Merge in history fix Fix double moveend call issue Remove console log Add announceMovement test --- src/mapml-viewer.js | 8 +- src/mapml.css | 2 +- src/mapml/handlers/AnnounceMovement.js | 124 +++++++++++++++++++++++++ src/mapml/index.js | 4 + src/mapml/layers/DebugLayer.js | 15 +++ src/mapml/layers/TemplatedTileLayer.js | 2 +- src/mapml/utils/Util.js | 8 ++ src/web-map.js | 7 +- test/e2e/core/announceMovement.test.js | 119 ++++++++++++++++++++++++ 9 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 src/mapml/handlers/AnnounceMovement.js create mode 100644 test/e2e/core/announceMovement.test.js diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index f7fbbb155..d5e8fc46d 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -100,7 +100,6 @@ export class MapViewer extends HTMLElement { constructor() { // Always call super first in constructor super(); - this._source = this.outerHTML; let tmpl = document.createElement('template'); tmpl.innerHTML = @@ -110,7 +109,10 @@ export class MapViewer extends HTMLElement { let shadowRoot = this.attachShadow({mode: 'open'}); this._container = document.createElement('div'); - + + let output = ""; + this._container.insertAdjacentHTML("beforeend", output); + // Set default styles for the map element. let mapDefaultCSS = document.createElement('style'); mapDefaultCSS.innerHTML = @@ -198,6 +200,8 @@ export class MapViewer extends HTMLElement { projection: this.projection, query: true, contextMenu: true, + //Will replace with M.options.announceMoves + announceMovement: true, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/src/mapml.css b/src/mapml.css index b2f279be0..8e9829b6d 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -73,7 +73,7 @@ .mapml-layer-item-name a { color: revert; } - + .leaflet-top .leaflet-control { margin-top: 5px; } diff --git a/src/mapml/handlers/AnnounceMovement.js b/src/mapml/handlers/AnnounceMovement.js new file mode 100644 index 000000000..c4a4ca4e4 --- /dev/null +++ b/src/mapml/handlers/AnnounceMovement.js @@ -0,0 +1,124 @@ +export var AnnounceMovement = L.Handler.extend({ + addHooks: function () { + this._map.on({ + layeradd: this.totalBounds, + layerremove: this.totalBounds, + }); + + this._map.options.mapEl.addEventListener('moveend', this.announceBounds); + this._map.options.mapEl.addEventListener('focus', this.focusAnnouncement); + this._map.dragging._draggable.addEventListener('dragstart', this.dragged); + }, + removeHooks: function () { + this._map.off({ + layeradd: this.totalBounds, + layerremove: this.totalBounds, + }); + + this._map.options.mapEl.removeEventListener('moveend', this.announceBounds); + this._map.options.mapEl.removeEventListener('focus', this.focusAnnouncement); + this._map.dragging._draggable.removeEventListener('dragstart', this.dragged); + }, + + focusAnnouncement: function () { + let el = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".leaflet-container") : + this.shadowRoot.querySelector(".leaflet-container"); + + let mapZoom = this._map.getZoom(); + let location = M.gcrsToTileMatrix(this); + let standard = " zoom level " + mapZoom + " column " + location[0] + " row " + location[1]; + + if(mapZoom === this._map._layersMaxZoom){ + standard = "At maximum zoom level, zoom in disabled " + standard; + } + else if(mapZoom === this._map._layersMinZoom){ + standard = "At minimum zoom level, zoom out disabled " + standard; + } + + el.setAttribute("aria-roledescription", "region " + standard); + }, + + announceBounds: function () { + if(this._traversalCall > 0){ + return; + } + let mapZoom = this._map.getZoom(); + let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection); + + let visible = true; + if(this._map.totalLayerBounds){ + visible = mapZoom <= this._map._layersMaxZoom && mapZoom >= this._map._layersMinZoom && + this._map.totalLayerBounds.overlaps(mapBounds); + } + + let output = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".mapml-screen-reader-output") : + this.shadowRoot.querySelector(".mapml-screen-reader-output"); + + //GCRS to TileMatrix + let location = M.gcrsToTileMatrix(this); + let standard = "zoom level " + mapZoom + " column " + location[0] + " row " + location[1]; + + if(!visible){ + let outOfBoundsPos = this._history[this._historyIndex]; + let inBoundsPos = this._history[this._historyIndex - 1]; + this.back(); + this._history.pop(); + + if(outOfBoundsPos.zoom !== inBoundsPos.zoom){ + output.innerText = "Zoomed out of bounds, returning to"; + } + else if(this._map.dragging._draggable.wasDragged){ + output.innerText = "Dragged out of bounds, returning to "; + } + else if(outOfBoundsPos.x > inBoundsPos.x){ + output.innerText = "Reached east bound, panning east disabled"; + } + else if(outOfBoundsPos.x < inBoundsPos.x){ + output.innerText = "Reached west bound, panning west disabled"; + } + else if(outOfBoundsPos.y < inBoundsPos.y){ + output.innerText = "Reached north bound, panning north disabled"; + } + else if(outOfBoundsPos.y > inBoundsPos.y){ + output.innerText = "Reached south bound, panning south disabled"; + } + + } + else{ + let prevZoom = this._history[this._historyIndex - 1].zoom; + if(mapZoom === this._map._layersMaxZoom && mapZoom !== prevZoom){ + output.innerText = "At maximum zoom level, zoom in disabled " + standard; + } + else if(mapZoom === this._map._layersMinZoom && mapZoom !== prevZoom){ + output.innerText = "At minimum zoom level, zoom out disabled " + standard; + } + else { + output.innerText = standard; + } + } + this._map.dragging._draggable.wasDragged = false; + }, + + totalBounds: function () { + let layers = Object.keys(this._layers); + let bounds = L.bounds(); + + layers.forEach(i => { + if(this._layers[i].layerBounds){ + if(!bounds){ + let point = this._layers[i].layerBounds.getCenter(); + bounds = L.bounds(point, point); + } + bounds.extend(this._layers[i].layerBounds.min); + bounds.extend(this._layers[i].layerBounds.max); + } + }); + + this.totalLayerBounds = bounds; + }, + + dragged: function () { + this.wasDragged = true; + } + +}); \ No newline at end of file diff --git a/src/mapml/index.js b/src/mapml/index.js index 5501f0065..6f4a9b97a 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -56,6 +56,7 @@ import { Crosshair, crosshair } from "./layers/Crosshair"; import { Feature, feature } from "./features/feature"; import { FeatureRenderer, featureRenderer } from './features/featureRenderer'; import { FeatureGroup, featureGroup} from './features/featureGroup'; +import {AnnounceMovement} from "./handlers/AnnounceMovement"; import { Options } from "./options"; import "./keyboard"; @@ -600,13 +601,16 @@ M.coordsToArray = Util.coordsToArray; M.parseStylesheetAsHTML = Util.parseStylesheetAsHTML; M.pointToPCRSPoint = Util.pointToPCRSPoint; M.pixelToPCRSPoint = Util.pixelToPCRSPoint; +M.gcrsToTileMatrix = Util.gcrsToTileMatrix; M.QueryHandler = QueryHandler; M.ContextMenu = ContextMenu; +M.AnnounceMovement = AnnounceMovement; // see https://leafletjs.com/examples/extending/extending-3-controls.html#handlers L.Map.addInitHook('addHandler', 'query', M.QueryHandler); L.Map.addInitHook('addHandler', 'contextMenu', M.ContextMenu); +L.Map.addInitHook('addHandler', 'announceMovement', M.AnnounceMovement); M.MapMLLayer = MapMLLayer; M.mapMLLayer = mapMLLayer; diff --git a/src/mapml/layers/DebugLayer.js b/src/mapml/layers/DebugLayer.js index 66d02e205..0b2a0bf07 100644 --- a/src/mapml/layers/DebugLayer.js +++ b/src/mapml/layers/DebugLayer.js @@ -178,6 +178,7 @@ export var DebugVectors = L.LayerGroup.extend({ j = 0; this.addLayer(this._centerVector); + for (let i of id) { if (layers[i].layerBounds) { let boundsArray = [ @@ -199,6 +200,20 @@ export var DebugVectors = L.LayerGroup.extend({ j++; } } + + if(map.totalLayerBounds){ + let totalBoundsArray = [ + map.totalLayerBounds.min, + L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y), + map.totalLayerBounds.max, + L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y) + ]; + + let totalBounds = projectedExtent( + totalBoundsArray, + {color: "#808080", weight: 5, opacity: 0.5, fill: false}); + this.addLayer(totalBounds); + } }, _mapLayerUpdate: function (e) { diff --git a/src/mapml/layers/TemplatedTileLayer.js b/src/mapml/layers/TemplatedTileLayer.js index 6431ca192..f30518ebd 100644 --- a/src/mapml/layers/TemplatedTileLayer.js +++ b/src/mapml/layers/TemplatedTileLayer.js @@ -57,7 +57,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({ let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection); this.isVisible = mapZoom <= this.options.maxZoom && mapZoom >= this.options.minZoom && this.layerBounds.overlaps(mapBounds); - if(!(this.isVisible))return; + if(!(this.isVisible))return; this._parentOnMoveEnd(); }, createTile: function (coords) { diff --git a/src/mapml/utils/Util.js b/src/mapml/utils/Util.js index 3b7a74851..5e969a27a 100644 --- a/src/mapml/utils/Util.js +++ b/src/mapml/utils/Util.js @@ -385,4 +385,12 @@ export var Util = { map.getContainer().focus(); } }, + + gcrsToTileMatrix: function (mapEl) { + let point = mapEl._map.project(mapEl._map.getCenter()); + let tileSize = mapEl._map.options.crs.options.crs.tile.bounds.max.y; + let column = Math.trunc(point.x / tileSize); + let row = Math.trunc(point.y / tileSize); + return [column, row]; + } }; \ No newline at end of file diff --git a/src/web-map.js b/src/web-map.js index 0fb1624f7..9331a9ce8 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -117,7 +117,10 @@ export class WebMap extends HTMLMapElement { let shadowRoot = rootDiv.attachShadow({mode: 'open'}); this._container = document.createElement('div'); - + + let output = ""; + this._container.insertAdjacentHTML("beforeend", output); + // Set default styles for the map element. let mapDefaultCSS = document.createElement('style'); mapDefaultCSS.innerHTML = @@ -211,6 +214,8 @@ export class WebMap extends HTMLMapElement { projection: this.projection, query: true, contextMenu: true, + //Will replace with M.options.announceMoves + announceMovement: true, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/test/e2e/core/announceMovement.test.js b/test/e2e/core/announceMovement.test.js new file mode 100644 index 000000000..77dd61985 --- /dev/null +++ b/test/e2e/core/announceMovement.test.js @@ -0,0 +1,119 @@ +const playwright = require("playwright"); +jest.setTimeout(50000); +(async () => { + for (const browserType of BROWSER) { + describe( + "Announce movement test " + browserType, + ()=> { + beforeAll(async () => { + browser = await playwright[browserType].launch({ + headless: ISHEADLESS, + slowMo: 100, + }); + context = await browser.newContext(); + page = await context.newPage(); + if (browserType === "firefox") { + await page.waitForNavigation(); + } + await page.goto(PATH + "mapml-viewer.html"); + }); + afterAll(async function () { + await browser.close(); + }); + + test("[" + browserType + "]" + " Output values are correct during regular movement", async ()=>{ + const announceMovement = await page.$eval( + "body > mapml-viewer", + (map) => map._map.announceMovement._enabled + ); + if(!announceMovement){ + return; + } + await page.keyboard.press("Tab"); + await page.keyboard.press("ArrowUp"); + await page.waitForTimeout(100); + + const movedUp = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(movedUp).toEqual("zoom level 0 column 3 row 3"); + + for(let i = 0; i < 2; i++){ + await page.keyboard.press("ArrowLeft"); + await page.waitForTimeout(100); + } + + const movedLeft = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(movedLeft).toEqual("zoom level 0 column 2 row 3"); + + await page.keyboard.press("Equal"); + await page.waitForTimeout(100); + + const zoomedIn = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(zoomedIn).toEqual("zoom level 1 column 4 row 6"); + }); + + test("[" + browserType + "]" + " Output values are correct at bounds and bounces back", async ()=>{ + const announceMovement = await page.$eval( + "body > mapml-viewer", + (map) => map._map.announceMovement._enabled + ); + if(!announceMovement){ + return; + } + //Zoom out to min layer bound + await page.keyboard.press("Minus"); + await page.waitForTimeout(100); + + const minZoom = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(minZoom).toEqual("At minimum zoom level, zoom out disabled zoom level 0 column 2 row 3"); + + //Pan out of west bounds, expect the map to bounce back + for(let i = 0; i < 4; i++){ + await page.waitForTimeout(100); + await page.keyboard.press("ArrowLeft"); + } + + const westBound = await page.waitForFunction(() => + document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Reached west bound, panning west disabled", + {}, {timeout: 1000} + ); + expect(await westBound.jsonValue()).toEqual(true); + + const bouncedBack = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(bouncedBack).toEqual("zoom level 0 column 1 row 3"); + + //Zoom in out of bounds, expect the map to zoom back + await page.keyboard.press("Equal"); + + const zoomedOutOfBounds = await page.waitForFunction(() => + document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Zoomed out of bounds, returning to", + {}, {timeout: 1000} + ); + expect(await zoomedOutOfBounds.jsonValue()).toEqual(true); + + const zoomedBack = await page.$eval( + "body > mapml-viewer div > output", + (output) => output.innerHTML + ); + expect(zoomedBack).toEqual("zoom level 0 column 1 row 3"); + + }); + + } + ); + } +})(); \ No newline at end of file From 9afeeff6a97cb9955ea38721c969bdc44f40fa64 Mon Sep 17 00:00:00 2001 From: ben-lu-uw Date: Wed, 10 Nov 2021 09:49:39 -0500 Subject: [PATCH 2/6] Readd output CSS --- src/mapml.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mapml.css b/src/mapml.css index 8e9829b6d..239a3b035 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -703,3 +703,13 @@ label.mapml-layer-item-toggle { padding-inline-start: 2rem; padding-inline-end: 1rem; } + +.mapml-screen-reader-output { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} \ No newline at end of file From c38162d1ea2883d9d2172022d77fdf1633c45273 Mon Sep 17 00:00:00 2001 From: ben-lu-uw Date: Mon, 15 Nov 2021 09:24:41 -0500 Subject: [PATCH 3/6] Implement Ahmad's M.options.announceMovement --- src/mapml-viewer.js | 3 +-- src/web-map.js | 3 +-- test/e2e/core/announceMovement.test.js | 16 +--------------- test/e2e/mapml-viewer/mapml-viewer.html | 5 +++++ 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index d5e8fc46d..30bc074fa 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -200,8 +200,7 @@ export class MapViewer extends HTMLElement { projection: this.projection, query: true, contextMenu: true, - //Will replace with M.options.announceMoves - announceMovement: true, + announceMovement: M.options.announceMovement, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/src/web-map.js b/src/web-map.js index 9331a9ce8..84ffef061 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -214,8 +214,7 @@ export class WebMap extends HTMLMapElement { projection: this.projection, query: true, contextMenu: true, - //Will replace with M.options.announceMoves - announceMovement: true, + announceMovement: M.options.announceMovement, mapEl: this, crs: M[this.projection], zoom: this.zoom, diff --git a/test/e2e/core/announceMovement.test.js b/test/e2e/core/announceMovement.test.js index 77dd61985..151ccdb77 100644 --- a/test/e2e/core/announceMovement.test.js +++ b/test/e2e/core/announceMovement.test.js @@ -1,4 +1,4 @@ -const playwright = require("playwright"); +const playwright = require("playwright"); jest.setTimeout(50000); (async () => { for (const browserType of BROWSER) { @@ -22,13 +22,6 @@ jest.setTimeout(50000); }); test("[" + browserType + "]" + " Output values are correct during regular movement", async ()=>{ - const announceMovement = await page.$eval( - "body > mapml-viewer", - (map) => map._map.announceMovement._enabled - ); - if(!announceMovement){ - return; - } await page.keyboard.press("Tab"); await page.keyboard.press("ArrowUp"); await page.waitForTimeout(100); @@ -61,13 +54,6 @@ jest.setTimeout(50000); }); test("[" + browserType + "]" + " Output values are correct at bounds and bounces back", async ()=>{ - const announceMovement = await page.$eval( - "body > mapml-viewer", - (map) => map._map.announceMovement._enabled - ); - if(!announceMovement){ - return; - } //Zoom out to min layer bound await page.keyboard.press("Minus"); await page.waitForTimeout(100); diff --git a/test/e2e/mapml-viewer/mapml-viewer.html b/test/e2e/mapml-viewer/mapml-viewer.html index 8efbfec45..1982a71db 100644 --- a/test/e2e/mapml-viewer/mapml-viewer.html +++ b/test/e2e/mapml-viewer/mapml-viewer.html @@ -4,6 +4,11 @@ Basic Mapml-Viewer +