diff --git a/public/ui.ts b/public/ui.ts index 03299ee..479bf30 100644 --- a/public/ui.ts +++ b/public/ui.ts @@ -44,17 +44,28 @@ ["export-picker", "export-menu"], ]; + function pickerToggleButton(pickerId: string): HTMLElement | null { + const btn = $(pickerId)?.querySelector('[data-action="toggle"]'); + return btn instanceof HTMLElement ? btn : null; + } + + // Returns true if the menu was open and is now closed. + function closePicker(pickerId: string, menuId: string): boolean { + const menu = $(menuId); + if (!menu || menu.classList.contains("hidden")) return false; + menu.classList.add("hidden"); + pickerToggleButton(pickerId)?.setAttribute("aria-expanded", "false"); + return true; + } + document.addEventListener("click", (e) => { const target = e.target instanceof Element ? e.target : null; if (!target) return; // Outside-click dismiss for popup menus. - for (const pair of PICKER_PAIRS) { - const picker = $(pair[0]); - if (picker && !picker.contains(target)) { - const m = $(pair[1]); - if (m) m.classList.add("hidden"); - } + for (const [pickerId, menuId] of PICKER_PAIRS) { + const picker = $(pickerId); + if (picker && !picker.contains(target)) closePicker(pickerId, menuId); } // Close search modal when any result link is clicked. @@ -68,7 +79,11 @@ if (action === "toggle") { const id = trigger.getAttribute("data-target"); - if (id) $(id)?.classList.toggle("hidden"); + const menu = id ? $(id) : null; + if (menu) { + const nowHidden = menu.classList.toggle("hidden"); + trigger.setAttribute("aria-expanded", String(!nowHidden)); + } return; } @@ -284,6 +299,18 @@ ); }); + // Escape dismisses an open picker menu and returns focus to its trigger, + // mirroring the native behaviour keyboard users get from . + document.addEventListener("keydown", (e) => { + if (e.key !== "Escape") return; + for (const [pickerId, menuId] of PICKER_PAIRS) { + if (closePicker(pickerId, menuId)) { + pickerToggleButton(pickerId)?.focus(); + e.preventDefault(); + } + } + }); + // Search palette keyboard nav. Focus rovers over the real results rather // than tracking a selected index, so Enter, middle-click and Tab keep their // native link behaviour and no aria-activedescendant bookkeeping is needed. diff --git a/src/views/layout.ts b/src/views/layout.ts index 9dd585e..78862c5 100644 --- a/src/views/layout.ts +++ b/src/views/layout.ts @@ -171,7 +171,7 @@ function renderWithSidebar(body: string, options: LayoutOptions): string { : ""; const exportDropdown = `
- @@ -258,10 +258,10 @@ export function layout( const localeItems = SUPPORTED_LOCALES.map( (l) => - `${LOCALE_LABELS[l] ?? l.toUpperCase()}`, + ``, ).join("\n"); const localePicker = `
- @@ -272,10 +272,10 @@ export function layout( const themeItems = USER_THEMES.map( (t) => - `${msg.nav[`theme${t.charAt(0).toUpperCase()}${t.slice(1)}` as "themeLight" | "themeDark" | "themeSystem"]}`, + ``, ).join("\n"); const themePicker = `
-
@@ -289,7 +289,7 @@ export function layout( : ICONS.user; const profileOrLogin = session ? `
-