diff --git a/Gruntfile.js b/Gruntfile.js index aaf1c7d38..83d80fd06 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -25,6 +25,7 @@ module.exports = function(grunt) { 'dist/mapml.js': ['<%= rollup.main.dest %>'], 'dist/web-map.js': ['src/web-map.js'], 'dist/mapml-viewer.js': ['src/mapml-viewer.js'], + 'dist/map-caption.js': ['src/map-caption.js'], 'dist/map-area.js': ['src/map-area.js'], 'dist/layer.js': ['src/layer.js'], 'dist/leaflet.js': ['dist/leaflet-src.js', diff --git a/index.html b/index.html index 753657e7a..e98405f41 100644 --- a/index.html +++ b/index.html @@ -73,6 +73,7 @@ + A pleasing map of Canada diff --git a/src/map-caption.js b/src/map-caption.js new file mode 100644 index 000000000..c42b9821e --- /dev/null +++ b/src/map-caption.js @@ -0,0 +1,42 @@ +/* +implemented for both mapml-viewer and web-map; however web-map does not focus on map element in the browser resulting in NVDA +not being able to read out map-caption and stating that it's an interactive map region +*/ +export class MapCaption extends HTMLElement { + constructor() { + super(); + } + + // called when element is inserted into DOM (setup code) + connectedCallback() { + if (this.parentElement.nodeName === "MAPML-VIEWER" || this.parentElement.nodeName === "MAP") { + + // calls MutationObserver; needed to observe changes to content between tags and update to aria-label + let mapcaption = this.parentElement.querySelector('map-caption').textContent; + + this.observer = new MutationObserver(() => { + let mapcaptionupdate = this.parentElement.querySelector('map-caption').textContent; + + if (mapcaptionupdate !== mapcaption) { + this.parentElement.setAttribute('aria-label', this.parentElement.querySelector('map-caption').textContent); + } + }); + + this.observer.observe(this, { + characterData: true, + subtree: true, + attributes: true, + childList: true + }); + + // don't change aria-label if one already exists from user (checks when element is first created) + if (!this.parentElement.hasAttribute('aria-label')) { + const ariaLabel = this.textContent; + this.parentElement.setAttribute('aria-label', ariaLabel); + } + } + } + disconnectedCallback() { + this.observer.disconnect(); + } +} diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 7a286d9a1..7e158ccee 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -1,6 +1,7 @@ import './leaflet.js'; // bundled with proj4, proj4leaflet, modularized import './mapml.js'; import { MapLayer } from './layer.js'; +import { MapCaption } from './map-caption.js'; export class MapViewer extends HTMLElement { static get observedAttributes() { @@ -233,6 +234,31 @@ export class MapViewer extends HTMLElement { if(!custom){ this.dispatchEvent(new CustomEvent('createmap')); } + + /* + 1. only deletes aria-label when the last (only remaining) map caption is removed + 2. only deletes aria-label if the aria-label was defined by the map caption element itself + */ + + let mapcaption = this.querySelector('map-caption'); + + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); + + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); + } + }, 0); + } } } disconnectedCallback() { @@ -783,3 +809,4 @@ export class MapViewer extends HTMLElement { // need to provide options { extends: ... } for custom built-in elements window.customElements.define('mapml-viewer', MapViewer); window.customElements.define('layer-', MapLayer); +window.customElements.define('map-caption',MapCaption); diff --git a/src/web-map.js b/src/web-map.js index f4808bf22..20a76d819 100644 --- a/src/web-map.js +++ b/src/web-map.js @@ -2,6 +2,7 @@ import './leaflet.js'; // a lightly modified version of Leaflet for use as brow import './mapml.js'; // refactored URI usage, replaced with URL standard import { MapLayer } from './layer.js'; import { MapArea } from './map-area.js'; +import { MapCaption } from './map-caption.js'; export class WebMap extends HTMLMapElement { static get observedAttributes() { @@ -274,6 +275,31 @@ export class WebMap extends HTMLMapElement { if(!custom){ this.dispatchEvent(new CustomEvent('createmap')); } + + /* + 1. only deletes aria-label when the last (only remaining) map caption is removed + 2. only deletes aria-label if the aria-label was defined by the map caption element itself + */ + + let mapcaption = this.querySelector('map-caption'); + + if (mapcaption !== null) { + setTimeout(() => { + let ariaupdate = this.getAttribute('aria-label'); + + if (ariaupdate === mapcaption.innerHTML) { + this.mapCaptionObserver = new MutationObserver((m) => { + let mapcaptionupdate = this.querySelector('map-caption'); + if (mapcaptionupdate !== mapcaption) { + this.removeAttribute('aria-label'); + } + }); + this.mapCaptionObserver.observe(this, { + childList: true + }); + } + }, 0); + } } } disconnectedCallback() { @@ -844,3 +870,4 @@ export class WebMap extends HTMLMapElement { window.customElements.define('web-map', WebMap, { extends: 'map' }); window.customElements.define('layer-', MapLayer); window.customElements.define('map-area', MapArea, {extends: 'area'}); +window.customElements.define('map-caption',MapCaption); \ No newline at end of file diff --git a/test/e2e/mapml-viewer/mapml-viewerCaption.html b/test/e2e/mapml-viewer/mapml-viewerCaption.html new file mode 100644 index 000000000..c3340d946 --- /dev/null +++ b/test/e2e/mapml-viewer/mapml-viewerCaption.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + This is a test for mapml-viewer + Test2 + Test3 + + + Layer Test + + + + + diff --git a/test/e2e/mapml-viewer/mapml-viewerCaption.test.js b/test/e2e/mapml-viewer/mapml-viewerCaption.test.js new file mode 100644 index 000000000..23721b897 --- /dev/null +++ b/test/e2e/mapml-viewer/mapml-viewerCaption.test.js @@ -0,0 +1,47 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe("Playwright mapml-viewer map-captions Test", () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); + page = await context.newPage(); + await page.goto("mapml-viewerCaption.html"); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test("Aria-label matches map-caption", async () => { + let arialabel = await page.evaluate(`document.querySelector('mapml-viewer').getAttribute('aria-label')`); + expect(arialabel).toEqual("This is a test for mapml-viewer"); + }); + test("Changing first map-caption changes aria-label", async () => { + await page.evaluateHandle(() => document.querySelector('map-caption').innerHTML="Testing 1"); + let arialabel = await page.evaluate(`document.querySelector('mapml-viewer').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); + }); + test("Changing not-first map-caption doesn't change aria-label", async () => { + await page.evaluateHandle(() => document.getElementById('test2').innerHTML="Testing 2"); + let arialabel = await page.evaluate(`document.querySelector('mapml-viewer').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); // since aria-label didn't change, should still = "Testing 1" from previous test + }); + test("Removing not-first map-caption doesn't remove aria-label", async () => { + await page.evaluateHandle(() => document.getElementById('test3').remove()); + let arialabel = await page.evaluate(`document.querySelector('mapml-viewer').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); // since aria-label is still there, shoudl still = "Testing 1" from previous test + }); + test("Removing first map-caption removes aria-label", async () => { + await page.evaluateHandle(() => document.querySelector('map-caption').remove()); + let arialabel = await page.evaluate(`document.querySelector('mapml-viewer').getAttribute('aria-label')`); + expect(arialabel).toEqual(null); // since aria-label is removed, should = null + }); + test("Map Caption doesn't create aria-label on a layer", async () => { + let arialabel = await page.evaluate(`document.querySelector('layer-').getAttribute('aria-label')`); + expect(arialabel).toEqual(null); + }); + + +}); diff --git a/test/e2e/web-map/mapCaption.html b/test/e2e/web-map/mapCaption.html new file mode 100644 index 000000000..700a23b4c --- /dev/null +++ b/test/e2e/web-map/mapCaption.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + This is a test for web-map + Test2 + Test3 + + + Layer Test + + + + + \ No newline at end of file diff --git a/test/e2e/web-map/mapCaption.test.js b/test/e2e/web-map/mapCaption.test.js new file mode 100644 index 000000000..5e41b522c --- /dev/null +++ b/test/e2e/web-map/mapCaption.test.js @@ -0,0 +1,46 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe("Playwright web-map map-captions Test", () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); + page = await context.newPage(); + await page.goto("mapCaption.html"); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test("Aria-label matches map-caption", async () => { + let arialabel = await page.evaluate(`document.querySelector('map').getAttribute('aria-label')`); + expect(arialabel).toEqual("This is a test for web-map"); + }); + test("Changing map-caption changes aria-label", async () => { + await page.evaluateHandle(() => document.querySelector('map-caption').innerHTML="Testing 1"); + let arialabel = await page.evaluate(`document.querySelector('map').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); + }); + test("Changing not-first map-caption doesn't change aria-label", async () => { + await page.evaluateHandle(() => document.getElementById('test2').innerHTML="Testing 2"); + let arialabel = await page.evaluate(`document.querySelector('map').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); // since aria-label didn't change, should still = "Testing 1" from previous test + }); + test("Removing not-first map-caption doesn't remove aria-label", async () => { + await page.evaluateHandle(() => document.getElementById('test3').remove()); + let arialabel = await page.evaluate(`document.querySelector('map').getAttribute('aria-label')`); + expect(arialabel).toEqual("Testing 1"); // since aria-label is still there, shoudl still = "Testing 1" from previous test + }); + test("Removing first map-caption removes aria-label", async () => { + await page.evaluateHandle(() => document.querySelector('map-caption').remove()); + let arialabel = await page.evaluate(`document.querySelector('map').getAttribute('aria-label')`); + expect(arialabel).toEqual(null); // since aria-label is removed, should = null + }); + test("Map Caption doesn't create aria-label on a layer", async () => { + let arialabel = await page.evaluate(`document.querySelector('layer-').getAttribute('aria-label')`); + expect(arialabel).toEqual(null); + }); + +}); \ No newline at end of file