import type * as React from 'react'; import type { AutoFocusEvent } from '../utils/dismissal.js'; import type { PassThroughProps } from '../utils/passThrough.js'; import type { Align, PopperContentProps, Side } from '../utils/popper.js'; import type { PortalProps } from '../utils/portal.js'; import type { PrimitivePropsWithRef } from '../utils/primitive.js'; import type { FocusOutsideEvent, InteractOutsideEvent } from '../utils/useDismiss.js'; /** What the panel is: an operable `role="dialog"`, or an inert `role="tooltip"` bubble. */ export type PopoverRole = 'dialog' | 'tooltip'; /** Which gesture reveals the panel: a click on the trigger, or the pointer entering it. */ export type PopoverOpenOn = 'click' | 'hover'; /** The panel's `data-state`: a tooltip bubble reports whether it paid its open delay or skipped it. */ export type PopoverState = 'closed' | 'open' | 'delayed-open' | 'instant-open'; /** @internal Timing shared by every `role="tooltip"` popover under one {@link PopoverProvider}. */ export interface PopoverProviderContextValue { delayDuration: number; skipDelayDuration: number; closeDelay: number; disableHoverableContent: boolean; /** False during the skip window that follows a close, when a sibling bubble opens at once. */ isOpenDelayedRef: React.RefObject; onOpen: () => void; onClose: () => void; } /** Props for {@link PopoverProvider}, which coordinates hover-delay timing across the tooltips beneath it. */ export interface PopoverProviderProps { /** * The subtree whose `role="tooltip"` popovers share this timing. * @default undefined — renders nothing if omitted. */ children?: React.ReactNode | undefined; /** * Delay in milliseconds before a descendant tooltip opens on hover; each may override it with its own `openDelay`. * @default 700 */ delayDuration?: number | undefined; /** * Window in milliseconds after a tooltip closes during which sibling tooltips open instantly, so sweeping a row of triggers pays the delay once. * @default 300 */ skipDelayDuration?: number | undefined; /** * Delay in milliseconds before a descendant tooltip closes once the pointer has left its trigger; a hoverable bubble still applies the 150ms floor. * @default 0 */ closeDelay?: number | undefined; /** * Whether descendant bubbles ignore the pointer entirely, so moving off the trigger closes them; each may override it. * @default false */ disableHoverableContent?: boolean | undefined; } /** @internal Shared state distributed from {@link Popover} to its parts via context. Not part of the public API. */ export interface PopoverContextValue { triggerRef: React.RefObject; contentRef: React.RefObject; contentId: string; titleId: string; descriptionId: string; /** Whether a {@link PopoverTitle} is mounted, so Content can withhold a dangling `aria-labelledby`. */ hasTitle: boolean; setHasTitle: (present: boolean) => void; /** Whether a {@link PopoverDescription} is mounted, gating `aria-describedby` the same way. */ hasDescription: boolean; setHasDescription: (present: boolean) => void; open: boolean; onOpenChange: (open: boolean) => void; modal: PopoverModality; /** What the panel is; `tooltip` makes it an inert bubble the trigger points `aria-describedby` at. */ role: PopoverRole; /** Which gesture reveals the panel; `hover` arms the delay handlers below. */ openOn: PopoverOpenOn; /** The panel's `data-state` value, which a tooltip splits into delayed-open and instant-open. */ state: PopoverState; /** Whether the pointer may travel from the trigger onto the panel and keep it open. */ hoverable: boolean; /** Opens at once — focus of the trigger, and the pointer arriving on the panel itself. */ onOpen: () => void; /** Opens after the resolved open delay, unless a Provider's skip window waives it. */ onDelayedOpen: () => void; /** Closes at once — blur, click, Escape. */ onClose: () => void; /** Closes after the resolved close delay, floored while the panel is hoverable. */ onDelayedClose: () => void; /** Recomputes the panel's position immediately; a no-op while the panel is closed or unmounted. */ reposition: () => void; } /** * How much of the page a popover contains: `'none'` nothing, `'trap-focus'` focus and outside interaction, `'modal'` adds the scroll lock too. */ export type PopoverModality = 'none' | 'modal' | 'trap-focus'; /** Props for the {@link Popover} root; standard attributes ({@link PassThroughProps}) land on the derived `PopoverContent` panel. */ export interface PopoverProps extends PassThroughProps { /** * Plain body content, or a hand-composed `PopoverTrigger`/`PopoverPortal` tree that replaces the derived anatomy. * @default undefined */ children?: React.ReactNode | undefined; /** * The trigger element for the derived anatomy, decorated with the popover's open/close behaviour via `asChild`. * @default undefined — no derived trigger renders. */ trigger?: React.ReactElement | undefined; /** * The derived panel's title, rendered as a `PopoverTitle` when `children` is plain body content. * @default undefined — no derived title. */ title?: string | undefined; /** * The derived panel's description, rendered as a `PopoverDescription` when `children` is plain body content. * @default undefined — no derived description. */ description?: string | undefined; /** * Which side of the trigger the derived panel opens on, read only when `children` is plain body content. * @default 'bottom' */ side?: Side | undefined; /** * Alignment of the derived panel along `side`, read only when `children` is plain body content. * @default 'center' */ align?: Align | undefined; /** * Whether the derived panel renders a `PopoverArrow` pointing back at the trigger. * @default false */ showArrow?: boolean | undefined; /** * Whether the popover is open, for controlled usage. Pair with `onOpenChange`. * @default undefined — uncontrolled, tracks its own open state internally. */ open?: boolean | undefined; /** * Whether the popover is initially open, for uncontrolled usage. * @default false */ defaultOpen?: boolean | undefined; /** * Fires with the new open state whenever the popover opens or closes, controlled or not. * @default undefined — no-op if omitted. */ onOpenChange?: ((open: boolean) => void) | undefined; /** * Fires with the new open state once the change has finished on screen, after any enter/exit animation ends. * @default undefined — no-op if omitted. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * How much of the page the open popover takes over: see {@link PopoverModality}. Forced to `'none'` under `role="tooltip"`, which has no focus to contain. * @default 'none' */ modal?: PopoverModality | undefined; /** * Which gesture reveals the panel: `click` toggles it from the trigger, `hover` opens it on pointer enter after `openDelay` and closes it after `closeDelay`. * @default 'click' */ openOn?: PopoverOpenOn | undefined; /** * What the panel is: an operable `dialog`, or an inert `tooltip` bubble the trigger points `aria-describedby` at. * @default 'dialog' */ role?: PopoverRole | undefined; /** * Delay in milliseconds before a hover popover opens on pointer enter; under `role="tooltip"` an ancestor `PopoverProvider`'s `delayDuration` applies unless this is set. * @default 700 */ openDelay?: number | undefined; /** * Delay in milliseconds before a hover popover closes once the pointer has left trigger and panel; a hoverable panel floors it at 150ms. * @default 300 */ closeDelay?: number | undefined; /** * Whether the pointer moving from the trigger onto the panel closes it instead of keeping it open. * @default false */ disableHoverableContent?: boolean | undefined; /** * Whether the panel is suppressed: it never opens, never describes the trigger, and never fires `onOpenChange`. * @default false */ disabled?: boolean | undefined; /** * DOM node the derived Portal renders the overlay into, so it inherits that subtree's theme and stacking context (e.g. a `[data-skdx-theme]` wrapper). * @default undefined — `document.body`. */ portalContainer?: Element | DocumentFragment | null | undefined; } /** Props for {@link PopoverTrigger}, the button that opens the popover and anchors its position; use `asChild` with an `` for a hover popover's link trigger. */ export interface PopoverTriggerProps extends PrimitivePropsWithRef<'button'> { } /** Props for {@link PopoverPortal}, which teleports its children to `document.body`. */ export interface PopoverPortalProps extends PortalProps { /** * Render the portal even while the popover is closed, instead of unmounting it. * @default false */ forceMount?: boolean | undefined; } /** Props for {@link PopoverContent}, the floating panel, extending the shared popper positioning props. */ export interface PopoverContentProps extends PopperContentProps { /** * Keep this panel mounted even while the popover is closed. * @default false */ forceMount?: boolean | undefined; /** * Clearance in pixels between the arrow and each end of the panel's edge, keeping it off rounded corners. * @default 8 */ arrowPadding?: number | undefined; /** * Hide the panel (`visibility: hidden`) once its anchor scrolls fully out of view inside a clipping ancestor. * @default false */ hideWhenDetached?: boolean | undefined; /** * CSS selector, resolved inside `PopoverContent`, for the element to focus when a focus-containing popover opens. * @default undefined — the panel's first tabbable element, then the panel itself. */ initialFocus?: string | undefined; /** * CSS selector for the element to focus when the popover closes, resolved at the document level. * @default undefined — the trigger, or whatever was focused before opening. */ finalFocus?: string | undefined; /** * Fires when Escape is pressed while the popover is open. Call `event.preventDefault()` to stop it from closing. * @default undefined — no-op if omitted. */ onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; /** * Fires on pointer-down outside the content. Call `event.preventDefault()` to stop the popover from closing. * @default undefined — no-op if omitted. */ onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; /** * Fires when focus moves outside the content, as a cancelable stand-in for the native `focusin`; call `event.preventDefault()` to stop it from closing. * @default undefined — no-op if omitted. */ onFocusOutside?: ((event: FocusOutsideEvent) => void) | undefined; /** * Fires for either outside interaction — pointer-down or focus — before the specific handler, so one `event.preventDefault()` here vetoes either. * @default undefined — no-op if omitted. */ onInteractOutside?: ((event: InteractOutsideEvent) => void) | undefined; /** * Fires before focus moves into the content on open, modal popovers only; call `event.preventDefault()` to place focus yourself. * @default undefined — focus moves to the first tabbable element. */ onOpenAutoFocus?: ((event: AutoFocusEvent) => void) | undefined; /** * Fires before focus returns to the trigger (or, when `modal`, to whatever was focused before the popover opened). Call `event.preventDefault()` to suppress the return. * @default undefined — focus returns to the trigger. */ onCloseAutoFocus?: ((event: AutoFocusEvent) => void) | undefined; } /** Props for {@link PopoverTitle}, the popover's accessible name. */ export interface PopoverTitleProps extends PrimitivePropsWithRef<'h2'> { } /** Props for {@link PopoverDescription}, the popover's accessible description. */ export interface PopoverDescriptionProps extends PrimitivePropsWithRef<'p'> { } /** Props for {@link PopoverClose}, a button that closes the popover. */ export interface PopoverCloseProps extends PrimitivePropsWithRef<'button'> { }