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:
@@ -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)';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user