Skip to content

Commit

Permalink
Announce move and zoom and implement bounce back at bounds
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ben-lu-uw committed Nov 10, 2021
1 parent a086e23 commit dd5dd10
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 6 deletions.
8 changes: 6 additions & 2 deletions src/mapml-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -110,7 +109,10 @@ export class MapViewer extends HTMLElement {

let shadowRoot = this.attachShadow({mode: 'open'});
this._container = document.createElement('div');


let output = "<output role='status' aria-live='polite' aria-atomic='true' class='mapml-screen-reader-output'></output>";
this._container.insertAdjacentHTML("beforeend", output);

// Set default styles for the map element.
let mapDefaultCSS = document.createElement('style');
mapDefaultCSS.innerHTML =
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/mapml.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
.mapml-layer-item-name a {
color: revert;
}

.leaflet-top .leaflet-control {
margin-top: 5px;
}
Expand Down
124 changes: 124 additions & 0 deletions src/mapml/handlers/AnnounceMovement.js
Original file line number Diff line number Diff line change
@@ -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;
}

});
4 changes: 4 additions & 0 deletions src/mapml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/* global L, Node */
Expand Down Expand Up @@ -599,13 +600,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;
Expand Down
15 changes: 15 additions & 0 deletions src/mapml/layers/DebugLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/mapml/layers/TemplatedTileLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions src/mapml/utils/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
};
7 changes: 6 additions & 1 deletion src/web-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ export class WebMap extends HTMLMapElement {

let shadowRoot = rootDiv.attachShadow({mode: 'open'});
this._container = document.createElement('div');


let output = "<output role='status' aria-live='polite' aria-atomic='true' class='mapml-screen-reader-output'></output>";
this._container.insertAdjacentHTML("beforeend", output);

// Set default styles for the map element.
let mapDefaultCSS = document.createElement('style');
mapDefaultCSS.innerHTML =
Expand Down Expand Up @@ -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,
Expand Down
119 changes: 119 additions & 0 deletions test/e2e/core/announceMovement.test.js
Original file line number Diff line number Diff line change
@@ -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");

});

}
);
}
})();
2 changes: 1 addition & 1 deletion test/e2e/core/debugMode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ jest.setTimeout(50000);
"xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > svg > g",
(tile) => tile.childElementCount
);
expect(feature).toEqual(3);
expect(feature).toEqual(4);
});

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

0 comments on commit dd5dd10

Please sign in to comment.