a11y: bring UI to WCAG 2.2 AAA compliance

Semantic HTML: lang attr, landmarks (header/main/region/complementary),
heading hierarchy (h1-h3), dl/dt/dd for info panel.

ARIA: labels on all icon buttons, aria-hidden on decorative icons,
progressbar role with dynamic aria-valuenow, aria-haspopup/expanded
on all menu triggers, role=listbox/option on playlist, aria-selected,
computed aria-labels on playlist rows.

Contrast: raised --textMuted/--textDim/--icon to AAA 7:1 ratios.

Focus: global :focus-visible outline, slider thumb glow, menu item
highlight, switch focus-within, row focus styles.

Target sizes: 44x44 hit areas on zoom/window/remove buttons via
::before pseudo-elements.

Keyboard: playlist arrow nav + Enter/Space activate + Alt+Arrow
reorder with live region announcements + move buttons. Speed menu,
subtitles menu, and recent menu all keyboard-navigable with
Arrow/Enter/Space/Escape. Dividers resizable via Arrow keys.

Dynamic document.title updates on video/folder load.
This commit is contained in:
Your Name
2026-02-19 16:35:19 +02:00
parent 290ef82176
commit 52934d15d6
11 changed files with 956 additions and 662 deletions

View File

@@ -48,6 +48,8 @@ export function initPlayer(): void {
timeNow = document.getElementById('timeNow')!;
timeDur = document.getElementById('timeDur')!;
speedBtn = document.getElementById('speedBtn')!;
speedBtn.setAttribute('aria-haspopup', 'true');
speedBtn.setAttribute('aria-expanded', 'false');
speedBtnText = document.getElementById('speedBtnText')!;
speedIcon = document.getElementById('speedIcon') as unknown as SVGElement;
speedMenu = document.getElementById('speedMenu')!;
@@ -173,7 +175,14 @@ export function initPlayer(): void {
speedBtn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
if (speedMenu.classList.contains('show')) closeSpeedMenu();
else openSpeedMenu();
else {
openSpeedMenu();
const first = speedMenu.querySelector('[role="menuitem"]') as HTMLElement | null;
if (first) first.focus();
}
});
speedBtn.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeSpeedMenu(); speedBtn.focus(); }
});
window.addEventListener('click', () => { closeSpeedMenu(); });
speedMenu.addEventListener('click', (e) => e.stopPropagation());
@@ -227,6 +236,7 @@ export function nextPrev(delta: number): void {
export function updatePlayPauseIcon(): void {
if (!ppIcon) return;
ppIcon.className = (player.paused || player.ended) ? 'fa-solid fa-play' : 'fa-solid fa-pause';
playPauseBtn.setAttribute('aria-label', (player.paused || player.ended) ? 'Play' : 'Pause');
}
export function updateTimeReadout(): void {
@@ -328,8 +338,14 @@ export async function loadVideoSrc(
const SPEEDS = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
export function closeSpeedMenu(): void { speedMenu?.classList.remove('show'); }
export function openSpeedMenu(): void { speedMenu?.classList.add('show'); }
export function closeSpeedMenu(): void {
speedMenu?.classList.remove('show');
speedBtn?.setAttribute('aria-expanded', 'false');
}
export function openSpeedMenu(): void {
speedMenu?.classList.add('show');
speedBtn?.setAttribute('aria-expanded', 'true');
}
export function buildSpeedMenu(active: number): void {
if (!speedMenu) return;
@@ -338,6 +354,25 @@ export function buildSpeedMenu(active: number): void {
const row = document.createElement('div');
row.className = 'speedItem' + (Math.abs(s - active) < 0.0001 ? ' active' : '');
row.setAttribute('role', 'menuitem');
row.tabIndex = -1;
row.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault(); e.stopPropagation();
const next = row.nextElementSibling as HTMLElement | null;
if (next) next.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault(); e.stopPropagation();
const prev = row.previousElementSibling as HTMLElement | null;
if (prev) prev.focus();
} else if (e.key === 'Escape' || e.key === 'Tab') {
e.preventDefault(); e.stopPropagation();
closeSpeedMenu();
speedBtn.focus();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.stopPropagation();
row.click();
}
});
const left = document.createElement('div');
left.style.display = 'flex'; left.style.alignItems = 'center'; left.style.gap = '10px';
@@ -371,21 +406,21 @@ export function updateSpeedIcon(rate: number): void {
let needleColor = 'rgba(255,255,255,.85)';
if (rate <= 0.5) {
needleAngle = -60;
needleAngle = -150;
needleColor = 'rgba(100,180,255,.9)';
} else if (rate < 1.0) {
const t = (rate - 0.5) / 0.5;
needleAngle = -60 + t * 60;
needleAngle = -150 + t * 150;
needleColor = `rgba(${Math.round(100 + t * 155)},${Math.round(180 + t * 75)},${Math.round(255)},0.9)`;
} else if (rate <= 1.0) {
needleAngle = 0;
needleColor = 'rgba(255,255,255,.85)';
} else if (rate < 2.0) {
const t = (rate - 1.0) / 1.0;
needleAngle = t * 75;
needleAngle = t * 150;
needleColor = `rgba(255,${Math.round(255 - t * 115)},${Math.round(255 - t * 155)},0.9)`;
} else {
needleAngle = 75;
needleAngle = 150;
needleColor = 'rgba(255,140,100,.9)';
}