diff --git a/src/app/globals.css b/src/app/globals.css index d0971df7b..20eb977e7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -568,6 +568,7 @@ summary::-webkit-details-marker { width: min(100%, clamp(19rem, 90vw, 52rem)); max-width: calc(100vw - 1rem - var(--safe-area-left) - var(--safe-area-right)); margin-inline: auto; + overflow: visible; } .universal-home-search-edge { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 14ae434f9..de0809e93 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3882,7 +3882,9 @@ export function ClinicalDashboard({ ? // On tall phones the centred home leans slightly toward the // bottom composer (matches the committed vertical-weighting // guard); short phones skip the bias so content still fits. - "grid w-full place-items-center max-sm:[@media(min-height:800px)]:pt-[5vh]" + // Mobile uses top alignment so the integrated action menu is + // not clipped by the dead space below vertically centred homes. + "grid w-full place-items-center max-sm:place-content-start max-sm:justify-items-center max-sm:pt-[clamp(0.75rem,3vh,2rem)] max-sm:[@media(min-height:800px)]:pt-[5vh]" : activeModeResultKind === "tools" || activeModeResultKind === "favourites" || activeModeResultKind === "differentials" diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 002dcf7db..aee03b76f 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -641,11 +641,20 @@ export function MasterSearchHeader({ } } + const restoreActionMenuFocusRef = useRef(false); const closeScope = useCallback((restoreFocus = false) => { + restoreActionMenuFocusRef.current = restoreFocus; setScopeOpen(false); - if (restoreFocus) actionMenuTriggerRef.current?.focus(); }, []); + useEffect(() => { + if (scopeOpen || !restoreActionMenuFocusRef.current) return; + restoreActionMenuFocusRef.current = false; + window.requestAnimationFrame(() => { + actionMenuTriggerRef.current?.focus({ preventScroll: true }); + }); + }, [scopeOpen]); + const closeScopeSheet = useCallback(() => { setScopeSheetOpen(false); window.requestAnimationFrame(() => actionMenuTriggerRef.current?.focus()); diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx index 6f9a2c5ad..5103a460f 100644 --- a/src/components/clinical-dashboard/mode-action-popup.tsx +++ b/src/components/clinical-dashboard/mode-action-popup.tsx @@ -370,7 +370,7 @@ export function ModeActionPopup({ const edgePadding = 12; const availableAbove = Math.max(0, rect.top - viewportTop - edgePadding); const availableBelow = Math.max(0, viewportBottom - rect.bottom - edgePadding); - const { minBodyHeight, minSurfaceHeight, headerHeight } = estimateIntegratedMenuHeights(items.length, integrated); + const { minSurfaceHeight, headerHeight } = estimateIntegratedMenuHeights(items.length, integrated); const detachedUpOffset = 16; const integratedDownOffset = integratedChipRow ? 58 : 14; const detachedDownOffset = integrated ? integratedDownOffset : 14; diff --git a/src/components/use-dismissable-layer.ts b/src/components/use-dismissable-layer.ts index 05285b417..783294083 100644 --- a/src/components/use-dismissable-layer.ts +++ b/src/components/use-dismissable-layer.ts @@ -38,7 +38,11 @@ export function useDismissableLayer({ if (event.key !== "Escape") return; event.preventDefault(); onDismiss("escape"); - window.requestAnimationFrame(() => restoreFocusRef?.current?.focus({ preventScroll: true })); + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + restoreFocusRef?.current?.focus({ preventScroll: true }); + }); + }); } document.addEventListener("pointerdown", handlePointerDown, true); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 49ada2c4a..9d9016b5d 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1004,7 +1004,7 @@ test.describe("Responsive layout guards", () => { }); } - test("prescribing mode home bottom-anchors its content on phones but centres on tablet", async ({ page }) => { + test("prescribing mode home top-aligns on phones but centres on tablet", async ({ page }) => { async function verticalWeighting(width: number) { // Tall viewport exaggerates the free space so the anchor is unambiguous. await page.setViewportSize({ width, height: 900 }); @@ -1019,10 +1019,11 @@ test.describe("Responsive layout guards", () => { }); } - // Phone (< sm): content is pushed toward the bottom, so the gap above exceeds the gap below. + // Phone (< sm): content is top-aligned so integrated action menus are not + // clipped by dead space below vertically centred homes. const phone = await verticalWeighting(375); expect(phone).not.toBeNull(); - expect(phone?.topGap ?? 0).toBeGreaterThan(phone?.bottomGap ?? 0); + expect(phone?.topGap ?? 0).toBeLessThan(phone?.bottomGap ?? 0); // Tablet hero-composer homes include the portaled search shell in the measured // block, so viewport gap balance is looser than phone bottom-anchoring.