diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 05fb92a664..1ec7998a20 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3366,6 +3366,7 @@ export function ClinicalDashboard({ composerFollowUpSuggestions={searchMode === "answer" ? answerFollowUpSuggestions : undefined} onPickComposerFollowUpSuggestion={handlePickFollowUpSuggestion} composerFollowUpSuggestionsDisabled={loading} + showPhoneSuggestionTickerOnHome={heroOwnsPhoneComposer} sharedHomeIdentity={showSharedHome} composerPlaceholder={searchMode === "answer" && latestAnswerQuery ? "Ask a follow-up..." : undefined} mobileSearchPlacement={hasMobileBottomSearch ? "bottom" : "default"} diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 651a4cb0c3..84247b396a 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -877,6 +877,7 @@ function GlobalStandaloneSearchShellBody({ differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined } desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"} + showPhoneSuggestionTickerOnHome={isStandaloneModeHome || pathname === "/"} searchComposerVisible={shouldShowSearchComposer} desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined} desktopPageComposerSlotId={ diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 1cc6aa23e3..e87b6c29a8 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -195,6 +195,7 @@ export function MasterSearchHeader({ mobileBottomSearchVariant = "default", desktopSearchPlacement = "default", searchComposerVisible = true, + showPhoneSuggestionTickerOnHome = false, desktopHomeComposerSlotId, desktopPageComposerSlotId, heroComposerBreakpoint = "all", @@ -250,6 +251,8 @@ export function MasterSearchHeader({ * content keeps maximum screen space. Every phone dock uses it now; the * "default" value remains for hosts that need the taller legacy dock. */ mobileBottomSearchVariant?: "default" | "compact"; + /** Show the compact phone suggestion ticker only for standalone-mode homes. */ + showPhoneSuggestionTickerOnHome?: boolean; desktopSearchPlacement?: "default" | "hero"; searchComposerVisible?: boolean; /** Mode-home slot the composer portals into so the search pill sits in the @@ -1816,6 +1819,7 @@ export function MasterSearchHeader({ onListboxIdReady={setCommandListboxId} onActiveItemIdChange={setCommandActiveItemId} onFocusSearchInput={handleFocusSearchInput} + showPhoneSuggestionTicker={showPhoneSuggestionTickerOnHome} >
void; +}) { const [activeExampleIndex, setActiveExampleIndex] = useState(0); + const [heldTickerExample, setHeldTickerExample] = useState(null); + const [isTickerHeld, setIsTickerHeld] = useState(false); const activeExample = examples[activeExampleIndex % examples.length]; useEffect(() => { + if (isTickerHeld) { + return; + } + if (examples.length <= 1) return; const intervalId = window.setInterval(() => { setActiveExampleIndex((current) => (current + 1) % examples.length); }, SMART_HINT_ROTATION_MS); return () => window.clearInterval(intervalId); - }, [examples]); + }, [examples, isTickerHeld]); + + const freezeTicker = useCallback(() => { + setHeldTickerExample(activeExample); + setIsTickerHeld(true); + }, [activeExample]); + + const releaseTicker = useCallback(() => { + setIsTickerHeld(false); + }, []); + + const resolvedTickerExample = isTickerHeld ? (heldTickerExample ?? activeExample) : activeExample; if (!activeExample) return null; return ( -
- Smart search - - - Try “{activeExample}” in {modeLabel}. - -
+ <> +
+ Smart search + + + Try “{activeExample}” in {modeLabel}. + +
+ {showPhoneTicker ? ( + + ) : null} + ); } @@ -364,6 +430,7 @@ export function UniversalSearchCommandSurface({ onFocusSearchInput, onListboxIdReady, onActiveItemIdChange, + showPhoneSuggestionTicker = false, placement = "inline", children, }: { @@ -384,6 +451,8 @@ export function UniversalSearchCommandSurface({ onFocusSearchInput?: () => void; onListboxIdReady?: (listboxId: string) => void; onActiveItemIdChange?: (activeItemId: string | null) => void; + /** Show the compact, tappable suggestion ticker below an in-flow phone home composer. */ + showPhoneSuggestionTicker?: boolean; placement?: CommandSurfacePlacement; children: ReactNode; }) { @@ -992,7 +1061,15 @@ export function UniversalSearchCommandSurface({ placement === "bottom-dock" ? "gap-1" : "gap-2", )} > - + { + onQueryChange(example); + onFocusSearchInput?.(); + }} + />
{ diff --git a/tests/ui-overlap.spec.ts b/tests/ui-overlap.spec.ts index 79923b3f6a..f04fb4bb7c 100644 --- a/tests/ui-overlap.spec.ts +++ b/tests/ui-overlap.spec.ts @@ -266,12 +266,43 @@ test.describe("Header element overlap coverage", () => { ); }); - test("phone smart search does not show the desktop rotating text or prompt row", async ({ page }) => { + test("phone smart search replaces desktop rows with one tappable ticker", async ({ page }) => { await page.setViewportSize({ width: 390, height: 820 }); await mockDemoDashboard(page); await gotoHome(page); await expect(page.getByTestId("smart-search-rotating-text")).toBeHidden(); await expect(page.getByTestId("smart-search-prompt-row")).toBeHidden(); + + const ticker = page.getByTestId("smart-search-phone-ticker"); + await expect(ticker).toBeVisible(); + await expect(ticker).toContainText("Try this"); + await expect(ticker).toContainText("Tap to search"); + + const tickerBox = await ticker.boundingBox(); + expect(tickerBox, "phone suggestion ticker must render").not.toBeNull(); + expect(tickerBox!.height, "phone ticker must meet the tap-target floor").toBeGreaterThanOrEqual(48); + + const suggestion = (await ticker.getAttribute("aria-label"))?.replace("Try suggested search: ", ""); + expect(suggestion).toBeTruthy(); + await ticker.click(); + await expect(page.locator('[data-testid="global-search-input"]:visible').first()).toHaveValue(suggestion ?? ""); + }); + + test("phone suggestion ticker renders on /documents mode home", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 820 }); + await mockDemoDashboard(page); + await page.goto("/documents", { waitUntil: "domcontentloaded" }); + await expect(async () => { + const header = page.locator("header#search"); + await expect(header).toHaveCount(1); + await expect(header).toBeVisible(); + }).toPass({ timeout: 30_000 }); + + const ticker = page.getByTestId("smart-search-phone-ticker"); + await expect(ticker).toBeVisible(); + const tickerBox = await ticker.boundingBox(); + expect(tickerBox, "phone suggestion ticker must render on /documents home").not.toBeNull(); + expect(tickerBox!.height, "phone ticker must meet the tap-target floor on /documents").toBeGreaterThanOrEqual(48); }); });