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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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