From 76be8d3949b5ec2698b5b00cfd67adb010342d06 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 03:37:14 +0200 Subject: [PATCH] =?UTF-8?q?a11y:=20VOD=20cards=20keyboard-activatable=20?= =?UTF-8?q?=E2=80=94=20opens=20VOD=20on=20Twitch=20via=20Enter/Space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the chip + row + nav a11y passes, the VOD cards in the main grid were the last big mouse-only surface in the sidebar + main panel flow. Each card already delegated click via a single vodGrid handler — but the card div itself was unfocusable, so a keyboard user could only reach the +Queue / Trim VOD buttons inside, never the card thumbnail click that opens the VOD page on Twitch. Added on each .vod-card: - role="button" + tabindex="0" - aria-label set to the VOD title so AT announces it correctly ("32h37m9s VOD: Cyborg Watchparty button") instead of reading the whole card content row by row Added to the existing delegated vodGrid handler: - A keydown branch that opens the VOD on Twitch when Enter / Space fires on a focused .vod-card and the event target is the card itself (not a child action button or checkbox — those have their own native button / checkbox semantics that handle Enter / Space correctly already) CSS adds a 3px purple focus-visible ring + accent-coloured border on the focused card, mirroring the hover state's purple glow. Tab order through the VOD grid now goes: VOD card -> checkbox -> Trim button -> +Queue button -> next VOD card. Predictable enough for keyboard navigation through a 38-VOD streamer profile. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer-streamers.ts | 19 +++++++++++++++++++ src/styles.css | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/src/renderer-streamers.ts b/src/renderer-streamers.ts index 00a1b15..7ef3e56 100644 --- a/src/renderer-streamers.ts +++ b/src/renderer-streamers.ts @@ -248,6 +248,9 @@ function buildVodCardHtml(vod: VOD, streamer: string, downloadedIds?: Set { + if (e.key !== 'Enter' && e.key !== ' ') return; + const target = e.target as HTMLElement | null; + if (!target) return; + const card = target.closest('.vod-card') as HTMLElement | null; + if (!card || card !== target) return; + const ctx = readVodCardContext(card); + if (!ctx) return; + e.preventDefault(); + void window.api.openExternal(ctx.url); + }); } let activeVodContextMenu: HTMLElement | null = null; diff --git a/src/styles.css b/src/styles.css index 12f5913..e3261e9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1288,6 +1288,12 @@ select option { border-color: rgba(145, 70, 255, 0.35); } +.vod-card:focus-visible { + outline: none; + border-color: rgba(145, 70, 255, 0.7); + box-shadow: 0 0 0 3px rgba(145, 70, 255, 0.35); +} + .vod-card.selected { box-shadow: 0 0 0 2px #9146FF, 0 8px 25px rgba(145, 70, 255, 0.25); }