From 5d5e58ae094b09062fc44aeaa231d2d1641aaa4a Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 04:40:32 +0200 Subject: [PATCH] a11y: app toast notifications become a live region for screen readers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/renderer.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/renderer.ts b/src/renderer.ts index 3693e7c..58a584c 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -689,14 +689,28 @@ function showAppToast(message: string, type: 'info' | 'warn' = 'info'): void { toast = document.createElement('div'); toast.id = 'appToast'; 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); } - toast.textContent = message; toast.classList.remove('warn', 'show'); if (type === '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(() => { toast?.classList.add('show');