From 35769959f41ca6823a77d6b8ff0eaf2338ac8835 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Mon, 11 May 2026 03:18:18 +0200 Subject: [PATCH] a11y: remove-X spans become keyboard-accessible with aria-label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two `x` glyphs (one per queue item, one per streamer-list item) had no semantic role, no aria-label, no tabindex — entirely mouse-only and screen-readers just announced them as the bare "x" character. Made both fully keyboard-accessible: - role="button" + tabindex="0" so they enter the tab order and read as buttons to AT - aria-label="Remove" / "Entfernen" via new streamers.removeAria locale key (DE + EN) - Keydown handler on Enter + Space synthesizes the same removal callback (mirroring native button behaviour for synthetic buttons — Enter on real buttons fires click, Space does too) - Focus-visible state: 2px red glow ring + force opacity:1 on the streamer-list .remove (which is normally opacity:0 until the streamer-item is hovered) so keyboard navigators can see the focused X Both call sites preserved e.stopPropagation in the keydown handler so Enter on a focused X doesn't bubble up to the row's click handler (which would trigger streamer-select). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer-locale-de.ts | 3 ++- src/renderer-locale-en.ts | 3 ++- src/renderer-queue.ts | 2 +- src/renderer-streamers.ts | 10 ++++++++++ src/styles.css | 10 +++++++++- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/renderer-locale-de.ts b/src/renderer-locale-de.ts index 49822d3..b136ebe 100644 --- a/src/renderer-locale-de.ts +++ b/src/renderer-locale-de.ts @@ -356,7 +356,8 @@ const UI_TEXT_DE = { autoRecordScanEmpty: 'Manueller Scan: kein Streamer ist gerade live.', liveNowTooltip: 'Aktuell live auf Twitch', modalCloseAria: 'Dialog schliessen', - sidebarEmpty: 'Noch keine Streamer. Fuege oben rechts einen hinzu.' + sidebarEmpty: 'Noch keine Streamer. Fuege oben rechts einen hinzu.', + removeAria: 'Entfernen' }, vods: { noneTitle: 'Keine VODs', diff --git a/src/renderer-locale-en.ts b/src/renderer-locale-en.ts index 7168837..12618fd 100644 --- a/src/renderer-locale-en.ts +++ b/src/renderer-locale-en.ts @@ -356,7 +356,8 @@ const UI_TEXT_EN = { autoRecordScanEmpty: 'Manual scan: no streamers currently live.', liveNowTooltip: 'Currently live on Twitch', modalCloseAria: 'Close dialog', - sidebarEmpty: 'No streamers yet. Add one via the input at the top right.' + sidebarEmpty: 'No streamers yet. Add one via the input at the top right.', + removeAria: 'Remove' }, vods: { noneTitle: 'No VODs', diff --git a/src/renderer-queue.ts b/src/renderer-queue.ts index 4f88e8b..f0430ff 100644 --- a/src/renderer-queue.ts +++ b/src/renderer-queue.ts @@ -572,7 +572,7 @@ function renderQueue(): void { ${item.status === 'error' ? `` : ''} - x + x `; }).join(''); diff --git a/src/renderer-streamers.ts b/src/renderer-streamers.ts index 3079c36..c4f6af8 100644 --- a/src/renderer-streamers.ts +++ b/src/renderer-streamers.ts @@ -525,10 +525,20 @@ function renderStreamers(): void { const removeSpan = document.createElement('span'); removeSpan.className = 'remove'; removeSpan.textContent = 'x'; + removeSpan.setAttribute('role', 'button'); + removeSpan.setAttribute('tabindex', '0'); + removeSpan.setAttribute('aria-label', UI_TEXT.streamers.removeAria); removeSpan.addEventListener('click', (e) => { e.stopPropagation(); void removeStreamer(streamer); }); + removeSpan.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + void removeStreamer(streamer); + } + }); item.append(nameSpan, autoBtn, vodBtn, recBtn, removeSpan); item.addEventListener('click', () => { diff --git a/src/styles.css b/src/styles.css index 64597a1..e80b146 100644 --- a/src/styles.css +++ b/src/styles.css @@ -443,10 +443,18 @@ body { cursor: pointer; } -.streamer-item:hover .remove { +.streamer-item:hover .remove, +.streamer-item .remove:focus-visible { opacity: 1; } +.streamer-item .remove:focus-visible, +.queue-item .remove:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.5); + border-radius: 3px; +} + /* 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 {