REACT TREE MULTI SELECTv4.1.0

TreeMultiSelect API

API docs for the TreeMultiSelect component.

/**
 * Enum representing the different types of the component.
 */
export enum Type {
  /** Component behaves as a normal tree structure. */
  TREE_SELECT = 'TREE_SELECT',

  /** Component behaves as a flat tree structure (selecting a node has no effect on its descendants or ancestors). */
  TREE_SELECT_FLAT = 'TREE_SELECT_FLAT',

  /** Component behaves as a multi-select. */
  MULTI_SELECT = 'MULTI_SELECT',

  /** Component behaves as a single-select. */
  SINGLE_SELECT = 'SINGLE_SELECT'
}

/**
 * Enum representing the aggregate selection state of all nodes.
 */
export enum SelectionAggregateState {
  /** All nodes are selected. */
  ALL = 'ALL',

  /** All selectable (non-disabled) nodes are selected. */
  EFFECTIVE_ALL = 'EFFECTIVE_ALL',

  /** Some, but not all, nodes are selected. */
  PARTIAL = 'PARTIAL',

  /** No nodes are selected. */
  NONE = 'NONE'
}
/**
 * Interface representing a node.
 */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export interface TreeNode<T extends TreeNode<T> = any> {
  /**
   * Unique identifier for the node.
   *
   * - The ID MUST be unique across the entire tree.
   * - The ID is used internally for tracking, selection, expansion, and virtual focus management.
   *
   * ### Important
   * Node IDs **MUST NOT conflict** with any predefined virtual focus
   * identifier suffixes (e.g., `INPUT_SUFFIX`, `CLEAR_ALL_SUFFIX`, `SELECT_ALL_SUFFIX`, `FOOTER_SUFFIX` constants),
   * as these are reserved for internal components and may cause unexpected behavior.
   * See `VirtualFocusId` type for more details.
   */
  id: string;

  /**
   * The display label of the node.
   */
  label: string;

  /**
   * Optional child nodes, enabling a nested tree structure.
   */
  children?: T[];

  /**
   * Whether the node is disabled.
   */
  disabled?: boolean;

  /**
   * Indicates whether the node has child nodes.
   *
   * When set to `true`, the node is treated as expandable, even if its `children`
   * array is not yet populated. This is commonly used with lazy loading, where
   * the actual children are loaded on demand (via `onLoadChildren`).
   *
   * If omitted or set to `false`, the node is treated as a leaf node unless
   * child nodes are explicitly provided.
   */
  hasChildren?: boolean;
}
import {VirtualFocusId} from './virtualFocus';

/**
 * Controls when the Footer component is rendered in the dropdown.
 */
export type FooterConfig = {
  /**
   * Renders the Footer when the component is in the search mode (when the input contains value).
   *
   * @default false
   */
  showWhenSearching?: boolean;

  /**
   * Renders the Footer when no items are available in the dropdown
   * (takes precedence over `showWhenSearching` if both apply).
   *
   * @default false
   */
  showWhenNoItems?: boolean;
};

/**
 * Configuration options for keyboard behavior in the Field component.
 */
export type FieldKeyboardOptions = {
  /**
   * Enables looping when navigating left with the ArrowLeft key.
   * If `true`, pressing ArrowLeft on the first item will move focus to the last item.
   *
   * @default false
   */
  loopLeft?: boolean;

  /**
   * Enables looping when navigating right with the ArrowRight key.
   * If `true`, pressing ArrowRight on the last item will move focus to the first item.
   *
   * @default false
   */
  loopRight?: boolean;
};

/**
 * Configuration options for keyboard behavior in the Dropdown component.
 */
export type DropdownKeyboardOptions = {
  /**
   * Enables looping when navigating upward with the ArrowUp key.
   * If `true`, pressing ArrowUp on the first item will move focus to the last item.
   *
   * @default true
   */
  loopUp?: boolean;

  /**
   * Enables looping when navigating downward with the ArrowDown key.
   * If `true`, pressing ArrowDown on the last item will move focus to the first item.
   *
   * @default true
   */
  loopDown?: boolean;
};

/**
 * Controls keyboard navigation behavior for the component.
 */
export type KeyboardConfig = {
  /**
   * Configuration for the Field component.
   */
  field?: FieldKeyboardOptions;

  /**
   * Configuration for the Dropdown component.
   */
  dropdown?: DropdownKeyboardOptions;
};

/**
 * Controls virtual focus behavior for the component.
 */
export type VirtualFocusConfig = {
  /**
   * Virtual focus IDs excluded from the virtual focus system.
   *
   * Items listed here:
   *  - Cannot receive focus via keyboard navigation
   *  - Cannot receive focus via mouse or pointer interaction
   *  - Can still respond to actions such as click, select, or expand
   */
  excludedVirtualFocusIds?: VirtualFocusId[];
};
/**
 * String prefix used to identify virtual focus elements within the field area.
 * Combined with `VIRTUAL_FOCUS_ID_DELIMITER` and an element-specific suffix
 * to form a unique virtual focus identifier.
 */
export const FIELD_PREFIX = 'field';

/**
 * String prefix used to identify virtual focus elements within the dropdown area.
 * Combined with `VIRTUAL_FOCUS_ID_DELIMITER` and an element-specific suffix
 * to form a unique virtual focus identifier.
 */
export const DROPDOWN_PREFIX = 'dropdown';

/**
 * Delimiter used to join the region prefix and element-specific suffix
 * when forming a virtual focus identifier.
 */
export const VIRTUAL_FOCUS_ID_DELIMITER = ':';

/**
 * String suffix used to identify the virtual focus element
 * associated with the input field.
 */
export const INPUT_SUFFIX = 'rtms-input';

/**
 * String suffix used to identify the virtual focus element
 * associated with the `SelectAllContainer` component.
 */
export const SELECT_ALL_SUFFIX = 'rtms-select-all';

/**
 * String suffix used to identify the virtual focus element
 * associated with the `FieldClear` component.
 */
export const CLEAR_ALL_SUFFIX = 'rtms-clear-all';

/**
 * String suffix used to identify the virtual focus element
 * associated with the `Footer` component.
 */
export const FOOTER_SUFFIX = 'rtms-footer';

/**
 * Represents the identifier of a virtually focusable element within the component.
 *
 * The value is a string prefixed with either the `FIELD_PREFIX` or `DROPDOWN_PREFIX` constant,
 * followed by the `VIRTUAL_FOCUS_ID_DELIMITER` and an element-specific suffix.
 *
 * ### Format
 * A `virtualFocusId` for a node follows the format:
 *
 * ```
 * <region-prefix><delimiter><node-id>
 * ```
 *
 * **Examples:**
 *
 * ```
 * field:1
 * dropdown:123
 * ```
 *
 * ### Predefined Virtual Focus IDs:
 * Some virtually focusable elements use predefined `virtualFocusId` values:
 *
 * - `${FIELD_PREFIX}${VIRTUAL_FOCUS_ID_DELIMITER}${INPUT_SUFFIX}` — **Input** component in a **Field**
 * - `${FIELD_PREFIX}${VIRTUAL_FOCUS_ID_DELIMITER}${CLEAR_ALL_SUFFIX}` — **FieldClear** component in a **Field**
 * - `${DROPDOWN_PREFIX}${VIRTUAL_FOCUS_ID_DELIMITER}${SELECT_ALL_SUFFIX}` — **SelectAll** component in a **Dropdown**
 * - `${DROPDOWN_PREFIX}${VIRTUAL_FOCUS_ID_DELIMITER}${FOOTER_SUFFIX}` — **Footer** component in a **Dropdown**
 */
export type VirtualFocusId =
  | `${typeof FIELD_PREFIX}${typeof VIRTUAL_FOCUS_ID_DELIMITER}${string}`
  | `${typeof DROPDOWN_PREFIX}${typeof VIRTUAL_FOCUS_ID_DELIMITER}${string}`;
import React, {JSX} from 'react';
import {SelectionAggregateState, Type} from './core';

export interface FieldOwnProps {
  type: Type;
  isDropdownOpen: boolean;
  withClearAll: boolean;
  componentDisabled: boolean;
}

export interface ChipContainerOwnProps {
  label: string;
  focused: boolean;
  disabled: boolean;
  componentDisabled: boolean;
  withChipClear: boolean;
}

export interface ChipLabelOwnProps {
  label: string;
  componentDisabled: boolean;
}

export interface ChipClearOwnProps {
  componentDisabled: boolean;
}

export interface InputOwnProps {
  placeholder: string;
  value: string;
  disabled: boolean;
}

export interface FieldClearOwnProps {
  focused: boolean;
  componentDisabled: boolean;
}

export interface FieldToggleOwnProps {
  expanded: boolean;
  componentDisabled: boolean;
}

export interface DropdownOwnProps {
  componentDisabled: boolean;
}

export interface SelectAllContainerOwnProps {
  label: string;
  selectionAggregateState: SelectionAggregateState;
  focused: boolean;
}

export interface SelectAllCheckboxOwnProps {
  selectionAggregateState: SelectionAggregateState;
}

export interface SelectAllLabelOwnProps {
  label: string;
}

export interface NodeContainerOwnProps {
  label: string;
  disabled: boolean;
  selected: boolean;
  partial: boolean;
  expanded: boolean;
  focused: boolean;
  matched: boolean;
}

export interface NodeToggleOwnProps {
  expanded: boolean;
}

export interface NodeCheckboxOwnProps {
  checked: boolean;
  partial: boolean;
  disabled: boolean;
}

export interface NodeLabelOwnProps {
  label: string;
}

export interface FooterOwnProps {
  focused: boolean;
}

export interface NoDataOwnProps {
  label: string;
}

/* eslint-disable-next-line @typescript-eslint/no-empty-object-type */
export interface SpinnerOwnProps {
}

export type Attributes<Tag extends keyof JSX.IntrinsicElements> = JSX.IntrinsicElements[Tag] & {
  'data-rtms-virtual-focus-id'?: string;
};

export interface ComponentProps<Tag extends keyof JSX.IntrinsicElements, OwnProps, CustomProps = unknown> {
  attributes: Attributes<Tag>;
  ownProps: OwnProps;
  customProps: CustomProps;
  children?: React.ReactNode;
}

export interface Component<ComponentProps, CustomProps = unknown> {
  component: React.ComponentType<ComponentProps>;
  props?: CustomProps;
}

export type FieldProps<CustomProps = unknown> = ComponentProps<'div', FieldOwnProps, CustomProps>;
export type ChipContainerProps<CustomProps = unknown> = ComponentProps<'div', ChipContainerOwnProps, CustomProps>;
export type ChipLabelProps<CustomProps = unknown> = ComponentProps<'div', ChipLabelOwnProps, CustomProps>;
export type ChipClearProps<CustomProps = unknown> = ComponentProps<'div', ChipClearOwnProps, CustomProps>;
export type InputProps<CustomProps = unknown> = ComponentProps<'input', InputOwnProps, CustomProps>;
export type FieldClearProps<CustomProps = unknown> = ComponentProps<'div', FieldClearOwnProps, CustomProps>;
export type FieldToggleProps<CustomProps = unknown> = ComponentProps<'div', FieldToggleOwnProps, CustomProps>;
export type DropdownProps<CustomProps = unknown> = ComponentProps<'div', DropdownOwnProps, CustomProps>;
export type SelectAllContainerProps<CustomProps = unknown> = ComponentProps<'div', SelectAllContainerOwnProps, CustomProps>;
export type SelectAllCheckboxProps<CustomProps = unknown> = ComponentProps<'div', SelectAllCheckboxOwnProps, CustomProps>;
export type SelectAllLabelProps<CustomProps = unknown> = ComponentProps<'div', SelectAllLabelOwnProps, CustomProps>;
export type NodeContainerProps<CustomProps = unknown> = ComponentProps<'div', NodeContainerOwnProps, CustomProps>;
export type NodeToggleProps<CustomProps = unknown> = ComponentProps<'div', NodeToggleOwnProps, CustomProps>;
export type NodeCheckboxProps<CustomProps = unknown> = ComponentProps<'div', NodeCheckboxOwnProps, CustomProps>;
export type NodeLabelProps<CustomProps = unknown> = ComponentProps<'div', NodeLabelOwnProps, CustomProps>;
export type FooterProps<CustomProps = unknown> = ComponentProps<'div', FooterOwnProps, CustomProps>;
export type NoDataProps<CustomProps = unknown> = ComponentProps<'div', NoDataOwnProps, CustomProps>;
export type SpinnerProps<CustomProps = unknown> = ComponentProps<'div', SpinnerOwnProps, CustomProps>;

export type FieldType<CustomProps = unknown> = Component<FieldProps<CustomProps>, CustomProps>;
export type ChipContainerType<CustomProps = unknown> = Component<ChipContainerProps<CustomProps>, CustomProps>;
export type ChipLabelType<CustomProps = unknown> = Component<ChipLabelProps<CustomProps>, CustomProps>;
export type ChipClearType<CustomProps = unknown> = Component<ChipClearProps<CustomProps>, CustomProps>;
export type InputType<CustomProps = unknown> = Component<InputProps<CustomProps>, CustomProps>;
export type FieldClearType<CustomProps = unknown> = Component<FieldClearProps<CustomProps>, CustomProps>;
export type FieldToggleType<CustomProps = unknown> = Component<FieldToggleProps<CustomProps>, CustomProps>;
export type DropdownType<CustomProps = unknown> = Component<DropdownProps<CustomProps>, CustomProps>;
export type SelectAllContainerType<CustomProps = unknown> = Component<SelectAllContainerProps<CustomProps>, CustomProps>;
export type SelectAllCheckboxType<CustomProps = unknown> = Component<SelectAllCheckboxProps<CustomProps>, CustomProps>;
export type SelectAllLabelType<CustomProps = unknown> = Component<SelectAllLabelProps<CustomProps>, CustomProps>;
export type NodeContainerType<CustomProps = unknown> = Component<NodeContainerProps<CustomProps>, CustomProps>;
export type NodeToggleType<CustomProps = unknown> = Component<NodeToggleProps<CustomProps>, CustomProps>;
export type NodeCheckboxType<CustomProps = unknown> = Component<NodeCheckboxProps<CustomProps>, CustomProps>;
export type NodeLabelType<CustomProps = unknown> = Component<NodeLabelProps<CustomProps>, CustomProps>;
export type FooterType<CustomProps = unknown> = Component<FooterProps<CustomProps>, CustomProps>;
export type NoDataType<CustomProps = unknown> = Component<NoDataProps<CustomProps>, CustomProps>;
export type SpinnerType<CustomProps = unknown> = Component<SpinnerProps<CustomProps>, CustomProps>;

/* eslint-disable @typescript-eslint/no-explicit-any */
export type ComponentTypes = {
  Field: FieldType<any>;
  ChipContainer: ChipContainerType<any>;
  ChipLabel: ChipLabelType<any>;
  ChipClear: ChipClearType<any>;
  Input: InputType<any>;
  FieldClear: FieldClearType<any>;
  FieldToggle: FieldToggleType<any>;
  Dropdown: DropdownType<any>;
  SelectAllContainer: SelectAllContainerType<any>;
  SelectAllCheckbox: SelectAllCheckboxType<any>;
  SelectAllLabel: SelectAllLabelType<any>;
  NodeContainer: NodeContainerType<any>;
  NodeToggle: NodeToggleType<any>;
  NodeCheckbox: NodeCheckboxType<any>;
  NodeLabel: NodeLabelType<any>;
  Footer: FooterType<any>;
  NoData: NoDataType<any>;
  Spinner: SpinnerType<any>;
};

export type ComponentNames = keyof ComponentTypes;

export type Components<
  ComponentsMap extends Partial<ComponentTypes>
    & Record<Exclude<keyof ComponentsMap, ComponentNames>, never> = any
> = {
  [K in ComponentNames]?: Component<
    ComponentTypes[K] extends Component<infer ComponentProps, any> ? ComponentProps : never,
    K extends keyof ComponentsMap
      ? ComponentsMap[K] extends Component<any, infer CustomProps>
        ? CustomProps
        : unknown
      : unknown
  >;
};
import {SelectionAggregateState} from './core';
import {VirtualFocusId} from './virtualFocus';

/**
 * Represents the internal state of the component.
 */
export interface State {
  /**
   * The IDs of the nodes that are currently selected.
   *
   * - In **controlled mode**, this value mirrors the `selectedIds` prop.
   * - In **uncontrolled mode**, this value is managed internally by the component.
   * - For `Type.SELECT`, this array contains at most **one ID**.
   */
  selectedIds: string[];

  /**
   * The IDs of the nodes that are currently expanded.
   *
   * - In **controlled mode**, this value mirrors the `expandedIds` prop.
   * - In **uncontrolled mode**, this value is managed internally by the component.
   * - For component types other than `Type.TREE_SELECT` and `Type.TREE_SELECT_FLAT`,
   * this value is always an **empty array**.
   */
  expandedIds: string[];

  /**
   * Represents the current overall selection state of all nodes.
   */
  selectionAggregateState: SelectionAggregateState;

  /**
   * The current search input value.
   */
  inputValue: string;

  /**
   * Indicates whether the dropdown is currently open (rendered).
   */
  isDropdownOpen: boolean;

  /**
   * The identifier of the currently virtually focused element,
   * or `null` if no element is virtually focused.
   */
  virtualFocusId: VirtualFocusId | null;
}
import React from 'react';
import {Components} from './components';
import {SelectionAggregateState, Type} from './core';
import {FooterConfig, KeyboardConfig, VirtualFocusConfig} from './configs';
import {TreeNode} from './nodes';

/**
 * Props for the `TreeMultiSelect` component.
 *
 * Defines all configuration options, event callbacks, and customization points
 * for controlling the behavior, appearance, and data handling of the component.
 */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export interface TreeMultiSelectProps<T extends TreeNode<T> = any> {
  /**
   * The data to be rendered in the component.
   */
  data: T[];

  /**
   * Specifies the type of the component, determining its behavior and rendering.
   *
   * @default Type.TREE_SELECT
   */
  type?: Type;

  /**
   * The IDs of the nodes that should be selected.
   *
   * - For `Type.SELECT` type, exactly **one ID** should be passed;
   *   if more than one ID is provided, only the **first ID** will be used.
   * - For other types it can contain multiple IDs.
   *
   * - The component treats this as a **controlled prop**.
   */
  selectedIds?: string[];

  /**
   * The IDs of the nodes that should be selected initially (uncontrolled mode).
   *
   * - For `Type.SELECT` type, exactly **one ID** should be passed;
   *   if more than one ID is provided, only the **first ID** will be used.
   * - For other types it can contain multiple IDs.
   * - Used **only when `selectedIds` is not provided**.
   * - The component will initialize its internal selection state using this value
   *   and will manage selection internally afterward.
   * - Changes to this prop after the initial render are ignored.
   */
  defaultSelectedIds?: string[];

  /**
   * The IDs of the nodes that should be expanded.
   *
   * - Used only when `type` is `Type.TREE_SELECT` or `Type.TREE_SELECT_FLAT`.
   *   For all other types, this prop is ignored.
   *
   * - The component treats this as a **controlled prop**.
   */
  expandedIds?: string[];

  /**
   * The IDs of the nodes that should be expanded initially (uncontrolled mode).
   *
   * - Used only when `type` is `Type.TREE_SELECT` or `Type.TREE_SELECT_FLAT`.
   *   For all other types, this prop is ignored.
   * - Used **only when `expandedIds` is not provided**.
   * - The component will initialize its internal expansion state using this value
   *   and will manage expansion internally afterward.
   * - Changes to this prop after the initial render are ignored.
   */
  defaultExpandedIds?: string[];

  /**
   * The `id` attribute to apply to the root `<div>` of the component.
   */
  id?: string;

  /**
   * The `className` to apply to the root `<div>` of the component.
   */
  className?: string;

  /**
   * Placeholder text displayed in the search input field.
   *
   * @default "search..."
   */
  inputPlaceholder?: string;

  /**
   * Text displayed when there is no data to show in the dropdown.
   *
   * @default "No data"
   */
  noDataText?: string;

  /**
   * Text displayed when no matching results are found during a search.
   *
   * @default "No matches"
   */
  noMatchesText?: string;

  /**
   * Disables the entire component, preventing user interaction.
   *
   * @default false
   */
  isDisabled?: boolean;

  /**
   * Controls whether the search input is rendered.
   * When `true`, a search input is shown either in the field
   * or in the dropdown (if `withDropdownInput` is also `true`).
   *
   * @default true
   */
  isSearchable?: boolean;

  /**
   * Controls whether the chip-level clear button (`ChipClear`) is displayed for each selected item.
   *
   * @default true
   */
  withChipClear?: boolean;

  /**
   * Controls whether the field-level clear button (`FieldClear`) is displayed to clear all selected items at once.
   *
   * @default true
   */
  withClearAll?: boolean;

  /**
   * Controls whether a sticky "SelectAll" component is rendered at the top of the dropdown.
   *
   * This option is automatically hidden when:
   * - `type` is `Type.SELECT`
   * - the search input has a value (search mode)
   * - there is no available data
   *
   * @default false
   */
  withSelectAll?: boolean;

  /**
   * Controls whether a sticky search input is rendered at the top of the dropdown.
   * A hidden input is rendered in the field to preserve focus behavior.
   *
   * @default false
   */
  withDropdownInput?: boolean;

  /**
   * Closes the dropdown automatically after a node is changed (selected/deselected in dropdown).
   * Useful when `type` is `Type.SELECT`.
   *
   * @default false
   */
  closeDropdownOnNodeChange?: boolean;

  /**
   * Controls whether the dropdown is rendered (open) or hidden (closed).
   * This enables external control over the dropdown's rendering state.
   *
   * When set to `true`, the dropdown is rendered (opened).
   * When set to `false`, the dropdown is hidden (closed).
   *
   * The component treats this as a **controlled prop**.
   *
   * If omitted, the component manages the dropdown state internally.
   * For full control, use this prop in conjunction with the `onDropdownToggle` callback.
   */
  isDropdownOpen?: boolean;

  /**
   * Controls whether the dropdown should be open initially (uncontrolled mode).
   *
   * - Used only when `isDropdownOpen` is not provided.
   * - Initializes the internal open/close state on first render.
   * - The component manages the dropdown’s visibility internally afterward.
   * - Changes to this prop after the initial render are ignored.
   */
  defaultIsDropdownOpen?: boolean;

  /**
   * Dropdown height in pixels. If the content height is smaller than this value,
   * the dropdown height is automatically reduced to fit the content.
   *
   * @default 300
   */
  dropdownHeight?: number;

  /**
   * The number of items to render outside the visible viewport (above and below)
   * to improve scroll performance and reduce flickering during fast scrolling.
   *
   * @default 1
   */
  overscan?: number;

  /**
   * Determines whether the dropdown list is rendered using virtualization.
   * When enabled, only the visible portion of the list (plus overscan items)
   * is rendered to improve performance with large datasets.
   *
   * @default true
   */
  isVirtualized?: boolean;

  /**
   * Controls when the Footer component is rendered in the dropdown.
   */
  footerConfig?: FooterConfig;

  /**
   * Controls keyboard navigation behavior for the component.
   */
  keyboardConfig?: KeyboardConfig;

  /**
   * Controls virtual focus behavior for the component.
   */
  virtualFocusConfig?: VirtualFocusConfig;

  /**
   * Custom components used to override the default UI elements of the TreeMultiSelect.
   *
   * Allows you to replace built-in components with your own implementations
   * to match your design and behavior requirements.
   */
  components?: Components;

  /**
   * Callback triggered when the dropdown is opened or closed.
   *
   * @param isOpen - `true` if the dropdown was opened, `false` if it was closed.
   */
  onDropdownToggle?: (isOpen: boolean) => void;

  /**
   * Callback triggered when a node is selected or deselected.
   * This includes interactions from the dropdown as well as chip removal in the field.
   *
   * @param node - The node that was changed.
   * @param selectedIds - The list of currently selected nodes IDs.
   */
  onNodeChange?: (node: T, selectedIds: string[]) => void;

  /**
   * Callback triggered when a node is toggled (expanded or collapsed).
   *
   * @param node - The node that was toggled.
   * @param expandedIds - The list of currently expanded nodes IDs.
   */
  onNodeToggle?: (node: T, expandedIds: string[]) => void;

  /**
   * Callback triggered when the `FieldClear` component is activated by user interaction,
   * such as a mouse click or pressing the Backspace key.
   *
   * This is used to clear all selected nodes, except for nodes that are disabled.
   *
   * @param selectedIds - The list of currently selected nodes Ids.
   * @param selectionAggregateState - The current overall selection state of all nodes.
   */
  onClearAll?: (selectedIds: string[], selectionAggregateState: SelectionAggregateState) => void;

  /**
   * Callback triggered when the `SelectAll` component is activated by user interaction,
   * such as a mouse click or pressing the Enter key.
   *
   * This is used to select or deselect all nodes, except for nodes that are disabled.
   *
   * @param selectedIds - The list of currently selected nodes IDs.
   * @param selectionAggregateState - The current overall selection state of all nodes.
   */
  onSelectAllChange?: (selectedIds: string[], selectionAggregateState: SelectionAggregateState) => void;

  /**
   * Callback triggered when the component receives focus.
   *
   * @param event - The React focus event.
   */
  onFocus?: (event: React.FocusEvent) => void;

  /**
   * Callback triggered when the component loses focus.
   *
   * @param event - The React blur event.
   */
  onBlur?: (event: React.FocusEvent) => void;

  /**
   * Callback triggered on keyboard interaction within the component.
   *
   * This allows interception or customization of the built-in keyboard behavior.
   *
   * - Returning `true` prevents the component’s default keyboard handling for the event.
   * - Returning `false` or `undefined` allows the component’s default handling to continue.
   *
   * This means the user can simply omit a return statement if they do not want
   * to block the default behavior.
   *
   * @param event - The original keyboard event.
   * @returns `true` to stop the default keyboard handling; otherwise `false` or `undefined`.
   */
  onKeyDown?: (event: React.KeyboardEvent) => boolean | undefined;

  /**
   * Callback triggered when the last item in the dropdown is rendered.
   * This is useful for implementing infinite scrolling or lazy loading.
   *
   * Note: The callback is invoked when the last item (including overscan)
   * is rendered, not based on actual scroll position.
   *
   * @param inputValue - The current search input value.
   * @param displayedNodes - An array of TreeNode objects currently displayed in the dropdown.
   */
  onDropdownLastItemReached?: (inputValue: string, displayedNodes: T[]) => void;

  /**
   * Callback for loading additional data to be appended to the end of the existing dataset.
   *
   * This is useful for implementing infinite scrolling, pagination, lazy loading, or incremental data fetching
   * where new items extend the current list rather than replacing it.
   *
   * This function is *not* invoked automatically by the component. Instead, it is triggered manually by the consumer
   * via the imperative `TreeMultiSelectHandle.loadData()` method.
   *
   * Note: The component automatically appends the nodes returned by this callback to the existing dataset.
   * It does not remove duplicates, merge updates, or otherwise reconcile data conflicts.
   * These responsibilities must be handled by the consumer.
   *
   * @returns A Promise resolving to an array of TreeNode objects to append.
   */
  onLoadData?: () => Promise<T[]>;

  /**
   * Callback for loading children of a specific node on demand.
   *
   * This function is called automatically by the component when the user expands a node that has not yet loaded
   * its children (i.e., the node’s `hasChildren` prop is `true`, and its children prop is not set or empty).
   * It enables lazy loading of hierarchical data for large trees or server-driven datasets.
   *
   * The function should return a Promise resolving to an array of TreeNode objects,
   * which will be set as the children of the specified node.
   *
   * Note: The component does not perform deduplication, merging, or conflict
   * resolution, so ensure the data returned is complete and correct for that node.
   *
   * @param id - The unique identifier of the node whose children are being loaded.
   * @returns A Promise resolving to an array of TreeNode objects to be set as the node’s children.
   */
  onLoadChildren?: (id: string) => Promise<T[]>;
}
import {TreeNode} from './nodes';
import {State} from './state';
import {DROPDOWN_PREFIX, FIELD_PREFIX, VirtualFocusId} from './virtualFocus';

/**
 * Imperative API for interacting with the TreeMultiSelect component.
 *
 * A `TreeMultiSelectHandle` instance can be obtained by passing a `ref`
 * to the component (e.g., `useRef<TreeMultiSelectHandle>(null)`).
 *
 * All methods operate on the most recent internal state at the moment
 * they are called.
 */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export interface TreeMultiSelectHandle<T extends TreeNode<T> = any> {
  /**
   * Returns a snapshot of the current internal component state.
   *
   * The returned object is always up-to-date at the moment of the call.
   *
   * @returns The current internal state of the TreeMultiSelect component.
   */
  getState: () => State;

  /**
   * Returns a node by its unique identifier.
   *
   * The identifier corresponds to the node `id`.
   *
   * @param id - The unique identifier of the node.
   * @returns The matching `TreeNode` if found; otherwise `undefined`.
   */
  getById: (id: string) => T | undefined;

  /**
   * Loads additional data and appends it to the current dataset.
   *
   * This method calls the `onLoadData` callback internally and merges the returned
   * nodes with the existing data. It does nothing if `onLoadData` is not provided
   * or returns an empty array.
   *
   * @returns A Promise that resolves once the data has been loaded and appended.
   */
  loadData: () => Promise<void>;

  /**
   * Opens (renders) the dropdown.
   */
  openDropdown: () => void;

  /**
   * Closes (hides) the dropdown.
   */
  closeDropdown: () => void;

  /**
   * Toggles the dropdown's visibility.
   *
   * - If the dropdown is currently closed, it will be opened.
   * - If the dropdown is currently open, it will be closed.
   */
  toggleDropdown: () => void;

  /**
   * Selects all nodes, except for nodes that are disabled.
   */
  selectAll: () => void;

  /**
   * Deselects all nodes, except for nodes that are disabled.
   */
  deselectAll: () => void;

  /**
   * Toggles the selection state of all selectable nodes.
   *
   * - If all selectable nodes are currently selected, this will deselect all of them.
   * - Otherwise, it will select all selectable nodes.
   */
  toggleAllSelection: () => void;

  /**
   * Expands a node in the tree.
   *
   * Does nothing if the node is already expanded or not expandable.
   *
   * @param id - The unique identifier of the node to expand.
   * If omitted, the currently virtually focused node in the dropdown will be expanded
   * if it exists and is expandable.
   */
  expandNode: (id?: string) => void;

  /**
   * Collapses a node in the tree.
   *
   * Does nothing if the node is already collapsed or not collapsible.
   *
   * @param id - The unique identifier of the node to collapse.
   * If omitted, the currently virtually focused node in the dropdown will be collapsed
   * if it exists and is collapsible.
   */
  collapseNode: (id?: string) => void;

  /**
   * Toggles the expansion state of a node.
   *
   * - If the node is expanded, it will be collapsed.
   * - If the node is collapsed and expandable, it will be expanded.
   *
   * @param id - The unique identifier of the node to toggle.
   * If omitted, the currently virtually focused node in the dropdown will be toggled
   * if it exists and is expandable/collapsible.
   */
  toggleNodeExpansion: (id?: string) => void;

  /**
   * Selects a node explicitly.
   *
   * Does nothing if the node is already selected or not selectable.
   *
   * @param id - The unique identifier of the node to select.
   * If omitted, the currently virtually focused node will be selected if it exists and is selectable.
   */
  selectNode: (id?: string) => void;

  /**
   * Deselects a node explicitly.
   *
   * Does nothing if the node is already unselected or not selectable.
   *
   * @param id - The unique identifier of the node to deselect.
   * If omitted, the currently virtually focused node will be deselected if it exists and is selectable.
   */
  deselectNode: (id?: string) => void;

  /**
   * Toggles the selection of a node based on its current state.
   *
   * - If the node is fully selected or partially selected (with all non-disabled children selected),
   *   it will be deselected.
   * - Otherwise, it will be selected.
   *
   * @param id - The unique identifier of the node whose selection state should be toggled.
   * If omitted, the currently virtually focused node will toggle its selection state if it exists and is selectable.
   */
  toggleNodeSelection: (id?: string) => void;

  /**
   * Moves virtual focus to the first virtually focusable element.
   *
   * - If `region` is provided, moves focus to the first element in that region (`FIELD` or `DROPDOWN`),
   *   ignoring the current virtual focus.
   * - If `region` is omitted, moves focus within the same region as the currently focused element.
   *   Does nothing if there is no currently focused element (`virtualFocusId` is `null`).
   *
   * @param region - The focus region to target.
   */
  focusFirstItem: (region?: typeof FIELD_PREFIX | typeof DROPDOWN_PREFIX) => void;

  /**
   * Moves virtual focus to the last virtually focusable element.
   *
   * - If `region` is provided, moves focus to the last element in that region (`FIELD` or `DROPDOWN`),
   *   ignoring the current virtual focus.
   * - If `region` is omitted, moves focus within the same region as the currently focused element.
   *   Does nothing if there is no currently focused element (`virtualFocusId` is `null`).
   *
   * @param region - The focus region to target.
   */
  focusLastItem: (region?: typeof FIELD_PREFIX | typeof DROPDOWN_PREFIX) => void;

  /**
   * Moves virtual focus to the previous virtually focusable element.
   *
   * - If `virtualFocusId` is provided, moves focus to the element immediately preceding
   *   the specified element, ignoring the currently focused element.
   * - If `virtualFocusId` is omitted, moves focus to the element immediately preceding
   *   the currently virtually focused element.
   *   Does nothing if there is no currently focused element (`virtualFocusId` is `null`).
   *
   * @param virtualFocusId - The identifier of the virtually focusable element
   * from which to move the virtual focus.
   */
  focusPrevItem: (virtualFocusId?: VirtualFocusId) => void;

  /**
   * Moves virtual focus to the next virtually focusable element.
   *
   * - If `virtualFocusId` is provided, moves focus to the element immediately following
   *   the specified element, ignoring the currently focused element.
   * - If `virtualFocusId` is omitted, moves focus to the element immediately following
   *   the currently virtually focused element.
   *   Does nothing if there is no currently focused element (`virtualFocusId` is `null`).
   *
   * @param virtualFocusId - The identifier of the virtually focusable element
   * from which to move the virtual focus.
   */
  focusNextItem: (virtualFocusId?: VirtualFocusId) => void;
}