Compare commits

..

2 Commits

Author SHA1 Message Date
xRangerDE
58f8164db4 release: 4.6.67 toast live region for screen readers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:40:33 +02:00
xRangerDE
5d5e58ae09 a11y: app toast notifications become a live region for screen readers
showAppToast spawns / reuses a single floating toast at the bottom-
right of the window for transient status (e.g. "1 new VOD auto-queued",
"Cannot start recording", etc). The toast had no a11y semantics —
screen readers never announced it, so the entire transient-feedback
channel was silent for AT users.

Promoted the toast container to a live region:
- role="status" for info toasts + aria-live="polite" so the reader
  waits for a natural break in current speech before announcing
- role="alert" for warn toasts + aria-live="assertive" so the reader
  interrupts whatever it was saying (matches the visual amber-left-
  border meaning — warn IS urgent)
- aria-atomic="true" so the reader announces the whole message at
  once instead of attempting to diff against the previous toast

Critical detail: aria-live attributes have to be in place BEFORE the
text changes for AT to register the change as a live-region update.
The current implementation now sets role / aria-live first and only
then writes the new textContent.

WCAG 4.1.3 — Status Messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:40:32 +02:00
3 changed files with 18 additions and 4 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.66", "version": "4.6.67",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.66", "version": "4.6.67",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.6.66", "version": "4.6.67",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -689,14 +689,28 @@ function showAppToast(message: string, type: 'info' | 'warn' = 'info'): void {
toast = document.createElement('div'); toast = document.createElement('div');
toast.id = 'appToast'; toast.id = 'appToast';
toast.className = 'app-toast'; toast.className = 'app-toast';
// Live region — screen readers announce the toast text whenever
// it changes. Warn toasts go through aria-live="assertive" so the
// reader interrupts whatever it was speaking; info toasts use
// "polite" so they wait for a natural break in current speech.
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.setAttribute('aria-atomic', 'true');
document.body.appendChild(toast); document.body.appendChild(toast);
} }
toast.textContent = message;
toast.classList.remove('warn', 'show'); toast.classList.remove('warn', 'show');
if (type === 'warn') { if (type === 'warn') {
toast.classList.add('warn'); toast.classList.add('warn');
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
} else {
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
} }
// Setting textContent AFTER the aria-live attribute is in place
// ensures the change is captured as a live-region update by AT.
toast.textContent = message;
requestAnimationFrame(() => { requestAnimationFrame(() => {
toast?.classList.add('show'); toast?.classList.add('show');