fix(labs): keep what is typed in the search field when the URL catches up

The labs search writes its trimmed query to the URL after a short pause,
and every change of the URL query replaced the field's text. The
field's own write came back as well: a trailing space vanished, so
"vitamin " followed by "d" searched for "vitamind", and a letter
typed while the write landed was lost. A query the field wrote itself
now leaves the field alone; one from back/forward, a link or Reset
still replaces it.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 00:15:24 +01:00
1 parent 04a4353e50
commit 74464184c5
2 files changed
+56 -6

No files matched your search

@@ -289,6 +289,47 @@ describe("LabList stale category and group header handling", () => {
expect(readStoredOpen("collapsed")).toEqual({});
});
it("keeps a trailing space in the search field after the URL takes the query", async () => {
setSearch("");
const queryClient = labQueryClient();
queryClient.setQueryData(
queryKeys.labResultsOverview("en"),
overviewWithAbnormalMarker,
);
const { screen } = await renderWithApp(<LabList />, { queryClient });
const search = screen.getByRole("searchbox", { name: "Search biomarkers" });
await search.fill("iron ");
await expect.poll(currentSearch).toBe("?q=iron");
// One more frame for the URL change to reach the hook.
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
await expect.element(search).toHaveValue("iron ");
});
it("takes the query from the URL on back navigation", async () => {
setSearch("?q=iron");
const queryClient = labQueryClient();
queryClient.setQueryData(
queryKeys.labResultsOverview("en"),
overviewWithAbnormalMarker,
);
const { screen } = await renderWithApp(<LabList />, { queryClient });
const search = screen.getByRole("searchbox", { name: "Search biomarkers" });
await expect.element(search).toHaveValue("iron");
await screen.getByRole("button", { name: "Reset" }).click();
await expect.poll(currentSearch).toBe("");
await expect.element(search).toHaveValue("");
window.history.back();
await expect.poll(currentSearch).toBe("?q=iron");
await expect.element(search).toHaveValue("iron");
});
it("memoizes setCategoryFilter callback identity across re-renders", async () => {
const callbacks: unknown[] = [];
const { screen } = await renderWithApp(
+15 -6
View File
@@ -19,20 +19,29 @@ export function useLabListFilters() {
serialise: labListFiltersToSearch,
});
// Debounced search draft
const [searchDraft, setSearchDraft] = useState(filters.q ?? "");
const [lastUrlQ, setLastUrlQ] = useState(filters.q ?? "");
// The field edits a draft; the URL takes it, trimmed, 200 ms after the
// last keystroke. A `q` the URL gains from elsewhere (back/forward, a
// shared link, Reset) replaces the draft. One the draft wrote itself
// does not: adopting it would drop a trailing space ("vitamin " became
// "vitamin", and the next letter ran on), or a letter typed while the
// write was landing.
const urlQ = filters.q ?? "";
const [searchDraft, setSearchDraft] = useState(urlQ);
const [lastUrlQ, setLastUrlQ] = useState(urlQ);
const [ownWrite, setOwnWrite] = useState<string | null>(null);
const searchInputRef = useRef<HTMLInputElement | null>(null);
if ((filters.q ?? "") !== lastUrlQ) {
setLastUrlQ(filters.q ?? "");
setSearchDraft(filters.q ?? "");
if (urlQ !== lastUrlQ) {
setLastUrlQ(urlQ);
if (urlQ === ownWrite) setOwnWrite(null);
else if (urlQ !== searchDraft.trim()) setSearchDraft(urlQ);
}
useEffect(() => {
const handle = setTimeout(() => {
const trimmed = searchDraft.trim();
if (trimmed === (filters.q ?? "")) return;
setOwnWrite(trimmed);
applyFilters(
(current) =>
trimmed === (current.q ?? "")