import type * as React from 'react'; import type { TreeCheckedState } from '../tree/types.js'; 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 { InteractOutsideEvent } from '../utils/useDismiss.js'; import type { TreeSelectFlatNode, TreeSelectIndex } from './collection.js'; /** * One node of a TreeSelect's collection — plain data, not elements, needed * for cascading state, filtering, and projection across collapsed branches. */ export interface TreeSelectNode { /** Unique identifier across the whole collection; what lands in value and callback names. */ value: string; /** * Human-readable text rendered as node content, matched by filter, shown by TreeSelectValue. * @default undefined — falls back to `value`. */ label?: string | undefined; /** * Excludes node from checking, cascade, navigation, and parent calculations; * inherited by the whole subtree. * @default false */ disabled?: boolean | undefined; /** * Child nodes. Empty or absent children makes this a leaf (not expandable). * @default undefined — a leaf. */ children?: TreeSelectNode[] | undefined; /** * Marks an unloaded branch as expandable before children arrive, giving * loadChildren something to run on; ignored once the node has children. * @default undefined — branch-ness decided by children alone. */ hasChildren?: boolean | undefined; } /** Propagation direction when checking a node — both ways, down only, up only, or none — meaningful only when multiple is set. */ export type TreeSelectCheckStrategy = 'cascade' | 'descendants' | 'ancestors' | 'independent'; /** Which checked nodes appear in value — all, leaves only, or the shallowest node of each fully-checked subtree — ignored where projection cannot be undone. */ export type TreeSelectValueStrategy = 'all' | 'leaf' | 'shallowest'; /** Predicate deciding whether a node matches the current filter text. */ export type TreeSelectFilter = (node: TreeSelectNode, inputValue: string) => boolean; /** How the popup draws the hierarchy — one indented tree, or one tree per open level side by side. */ export type TreeSelectLayout = 'tree' | 'columns'; /** @internal One showing column: the branch it belongs to plus the rows TreeSelectTree renders for it. */ export interface TreeSelectColumnView { /** The branch whose children this column lists, or `null` for the root column. */ parent: string | null; rows: readonly { value: string; depth: number; }[]; } /** @internal Shared state distributed from TreeSelect to its parts via context. */ export interface TreeSelectContextValue { collection: readonly TreeSelectNode[]; index: TreeSelectIndex; /** Every node value in collection pre-order. */ order: readonly string[]; /** The checked set projected through valueStrategy. */ value: readonly string[]; multiple: boolean; /** Whether checking propagates: checkStrategy is not independent and multiple is set. */ cascade: boolean; disabled: boolean; readOnly: boolean; /** Node's tri-state value derived from enabled leaf descendants. */ checkStateOf: (value: string) => TreeCheckedState; /** Toggle node, emit projected value, close if closeOnSelect is set. */ commit: (value: string) => void; open: boolean; onOpenChange: (open: boolean) => void; /** Caller's expansion plus, while filtering, ancestors of every match. */ expandedIds: string[]; expanded: ReadonlySet; setExpanded: (value: string, expand: boolean) => void; /** Node values whose loadChildren call is still pending. */ loadingValues: ReadonlySet; /** Adapter for Tree's array expansion callback to single-node change. */ onTreeExpandedIdsChange: (ids: string[]) => void; /** Whether clicking a branch row toggles its expansion as well as checking. */ expandOnClick: boolean; /** Whether a node survives the current filter (always true when not filtering). */ isVisible: (value: string) => boolean; /** The layout actually drawn: `'tree'` while a filter narrows a `'columns'` popup. */ layout: TreeSelectLayout; /** The showing columns left to right; empty unless the effective layout is `'columns'`. */ columns: readonly TreeSelectColumnView[]; /** DOM id of a column's tree; column 0 keeps `treeId` so the tree layout is unaffected. */ columnTreeId: (columnIndex: number) => string; /** DOM id of the tree holding the active node, which `aria-controls` must name. */ activeTreeId: string; /** Visible, expanded nodes in navigation order. */ flat: readonly TreeSelectFlatNode[]; flatMap: ReadonlyMap; activeValue: string | null; setActiveValue: (value: string | null) => void; /** DOM id of a node's treeitem for aria-activedescendant. */ itemId: (value: string) => string; treeId: string; triggerRef: React.RefObject; contentRef: React.RefObject; inputRef: React.RefObject; /** Whether TreeSelectInput is mounted; it takes focus and aria-activedescendant from trigger. */ hasInput: boolean; setHasInput: (hasInput: boolean) => void; inputValue: string; onInputValueChange: (value: string) => void; /** Node's label or its value when label is absent. */ getLabel: (value: string) => string; /** Shared keydown handler that both TreeSelectTrigger and TreeSelectInput delegate to. */ onNavigationKeyDown: (event: React.KeyboardEvent, options: { typeahead: boolean; selectOnSpace: boolean; }) => void; /** Node values `TreeSelectTree` actually renders when `rows` windows the collection, or `null` when unrestricted; navigation draws from this set. */ visibleRowValues: ReadonlySet | null; /** Reported by TreeSelectTree whenever its `rows` prop changes. */ setVisibleRowValues: (values: ReadonlySet | null) => void; } /** @internal Per-node state distributed from TreeSelectItem to TreeSelectItemIndicator via context. */ export interface TreeSelectItemContextValue { value: string; checkState: TreeCheckedState; disabled: boolean; expanded: boolean; hasChildren: boolean; /** Whether this node is the virtually-focused one named by aria-activedescendant. */ active: boolean; } /** Props for the TreeSelect root; standard attributes land on the derived TreeSelectTrigger, since TreeSelect itself renders no DOM. */ export interface TreeSelectProps extends PassThroughProps { /** * The TreeSelectTrigger/TreeSelectPortal parts, rendered as children. * @default undefined — renders nothing without children. */ children?: React.ReactNode | undefined; /** * Hierarchical data the tree is built from; node values must be unique across the whole collection. */ collection: readonly TreeSelectNode[]; /** * Controlled checked node values, always in the shape valueStrategy describes. Pair with onValueChange. * @default undefined — uncontrolled. */ value?: string[] | undefined; /** * Initially checked node values for uncontrolled usage. * @default [] — nothing checked. */ defaultValue?: string[] | undefined; /** * Fires with the new value whenever the checked set changes, projected through valueStrategy. * @default undefined — no-op if omitted. */ onValueChange?: ((value: string[]) => void) | undefined; /** * Allows more than one node to be checked; tree gets aria-multiselectable and checkStrategy becomes meaningful. * @default false */ multiple?: boolean | undefined; /** * Whether and in which direction checking propagates; see TreeSelectCheckStrategy. Ignored unless multiple. * @default 'cascade' */ checkStrategy?: TreeSelectCheckStrategy | undefined; /** * Which checked nodes appear in the emitted value; see TreeSelectValueStrategy. Ignored where checkStrategy lacks the direction to rebuild. * @default 'all' */ valueStrategy?: TreeSelectValueStrategy | undefined; /** * Controlled expanded node ids, paired with onExpandedChange; while filtering, ancestors of every match are expanded on top of this value. * @default undefined — uncontrolled. */ expanded?: string[] | undefined; /** * Initially expanded node values for uncontrolled usage. * @default [] — every branch collapsed. */ defaultExpanded?: string[] | undefined; /** * Fires with the new expanded set whenever a branch expands or collapses, never for filter-driven expansion. * @default undefined — no-op if omitted. */ onExpandedChange?: ((expanded: string[]) => void) | undefined; /** * Whether the popup is open for controlled usage. Pair with onOpenChange. * @default undefined — uncontrolled, tracks its own open state internally. */ open?: boolean | undefined; /** * Initial open state for uncontrolled usage. * @default false */ defaultOpen?: boolean | undefined; /** * Fires with the new open state whenever it changes, controlled or not. * @default undefined — no-op if omitted. */ onOpenChange?: ((open: boolean) => void) | undefined; /** * Fires once the open state change finishes on screen, after animation ends or immediately if none. * @default undefined — no-op if omitted. */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * The TreeSelectInput's filter text for controlled usage. Pair with onInputValueChange. * @default undefined — uncontrolled, tracks its own filter text internally. */ inputValue?: string | undefined; /** * Initial filter text for uncontrolled usage. * @default '' — unfiltered. */ defaultInputValue?: string | undefined; /** * Fires with the new filter text as the user types, controlled or not. * @default undefined — no-op if omitted. */ onInputValueChange?: ((inputValue: string) => void) | undefined; /** * Decides whether a node matches the filter; matches keep their ancestor chain and subtree visible. * @default defaultTreeSelectFilter */ filter?: TreeSelectFilter | undefined; /** * Blocks the whole control; trigger is disabled and no key changes anything. * @default false */ disabled?: boolean | undefined; /** * Form field name; each selected node value submits separately under this name. * @default undefined — submits nothing. */ name?: string | undefined; /** * Id of the form the submitted entries belong to, for a TreeSelect not a DOM descendant of it. * @default undefined — entries belong to the nearest ancestor form. */ form?: string | undefined; /** * Lets the user open and navigate the tree without changing the value, unlike disabled which also removes from the tab order. * @default false */ readOnly?: boolean | undefined; /** * Closes the popup as soon as a node is checked, returning focus to trigger. * @default !multiple */ closeOnSelect?: boolean | undefined; /** * Loads a node's children the first time it expands, for non-materialized collection. Mark branches with hasChildren; does not run while filtering. * @default undefined — collection is complete as given. */ loadChildren?: ((nodeValue: string) => void | Promise) | undefined; /** * Formats the announced message each time filter matches change while the popup is open. * @default defaultGetMatchCountLabel */ getMatchCountLabel?: ((count: number) => string) | undefined; /** * Supplies the label for a value whose node is not in collection, consulted by TreeSelectValue before the raw-value fallback. * @default undefined — unknown value renders as itself. */ getValueLabel?: ((value: string) => string) | undefined; /** * Whether clicking a branch row toggles its expansion as well as checking it; ArrowRight/ArrowLeft are unaffected either way. Ignored (treated as true) under `layout="columns"`, where the row is the only thing that opens a column. * @default false */ expandOnClick?: boolean | undefined; /** * How the popup presents the hierarchy: one indented tree, or one tree per open level side by side. A non-empty filter draws `'columns'` as a tree. * @default 'tree' */ layout?: TreeSelectLayout | undefined; /** * Renders a node's content when deriving anatomy, forwarded to TreeSelectTree; ignored under full manual composition. * @default undefined — renders each node's label ?? value as plain text. */ renderNode?: ((node: TreeSelectNode) => React.ReactNode) | undefined; /** * Placeholder for the derived trigger while nothing is checked, read only while deriving anatomy. * @default undefined */ placeholder?: string | undefined; /** * Empty-state message shown when no node matches the filter; only read while deriving anatomy. * @default 'No matches' */ emptyMessage?: string | undefined; /** * Accessible name for derived components; only read while deriving anatomy. Falls back to placeholder. * @default undefined — if neither is set, no attribute is emitted. */ 'aria-label'?: string | undefined; /** * Size rung the skin scales the panel by, fed to the derived Content. * @default undefined — the skin's own default size. */ size?: 'sm' | '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 TreeSelectTrigger}, the `role="combobox"` button that opens the popup. */ export interface TreeSelectTriggerProps extends PrimitivePropsWithRef<'button'> { } /** Props for TreeSelectValue, the trigger's read-out of the current selection. */ export interface TreeSelectValueProps extends PrimitivePropsWithRef<'span'> { /** * Stand-in content rendered while nothing is checked. * @default undefined */ placeholder?: React.ReactNode | undefined; } /** Props for TreeSelectPortal, which teleports the popup to document.body while open. */ export interface TreeSelectPortalProps extends PortalProps { /** * Renders and mounts TreeSelectContent even while closed, for animation libraries needing the outgoing content during exit. * @default false */ forceMount?: boolean | undefined; } /** Props for TreeSelectContent, the popup panel; extends PopperContentProps (side, align, sideOffset, alignOffset, avoidCollisions, collisionPadding). */ export interface TreeSelectContentProps extends PopperContentProps { /** * Mounts this popup even while closed, instead of unmounting it. * @default false */ forceMount?: boolean | undefined; /** * Fires on Escape while the popup is open, before it closes; call preventDefault() to keep it open. * @default undefined — no-op if omitted. */ onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; /** * Fires on a pointer-down outside the popup and trigger; call preventDefault() to keep it open. * @default undefined — no-op if omitted. */ onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; /** * Fires when focus moves outside the popup and trigger; call preventDefault() to keep open. * @default undefined — no-op if omitted. */ onFocusOutside?: ((event: CustomEvent) => void) | undefined; /** * Fires for either kind of outside interaction, before the specific handler with the same event. * @default undefined — no-op if omitted. */ onInteractOutside?: ((event: InteractOutsideEvent) => void) | undefined; /** * Size rung the skin scales the panel by, mirrored to `data-size`. * @default undefined — the skin's own default size. */ size?: 'sm' | 'lg' | (string & {}) | undefined; } /** Props for TreeSelectTree, the role="tree" inside the popup. */ export interface TreeSelectTreeProps extends Omit, 'children' | 'defaultValue' | 'ref'> { /** * Renders a node's content (label, indicators); returned nodes render inside the node's treeitem. * @default undefined — renders the node's label ?? value as plain text. */ renderLabel?: ((node: TreeSelectNode) => React.ReactNode) | undefined; /** * Renders exactly this row set instead of the whole collection; rows render flat as { value, depth } pairs with row.depth supplying aria-level. * @default undefined — renders every visible node from collection, nested. */ rows?: readonly Pick[] | undefined; } /** Props for TreeSelectColumns, the `role="presentation"` track holding one tree per open level. */ export type TreeSelectColumnsProps = Omit, 'defaultValue'>; /** Props for a single TreeSelectItem node. Normally rendered by TreeSelectTree. */ export interface TreeSelectItemProps extends Omit, 'children'> { /** The collection node this row stands for. */ node: TreeSelectNode; /** * The row's content — the label, plus any indicators. * @default undefined */ children?: React.ReactNode | undefined; } /** Props for TreeSelectItemIndicator, the checkmark shown on checked or partially-checked nodes. */ export interface TreeSelectItemIndicatorProps extends PrimitivePropsWithRef<'span'> { /** * Renders even while the node is unchecked, for animating the check or reserving its space. * @default false */ forceMount?: boolean | undefined; } /** Props for TreeSelectEmpty, shown in place of the tree when the filter matches nothing. */ export interface TreeSelectEmptyProps extends PrimitivePropsWithRef<'div'> { } /** Props for TreeSelectInput, the optional filter text field inside the popup. */ export interface TreeSelectInputProps extends Omit, 'value' | 'defaultValue'> { }