import type { JSX } from "@solidjs/web"; /** * Stage 3 port of Base UI React's `FloatingTree` * (`packages/react/src/floating-ui-react/components/FloatingTree.tsx`). * * Supplies two contexts used by nested floating surfaces that are not DOM * descendants of their parent: * * - `FloatingNodeContext` — the parent node for the subtree (id + parentId). * - `FloatingTreeContext` — the shared `FloatingTreeStore` that tracks * every node in the subtree. * * Consumers use these to look up siblings / parents for dismiss bubbling, * virtual focus navigation between nested menus, and cross-tree events. * * Port notes: * - React's `useRefWithInit(() => externalTree ?? new FloatingTreeStore())` * pattern becomes a plain singleton assignment during setup (Solid * components only "render" once, so `new FloatingTreeStore()` in the * body runs exactly once per `FloatingTree` mount). * - `useFloatingNodeId` registers the node via `useIsoLayoutEffect` in * React; the Solid port uses `useIsoLayoutEffect` (from * `src/internals/use-iso-layout-effect.ts`, which aliases * `createRenderEffect`). Cleanup runs via `onCleanup` inside the effect * body. * - `FloatingNode` creates a per-render memo for its context value in * React; Solid provides the value via a plain object since the * component body doesn't re-run. */ import { createContext, createUniqueId, useContext } from "solid-js"; import { useIsoLayoutEffect } from "../../use-iso-layout-effect"; import type { FloatingNodeType } from "../types"; import { FloatingTreeStore } from "./floating-tree-store"; const FloatingNodeContext = createContext(null); const FloatingTreeContext = createContext(null); /** * Returns the parent node id for nested floating elements, if available. * Returns `null` for top-level floating elements. */ export function useFloatingParentNodeId(): string | null { const ctx = useContext(FloatingNodeContext); return ctx?.id ?? null; } /** * Returns the nearest floating tree context, if available. */ export function useFloatingTree( externalTree?: FloatingTreeStore, ): FloatingTreeStore | null { const contextTree = useContext(FloatingTreeContext); return externalTree ?? contextTree; } /** * Registers a node into the `FloatingTree`, returning its id. If no tree is * mounted above, the id is still returned but no registration happens. * * @see https://floating-ui.com/docs/FloatingTree */ export function useFloatingNodeId( externalTree?: FloatingTreeStore, ): string | undefined { const id = createUniqueId(); const tree = useFloatingTree(externalTree); const parentId = useFloatingParentNodeId(); useIsoLayoutEffect( () => undefined, () => { if (!id) { return; } const node: FloatingNodeType = { id, parentId }; tree?.addNode(node); return () => { tree?.removeNode(node); }; }, ); return id; } export interface FloatingNodeProps { children?: JSX.Element; id: string | undefined; } /** * Provides parent node context for nested floating elements. * @see https://floating-ui.com/docs/FloatingTree * @internal */ export function FloatingNode(props: FloatingNodeProps): JSX.Element { const parentId = useFloatingParentNodeId(); // Matches React's `useMemo(() => ({ id, parentId }), [id, parentId])` — // a getter object keeps the `id` field reactive if callers pass a // signal-driven `props.id`. const value: FloatingNodeType = { get id() { return props.id; }, parentId, }; return ( {props.children} ); } export interface FloatingTreeProps { children?: JSX.Element; externalTree?: FloatingTreeStore | undefined; } /** * Provides context for nested floating elements when they are not children * of each other on the DOM. * * Not necessary in all cases — only required when there must be explicit * communication between parent and child floating elements, including: * * - The `bubbles` option in `useDismiss()`. * - Nested virtual list navigation. * - Nested floating elements that each open on hover. * - Custom communication between parent and child floating elements. * * @see https://floating-ui.com/docs/FloatingTree * @internal */ export function FloatingTree(props: FloatingTreeProps): JSX.Element { // One-shot store instantiation — Solid component bodies only execute // once per mount so this matches React's `useRefWithInit` exactly. const tree = props.externalTree ?? new FloatingTreeStore(); return ( {props.children} ); }