diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx index d0b76dee66..dc77ad785a 100644 --- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx +++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx @@ -37,6 +37,8 @@ const mockupQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }> { value: "required_documentation", label: "Documentation" }, { value: "compare_guidance", label: "Compare" }, ]; +// Re-apply focus shortly after the first frame to survive initial hydration remounts. +const focusHydrationRetryDelayMs = 300; type GlobalMockupSearchShellProps = { children: ReactNode; @@ -86,6 +88,7 @@ function GlobalMockupSearchShellClient({ const fallbackMode = visibleShellModes[0]?.id ?? initialMode; const initialSearchMode = availableModeIds?.length && !availableModeIds.includes(initialMode) ? fallbackMode : initialMode; + const requestedFocus = searchParams.get("focus") === "1"; const requestedRun = searchParams.get("run") === "1"; const currentUrlHasQuery = searchParams.has("q") || searchParams.has("query"); const requestedQuery = (searchParams.get("q") ?? searchParams.get("query") ?? "").trim(); @@ -137,12 +140,22 @@ function GlobalMockupSearchShellClient({ // because the state above is already seeded from the URL. if (lastSyncedSearchParamsRef.current === searchParamString) return; lastSyncedSearchParamsRef.current = searchParamString; - setSearchMode(resolvedSearchMode); setQuery(currentUrlHasQuery ? requestedQuery : ""); + }, [currentUrlHasQuery, requestedQuery, resolvedSearchMode, searchParamString]); - if (searchParams.get("focus") === "1") inputRef.current?.focus({ preventScroll: true }); - }, [currentUrlHasQuery, requestedQuery, resolvedSearchMode, searchParamString, searchParams]); + useEffect(() => { + if (!requestedFocus) return undefined; + const focusInput = () => { + inputRef.current?.focus({ preventScroll: true }); + }; + const frame = window.requestAnimationFrame(focusInput); + const timeout = window.setTimeout(focusInput, focusHydrationRetryDelayMs); + return () => { + window.cancelAnimationFrame(frame); + window.clearTimeout(timeout); + }; + }, [pathname, requestedFocus, searchParamString]); useEffect(() => { let cancelled = false; diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index dece712d18..273b33c497 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -221,6 +221,17 @@ test.describe("Clinical KB applications launcher", () => { } }); + test("mode home deep links preserve focus=1 on initial load", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + + for (const path of ["/services?focus=1", "/forms?focus=1"]) { + await gotoLauncher(page, path); + const sharedSearch = page.getByTestId("global-search-input"); + await expect(sharedSearch).toBeVisible(); + await expect(sharedSearch).toBeFocused(); + } + }); + test("services mode shows source-backed records in search results", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await gotoLauncher(page, "/services?q=13YARN&focus=1&run=1");