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) <noreply@anthropic.com>
This commit is contained in:
parent
6086cd51c1
commit
1d4b6718b9
@ -482,45 +482,72 @@ function renderStreamers(): void {
|
|||||||
nameSpan.className = 'streamer-name' + (isLive ? ' is-live' : '');
|
nameSpan.className = 'streamer-name' + (isLive ? ' is-live' : '');
|
||||||
nameSpan.textContent = streamer;
|
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
|
// AUTO toggle — when enabled, the main-process auto-record poller
|
||||||
// watches this channel for offline->live transitions and queues a
|
// 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 autoList = (config.auto_record_streamers as string[] | undefined) || [];
|
||||||
const isAutoOn = autoList.includes(streamer);
|
const isAutoOn = autoList.includes(streamer);
|
||||||
const autoBtn = document.createElement('span');
|
const autoBtn = document.createElement('span');
|
||||||
autoBtn.className = 'streamer-auto' + (isAutoOn ? ' active' : '');
|
autoBtn.className = 'streamer-auto' + (isAutoOn ? ' active' : '');
|
||||||
autoBtn.textContent = 'AUTO';
|
autoBtn.textContent = 'AUTO';
|
||||||
autoBtn.title = UI_TEXT.streamers?.autoRecordTitle || 'Auto-record when this streamer goes live';
|
autoBtn.title = UI_TEXT.streamers?.autoRecordTitle || 'Auto-record when this streamer goes live';
|
||||||
autoBtn.addEventListener('click', (e) => {
|
wireChipButton(autoBtn, {
|
||||||
e.stopPropagation();
|
handler: () => { void toggleAutoRecord(streamer); },
|
||||||
void toggleAutoRecord(streamer);
|
ariaLabel: UI_TEXT.streamers?.autoRecordTitle || 'Auto-record',
|
||||||
|
pressed: isAutoOn
|
||||||
});
|
});
|
||||||
|
|
||||||
// VOD-auto-download toggle — when enabled, the main-process auto-VOD
|
// VOD-auto-download toggle — periodic scan of this streamer's
|
||||||
// poller scans this streamer's VOD list periodically and queues new
|
// VOD list, auto-queues anything new within the age window.
|
||||||
// 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.
|
|
||||||
const vodList = (config.auto_vod_download_streamers as string[] | undefined) || [];
|
const vodList = (config.auto_vod_download_streamers as string[] | undefined) || [];
|
||||||
const isVodOn = vodList.includes(streamer);
|
const isVodOn = vodList.includes(streamer);
|
||||||
const vodBtn = document.createElement('span');
|
const vodBtn = document.createElement('span');
|
||||||
vodBtn.className = 'streamer-vod' + (isVodOn ? ' active' : '');
|
vodBtn.className = 'streamer-vod' + (isVodOn ? ' active' : '');
|
||||||
vodBtn.textContent = 'VOD';
|
vodBtn.textContent = 'VOD';
|
||||||
vodBtn.title = UI_TEXT.streamers?.autoVodTitle || 'Auto-download new VODs';
|
vodBtn.title = UI_TEXT.streamers?.autoVodTitle || 'Auto-download new VODs';
|
||||||
vodBtn.addEventListener('click', (e) => {
|
wireChipButton(vodBtn, {
|
||||||
e.stopPropagation();
|
handler: () => { void toggleAutoVodDownload(streamer); },
|
||||||
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
|
// Live-record one-shot — triggers a recording immediately (server
|
||||||
// when the streamer is currently online (server checks via Helix).
|
// verifies the streamer is online before honoring the request).
|
||||||
const recBtn = document.createElement('span');
|
const recBtn = document.createElement('span');
|
||||||
recBtn.className = 'streamer-rec';
|
recBtn.className = 'streamer-rec';
|
||||||
recBtn.textContent = 'REC';
|
recBtn.textContent = 'REC';
|
||||||
recBtn.title = UI_TEXT.streamers?.recordLiveTitle || 'Record live now';
|
recBtn.title = UI_TEXT.streamers?.recordLiveTitle || 'Record live now';
|
||||||
recBtn.addEventListener('click', (e) => {
|
wireChipButton(recBtn, {
|
||||||
e.stopPropagation();
|
handler: () => { void triggerLiveRecording(streamer); },
|
||||||
void triggerLiveRecording(streamer);
|
ariaLabel: UI_TEXT.streamers?.recordLiveTitle || 'Record live now'
|
||||||
});
|
});
|
||||||
const removeSpan = document.createElement('span');
|
const removeSpan = document.createElement('span');
|
||||||
removeSpan.className = 'remove';
|
removeSpan.className = 'remove';
|
||||||
|
|||||||
@ -455,6 +455,24 @@ body {
|
|||||||
border-radius: 3px;
|
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
|
/* Live-dot — red pulsing indicator shown next to a streamer's name in
|
||||||
the sidebar when they are currently broadcasting on Twitch. */
|
the sidebar when they are currently broadcasting on Twitch. */
|
||||||
.streamer-live-dot {
|
.streamer-live-dot {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user