Skip to content

Commit dd5dd10

Browse files
committed
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
1 parent a086e23 commit dd5dd10

File tree

10 files changed

+285
-6
lines changed

10 files changed

+285
-6
lines changed

src/mapml-viewer.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ export class MapViewer extends HTMLElement {
100100
constructor() {
101101
// Always call super first in constructor
102102
super();
103-
104103
this._source = this.outerHTML;
105104
let tmpl = document.createElement('template');
106105
tmpl.innerHTML =
@@ -110,7 +109,10 @@ export class MapViewer extends HTMLElement {
110109

111110
let shadowRoot = this.attachShadow({mode: 'open'});
112111
this._container = document.createElement('div');
113-
112+
113+
let output = "<output role='status' aria-live='polite' aria-atomic='true' class='mapml-screen-reader-output'></output>";
114+
this._container.insertAdjacentHTML("beforeend", output);
115+
114116
// Set default styles for the map element.
115117
let mapDefaultCSS = document.createElement('style');
116118
mapDefaultCSS.innerHTML =
@@ -198,6 +200,8 @@ export class MapViewer extends HTMLElement {
198200
projection: this.projection,
199201
query: true,
200202
contextMenu: true,
203+
//Will replace with M.options.announceMoves
204+
announceMovement: true,
201205
mapEl: this,
202206
crs: M[this.projection],
203207
zoom: this.zoom,

src/mapml.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
.mapml-layer-item-name a {
7474
color: revert;
7575
}
76-
76+
7777
.leaflet-top .leaflet-control {
7878
margin-top: 5px;
7979
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
export var AnnounceMovement = L.Handler.extend({
2+
addHooks: function () {
3+
this._map.on({
4+
layeradd: this.totalBounds,
5+
layerremove: this.totalBounds,
6+
});
7+
8+
this._map.options.mapEl.addEventListener('moveend', this.announceBounds);
9+
this._map.options.mapEl.addEventListener('focus', this.focusAnnouncement);
10+
this._map.dragging._draggable.addEventListener('dragstart', this.dragged);
11+
},
12+
removeHooks: function () {
13+
this._map.off({
14+
layeradd: this.totalBounds,
15+
layerremove: this.totalBounds,
16+
});
17+
18+
this._map.options.mapEl.removeEventListener('moveend', this.announceBounds);
19+
this._map.options.mapEl.removeEventListener('focus', this.focusAnnouncement);
20+
this._map.dragging._draggable.removeEventListener('dragstart', this.dragged);
21+
},
22+
23+
focusAnnouncement: function () {
24+
let el = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".leaflet-container") :
25+
this.shadowRoot.querySelector(".leaflet-container");
26+
27+
let mapZoom = this._map.getZoom();
28+
let location = M.gcrsToTileMatrix(this);
29+
let standard = " zoom level " + mapZoom + " column " + location[0] + " row " + location[1];
30+
31+
if(mapZoom === this._map._layersMaxZoom){
32+
standard = "At maximum zoom level, zoom in disabled " + standard;
33+
}
34+
else if(mapZoom === this._map._layersMinZoom){
35+
standard = "At minimum zoom level, zoom out disabled " + standard;
36+
}
37+
38+
el.setAttribute("aria-roledescription", "region " + standard);
39+
},
40+
41+
announceBounds: function () {
42+
if(this._traversalCall > 0){
43+
return;
44+
}
45+
let mapZoom = this._map.getZoom();
46+
let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection);
47+
48+
let visible = true;
49+
if(this._map.totalLayerBounds){
50+
visible = mapZoom <= this._map._layersMaxZoom && mapZoom >= this._map._layersMinZoom &&
51+
this._map.totalLayerBounds.overlaps(mapBounds);
52+
}
53+
54+
let output = this.querySelector(".mapml-web-map") ? this.querySelector(".mapml-web-map").shadowRoot.querySelector(".mapml-screen-reader-output") :
55+
this.shadowRoot.querySelector(".mapml-screen-reader-output");
56+
57+
//GCRS to TileMatrix
58+
let location = M.gcrsToTileMatrix(this);
59+
let standard = "zoom level " + mapZoom + " column " + location[0] + " row " + location[1];
60+
61+
if(!visible){
62+
let outOfBoundsPos = this._history[this._historyIndex];
63+
let inBoundsPos = this._history[this._historyIndex - 1];
64+
this.back();
65+
this._history.pop();
66+
67+
if(outOfBoundsPos.zoom !== inBoundsPos.zoom){
68+
output.innerText = "Zoomed out of bounds, returning to";
69+
}
70+
else if(this._map.dragging._draggable.wasDragged){
71+
output.innerText = "Dragged out of bounds, returning to ";
72+
}
73+
else if(outOfBoundsPos.x > inBoundsPos.x){
74+
output.innerText = "Reached east bound, panning east disabled";
75+
}
76+
else if(outOfBoundsPos.x < inBoundsPos.x){
77+
output.innerText = "Reached west bound, panning west disabled";
78+
}
79+
else if(outOfBoundsPos.y < inBoundsPos.y){
80+
output.innerText = "Reached north bound, panning north disabled";
81+
}
82+
else if(outOfBoundsPos.y > inBoundsPos.y){
83+
output.innerText = "Reached south bound, panning south disabled";
84+
}
85+
86+
}
87+
else{
88+
let prevZoom = this._history[this._historyIndex - 1].zoom;
89+
if(mapZoom === this._map._layersMaxZoom && mapZoom !== prevZoom){
90+
output.innerText = "At maximum zoom level, zoom in disabled " + standard;
91+
}
92+
else if(mapZoom === this._map._layersMinZoom && mapZoom !== prevZoom){
93+
output.innerText = "At minimum zoom level, zoom out disabled " + standard;
94+
}
95+
else {
96+
output.innerText = standard;
97+
}
98+
}
99+
this._map.dragging._draggable.wasDragged = false;
100+
},
101+
102+
totalBounds: function () {
103+
let layers = Object.keys(this._layers);
104+
let bounds = L.bounds();
105+
106+
layers.forEach(i => {
107+
if(this._layers[i].layerBounds){
108+
if(!bounds){
109+
let point = this._layers[i].layerBounds.getCenter();
110+
bounds = L.bounds(point, point);
111+
}
112+
bounds.extend(this._layers[i].layerBounds.min);
113+
bounds.extend(this._layers[i].layerBounds.max);
114+
}
115+
});
116+
117+
this.totalLayerBounds = bounds;
118+
},
119+
120+
dragged: function () {
121+
this.wasDragged = true;
122+
}
123+
124+
});

src/mapml/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { Crosshair, crosshair } from "./layers/Crosshair";
5656
import { Feature, feature } from "./features/feature";
5757
import { FeatureRenderer, featureRenderer } from './features/featureRenderer';
5858
import { FeatureGroup, featureGroup} from './features/featureGroup';
59+
import {AnnounceMovement} from "./handlers/AnnounceMovement";
5960
import { Options } from "./options";
6061

6162
/* global L, Node */
@@ -599,13 +600,16 @@ M.coordsToArray = Util.coordsToArray;
599600
M.parseStylesheetAsHTML = Util.parseStylesheetAsHTML;
600601
M.pointToPCRSPoint = Util.pointToPCRSPoint;
601602
M.pixelToPCRSPoint = Util.pixelToPCRSPoint;
603+
M.gcrsToTileMatrix = Util.gcrsToTileMatrix;
602604

603605
M.QueryHandler = QueryHandler;
604606
M.ContextMenu = ContextMenu;
607+
M.AnnounceMovement = AnnounceMovement;
605608

606609
// see https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
607610
L.Map.addInitHook('addHandler', 'query', M.QueryHandler);
608611
L.Map.addInitHook('addHandler', 'contextMenu', M.ContextMenu);
612+
L.Map.addInitHook('addHandler', 'announceMovement', M.AnnounceMovement);
609613

610614
M.MapMLLayer = MapMLLayer;
611615
M.mapMLLayer = mapMLLayer;

src/mapml/layers/DebugLayer.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export var DebugVectors = L.LayerGroup.extend({
178178
j = 0;
179179

180180
this.addLayer(this._centerVector);
181+
181182
for (let i of id) {
182183
if (layers[i].layerBounds) {
183184
let boundsArray = [
@@ -199,6 +200,20 @@ export var DebugVectors = L.LayerGroup.extend({
199200
j++;
200201
}
201202
}
203+
204+
if(map.totalLayerBounds){
205+
let totalBoundsArray = [
206+
map.totalLayerBounds.min,
207+
L.point(map.totalLayerBounds.max.x, map.totalLayerBounds.min.y),
208+
map.totalLayerBounds.max,
209+
L.point(map.totalLayerBounds.min.x, map.totalLayerBounds.max.y)
210+
];
211+
212+
let totalBounds = projectedExtent(
213+
totalBoundsArray,
214+
{color: "#808080", weight: 5, opacity: 0.5, fill: false});
215+
this.addLayer(totalBounds);
216+
}
202217
},
203218

204219
_mapLayerUpdate: function (e) {

src/mapml/layers/TemplatedTileLayer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export var TemplatedTileLayer = L.TileLayer.extend({
5757
let mapBounds = M.pixelToPCRSBounds(this._map.getPixelBounds(),mapZoom,this._map.options.projection);
5858
this.isVisible = mapZoom <= this.options.maxZoom && mapZoom >= this.options.minZoom &&
5959
this.layerBounds.overlaps(mapBounds);
60-
if(!(this.isVisible))return;
60+
if(!(this.isVisible))return;
6161
this._parentOnMoveEnd();
6262
},
6363
createTile: function (coords) {

src/mapml/utils/Util.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,12 @@ export var Util = {
385385
map.getContainer().focus();
386386
}
387387
},
388+
389+
gcrsToTileMatrix: function (mapEl) {
390+
let point = mapEl._map.project(mapEl._map.getCenter());
391+
let tileSize = mapEl._map.options.crs.options.crs.tile.bounds.max.y;
392+
let column = Math.trunc(point.x / tileSize);
393+
let row = Math.trunc(point.y / tileSize);
394+
return [column, row];
395+
}
388396
};

src/web-map.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ export class WebMap extends HTMLMapElement {
117117

118118
let shadowRoot = rootDiv.attachShadow({mode: 'open'});
119119
this._container = document.createElement('div');
120-
120+
121+
let output = "<output role='status' aria-live='polite' aria-atomic='true' class='mapml-screen-reader-output'></output>";
122+
this._container.insertAdjacentHTML("beforeend", output);
123+
121124
// Set default styles for the map element.
122125
let mapDefaultCSS = document.createElement('style');
123126
mapDefaultCSS.innerHTML =
@@ -211,6 +214,8 @@ export class WebMap extends HTMLMapElement {
211214
projection: this.projection,
212215
query: true,
213216
contextMenu: true,
217+
//Will replace with M.options.announceMoves
218+
announceMovement: true,
214219
mapEl: this,
215220
crs: M[this.projection],
216221
zoom: this.zoom,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const playwright = require("playwright");
2+
jest.setTimeout(50000);
3+
(async () => {
4+
for (const browserType of BROWSER) {
5+
describe(
6+
"Announce movement test " + browserType,
7+
()=> {
8+
beforeAll(async () => {
9+
browser = await playwright[browserType].launch({
10+
headless: ISHEADLESS,
11+
slowMo: 100,
12+
});
13+
context = await browser.newContext();
14+
page = await context.newPage();
15+
if (browserType === "firefox") {
16+
await page.waitForNavigation();
17+
}
18+
await page.goto(PATH + "mapml-viewer.html");
19+
});
20+
afterAll(async function () {
21+
await browser.close();
22+
});
23+
24+
test("[" + browserType + "]" + " Output values are correct during regular movement", async ()=>{
25+
const announceMovement = await page.$eval(
26+
"body > mapml-viewer",
27+
(map) => map._map.announceMovement._enabled
28+
);
29+
if(!announceMovement){
30+
return;
31+
}
32+
await page.keyboard.press("Tab");
33+
await page.keyboard.press("ArrowUp");
34+
await page.waitForTimeout(100);
35+
36+
const movedUp = await page.$eval(
37+
"body > mapml-viewer div > output",
38+
(output) => output.innerHTML
39+
);
40+
expect(movedUp).toEqual("zoom level 0 column 3 row 3");
41+
42+
for(let i = 0; i < 2; i++){
43+
await page.keyboard.press("ArrowLeft");
44+
await page.waitForTimeout(100);
45+
}
46+
47+
const movedLeft = await page.$eval(
48+
"body > mapml-viewer div > output",
49+
(output) => output.innerHTML
50+
);
51+
expect(movedLeft).toEqual("zoom level 0 column 2 row 3");
52+
53+
await page.keyboard.press("Equal");
54+
await page.waitForTimeout(100);
55+
56+
const zoomedIn = await page.$eval(
57+
"body > mapml-viewer div > output",
58+
(output) => output.innerHTML
59+
);
60+
expect(zoomedIn).toEqual("zoom level 1 column 4 row 6");
61+
});
62+
63+
test("[" + browserType + "]" + " Output values are correct at bounds and bounces back", async ()=>{
64+
const announceMovement = await page.$eval(
65+
"body > mapml-viewer",
66+
(map) => map._map.announceMovement._enabled
67+
);
68+
if(!announceMovement){
69+
return;
70+
}
71+
//Zoom out to min layer bound
72+
await page.keyboard.press("Minus");
73+
await page.waitForTimeout(100);
74+
75+
const minZoom = await page.$eval(
76+
"body > mapml-viewer div > output",
77+
(output) => output.innerHTML
78+
);
79+
expect(minZoom).toEqual("At minimum zoom level, zoom out disabled zoom level 0 column 2 row 3");
80+
81+
//Pan out of west bounds, expect the map to bounce back
82+
for(let i = 0; i < 4; i++){
83+
await page.waitForTimeout(100);
84+
await page.keyboard.press("ArrowLeft");
85+
}
86+
87+
const westBound = await page.waitForFunction(() =>
88+
document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Reached west bound, panning west disabled",
89+
{}, {timeout: 1000}
90+
);
91+
expect(await westBound.jsonValue()).toEqual(true);
92+
93+
const bouncedBack = await page.$eval(
94+
"body > mapml-viewer div > output",
95+
(output) => output.innerHTML
96+
);
97+
expect(bouncedBack).toEqual("zoom level 0 column 1 row 3");
98+
99+
//Zoom in out of bounds, expect the map to zoom back
100+
await page.keyboard.press("Equal");
101+
102+
const zoomedOutOfBounds = await page.waitForFunction(() =>
103+
document.querySelector("body > mapml-viewer").shadowRoot.querySelector("div > output").innerHTML === "Zoomed out of bounds, returning to",
104+
{}, {timeout: 1000}
105+
);
106+
expect(await zoomedOutOfBounds.jsonValue()).toEqual(true);
107+
108+
const zoomedBack = await page.$eval(
109+
"body > mapml-viewer div > output",
110+
(output) => output.innerHTML
111+
);
112+
expect(zoomedBack).toEqual("zoom level 0 column 1 row 3");
113+
114+
});
115+
116+
}
117+
);
118+
}
119+
})();

test/e2e/core/debugMode.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ jest.setTimeout(50000);
169169
"xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g",
170170
(tile) => tile.childElementCount
171171
);
172-
expect(feature).toEqual(3);
172+
expect(feature).toEqual(4);
173173
});
174174

175175
test("[" + browserType + "]" + " Layer deselected then reselected", async () => {

0 commit comments

Comments
 (0)