/** * Stage 4.4 port of Base UI React's `useHoverInteractionSharedState`. * * React source: * `base-ui/packages/react/src/floating-ui-react/hooks/useHoverInteractionSharedState.ts` * * Exposes the `HoverInteraction` class — a plain mutable bag of state * shared between `useHoverReferenceInteraction` (reference-side) and * `useHoverFloatingInteraction` (floating-side) so both can cooperate on * cursor-type tracking, timers, and safe-polygon bookkeeping. * * The instance is attached to `rootStore.context.dataRef.current` under a * `hoverInteractionState` key so independent hook instances on the same * floating root share the same mutable bag (matches React). * * ### React → Solid deviations * * 1. `useRefWithInit(HoverInteraction.create)` → plain variable access. * Solid component bodies run once per mount, so `new HoverInteraction()` * at setup time matches React's one-shot initialization. * 2. `useOnMount(dispose)` → `onCleanup(dispose)` registered during setup. * Same effect: timers get cleared when the owning scope disposes. * 3. No reactivity is involved — all fields are plain mutables. The class * shape, `dispose()` semantics, and `Timeout` usage are copied 1:1. */ import { onCleanup } from "solid-js"; import { Timeout } from "../../use-timeout"; import type { FloatingRootStore } from "../components/floating-root-store"; import type { ContextData } from "../types"; import { isInteractiveElement } from "../utils/element"; import type { SafePolygonOptions } from "./use-hover-shared"; export { isInteractiveElement }; /** * Shared mutable state bag for a single floating root's hover cluster. */ export class HoverInteraction { pointerType: string | undefined; interactedInside: boolean; handler: ((event: MouseEvent) => void) | undefined; blockMouseMove: boolean; performedPointerEventsMutation: boolean; pointerEventsScopeElement: HTMLElement | SVGSVGElement | null; pointerEventsReferenceElement: HTMLElement | SVGSVGElement | null; pointerEventsFloatingElement: HTMLElement | null; restTimeoutPending: boolean; openChangeTimeout: Timeout; restTimeout: Timeout; handleCloseOptions: SafePolygonOptions | undefined; constructor() { this.pointerType = undefined; this.interactedInside = false; this.handler = undefined; this.blockMouseMove = true; this.performedPointerEventsMutation = false; this.pointerEventsScopeElement = null; this.pointerEventsReferenceElement = null; this.pointerEventsFloatingElement = null; this.restTimeoutPending = false; this.openChangeTimeout = new Timeout(); this.restTimeout = new Timeout(); this.handleCloseOptions = undefined; } static create(): HoverInteraction { return new HoverInteraction(); } dispose = (): void => { this.openChangeTimeout.clear(); this.restTimeout.clear(); }; } type PointerEventsMutationState = Pick< HoverInteraction, | "performedPointerEventsMutation" | "pointerEventsScopeElement" | "pointerEventsReferenceElement" | "pointerEventsFloatingElement" >; /** * Track which `HoverInteraction` instance currently owns the pointer-events * mutation for a given scope element. Only the owner may clear the styles, * preventing sibling hover surfaces from stomping on each other. */ const pointerEventsMutationOwnerByScopeElement = new WeakMap< HTMLElement | SVGSVGElement, PointerEventsMutationState >(); export function clearSafePolygonPointerEventsMutation( instance: PointerEventsMutationState, ): void { if (!instance.performedPointerEventsMutation) { return; } const scopeElement = instance.pointerEventsScopeElement; if ( scopeElement && pointerEventsMutationOwnerByScopeElement.get(scopeElement) === instance ) { instance.pointerEventsScopeElement?.style.removeProperty("pointer-events"); instance.pointerEventsReferenceElement?.style.removeProperty( "pointer-events", ); instance.pointerEventsFloatingElement?.style.removeProperty( "pointer-events", ); pointerEventsMutationOwnerByScopeElement.delete(scopeElement); } instance.performedPointerEventsMutation = false; instance.pointerEventsScopeElement = null; instance.pointerEventsReferenceElement = null; instance.pointerEventsFloatingElement = null; } export function applySafePolygonPointerEventsMutation( instance: PointerEventsMutationState, options: { scopeElement: HTMLElement | SVGSVGElement; referenceElement: HTMLElement | SVGSVGElement; floatingElement: HTMLElement; }, ): void { const { scopeElement, referenceElement, floatingElement } = options; const existingOwner = pointerEventsMutationOwnerByScopeElement.get(scopeElement); if (existingOwner && existingOwner !== instance) { clearSafePolygonPointerEventsMutation(existingOwner); } clearSafePolygonPointerEventsMutation(instance); instance.performedPointerEventsMutation = true; instance.pointerEventsScopeElement = scopeElement; instance.pointerEventsReferenceElement = referenceElement; instance.pointerEventsFloatingElement = floatingElement; pointerEventsMutationOwnerByScopeElement.set(scopeElement, instance); scopeElement.style.pointerEvents = "none"; referenceElement.style.pointerEvents = "auto"; floatingElement.style.pointerEvents = "auto"; } type HoverContextData = ContextData & { hoverInteractionState?: HoverInteraction | undefined; }; /** * Returns the `HoverInteraction` bag for the given root store, creating * and caching it on the shared `dataRef` if needed. All hover hooks that * cooperate on the same root share a single instance. */ export function useHoverInteractionSharedState( store: FloatingRootStore, ): HoverInteraction { const data = store.context.dataRef.current as HoverContextData; if (!data.hoverInteractionState) { data.hoverInteractionState = HoverInteraction.create(); } const instance = data.hoverInteractionState; // React uses `useOnMount(disposeEffect)` — run `dispose()` on unmount. // In Solid, the owning scope's `onCleanup` is the direct analogue. onCleanup(() => { instance.dispose(); }); return instance; }