a11y: sidebar nav-items keyboard-accessible + aria-current
All 7 sidebar nav-items (Twitch VODs / Clips / Cutter / Merge / Statistik / Archiv / Einstellungen) were plain `<div class="nav-item">` elements with only an onclick. Same a11y story as the previous two iterations: no role, no tabindex, no semantic active-state marker, no keyboard activation. Added on each nav item: - role="button" and tabindex="0" so they enter the tab order and read as activatable buttons to assistive tech - aria-current="page" applied to the active item, removed from the others — both managed in showTab() since that's the single switch point for active-state transitions - A delegated keydown handler on the .nav container (one listener, not seven) that fires showTab on Enter / Space for whatever nav-item descendant is currently focused. Bound once with a data-keynav-bound guard so init() re-running doesn't double-bind CSS adds a 2px purple focus-visible ring matching the rest of the keyboard-focus family added in 4.6.50 and 4.6.51. WCAG 2.1 success criterion 2.1.1 (Keyboard) — every interactive element activated by keyboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a82a8f97f7
commit
5fda4e2103
@ -186,31 +186,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<div class="nav-item active" data-tab="vods" onclick="showTab('vods')">
|
<div class="nav-item active" role="button" tabindex="0" aria-current="page" data-tab="vods" onclick="showTab('vods')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8l7 4-7 4V8z"/></svg>
|
||||||
<span id="navVodsText">Twitch VODs</span>
|
<span id="navVodsText">Twitch VODs</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="clips" onclick="showTab('clips')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="clips" onclick="showTab('clips')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||||
<span id="navClipsText">Twitch Clips</span>
|
<span id="navClipsText">Twitch Clips</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="cutter" onclick="showTab('cutter')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="cutter" onclick="showTab('cutter')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3h-3z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3h-3z"/></svg>
|
||||||
<span id="navCutterText">Video schneiden</span>
|
<span id="navCutterText">Video schneiden</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="merge" onclick="showTab('merge')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="merge" onclick="showTab('merge')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 20.41L18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 20.41L18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z"/></svg>
|
||||||
<span id="navMergeText">Videos zusammenfugen</span>
|
<span id="navMergeText">Videos zusammenfugen</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="stats" onclick="showTab('stats')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="stats" onclick="showTab('stats')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h2v8H3zm4-7h2v15H7zm4 4h2v11h-2zm4 4h2v7h-2zm4-8h2v15h-2z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h2v8H3zm4-7h2v15H7zm4 4h2v11h-2zm4 4h2v7h-2zm4-8h2v15h-2z"/></svg>
|
||||||
<span id="navStatsText">Statistik</span>
|
<span id="navStatsText">Statistik</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="archive" onclick="showTab('archive')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="archive" onclick="showTab('archive')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||||
<span id="navArchiveText">Archiv</span>
|
<span id="navArchiveText">Archiv</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-tab="settings" onclick="showTab('settings')">
|
<div class="nav-item" role="button" tabindex="0" data-tab="settings" onclick="showTab('settings')">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||||
<span id="navSettingsText">Einstellungen</span>
|
<span id="navSettingsText">Einstellungen</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,6 +43,26 @@ async function init(): Promise<void> {
|
|||||||
renderStreamers();
|
renderStreamers();
|
||||||
renderQueue();
|
renderQueue();
|
||||||
|
|
||||||
|
// Keyboard activation for nav-items (Enter / Space). The items are
|
||||||
|
// div[role="button"][tabindex="0"], so browsers won't synthesise a
|
||||||
|
// click on Enter/Space natively — we wire it here once via event
|
||||||
|
// delegation so the listener doesn't need re-binding per tab switch.
|
||||||
|
const nav = document.querySelector('.nav');
|
||||||
|
if (nav && !nav.hasAttribute('data-keynav-bound')) {
|
||||||
|
nav.setAttribute('data-keynav-bound', '1');
|
||||||
|
nav.addEventListener('keydown', (event) => {
|
||||||
|
const ev = event as KeyboardEvent;
|
||||||
|
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
||||||
|
const target = ev.target as HTMLElement | null;
|
||||||
|
const item = target?.closest('.nav-item') as HTMLElement | null;
|
||||||
|
if (!item) return;
|
||||||
|
const tab = item.dataset.tab;
|
||||||
|
if (!tab) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
showTab(tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Kick off live-status subscription so the sidebar dots populate.
|
// Kick off live-status subscription so the sidebar dots populate.
|
||||||
const liveStatusInit = (window as unknown as { initLiveStatusSubscription?: () => Promise<void> }).initLiveStatusSubscription;
|
const liveStatusInit = (window as unknown as { initLiveStatusSubscription?: () => Promise<void> }).initLiveStatusSubscription;
|
||||||
if (typeof liveStatusInit === 'function') void liveStatusInit();
|
if (typeof liveStatusInit === 'function') void liveStatusInit();
|
||||||
@ -777,7 +797,10 @@ function persistActiveTab(tab: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showTab(tab: string): void {
|
function showTab(tab: string): void {
|
||||||
queryAll('.nav-item').forEach((i) => i.classList.remove('active'));
|
queryAll('.nav-item').forEach((i) => {
|
||||||
|
i.classList.remove('active');
|
||||||
|
i.removeAttribute('aria-current');
|
||||||
|
});
|
||||||
queryAll('.tab-content').forEach((c) => c.classList.remove('active'));
|
queryAll('.tab-content').forEach((c) => c.classList.remove('active'));
|
||||||
|
|
||||||
const navItem = query(`.nav-item[data-tab="${tab}"]`);
|
const navItem = query(`.nav-item[data-tab="${tab}"]`);
|
||||||
@ -787,6 +810,7 @@ function showTab(tab: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navItem.classList.add('active');
|
navItem.classList.add('active');
|
||||||
|
navItem.setAttribute('aria-current', 'page');
|
||||||
byId(tab + 'Tab').classList.add('active');
|
byId(tab + 'Tab').classList.add('active');
|
||||||
|
|
||||||
const titles: Record<string, string> = UI_TEXT.tabs;
|
const titles: Record<string, string> = UI_TEXT.tabs;
|
||||||
|
|||||||
@ -89,6 +89,11 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-item:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(145, 70, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-item svg {
|
.nav-item svg {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user