46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useCallback, useState } from 'react';
|
||
|
|
|
||
|
|
export type PopoverPlacement = 'down' | 'up';
|
||
|
|
|
||
|
|
interface PopoverPlacementApi {
|
||
|
|
placement: PopoverPlacement;
|
||
|
|
/** Measure the trigger button and decide placement. Call this in
|
||
|
|
* the button's onClick before toggling the popover open so the
|
||
|
|
* placement is correct for the current scroll position. */
|
||
|
|
measure: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Decide whether a popover anchored to a button should render below
|
||
|
|
* the button (default) or above it. The "down" direction is the
|
||
|
|
* common case; we flip to "up" when the button is near the bottom of
|
||
|
|
* the viewport and the popover would otherwise extend below the
|
||
|
|
* visible area and get hidden behind the BottomNav (z-50).
|
||
|
|
*
|
||
|
|
* Reserves ~80px of headroom for the BottomNav (64px) + safe-area
|
||
|
|
* inset so the popover doesn't push right up against the chrome.
|
||
|
|
*
|
||
|
|
* The caller must still ensure the popover's parent stacking context
|
||
|
|
* sits above the BottomNav in the page context (z >= 50). On its own
|
||
|
|
* the popover can flip, but if its parent section is z-30 the popover
|
||
|
|
* still paints behind the BottomNav.
|
||
|
|
*/
|
||
|
|
export function usePopoverPlacement(
|
||
|
|
buttonRef: React.RefObject<HTMLElement | null>,
|
||
|
|
estimatedHeight: number = 240,
|
||
|
|
reservedForChrome: number = 80,
|
||
|
|
): PopoverPlacementApi {
|
||
|
|
const [placement, setPlacement] = useState<PopoverPlacement>('down');
|
||
|
|
|
||
|
|
const measure = useCallback(() => {
|
||
|
|
const btn = buttonRef.current;
|
||
|
|
if (!btn || typeof window === 'undefined') return;
|
||
|
|
const rect = btn.getBoundingClientRect();
|
||
|
|
const available = window.innerHeight - rect.bottom - reservedForChrome;
|
||
|
|
setPlacement(available < estimatedHeight ? 'up' : 'down');
|
||
|
|
}, [buttonRef, estimatedHeight, reservedForChrome]);
|
||
|
|
|
||
|
|
return { placement, measure };
|
||
|
|
}
|