Skip to content

Commit 0bf7572

Browse files
authored
Vary live region politeness based on priority (#50)
Fixes #27 This PR updates the polyfill so `priority: normal` messages go to an `aria-live: polite` live-region and `priority: high` messages go to an `aria-live: assertive` live-region, to more closely align with the spec and mappings.
2 parents f5f188a + f82b525 commit 0bf7572

2 files changed

Lines changed: 97 additions & 18 deletions

File tree

arianotify-polyfill.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ if (
2121
const passkey = Symbol();
2222

2323
/** @type {string} */
24-
const liveRegionCustomElementName = `live-region-${uniqueId}`;
24+
const politeLiveRegionCustomElementName = `polite-live-region-${uniqueId}`;
25+
26+
/** @type {string} */
27+
const assertiveLiveRegionCustomElementName = `assertive-live-region-${uniqueId}`;
2528

2629
/**
2730
* @param {number} ms
@@ -79,16 +82,19 @@ if (
7982
}
8083

8184
// Get root element
82-
let root = /** @type {Element} */ (
85+
let root = /** @type {Element | ShadowRoot | Document} */ (
8386
this.element.closest("dialog") || this.element.closest("[role='dialog']") || this.element.getRootNode()
8487
);
8588
if (!root || root instanceof Document) root = document.body;
8689

87-
// Get 'live-region', if it already exists
88-
/** @type {LiveRegionCustomElement | null} */
89-
let liveRegion = root.querySelector(liveRegionCustomElementName);
90+
const liveRegionCustomElementName =
91+
this.priority === "high"
92+
? assertiveLiveRegionCustomElementName
93+
: politeLiveRegionCustomElementName;
94+
let liveRegion = /** @type {LiveRegionCustomElement | null} */ (
95+
root.querySelector(liveRegionCustomElementName)
96+
);
9097

91-
// Create (or recreate) 'live-region', if it doesn’t exist
9298
if (!liveRegion) {
9399
liveRegion = /** @type {LiveRegionCustomElement} */ (
94100
document.createElement(liveRegionCustomElementName)
@@ -145,7 +151,6 @@ if (
145151
#shadowRoot = this.attachShadow({ mode: "closed" });
146152

147153
connectedCallback() {
148-
this.ariaLive = "polite";
149154
this.ariaAtomic = "true";
150155
this.style.marginLeft = "-1px";
151156
this.style.marginTop = "-1px";
@@ -171,7 +176,29 @@ if (
171176
this.#shadowRoot.textContent = message;
172177
}
173178
}
174-
customElements.define(liveRegionCustomElementName, LiveRegionCustomElement);
179+
180+
class PoliteLiveRegionCustomElement extends LiveRegionCustomElement {
181+
connectedCallback() {
182+
this.ariaLive = "polite";
183+
super.connectedCallback();
184+
}
185+
}
186+
187+
class AssertiveLiveRegionCustomElement extends LiveRegionCustomElement {
188+
connectedCallback() {
189+
this.ariaLive = "assertive";
190+
super.connectedCallback();
191+
}
192+
}
193+
194+
customElements.define(
195+
politeLiveRegionCustomElementName,
196+
PoliteLiveRegionCustomElement
197+
);
198+
customElements.define(
199+
assertiveLiveRegionCustomElementName,
200+
AssertiveLiveRegionCustomElement
201+
);
175202

176203
if (!("ariaNotify" in Element.prototype)) {
177204
/**
@@ -200,4 +227,4 @@ if (
200227
queue.enqueue(new Message({ element: this.documentElement, message, priority }));
201228
};
202229
}
203-
}
230+
}
Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,68 @@
11
import { expect } from "@esm-bundle/chai";
22

3+
function spyOn(object, methodName) {
4+
const calls = [];
5+
const method = object[methodName];
6+
7+
object[methodName] = function (...args) {
8+
calls.push(args);
9+
return method.call(this, ...args);
10+
};
11+
12+
return calls;
13+
}
14+
315
export async function tests() {
416
describe("ariaNotify polyfill", () => {
5-
it("<live-region> placement", () => {
6-
let count = 0;
7-
for (const container of document.querySelectorAll("[data-should-contain-live-region]")) {
8-
container.ariaNotify("Hello, world!");
9-
const liveRegion = Array.from(container.childNodes).find((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName.match(/^live-region/i));
10-
expect(liveRegion).to.not.be.undefined;
11-
count++;
17+
let container;
18+
19+
beforeEach(() => {
20+
container = document.querySelector("[data-should-contain-live-region]");
21+
if (!container) {
22+
throw new Error("Expected a live-region test container");
1223
}
13-
expect(count).to.be.above(0);
24+
25+
for (const liveRegion of Array.from(container.children).filter((node) =>
26+
node.tagName.match(/-live-region/i)
27+
)) {
28+
liveRegion.remove();
29+
}
30+
});
31+
32+
it("routes polite messages to the polite live region", async () => {
33+
container.ariaNotify("Normal-priority message");
34+
const liveRegions = Array.from(container.children).filter((node) =>
35+
node.tagName.match(/-live-region/i)
36+
);
37+
expect(liveRegions).to.have.length(1);
38+
39+
const liveRegion = liveRegions[0];
40+
expect(liveRegion.tagName.match(/^polite-live-region/i)).to.not.equal(null);
41+
expect(liveRegion.ariaLive).to.equal("polite");
42+
43+
const calls = spyOn(liveRegion, "handleMessage");
44+
45+
await new Promise((resolve) => setTimeout(resolve, 500));
46+
expect(calls).to.have.length(1);
47+
expect(calls[0][1]).to.equal("Normal-priority message");
48+
});
49+
50+
it("routes assertive messages to the assertive live region", async () => {
51+
container.ariaNotify("High-priority message", { priority: "high" });
52+
const liveRegions = Array.from(container.children).filter((node) =>
53+
node.tagName.match(/-live-region/i)
54+
);
55+
expect(liveRegions).to.have.length(1);
56+
57+
const liveRegion = liveRegions[0];
58+
expect(liveRegion.tagName.match(/^assertive-live-region/i)).to.not.equal(null);
59+
expect(liveRegion.ariaLive).to.equal("assertive");
60+
61+
const calls = spyOn(liveRegion, "handleMessage");
62+
63+
await new Promise((resolve) => setTimeout(resolve, 500));
64+
expect(calls).to.have.length(1);
65+
expect(calls[0][1]).to.equal("High-priority message");
1466
});
1567
});
16-
}
68+
}

0 commit comments

Comments
 (0)