import type { PropsLike } from "./merge-props-n"; /** * Cross-browser pointer-type a click originated from. * * Mirrors the string union in Base UI React's * `@base-ui/utils/useEnhancedClickHandler` — empty string means "unknown" * (no prior `pointerdown` was observed and the underlying `MouseEvent` * didn't surface a `pointerType`). */ export type InteractionType = "mouse" | "touch" | "pen" | "keyboard" | ""; /** * Accessor-or-value. Every gate on the handler may be supplied as a plain * value or a Solid accessor so consumers can thread reactive inputs without * materializing a memo. */ type MaybeAccessor = T | (() => T); function read(value: MaybeAccessor | undefined): T | undefined { return typeof value === "function" ? (value as () => T)() : value; } export interface UseEnhancedClickHandlerOptions { /** * When `true` the produced handlers short-circuit: neither the pointer * snapshot nor the consumer callback runs. Defaults to `false`. */ disabled?: MaybeAccessor | undefined; } /** * Minimal shape we need off incoming click / pointerdown events. Both Solid's * JSX events and the raw DOM events satisfy this. */ interface ClickLike { defaultPrevented?: boolean; /** `detail === 0` on a click event indicates keyboard activation. */ detail?: number; /** Present on Chrome/Edge `PointerEvent`s that bubble up as click. */ pointerType?: string; } interface PointerDownLike { defaultPrevented?: boolean; pointerType?: string; } /** * Port of Base UI React's `useEnhancedClickHandler`. Returns a pair of * handlers (`onClick`, `onPointerDown`) that cooperate to derive the * interaction type of a click: * * - Chromium reports `pointerType` directly on the click event — we use it. * - WebKit / Gecko emit a plain `MouseEvent` on click; we fall back to the * pointer type captured on the most recent `pointerdown`. * - Keyboard activation is detected via `event.detail === 0`. * * The returned object is shaped so consumers can hand it straight to * `mergePropsN` without further massaging. Solid 2 notes: * * - Handlers are created once per consumer; they read `options.disabled` * at call time, so reactive accessors are picked up without recreating * the primitive. * - Unlike the React version, the consumer handler is NOT memoized — Solid * components don't re-run, so there is no identity churn to guard * against. Callers are responsible for wrapping a changing consumer * callback in `createStableCallback` if they need a stable identity. */ export function useEnhancedClickHandler( handler: ( event: MouseEvent | PointerEvent, interactionType: InteractionType, ) => void, options: UseEnhancedClickHandlerOptions = {}, ): PropsLike { // Closure-captured latch for the most recent pointerdown pointerType. // Cleared after each click is observed so a stale value from a previous // interaction can't leak into a keyboard-activated click. let lastClickInteractionType: InteractionType = ""; const isDisabled = (): boolean => read(options.disabled) === true; function onPointerDown(event: PointerEvent): void { if (event.defaultPrevented) return; if (isDisabled()) return; const pointerType = (event as PointerDownLike).pointerType as | InteractionType | undefined; lastClickInteractionType = pointerType ?? ""; handler(event, lastClickInteractionType); } function onClick(event: MouseEvent | PointerEvent): void { if (isDisabled()) return; const ev = event as ClickLike; // `detail === 0` is the portable signal for "this click was // synthesized from a keyboard activation" (Enter/Space on a // focused button). It's set by the browser, not us. if (ev.detail === 0) { handler(event, "keyboard"); lastClickInteractionType = ""; return; } if (typeof ev.pointerType === "string") { handler(event, ev.pointerType as InteractionType); } else { handler(event, lastClickInteractionType); } lastClickInteractionType = ""; } // Plain object: consumers splat this into a merge array. Writing the // keys directly keeps the resulting identity inspectable at consumer // test boundaries — `mergePropsN` chains `on*` handlers regardless. return { onClick, onPointerDown } as PropsLike; }