/** * Stage 4.1 port of Base UI React's `useInteractions` hook. * * React source: * `base-ui/packages/react/src/floating-ui-react/hooks/useInteractions.ts`. * * Merges the prop bags produced by individual interaction hooks (`useHover`, * `useFocus`, `useClick`, `useDismiss`, `useListNavigation`, `useRole`, ...) * into three prop-getters (`getReferenceProps`, `getFloatingProps`, * `getItemProps`) plus a fourth `getTriggerProps` that Base UI's fork adds * for separate trigger/reference handling. * * ### React → Solid deviations * * 1. React memoises each getter with `useCallback` keyed on per-element * `propsList` slices so the identity is stable across renders. Solid * components don't re-run their bodies, so the returned getters are * plain functions — identity is stable for the lifetime of the call * site already. * * 2. React's hand-rolled `mergeProps` loop duplicates the merge semantics * that `mergePropsN` (`src/internals/merge-props-n.ts`) already * implements for the rest of the Solid port: event handlers chain * instead of overwriting, `class` / `style` compose, right-most * non-`undefined` wins for scalars. The port delegates to * `mergePropsN` instead of reimplementing. One consequence: the * `preventBaseUIHandler()` chain-short-circuit that `mergePropsN` * arms on the event is automatically honored here too (React's * source has no equivalent — it returns the first non-undefined * result from the handler array, which is a different contract). * * 3. React's `for…in` enumeration of source objects in `mergeProps` * walks prototype-inherited keys; Solid's `mergePropsN` walks * `Object.keys` only. Hook-produced bags are plain objects in both * ports, so the distinction doesn't matter in practice. * * 4. React uses `React.HTMLProps` for the user-prop parameter * type. The Solid port accepts `ElementPropsBag` from `../types`, * which is `JSX.HTMLAttributes & Record` — * the same loose bag already used for inter-hook prop passing. */ import { mergePropsN, type PropsLike } from "../../merge-props-n"; import type { ElementProps, ElementPropsBag, ExtendedUserProps, } from "../types"; import { ACTIVE_KEY, FOCUSABLE_ATTRIBUTE, SELECTED_KEY, } from "../utils/constants"; export type { ExtendedUserProps }; export interface UseInteractionsReturn { getReferenceProps: (userProps?: ElementPropsBag) => PropsLike; getFloatingProps: (userProps?: ElementPropsBag) => PropsLike; getItemProps: ( userProps?: Omit & ExtendedUserProps, ) => PropsLike; getTriggerProps: (userProps?: ElementPropsBag) => PropsLike; } type InteractionEntry = ElementProps | false | null | undefined; /** * Strip the `item`-scope-only `active` / `selected` keys from a prop bag — * they are used to parameterise item getter callbacks, not to be forwarded * as DOM attributes. */ function stripItemScopeKeys( source: PropsLike | undefined, ): PropsLike | undefined { if (!source) return source; if (!(ACTIVE_KEY in source) && !(SELECTED_KEY in source)) return source; const out: Record = {}; for (const key of Object.keys(source)) { if (key === ACTIVE_KEY || key === SELECTED_KEY) continue; out[key] = (source as Record)[key]; } return out; } /** * Resolve the per-element prop bag out of an interaction-hook return. For * `item`-scope, the bag can be a function that receives the user props * (including `active` / `selected`) and returns the concrete DOM props; * React's source invokes it lazily inside `mergeProps`. We do the same. */ function resolvePropsFor( entry: InteractionEntry, elementKey: Key, userProps: ElementPropsBag | undefined, ): PropsLike | undefined { if (!entry) return undefined; const propsOrGetter = entry[elementKey]; if (propsOrGetter == null) return undefined; if (typeof propsOrGetter === "function") { if (!userProps) return undefined; const resolved = propsOrGetter(userProps as ExtendedUserProps); return resolved as PropsLike | undefined; } return propsOrGetter as PropsLike; } /** * Merges an array of interaction-hook prop bags into the four prop-getters * consumed by Base UI's floating surfaces. Each getter runs `mergePropsN` * at call time, so event-handler chaining and `preventBaseUIHandler` work * consistently with the rest of the Solid internals. */ export function useInteractions( propsList: Array = [], ): UseInteractionsReturn { function mergeFor( elementKey: Key, userProps: ElementPropsBag | undefined, seed?: PropsLike, ): PropsLike { const isItem = elementKey === "item"; const sources: Array = []; if (seed) sources.push(seed); for (let i = 0; i < propsList.length; i += 1) { const resolved = resolvePropsFor(propsList[i], elementKey, userProps); sources.push(isItem ? stripItemScopeKeys(resolved) : resolved); } sources.push(isItem ? stripItemScopeKeys(userProps) : userProps); return mergePropsN(sources); } function getReferenceProps(userProps?: ElementPropsBag): PropsLike { return mergeFor("reference", userProps); } function getFloatingProps(userProps?: ElementPropsBag): PropsLike { // `tabIndex: -1` + `data-quoin-focusable` seed the floating element // as focusable-but-not-tabbable; later sources may override either. const seed: PropsLike = { tabIndex: -1, [FOCUSABLE_ATTRIBUTE]: "", }; return mergeFor("floating", userProps, seed); } function getItemProps( userProps?: Omit & ExtendedUserProps, ): PropsLike { return mergeFor("item", userProps as ElementPropsBag | undefined); } function getTriggerProps(userProps?: ElementPropsBag): PropsLike { return mergeFor("trigger", userProps); } return { getReferenceProps, getFloatingProps, getItemProps, getTriggerProps, }; }