From 8d0063d5146f33e94c16e342141e1b8b94aa4371 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 19 Sep 2026 19:30:27 +0200 Subject: [PATCH] fix(ui): the reader's own use of a stand-in stops the restore; the pill's name carries its cap Round 2 of the deep review: - A click on, or a key typed into, the focus keeper's stand-in fires no focusin, so a restore still polling for its list could take the focus from under the reader. A pointerdown or keydown now makes it the reader's own focus. - The Jump-to pill hid its "Ctrl K" from its name (the voice command "click Ctrl K" still matched nothing); the key cap's tooltip now equals its name, so it is not read twice as a description. - listNav's comment still cited DataTable's two layouts as the reason for its visibility filter. --- .../components/PaletteLauncher.dom.test.tsx | 6 +++-- frontend/src/components/PaletteLauncher.tsx | 7 ++---- frontend/src/lib/listNav.ts | 4 ++-- .../lib/useFocusKeeper.restore.dom.test.tsx | 22 +++++++++++++++++++ frontend/src/lib/useFocusKeeper.ts | 10 +++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/PaletteLauncher.dom.test.tsx b/frontend/src/components/PaletteLauncher.dom.test.tsx index 05c7dc29..d2b0e067 100644 --- a/frontend/src/components/PaletteLauncher.dom.test.tsx +++ b/frontend/src/components/PaletteLauncher.dom.test.tsx @@ -57,6 +57,8 @@ describe("the command palette's visible entry", () => { const cap = screen.getByRole("button", { name: /^palette\.open \((Ctrl|⌘) K\)$/ }); // Label in Name: the name carries what the cap shows, so "click Ctrl K" finds it. expect(cap.getAttribute("aria-label")).toContain(cap.textContent); + // …and a tooltip equal to that name, so a screen reader does not read it again as a description. + expect(cap.getAttribute("title")).toBe(cap.getAttribute("aria-label")); expect(screen.getByRole("textbox")).toBeTruthy(); fireEvent.click(cap); expect(isOpen()).toBe(true); @@ -66,8 +68,8 @@ describe("the command palette's visible entry", () => { page = "home"; const isOpen = mount(); expect(screen.queryByRole("textbox")).toBeNull(); - // Named by what it shows ("Jump to…"), not by a label that replaced it. - const pill = screen.getByRole("button", { name: "palette.title" }); + // Named by what it shows ("Jump to…" and its key cap), not by a label that replaced it. + const pill = screen.getByRole("button", { name: /^palette\.title ?(Ctrl|⌘) K$/ }); expect(pill.getAttribute("title")).toBe("palette.open"); fireEvent.click(pill); expect(isOpen()).toBe(true); diff --git a/frontend/src/components/PaletteLauncher.tsx b/frontend/src/components/PaletteLauncher.tsx index ce4cefbd..b1f1a206 100644 --- a/frontend/src/components/PaletteLauncher.tsx +++ b/frontend/src/components/PaletteLauncher.tsx @@ -34,7 +34,7 @@ export function PaletteKeycap() { type="button" onClick={openPalette} aria-label={`${t("palette.open")} (${cap})`} - title={t("palette.open")} + title={`${t("palette.open")} (${cap})`} className="shrink-0 rounded-md border border-border px-1.5 py-0.5 font-mono text-3xs text-muted transition hover:border-accent/50 hover:text-fg @max-[640px]/appmain:hidden" > {cap} @@ -55,10 +55,7 @@ export function PaletteLauncher() { >