/** * Stage 4.4 port of Base UI React's `useHoverShared` helpers. * * React source: * `base-ui/packages/react/src/floating-ui-react/hooks/useHoverShared.ts` * * Central home for: * * - `HandleClose` / `HandleCloseContext` / `HandleCloseOptions` types * consumed by `safePolygon` and the hover cluster. These originally * lived in a local placeholder inside `safe-polygon.ts` (Stage 1 * deviation, see `remaining-work.md` note) and are now re-homed here. * - `getDelay` / `getRestMs` scalar resolvers (accept either a raw * value or a getter function). * - `isClickLikeOpenEvent` — shared hover-vs-click disambiguation. * - `SafePolygonOptions` re-export for consumers that reference the * option bag name alone. * * The types stay framework-agnostic so `safe-polygon.ts` (which runs * pure geometry) can consume them without pulling in Solid runtime * code. `safe-polygon.ts` imports the types from this module — hover * hook files also import runtime helpers from here, which is why the * dependency direction is `safe-polygon → use-hover-shared` (types * only) and `use-hover-* → use-hover-shared` + `use-hover-* → * safe-polygon` (runtime + types). That avoids the circular reference * Stage 1 worked around with placeholder declarations. */ import type { ExtendedElements, FloatingTreeType, Placement } from "../types"; import { isMouseLikePointerType } from "../utils/event"; // --------------------------------------------------------------------------- // Types — moved out of `safe-polygon.ts` to resolve the Stage-1 deferral. // --------------------------------------------------------------------------- export interface HandleCloseOptions { blockPointerEvents?: boolean | undefined; getScope?: (() => HTMLElement | SVGSVGElement | null) | undefined; } export interface HandleCloseContext { x: number | null; y: number | null; placement: Placement | null; elements: Pick; onClose: () => void; nodeId?: string | undefined; tree?: FloatingTreeType | null | undefined; leave?: boolean | undefined; } export type HandleCloseContextBase = Omit< HandleCloseContext, "onClose" | "tree" | "x" | "y" >; export interface HandleClose { (context: HandleCloseContext): (event: MouseEvent) => void; __options?: HandleCloseOptions | undefined; } /** * Option bag accepted by the `safePolygon` factory. Matches * `HandleCloseOptions` shape 1:1 — the alias exists purely so * call-sites that reference `SafePolygonOptions` by name in the React * source keep working. */ export type SafePolygonOptions = HandleCloseOptions; // --------------------------------------------------------------------------- // Scalar resolvers // --------------------------------------------------------------------------- type HoverDelay = number | Partial<{ open: number; close: number }>; function resolveValue( value: T | (() => T) | undefined, pointerType?: PointerEvent["pointerType"], ): T | 0 | undefined { if (pointerType != null && !isMouseLikePointerType(pointerType)) { return 0; } if (typeof value === "function") { return (value as () => T)(); } return value; } export function getDelay( value: HoverDelay | (() => HoverDelay) | undefined, prop: "open" | "close", pointerType?: PointerEvent["pointerType"], ): number | undefined { const result = resolveValue(value, pointerType); if (typeof result === "number") { return result; } return result?.[prop]; } export function getRestMs(value: number | (() => number)): number { if (typeof value === "function") { return value(); } return value; } /** * `true` when the current open event was a click-style interaction (or when * a `pointerdown` inside the floating element promoted the surface to * "click-owned" via `interactedInside`). */ export function isClickLikeOpenEvent( openEventType: string | undefined, interactedInside: boolean, ): boolean { return ( interactedInside || openEventType === "click" || openEventType === "mousedown" ); }