import type { Placement, VirtualElement } from "@floating-ui/react"; import type { ReactElement, RefObject } from "react"; /** * The floating-ui block every anchored overlay — `Popover`, `Menu`, * `Listbox`, `Combobox` — wrote out for itself, consolidated once. * * `Popover` and `Menu` had the same twelve-line `useFloating` + * `useInteractions` + `useClonedChild` preamble with two constants * different (the offset and the role), and the fleet's four pickers * hand-rolled a fifth, sixth, seventh and eighth `computePosition` * that reimplemented `flip`/`shift` against `window.innerHeight`. * This is the one implementation they all take. * * ### The owner's "configurable, but those are the basics" * * Escape and outside-press both close, **on by default**, for every * anchored overlay — that is what an overlay is. A component switches * one off through `dismiss` (a `Combobox`'s Escape clears the query * before it closes; a menu opened from a context click wants no * outside-press race with the thing that spawned it), but it opts out * rather than opting in. * * ### `size`, for the pickers * * `matchTriggerWidth` and `maxHeightPx` are the two things * `PortalDropdown` did with a hand-measured `TriggerRect` and a * `window.innerHeight` clamp. The `size` middleware does both from the * platform: the panel takes the trigger's width, and it never grows * past the space actually left in the viewport. * * The computed width/height are written **straight onto the floating * element's style** rather than lifted into React state, which is * floating-ui's own documented shape and is deliberate twice over: a * `setState` inside `apply` re-runs the position, which re-runs * `apply`, which is a render loop; and because these properties never * enter the `floatingStyles` object React manages, a component * re-render (a keystroke in a combobox) does not wipe them — only an * `autoUpdate` tick recomputes them. * * ### Portal-agnostic * * This returns the pieces — `clonedTrigger`, `getFloatingProps`, * `setFloating`, `floatingStyles`, `context` — and renders nothing. * Whether the panel goes in a `FloatingPortal`, the top layer, or in * place is the component's call, which is what lets the same hook back * a portalled `Listbox` and (for now) a top-layer `Popover`. */ export type AnchoredOverlayRole = "dialog" | "listbox" | "menu"; /** * What a panel can hang off in anchor mode: a real element, or a * **virtual** one — an object that only knows its own rectangle. * * The virtual half is what a context menu needs. A menu opened by * a press-and-hold belongs at the finger, not at the corner of the * card underneath it, and floating-ui positions against a rect * without caring where the rect came from. `flip` and `shift` work * unchanged, which is the whole reason to express a point this way * rather than with `top`/`left` of our own. */ export type AnchoredOverlayAnchor = HTMLElement | VirtualElement; export type UseAnchoredOverlayOptions = { /** * Anchor the panel to a **pre-existing** element the consumer already * rendered, instead of cloning a `trigger`. For the attached-input * `Combobox`, whose reference is the consumer's own `` (both the * value and the query) — there is nothing to clone. Supply exactly one * of `anchorRef` / `trigger`; with `anchorRef`, `clonedTrigger` is * `null`. * * A **virtual** anchor is allowed here too, which is how a context * menu opens at the point that was pressed. */ anchorRef?: RefObject; /** * Both default **on** — Escape and outside-press are what makes an * overlay an overlay. Switch one off for the honest exception (a * combobox that swallows the first Escape to clear its query), never * to make a plain overlay sticky. Named for the flattened * floating-ui `useDismiss` options they carry. */ isEscapeDismissable?: boolean; /** * Clamp the panel to the space the viewport actually leaves below * (or above) the trigger, with no fixed cap. The panel owes itself * an `overflow`, or a clamped one simply hides its last rows. * * `maxHeightPx` implies this *and* caps it at a number — a `Listbox` * never wants to be 900px tall even on a monitor that would allow * it. A `Menu` wants the other half only: as tall as its items need, * and never off the bottom of the screen. */ isHeightClamped?: boolean; isOutsidePressDismissable?: boolean; /** Ports `PortalDropdown`'s anchor-width match, via `size`. */ isTriggerWidthMatched?: boolean; /** * Clamp the panel to the width the viewport actually leaves, with no * fixed cap — the inline twin of `isHeightClamped`, and `maxWidthPx` * without the number. * * A portalled panel is `position: fixed`, so its shrink-to-fit width * already stops at the *viewport*, not at the space `shift` left it * — which means a long label produces a panel exactly as wide as the * window, positioned 8px in, with 8px of itself off the right edge. * `availableWidth` is the shifted number, so this is what makes a * long label wrap instead. */ isWidthClamped?: boolean; isVisible: boolean; /** Clamps the panel to `min(this, available viewport space)`. */ maxHeightPx?: number; /** * Caps the panel's width at `min(this, available viewport space)`, * symmetric with `maxHeightPx`. Without it a picker grows to its * widest content — a long `Combobox` footer sentence or option label * drags the whole panel absurdly wide; the cap lets that content wrap * instead. Omit for width-follows-content (the default). */ maxWidthPx?: number; /** Popover 8, Menu/Listbox/Combobox 4. */ offsetValue?: number; onDismiss: () => void; placement?: Placement; role: AnchoredOverlayRole; /** * The control the panel hangs off. **Cloned, not wrapped.** Omit only * when `anchorRef` is supplied instead. */ trigger?: ReactElement; }; export declare const useAnchoredOverlay: ({ anchorRef, isEscapeDismissable, isHeightClamped, isOutsidePressDismissable, isTriggerWidthMatched, isVisible, isWidthClamped, maxHeightPx, maxWidthPx, offsetValue, onDismiss, placement, role, trigger, }: UseAnchoredOverlayOptions) => { clonedTrigger: ReactElement, string | import("react").JSXElementConstructor> | null; context: { placement: Placement; strategy: import("@floating-ui/react").Strategy; x: number; y: number; middlewareData: import("@floating-ui/react").MiddlewareData; isPositioned: boolean; update: () => void; floatingStyles: React.CSSProperties; open: boolean; onOpenChange: (open: boolean, event?: Event, reason?: import("@floating-ui/react").OpenChangeReason) => void; events: import("@floating-ui/react").FloatingEvents; dataRef: React.MutableRefObject; nodeId: string | undefined; floatingId: string | undefined; refs: import("@floating-ui/react").ExtendedRefs; elements: import("@floating-ui/react").ExtendedElements; }; floatingStyles: import("react").CSSProperties; getFloatingProps: (userProps?: React.HTMLProps) => Record; setFloating: ((node: HTMLElement | null) => void) & ((node: HTMLElement | null) => void); triggerId: string; }; //# sourceMappingURL=useAnchoredOverlay.d.ts.map