import type * as React from 'react'; import type { PassThroughProps } from '../utils/passThrough.js'; import type { PopperContentProps } 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'; /** Which way actions fan out from trigger (physical, not logical direction). */ export type SpeedDialDirection = 'up' | 'down' | 'left' | 'right'; /** @internal Route that opened the fan; hover skips focus-move on pointer-enter. */ export type SpeedDialOpenReason = 'pointer' | 'hover' | 'keyboard' | 'programmatic'; /** @internal Shared state distributed from {@link SpeedDial} to its parts via context. */ export interface SpeedDialContextValue { triggerRef: React.RefObject; contentRef: React.RefObject; contentId: string; /** Trigger id pointed to by Content `aria-labelledby` for inherited naming. */ triggerId: string; open: boolean; /** Whether content is in DOM (stays true through exit animation). */ direction: SpeedDialDirection; disabled: boolean; openOnHover: boolean; /** How fan was last asked to open; ref so effect can read it without re-rendering. */ openReason: React.RefObject; /** Guarded setter; disabled dial never opens. */ onOpenChange: (open: boolean, reason?: SpeedDialOpenReason) => void; onPointerEnter: () => void; /** Opens fan after `hoverOpenDelay` when `openOnHover` is set. */ onHoverOpen: () => void; onPointerLeave: () => void; } /** @internal Wires a {@link SpeedDialItemLabel} to the `aria-labelledby` of its {@link SpeedDialItem}. */ export interface SpeedDialItemContextValue { labelId: string | undefined; setLabelId: (id: string | undefined) => void; } /** Entry in {@link SpeedDial}'s fan, rendered as `SpeedDialItem`. */ export interface SpeedDialAction { /** Visible text rendered as `SpeedDialItemLabel` and accessible name. */ label: string; /** * Icon rendered before label. * @default undefined — text-only. */ icon?: React.ReactNode | undefined; /** * Called on click / Enter / Space before dial's `onSelect`. * @default undefined — no-op. */ onSelect?: (() => void) | undefined; /** * Excludes action from navigation and selection (keeps slot so others don't shuffle). * @default false */ disabled?: boolean | undefined; } /** Props for {@link SpeedDial}. Pass-through attributes land on derived `SpeedDialContent`. */ export interface SpeedDialProps extends PassThroughProps { /** * Floating action button (merged onto element via `asChild`, needs accessible name). * @default undefined — no trigger. */ trigger?: React.ReactElement | undefined; /** * Accessible name applied to the derived trigger; a `trigger` element's own `aria-label` still wins. * @default undefined — the trigger is named only by its content. */ triggerLabel?: string | undefined; /** * Fan entries; one item per entry with label built from `label`. * @default undefined — empty fan. */ actions?: ReadonlyArray | undefined; /** * Fires with activated action and index (entry's `onSelect` fires first). * @default undefined — no-op. */ onSelect?: ((action: SpeedDialAction, index: number) => void) | undefined; /** * Scrim behind fan; doesn't make dial modal (focus not trapped, scrim clicks normal). * @default false */ overlay?: boolean | undefined; /** * Whether actions are revealed for controlled usage; pair with `onOpenChange`. * @default undefined — uncontrolled. */ open?: boolean | undefined; /** * Initial reveal state for uncontrolled usage. * @default false — starts collapsed. */ defaultOpen?: boolean | undefined; /** * Fires with new open state when dial opens or closes. * @default undefined — no-op. */ onOpenChange?: ((open: boolean) => void) | undefined; /** * Fires once open-state change finishes on screen (after animation ends). * @default undefined — no-op. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * Which way actions fan out; decides positioning axis and `data-direction`. * @default 'up' */ direction?: SpeedDialDirection | undefined; /** * Also open on pointer-enter (click and keyboard still work either way). * @default false */ openOnHover?: boolean | undefined; /** * Milliseconds dial stays open after pointer leaves (lets pointer cross gap to item without closing). * @default 300 */ hoverCloseDelay?: number | undefined; /** * Milliseconds pointer must rest before fan opens with `openOnHover`. * @default 0 */ hoverOpenDelay?: number | undefined; /** * Blocks opening and disables trigger; also closes if already open. * @default false */ disabled?: boolean | undefined; /** * Size rung the skin scales the dial by, fed to the derived Trigger and Items. * @default undefined — the skin's own default size. */ size?: 'sm' | 'md' | 'lg' | (string & {}) | 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 SpeedDialTrigger}, the floating action button revealing actions. */ export interface SpeedDialTriggerProps extends PrimitivePropsWithRef<'button'> { /** * Size rung the skin scales the trigger by, mirrored to `data-size`. * @default undefined — the skin's own default size. */ size?: 'sm' | 'md' | 'lg' | (string & {}) | undefined; } /** Props for {@link SpeedDialPortal}, teleports actions to `document.body`. */ export interface SpeedDialPortalProps extends PortalProps { /** * Keep portal mounted while dial closed (only needed for permanent DOM presence). * @default false */ forceMount?: boolean | undefined; } /** Props for {@link SpeedDialOverlay}}, optional scrim; opt-in by rendering. */ export interface SpeedDialOverlayProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link SpeedDialContent}}, the fanned action list (extends popper positioning, except `side` is owned by `direction`). */ export interface SpeedDialContentProps extends Omit { /** * Keep actions mounted while dial closed (only needed for permanent DOM presence). * @default false */ forceMount?: boolean | undefined; /** * Whether arrow-key navigation wraps from last to first action. * @default true */ loop?: boolean | undefined; /** * Fires on Escape (call `preventDefault()` to stay open). * @default undefined — no-op. */ onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; /** * Fires on pointer-down outside (call `preventDefault()` to stay open). * @default undefined — no-op. */ onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; /** * Fires on focus outside (receives cancelable stand-in with `focusin` as detail). * @default undefined — no-op. */ onFocusOutside?: ((event: FocusOutsideEvent) => void) | undefined; /** * Fires for any outside interaction before specific handler; one `preventDefault()` vetoes either way. * @default undefined — no-op. */ onInteractOutside?: ((event: InteractOutsideEvent) => void) | undefined; } /** Props for {@link SpeedDialItem}, one action in the fan. */ export interface SpeedDialItemProps extends Omit, 'onSelect'> { /** * Excludes action from navigation and selection (keeps slot so others don't shuffle). * @default false */ disabled?: boolean | undefined; /** * Called on click / Enter / Space (call `preventDefault()` to keep dial open). * @default undefined — no-op. */ onSelect?: ((event: Event) => void) | undefined; /** * Size rung the skin scales this action by, mirrored to `data-size`. * @default undefined — the skin's own default size. */ size?: 'sm' | 'md' | 'lg' | (string & {}) | undefined; } /** Props for {@link SpeedDialItemLabel}, visible text naming the item. */ export interface SpeedDialItemLabelProps extends PrimitivePropsWithRef<'span'> { }