From 26b03da7650e0625126490c7241651995b937fba Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 03:32:03 +0200 Subject: [PATCH] a11y: streamer-item row itself keyboard-accessible + aria-current MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous a11y pass made all four chip-buttons inside a streamer row (AUTO / VOD / REC / remove) keyboard-accessible, but the row itself — the parent .streamer-item div whose click selects the streamer — was still mouse-only. A keyboard user could focus the chips but never the row, so they could never select a streamer without a mouse. Made the row a focusable role="button": - role + tabindex on the .streamer-item div - aria-label set to the streamer's name (so AT announces "xrohat button" rather than reading every chip child) - aria-current="true" on the currently selected row (mirroring the visual .active state) so AT understands which row is the current selection - A keydown handler on the row that fires selectStreamer on Enter / Space, but ONLY when the row itself (not a chip child) is the event target. The chips already preventDefault + stopPropagation on their own keydowns so they never reach this handler — and even if they did, the e.target check guards. Focus-visible adds an inset 2px purple ring (inset to match the row's left-border-marker styling for the active state). Tab order through the sidebar is now: nav-items → streamer row → AUTO → VOD → REC → remove-X → next streamer row. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer-streamers.ts | 18 ++++++++++++++++++ src/styles.css | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/src/renderer-streamers.ts b/src/renderer-streamers.ts index e2acc7d..00a1b15 100644 --- a/src/renderer-streamers.ts +++ b/src/renderer-streamers.ts @@ -465,6 +465,14 @@ function renderStreamers(): void { item.className = 'streamer-item' + (currentStreamer === streamer ? ' active' : ''); item.setAttribute('draggable', 'true'); item.dataset.streamerName = streamer; + // Keyboard a11y for the row itself — click selects the streamer. + // Each chip inside still gets its own focus + Enter/Space wiring + // and stops propagation, so tabbing through a row lands on row + // first, then AUTO / VOD / REC / remove in order. + item.setAttribute('role', 'button'); + item.setAttribute('tabindex', '0'); + item.setAttribute('aria-label', streamer); + if (currentStreamer === streamer) item.setAttribute('aria-current', 'true'); // Live-dot — red pulsing dot when this streamer is currently // broadcasting on Twitch. Populated from the live-status batch @@ -573,6 +581,16 @@ function renderStreamers(): void { if (draggedStreamerName === streamer) return; void selectStreamer(streamer); }); + item.addEventListener('keydown', (e) => { + // Activate row on Enter / Space when the row itself (not a + // chip child) is focused. The chips already preventDefault + // + stopPropagation on their own keydowns so they won't reach + // this handler. + if (e.key !== 'Enter' && e.key !== ' ') return; + if (e.target !== item) return; + e.preventDefault(); + void selectStreamer(streamer); + }); list.appendChild(item); }); diff --git a/src/styles.css b/src/styles.css index f76d775..12f5913 100644 --- a/src/styles.css +++ b/src/styles.css @@ -130,6 +130,11 @@ body { color: var(--text); } +.streamer-item:focus-visible { + outline: none; + box-shadow: inset 0 0 0 2px rgba(145, 70, 255, 0.55); +} + .streamer-item.active { background: linear-gradient(90deg, rgba(145, 71, 255, 0.28) 0%, rgba(145, 71, 255, 0.08) 100%); color: var(--text);