diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 49ba42517..e5b3e2118 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -5546,6 +5546,9 @@ export function ClinicalDashboard({ mobileSearchPlacement={hasMobileBottomSearch ? "bottom" : "default"} desktopHomeComposerSlotId={desktopHomeComposerSlotId} heroComposerFromTablet={Boolean(desktopHomeComposerSlotId)} + // Phone-only: the header sits above the internally scrolling
, + // so hiding must collapse its layout space to hand it to content. + hideOnScroll={{ strategy: "collapse", containerRef: mainRef }} />
-
+ {/* max-sm:contents lets the header's own `sticky top-0` engage against + the document scroll on phones (a plain wrapper div otherwise caps + its sticking range at its own height), which the phone + hide-on-scroll overlay relies on. */} +
diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 99cbde6b4..4041e4254 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -7,8 +7,10 @@ import { useMemo, useRef, useState, + type FocusEvent as ReactFocusEvent, type KeyboardEvent as ReactKeyboardEvent, type Ref, + type RefObject, } from "react"; import { createPortal } from "react-dom"; @@ -45,6 +47,7 @@ import { import { DocumentTagCloud } from "@/components/DocumentTagCloud"; import { useDismissableLayer } from "@/components/use-dismissable-layer"; +import { useHideOnScroll } from "@/components/clinical-dashboard/use-hide-on-scroll"; import { ModeActionPopup, modeActionItemsFor, @@ -208,6 +211,7 @@ export function MasterSearchHeader({ heroComposerFromTablet = false, mobileLeadingAction = "menu", onMobileBack, + hideOnScroll, }: { documents: ClinicalDocument[]; documentTotal?: number; @@ -247,6 +251,12 @@ export function MasterSearchHeader({ heroComposerFromTablet?: boolean; mobileLeadingAction?: "menu" | "back"; onMobileBack?: () => void; + /** Phone-only hide-on-scroll for the universal header. "overlay" translates + * the sticky header away (host scrolls the document, content already flows + * beneath); "collapse" also releases the header's layout space (host keeps + * the header above an internally scrolling element). `containerRef` points + * at the scrolling element; omit it to observe window scroll. */ + hideOnScroll?: { strategy: "overlay" | "collapse"; containerRef?: RefObject }; }) { const visibleAppModeOptions = defaultVisibleAppModeOptions; const trimmedQuery = query.trim(); @@ -279,6 +289,16 @@ export function MasterSearchHeader({ const [usesScopeSheet, setUsesScopeSheet] = useState(false); const [usesPhoneSearchLayout, setUsesPhoneSearchLayout] = useState(false); const [desktopHomeComposerActive, setDesktopHomeComposerActive] = useState(false); + // Phone-only hide-on-scroll: never hide while a header-owned surface is open + // or while focus sits inside the header chrome (keyboard users must not tab + // into invisible controls). + const [headerChromeFocused, setHeaderChromeFocused] = useState(false); + const scrollHidden = useHideOnScroll({ + containerRef: hideOnScroll?.containerRef, + disabled: !hideOnScroll, + }); + const headerChromeHidden = + scrollHidden && !modeMenuOpen && !actionMenuOpen && !scopeOpen && !scopeSheetOpen && !headerChromeFocused; // Stable, header-owned element the composer is portaled into; we move it in and // out of the page-owned slot rather than portaling into the slot directly. const [desktopHomeComposerHost, setDesktopHomeComposerHost] = useState(null); @@ -1361,13 +1381,32 @@ export function MasterSearchHeader({ ); } - return ( + const hideStrategy = hideOnScroll?.strategy; + const chromeFocusProps = hideOnScroll + ? { + onFocusCapture: () => setHeaderChromeFocused(true), + onBlurCapture: (event: ReactFocusEvent) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setHeaderChromeFocused(false); + }, + } + : undefined; + + const headerAndComposer = ( <>