import type { JSX } from "@solidjs/web"; /** * Port of Base UI React's `usePositioner` * (`packages/react/src/utils/usePositioner.tsx`). * * React's version calls `useRenderElement` internally and returns a JSX * element; the Solid port is more idiomatic as a helper that returns a * composable prop bag which consumers feed into `renderElement` themselves. * This keeps the compound positioners (popover, tooltip, menu, select, …) * in control of their context providers, `` gating, and portal * placement, without having to unwrap anything from this hook. * * ## React → Solid deviations * * - Returns a `UsePositionerReturn` object of accessors instead of a JSX * element. Consumers spread `.props` into their `renderElement` call and * apply `.style` reactively via a getter. * - The `inert` / `hidden` flags collapse into the returned style / * HTML-props bag, matching how the React source sets them. * - `transitionStatus` is accepted as an accessor; when it is `"starting"` * we emit `transition: none` to disable the initial mount transition * (mirrors React's `getDisabledMountTransitionStyles`). */ import type { Accessor } from "solid-js"; import type { TransitionStatus } from "../use-transition-status/use-transition-status"; export interface UsePositionerOptions { /** * Reactive accessor for the positioner's base style. Typically sourced * from `useAnchorPositioning(...).positionerStyles`. */ styles: Accessor; /** * Current transition status. Used to toggle off transitions during the * initial mount frame so enter animations don't play on first paint. */ transitionStatus: Accessor; /** * Whether the positioner DOM node should render with the `hidden` * attribute. Set to `true` while the popup is closed but `keepMounted` * keeps it in the tree. */ hidden?: Accessor | boolean | undefined; /** * When true, disables pointer events on the positioner. Used by modal * popups while the backdrop animates out. */ inert?: Accessor | boolean | undefined; } export interface UsePositionerReturn { /** * Resolved style to apply to the positioner element. Always returns a * fresh object so Solid's JSX binding picks up property changes. */ readonly style: JSX.CSSProperties; /** * Resolved `hidden` flag. Feed directly into `hidden={...}`. */ readonly hidden: boolean | undefined; /** * Standard HTML props bag for the positioner outer element: * `{ role: 'presentation', hidden, style }`. Consumers pass this into * their `renderElement(...)` call's `props` array. */ readonly props: JSX.HTMLAttributes; } function resolve(input: T | Accessor | undefined): T | undefined { if (input === undefined) return undefined; return typeof input === "function" ? (input as Accessor)() : input; } /** * Produce the shared prop bag + style for a popup positioner element. This * hook owns the common role/hidden/style/inert-aware shape that every Base * UI popup positioner renders; consumers layer their component-specific * props on top via `renderElement`'s `props` array. * * Safe to call outside of a tracking context — the returned object exposes * getters, so reactive reads only happen at access time (e.g. inside a * `renderElement` props-array entry). */ export function usePositioner( options: UsePositionerOptions, ): UsePositionerReturn { const getStyle = (): JSX.CSSProperties => { const base: JSX.CSSProperties = { ...options.styles() }; if (resolve(options.inert)) { (base as JSX.CSSProperties & { pointerEvents?: string }).pointerEvents = "none"; } if (options.transitionStatus() === "starting") { // Disables the initial mount transition to avoid a visual "pop" when // the element paints for the first time. Matches React's // `getDisabledMountTransitionStyles`. (base as JSX.CSSProperties & { transition?: string }).transition = "none"; } return base; }; const getHidden = (): boolean | undefined => resolve(options.hidden) || undefined; return { get style() { return getStyle(); }, get hidden() { return getHidden(); }, get props() { return { role: "presentation", hidden: getHidden(), get style(): JSX.CSSProperties { return getStyle(); }, } as JSX.HTMLAttributes; }, }; }