From 1d4b6718b9ad4dabe91fce7cbc672b2d1ca2d70f Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 03:23:29 +0200 Subject: [PATCH] a11y: AUTO / VOD / REC streamer chips keyboard-accessible The three streamer-row action chips (AUTO toggle, VOD toggle, REC one-shot) were spans wired only with click listeners. Same a11y gap as the .remove X chips in 4.6.50: no role, no tabindex, no keyboard activation, no semantic state for toggles. Screen readers read them as raw text "AUTO", "VOD", "REC" with no clue they were interactive controls. Factored a wireChipButton helper inside renderStreamers and ran all three chips through it. The helper stamps: - role="button" + tabindex="0" - aria-label (locale-driven, picked up the existing autoRecordTitle / autoVodTitle / recordLiveTitle locale keys that were previously only used for the visual title-tooltip) - aria-pressed="true"/"false" for the AUTO and VOD toggles so AT announces the on/off state - A keydown handler that synthesises the same click handler on Enter / Space and stops propagation so the row's click handler (streamer-select) does not also fire CSS adds three focus-visible rings (green for AUTO, blue for VOD, red for REC) matching each chips active-state colour palette. Keyboard navigators tabbing through a streamer item now see the ring on the focused chip clearly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer-streamers.ts | 61 ++++++++++++++++++++++++++++----------- src/styles.css | 18 ++++++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/renderer-streamers.ts b/src/renderer-streamers.ts index c4f6af8..e2acc7d 100644 --- a/src/renderer-streamers.ts +++ b/src/renderer-streamers.ts @@ -482,45 +482,72 @@ function renderStreamers(): void { nameSpan.className = 'streamer-name' + (isLive ? ' is-live' : ''); nameSpan.textContent = streamer; + // Three streamer-row action chips (AUTO toggle / VOD toggle / REC + // one-shot). All share the same accessibility wiring: + // role="button", tabindex="0", aria-pressed for the toggles + + // aria-label for screen readers, plus Enter/Space keydown + // activation. wireChipButton centralises that so each chip only + // declares its own visual class + label + handler. + const wireChipButton = (el: HTMLElement, opts: { + handler: () => void; + ariaLabel: string; + pressed?: boolean; + }): void => { + el.setAttribute('role', 'button'); + el.setAttribute('tabindex', '0'); + el.setAttribute('aria-label', opts.ariaLabel); + if (opts.pressed !== undefined) el.setAttribute('aria-pressed', String(opts.pressed)); + el.addEventListener('click', (e) => { + e.stopPropagation(); + opts.handler(); + }); + el.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + opts.handler(); + } + }); + }; + // AUTO toggle — when enabled, the main-process auto-record poller // watches this channel for offline->live transitions and queues a - // live recording automatically. Off by default, click to toggle. + // live recording automatically. const autoList = (config.auto_record_streamers as string[] | undefined) || []; const isAutoOn = autoList.includes(streamer); const autoBtn = document.createElement('span'); autoBtn.className = 'streamer-auto' + (isAutoOn ? ' active' : ''); autoBtn.textContent = 'AUTO'; autoBtn.title = UI_TEXT.streamers?.autoRecordTitle || 'Auto-record when this streamer goes live'; - autoBtn.addEventListener('click', (e) => { - e.stopPropagation(); - void toggleAutoRecord(streamer); + wireChipButton(autoBtn, { + handler: () => { void toggleAutoRecord(streamer); }, + ariaLabel: UI_TEXT.streamers?.autoRecordTitle || 'Auto-record', + pressed: isAutoOn }); - // VOD-auto-download toggle — when enabled, the main-process auto-VOD - // poller scans this streamer's VOD list periodically and queues new - // VODs published in the rolling window automatically. Complements - // AUTO (live capture): VOD covers downtime + transcoded archive, - // AUTO covers a stream as it happens. Useful for both. + // VOD-auto-download toggle — periodic scan of this streamer's + // VOD list, auto-queues anything new within the age window. const vodList = (config.auto_vod_download_streamers as string[] | undefined) || []; const isVodOn = vodList.includes(streamer); const vodBtn = document.createElement('span'); vodBtn.className = 'streamer-vod' + (isVodOn ? ' active' : ''); vodBtn.textContent = 'VOD'; vodBtn.title = UI_TEXT.streamers?.autoVodTitle || 'Auto-download new VODs'; - vodBtn.addEventListener('click', (e) => { - e.stopPropagation(); - void toggleAutoVodDownload(streamer); + wireChipButton(vodBtn, { + handler: () => { void toggleAutoVodDownload(streamer); }, + ariaLabel: UI_TEXT.streamers?.autoVodTitle || 'Auto-download new VODs', + pressed: isVodOn }); - // Live-record button — small red dot, only triggers a live capture - // when the streamer is currently online (server checks via Helix). + // Live-record one-shot — triggers a recording immediately (server + // verifies the streamer is online before honoring the request). const recBtn = document.createElement('span'); recBtn.className = 'streamer-rec'; recBtn.textContent = 'REC'; recBtn.title = UI_TEXT.streamers?.recordLiveTitle || 'Record live now'; - recBtn.addEventListener('click', (e) => { - e.stopPropagation(); - void triggerLiveRecording(streamer); + wireChipButton(recBtn, { + handler: () => { void triggerLiveRecording(streamer); }, + ariaLabel: UI_TEXT.streamers?.recordLiveTitle || 'Record live now' }); const removeSpan = document.createElement('span'); removeSpan.className = 'remove'; diff --git a/src/styles.css b/src/styles.css index e80b146..a4731fd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -455,6 +455,24 @@ body { border-radius: 3px; } +/* Keyboard focus rings for the AUTO / VOD / REC chip buttons. + Each picks up its own accent: AUTO + VOD use semantic green/blue + tints matching their active state, REC the red of the live dot. */ +.streamer-auto:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(0, 200, 83, 0.55); +} + +.streamer-vod:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.55); +} + +.streamer-rec:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 68, 68, 0.55); +} + /* Live-dot — red pulsing indicator shown next to a streamer's name in the sidebar when they are currently broadcasting on Twitch. */ .streamer-live-dot {