From a40c9e4ea4cb870466c8d3f2fdab11e759bc021c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 00:16:39 +0100 Subject: [PATCH] fix(labs): keep keyboard focus where it belongs around the search field Clearing the search with its button removed the button under the focus and dropped focus onto the page; it now stays in the field. The / shortcut no longer pulls focus out of an open dialog, menu or list, and ignores a key another handler already took or one typed while an input method is composing. --- .../lab-filter-rail.browser.test.tsx | 59 ++++++++++++++++++- src/components/labs/lab-filter-rail.tsx | 6 +- src/components/labs/use-lab-list-filters.ts | 18 +++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx b/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx index 4def8a3ab..a9d934326 100644 --- a/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx +++ b/src/components/labs/__tests__/lab-filter-rail.browser.test.tsx @@ -1,8 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { page } from "vitest/browser"; +import { page, userEvent } from "vitest/browser"; +import { useState } from "react"; import { renderWithApp } from "../../../../tests/browser/render"; import { LabFilterRail, type LabFilterRailProps } from "../lab-filter-rail"; +import { useLabListFilters } from "../use-lab-list-filters"; function props(): LabFilterRailProps { return { @@ -156,3 +158,58 @@ describe(" layout", () => { expect(Math.max(...centers) - Math.min(...centers)).toBeLessThan(2); }); }); + +/** The rail wired to the real filter hook, with a dialog to open over it. */ +function WiredRail() { + const lab = useLabListFilters(); + const [dialog, setDialog] = useState(false); + return ( + <> + + + {dialog ? ( +
+ +
+ ) : null} + + ); +} + +describe(" keyboard", () => { + it("focuses the search field on / and keeps focus there after clearing", async () => { + const { screen } = await renderWithApp(); + const search = screen.getByRole("searchbox", { name: "Search biomarkers" }); + + ( + screen.getByRole("button", { name: "Open dialog" }).element() as HTMLElement + ).focus(); + await userEvent.keyboard("/"); + await expect.element(search).toHaveFocus(); + await expect.element(search).toHaveValue(""); + + await userEvent.keyboard("iron"); + await screen.getByRole("button", { name: "Clear search" }).click(); + await expect.element(search).toHaveValue(""); + await expect.element(search).toHaveFocus(); + }); + + it("leaves / alone while focus is inside an open dialog", async () => { + const { screen } = await renderWithApp(); + await screen.getByRole("button", { name: "Open dialog" }).click(); + const inside = screen.getByRole("button", { name: "Inside" }); + await expect.element(inside).toHaveFocus(); + await userEvent.keyboard("/"); + await expect.element(inside).toHaveFocus(); + }); +}); diff --git a/src/components/labs/lab-filter-rail.tsx b/src/components/labs/lab-filter-rail.tsx index 29bff4c86..813455319 100644 --- a/src/components/labs/lab-filter-rail.tsx +++ b/src/components/labs/lab-filter-rail.tsx @@ -97,7 +97,11 @@ export function LabFilterRail({ ) : (