/** * Stage 3 port of Base UI React's `useFloatingRootContext` hook. * * React source: * `base-ui/packages/react/src/floating-ui-react/hooks/useFloatingRootContext.ts` * `base-ui/packages/react/src/floating-ui-react/components/FloatingRootStore.ts` * * Stage 2 landed an inline `Stage2RootStore` shim that implemented the * minimal subset of the React store contract that `useFloating` reads. * Stage 3 replaces that shim with the real `FloatingRootStore` * (see `components/floating-root-store.ts`). The public return shape of * this hook is unchanged — callers keep reading `store.useState('...')`, * `store.sync('...')`, `store.setOpen(...)`, and * `store.context.{events,dataRef,triggerElements,nested}`. * * ### Behavior deltas vs the Stage-2 shim * * - `setOpen` now emits `openchange` on the internal events bus and * performs the hover-vs-click `syncOpenEvent` bookkeeping before * forwarding to `onOpenChange`. The shim skipped both. * - `nested` is now seeded from `useFloatingParentNodeId()` (returns * `null` when no `FloatingTree` is mounted above — which is the same * default the shim hard-coded). * - `useState('referenceElement')` prefers `positionReference` over the * raw `referenceElement` slot to match React's selector. In practice * `useFloating` already threads `positionReference` through its own * state signal, so call sites see the same values either way. */ import { isElement } from "@floating-ui/utils/dom"; import { useBaseUiId } from "../../base-ui-id"; import type { BaseUIChangeEventDetails } from "../../change-event-details"; import { FloatingRootStore } from "../components/floating-root-store"; import { useFloatingParentNodeId } from "../components/floating-tree"; import { PopupTriggerMap } from "../popup-trigger-map"; import type { ReferenceType } from "../types"; export interface UseFloatingRootContextOptions { open?: boolean | undefined; onOpenChange?( open: boolean, eventDetails: BaseUIChangeEventDetails, ): void; elements?: | { reference?: ReferenceType | null | undefined; floating?: HTMLElement | null | undefined; } | undefined; } /** * Creates a minimal root context for a floating element. * * Consumers pass the current `open` state, an `onOpenChange` callback, and * optionally pre-registered `elements`. The returned store exposes a * reactive view of those values plus context (dataRef / events / nested / * triggerElements). */ export function useFloatingRootContext( options: UseFloatingRootContextOptions, ): FloatingRootStore { const { open = false, onOpenChange, elements } = options; const floatingId = useBaseUiId(); const nested = useFloatingParentNodeId() != null; if (process.env.NODE_ENV !== "production") { const optionDomReference = elements?.reference; if (optionDomReference && !isElement(optionDomReference)) { console.error( "Cannot pass a virtual element to the `elements.reference` option,", "as it must be a real DOM element. Use `context.setPositionReference()`", "instead.", ); } } const store = new FloatingRootStore({ open, transitionStatus: undefined, onOpenChange, referenceElement: elements?.reference ?? null, floatingElement: elements?.floating ?? null, triggerElements: new PopupTriggerMap(), floatingId, syncOnly: false, nested, }); // React's hook runs a `useIsoLayoutEffect` on [open, floatingId, // elements.reference, elements.floating, store] to sync partial state // into the store. In Solid, component bodies don't re-run — call // sites that want reactivity wrap this hook's inputs in reactive // accessors themselves. The hook therefore performs a one-shot // initialization and exposes `store.update(...)` for callers that // need to sync on their own cadence. // // `useFloating` (the sibling Stage 2 hook) consumes the store's // `useSyncedValue` methods from inside a `createEffect`, so the // reactive path is preserved for its caller. store.context.onOpenChange = onOpenChange; store.context.nested = nested; return store; }