feat(a11y): aria-live narration of scale/clip; hide decorative SVGs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 09:28:19 -07:00
parent 0db602ebd0
commit f1ce23c4fe
3 changed files with 43 additions and 4 deletions
+19
View File
@@ -78,3 +78,22 @@ test.describe("Task 6 — Keyboard + ARIA dial", () => {
await expect(dial).toHaveAttribute("aria-valuenow", "0");
});
});
test.describe("Task 7 — Screen-reader narration", () => {
test("an aria-live region narrates the current scale", async ({ page }) => {
await page.goto("/");
const live = page.locator("#sr-status");
await expect(live).toHaveAttribute("aria-live", "polite");
await page.locator("#dial").focus();
await page.locator("#dial").press("Home");
await expect
.poll(async () => (await live.textContent())?.toLowerCase())
.toContain("cosmos");
});
test("decorative overlay SVGs are hidden from assistive tech", async ({ page }) => {
await page.goto("/");
await expect(page.locator("#overlay")).toHaveAttribute("aria-hidden", "true");
await expect(page.locator("#affect")).toHaveAttribute("aria-hidden", "true");
});
});
+19
View File
@@ -593,9 +593,28 @@ function renderScaleReadout() {
const scaleName = HEFi18n.pickUiString("scale." + s.id, activeLang);
$("scale-name").textContent = `${scaleName} · ${member} (${ringIndex + 1}/${ring.scales.length}${poolTag})`;
renderDial();
announceState(); // narrate the new altitude (scale + clip) for screen readers
refreshDevClip(); // keep the Dev Mode pool picker + clip data in sync with the landing
}
// Turn the visual alteration into words for assistive tech: the current scale plus
// the clip you've landed on. Deduped so a settle on the same state stays quiet.
let lastAnnounced = "";
function announce(text) {
const el = $("sr-status");
if (!el || !text || text === lastAnnounced) return;
lastAnnounced = text;
el.textContent = text;
}
function announceState() {
if (!ring) return;
const s = ring.scales[ringIndex];
const scaleName = s ? HEFi18n.pickUiString("scale." + s.id, activeLang) : "";
const clip = activeClip();
const title = clip && clip.title ? clip.title : "";
announce(title ? `${scaleName}. ${title}` : scaleName);
}
function controls() {
// Video (on/off) and Audio (on/off) are orthogonal toggles (audio spec §2).
// Audio on = the per-altitude soundtrack (white-noise is deferred). One bipolar
+5 -4
View File
@@ -24,10 +24,10 @@
<video id="vid-loop" loop muted playsinline crossorigin="anonymous"></video>
<audio id="aud" loop preload="auto" crossorigin="anonymous"></audio>
<audio id="aud-b" loop preload="auto" crossorigin="anonymous"></audio>
<canvas id="paint"></canvas>
<div id="tint"></div>
<svg id="overlay" viewBox="0 0 100 100" preserveAspectRatio="none"></svg>
<svg id="affect" viewBox="0 0 100 100" preserveAspectRatio="none"></svg>
<canvas id="paint" aria-hidden="true"></canvas>
<div id="tint" aria-hidden="true"></div>
<svg id="overlay" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"></svg>
<svg id="affect" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"></svg>
<div id="black" class="black hidden"></div>
<button type="button" id="run-sim" class="run-sim hidden" data-i18n="run.button">Run simulation</button>
<div id="motion-warning" class="motion-warning hidden" role="dialog" aria-modal="true" aria-labelledby="mw-title">
@@ -41,6 +41,7 @@
</section>
<section class="panel">
<div id="sr-status" class="visually-hidden" role="status" aria-live="polite"></div>
<fieldset>
<legend data-i18n="output.legend">Output</legend>
<label class="dev-switch" for="visual">