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:
@@ -17,6 +17,9 @@ export function initSubtitles(): void {
|
||||
subsBtn = document.getElementById('subsBtn')!;
|
||||
subsMenu = document.getElementById('subsMenu')!;
|
||||
|
||||
subsBtn.setAttribute('aria-haspopup', 'true');
|
||||
subsBtn.setAttribute('aria-expanded', 'false');
|
||||
|
||||
subsBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!library) return;
|
||||
@@ -24,6 +27,13 @@ export function initSubtitles(): void {
|
||||
else await openSubsMenu();
|
||||
});
|
||||
|
||||
subsBtn.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && subsMenuOpen) {
|
||||
closeSubsMenu();
|
||||
subsBtn.focus();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('click', () => { if (subsMenuOpen) closeSubsMenu(); });
|
||||
subsMenu.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
@@ -92,6 +102,29 @@ export function clearSubtitles(): void {
|
||||
export function closeSubsMenu(): void {
|
||||
subsMenuOpen = false;
|
||||
subsMenu?.classList.remove('show');
|
||||
subsBtn?.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function menuItemKeyHandler(e: KeyboardEvent): void {
|
||||
const item = e.currentTarget as HTMLElement;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
let next = item.nextElementSibling as HTMLElement | null;
|
||||
while (next && !next.classList.contains('subsMenuItem')) next = next.nextElementSibling as HTMLElement | null;
|
||||
if (next) next.focus();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
let prev = item.previousElementSibling as HTMLElement | null;
|
||||
while (prev && !prev.classList.contains('subsMenuItem')) prev = prev.previousElementSibling as HTMLElement | null;
|
||||
if (prev) prev.focus();
|
||||
} else if (e.key === 'Escape' || e.key === 'Tab') {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
closeSubsMenu();
|
||||
subsBtn.focus();
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
item.click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openSubsMenu(): Promise<void> {
|
||||
@@ -111,7 +144,10 @@ export async function openSubsMenu(): Promise<void> {
|
||||
for (const sub of available.sidecar) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'subsMenuItem';
|
||||
item.innerHTML = `<i class="fa-solid fa-file-lines"></i> ${sub.label} <span style="opacity:.5;font-size:10px;margin-left:auto;">${sub.format}</span>`;
|
||||
item.setAttribute('role', 'menuitem');
|
||||
item.tabIndex = -1;
|
||||
item.addEventListener('keydown', menuItemKeyHandler);
|
||||
item.innerHTML = `<i class="fa-solid fa-file-lines" aria-hidden="true"></i> ${sub.label} <span style="opacity:.5;font-size:10px;margin-left:auto;">${sub.format}</span>`;
|
||||
item.onclick = async () => {
|
||||
closeSubsMenu();
|
||||
const res = await api.loadSidecarSubtitle(sub.path);
|
||||
@@ -142,7 +178,10 @@ export async function openSubsMenu(): Promise<void> {
|
||||
for (const track of available.embedded) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'subsMenuItem embedded';
|
||||
item.innerHTML = `<i class="fa-solid fa-film"></i> ${track.label} <span style="opacity:.5;font-size:10px;margin-left:auto;">${track.codec}</span>`;
|
||||
item.setAttribute('role', 'menuitem');
|
||||
item.tabIndex = -1;
|
||||
item.addEventListener('keydown', menuItemKeyHandler);
|
||||
item.innerHTML = `<i class="fa-solid fa-film" aria-hidden="true"></i> ${track.label} <span style="opacity:.5;font-size:10px;margin-left:auto;">${track.codec}</span>`;
|
||||
item.onclick = async () => {
|
||||
closeSubsMenu();
|
||||
const res = await api.extractEmbeddedSubtitle(track.index);
|
||||
@@ -169,7 +208,10 @@ export async function openSubsMenu(): Promise<void> {
|
||||
// "Load from file" option
|
||||
const loadItem = document.createElement('div');
|
||||
loadItem.className = 'subsMenuItem';
|
||||
loadItem.innerHTML = '<i class="fa-solid fa-file-import"></i> Load from file...';
|
||||
loadItem.setAttribute('role', 'menuitem');
|
||||
loadItem.tabIndex = -1;
|
||||
loadItem.addEventListener('keydown', menuItemKeyHandler);
|
||||
loadItem.innerHTML = '<i class="fa-solid fa-file-import" aria-hidden="true"></i> Load from file...';
|
||||
loadItem.onclick = async () => {
|
||||
closeSubsMenu();
|
||||
try {
|
||||
@@ -187,7 +229,10 @@ export async function openSubsMenu(): Promise<void> {
|
||||
// "Disable" option
|
||||
const disableItem = document.createElement('div');
|
||||
disableItem.className = 'subsMenuItem';
|
||||
disableItem.innerHTML = '<i class="fa-solid fa-xmark"></i> Disable subtitles';
|
||||
disableItem.setAttribute('role', 'menuitem');
|
||||
disableItem.tabIndex = -1;
|
||||
disableItem.addEventListener('keydown', menuItemKeyHandler);
|
||||
disableItem.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i> Disable subtitles';
|
||||
disableItem.onclick = () => {
|
||||
closeSubsMenu();
|
||||
try {
|
||||
@@ -203,4 +248,7 @@ export async function openSubsMenu(): Promise<void> {
|
||||
|
||||
subsMenu.classList.add('show');
|
||||
subsMenuOpen = true;
|
||||
subsBtn.setAttribute('aria-expanded', 'true');
|
||||
const first = subsMenu.querySelector('.subsMenuItem') as HTMLElement | null;
|
||||
if (first) first.focus();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user