import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, createMemo, useContext } from "solid-js"; import { createPopupCore } from "../internals/create-popup-core"; import { type MaybeAccessor, readOption } from "../internals/maybe-accessor"; import { SNAPSHOT_KEYS, type Snapshottable, snapshotKeys, } from "../internals/observability/snapshot"; /** * Why the tooltip's open state changed. * * - `"trigger-hover"` — pointer entered / left the trigger. * - `"trigger-focus"` — keyboard focus moved to / from the trigger. * - `"trigger-press"` — trigger was pressed (click / Enter / Space). * - `"escape-key"` — Escape dismissed the tooltip. * - `"outside-press"` — pointer pressed outside. * - `"imperative"` — `t.setOpen()` called programmatically. */ export type TooltipChangeReason = | "trigger-hover" | "trigger-focus" | "trigger-press" | "escape-key" | "outside-press" | "imperative"; /** * Options for {@link createTooltip}. */ export interface CreateTooltipOptions { /** * Open state. Consumer-owned signal — the primitive reads through the * accessor and does NOT keep an internal copy. */ open: MaybeAccessor; /** * Whether interaction is suppressed. * @default false */ disabled?: MaybeAccessor; /** * Pre-change callback. Fired via `runWithOwner(null, ...)` to escape the * reactive owner. */ onOpenChange?: ( open: boolean, event: Event, reason: TooltipChangeReason, ) => void; /** * Axis on which to track the cursor position. `"none"` anchors to the * trigger's bounding box. * @default "none" */ trackCursorAxis?: MaybeAccessor<"none" | "x" | "y" | "both">; /** * When true, the hover-intent `safePolygon` is NOT installed so any * cursor movement off the trigger closes the tooltip immediately. * @default false */ disableHoverablePopup?: MaybeAccessor; /** * Delay before opening (ms). * @default 600 */ delay?: MaybeAccessor; /** * Delay before closing on hover-out (ms). * @default 300 */ closeDelay?: MaybeAccessor; } /** * Public API returned by {@link createTooltip}. */ export interface TooltipApi { /** Reactive accessor for the current open state. */ open: Accessor; /** Whether interaction is suppressed. */ disabled: Accessor; /** Axis on which cursor tracking is applied. */ trackCursorAxis: Accessor<"none" | "x" | "y" | "both">; /** Whether the hoverable-popup safe-polygon is disabled. */ disableHoverablePopup: Accessor; /** Open delay in ms. */ delay: Accessor; /** Close delay in ms. */ closeDelay: Accessor; /** Imperative open setter. */ setOpen: (next: boolean, event?: Event, reason?: TooltipChangeReason) => void; /** Flip open (no-op when disabled). */ toggle: (event?: Event, reason?: TooltipChangeReason) => void; /** Context provider. */ Provider: (props: { children: JSX.Element }) => JSX.Element; } const TooltipContext = createContext(null); /** * Read the nearest {@link TooltipApi} provided by `t.Provider`. Throws if * used outside a Provider unless `optional` is true. */ export function useTooltip(): TooltipApi; export function useTooltip(optional: true): TooltipApi | null; export function useTooltip(optional?: boolean): TooltipApi | null { const ctx = useContext(TooltipContext); if (ctx == null && !optional) { throw new Error( "Base UI: useTooltip() must be called inside a .", ); } return ctx; } /** * Headless primitive for a tooltip compound. Owns logical state — open, * disabled, cursor-tracking knobs, hover delays. DOM machinery (floating-ui * positioning, portal, animation) lives in `` which composes * this primitive. */ export function createTooltip(options: CreateTooltipOptions): TooltipApi { const core = createPopupCore({ open: options.open, disabled: options.disabled, onOpenChange: options.onOpenChange, }); const trackCursorAxis = createMemo<"none" | "x" | "y" | "both">( () => (readOption(options.trackCursorAxis) ?? "none") as | "none" | "x" | "y" | "both", { name: "tooltip.trackCursorAxis" }, ); const disableHoverablePopup = createMemo( () => Boolean(readOption(options.disableHoverablePopup)), { name: "tooltip.disableHoverablePopup" }, ); const delay = createMemo(() => readOption(options.delay) ?? 600, { name: "tooltip.delay", }); const closeDelay = createMemo( () => readOption(options.closeDelay) ?? 300, { name: "tooltip.closeDelay" }, ); const api: TooltipApi = { open: core.open, disabled: core.disabled, trackCursorAxis, disableHoverablePopup, delay, closeDelay, setOpen: core.setOpen, toggle: core.toggle, Provider: (props) => ( {props.children} ), }; (api as TooltipApi & Snapshottable)[SNAPSHOT_KEYS] = TOOLTIP_SNAPSHOT_KEYS; return api; } const TOOLTIP_SNAPSHOT_KEYS = snapshotKeys( "open", "disabled", "trackCursorAxis", "disableHoverablePopup", "delay", "closeDelay", );