import type * as React from 'react'; import type { PrimitivePropsWithRef } from '../utils/primitive.js'; /** `'source'` is the pool, `'target'` is the array `Transfer`'s `value` holds; named by role, not position. */ export type TransferSide = 'source' | 'target'; /** Named by where items land: `'toTarget'` adds to value, `'toSource'` removes. */ export type TransferDirection = 'toTarget' | 'toSource'; /** How the built-in matcher compares queries against item labels. */ export type TransferFilterMatchMode = 'startsWith' | 'contains' | 'endsWith'; /** Where items land on target side: `'items'` keeps collection order; `'append'` / `'prepend'` follow moves. */ export type TransferTargetOrder = 'items' | 'append' | 'prepend'; /** One entry in a {@link Transfer}'s collection. */ export interface TransferItemData { /** Unique id. This is what appears in `Transfer`'s `value` once the item is on the target side. */ value: string; /** Human-readable text, used as the option's content and its typeahead `textValue` when the list renders itself. */ label: string; /** * Excludes item from selection, navigation, and moves. * @default undefined — item behaves normally. */ disabled?: boolean | undefined; } /** @internal Shared state distributed from {@link Transfer} to its parts via context. Not part of the public API. */ export interface TransferContextValue { baseId: string; disabled: boolean; /** Whether search is active; `TransferListSearch` renders nothing when off. */ searchable: boolean; /** All items on each side before search filtering. */ sideItems: Record; /** Items on each side that survive the current search query. */ visibleItems: Record; /** Each side's marked-for-move selection, pruned to values still present. */ marked: Record; setMarked: (side: TransferSide, values: string[]) => void; query: Record; setQuery: (side: TransferSide, query: string) => void; /** The roving-focus option id for each list. */ active: Record; setActive: (side: TransferSide, value: string | null) => void; /** Range-selection anchor per side (last option marked without Shift). */ anchorRef: React.RefObject>; /** Whether `TransferListHeader` is mounted (decides `aria-labelledby` vs `aria-label`). */ labelled: Record; registerHeader: (side: TransferSide, present: boolean) => void; registerList: (side: TransferSide, node: HTMLDivElement | null) => void; /** Flag to suppress `focusin` being mistaken for external Tab after programmatic focus. */ suppressFocusInRef: React.RefObject; /** Moves values, announces outcome, and re-places focus; silently drops disabled items. */ move: (direction: TransferDirection, values: string[]) => void; /** Accessible name for move triggers (from `getMoveLabel` or built-in English). */ moveLabel: (count: number, direction: TransferDirection, all: boolean) => string; emitListScroll: (side: TransferSide, list: HTMLElement) => void; } /** The payload {@link TransferProps.onMove} receives: the items that moved and which way they went. */ export interface TransferMoveEventDetail { /** The items that actually moved; disabled ones are dropped before the move. */ items: TransferItemData[]; /** Which way they went. */ direction: TransferDirection; } /** Move outcome, handed to {@link TransferProps.getMoveAnnouncement} for wording. */ export interface TransferMoveDetail { /** How many items moved (disabled items never counted). */ moved: number; /** Items left on the origin side. */ remaining: number; direction: TransferDirection; from: TransferSide; to: TransferSide; } /** Search results, handed to {@link TransferProps.getSearchResultsAnnouncement}. */ export interface TransferSearchResultsDetail { /** Items on this side that match the query. */ shown: number; /** Total items on this side before filtering. */ total: number; side: TransferSide; /** The settled query (empty if user cleared the box). */ query: string; } /** List scroll geometry, carried by {@link TransferProps.onListScroll}. */ export interface TransferListScrollDetail { side: TransferSide; scrollTop: number; scrollHeight: number; clientHeight: number; } /** Reason {@link TransferEmpty} is showing, handed to a function child. */ export interface TransferEmptyContext { /** `'empty'` when side holds nothing; `'no-results'` when filtered to nothing. */ reason: 'empty' | 'no-results'; /** The active search query (empty string if reason is `'empty'`). */ query: string; } /** Props for the {@link Transfer} root. */ export interface TransferProps extends Omit, 'onChange' | 'defaultValue'> { /** * Manually-composed parts; omitted, derives headed searchable lists from `titles` and `renderItem`. * @default undefined — derives anatomy. */ children?: React.ReactNode | undefined; /** Entire collection; both sides are derived from this plus `value`. */ items: TransferItemData[]; /** * Target-side ids for controlled usage; pair with `onValueChange`. * @default undefined — uncontrolled. */ value?: string[] | undefined; /** * Initial target-side ids for uncontrolled usage. * @default undefined — all items start on source. */ defaultValue?: string[] | undefined; /** * Fires with new target-side ids whenever items move. * @default undefined — no-op. */ onValueChange?: ((value: string[]) => void) | undefined; /** * Fires alongside `onValueChange` with the moved items and which way they went. * @default undefined — no-op. */ onMove?: ((detail: TransferMoveEventDetail) => void) | undefined; /** * Disables both lists and all move triggers; individual items have their own `disabled`. * @default false */ disabled?: boolean | undefined; /** * Form field name; target-side ids submit as separate entries (not joined). * @default undefined — submits nothing. */ name?: string | undefined; /** * Form id for the submitted entries when Transfer is outside the form. * @default undefined — entries belong to nearest ancestor form. */ form?: string | undefined; /** * Enables per-list filtering; while false, `TransferListSearch` renders nothing. * @default false */ searchable?: boolean | undefined; /** * Custom filter; returns true if item survives query. Only consulted when `searchable`. * @default undefined — case-insensitive substring match. */ filter?: ((item: TransferItemData, query: string) => boolean) | undefined; /** * How built-in matcher compares queries to labels; ignored if `filter` is given. * @default 'contains' */ filterMatchMode?: TransferFilterMatchMode | undefined; /** * Words move-trigger accessible names (count is items to move, including zero). * @default undefined — built-in English. */ getMoveLabel?: ((count: number, direction: TransferDirection, all: boolean) => string) | undefined; /** * Words the polite live-region announcement after moves (return empty string to silence). * @default undefined — built-in English. */ getMoveAnnouncement?: ((detail: TransferMoveDetail) => string) | undefined; /** * Words the polite live-region announcement when search settles per side (return empty to silence). * @default undefined — built-in English. */ getSearchResultsAnnouncement?: ((detail: TransferSearchResultsDetail) => string) | undefined; /** * Where items land on target: `'items'` keeps collection order; `'append'` / `'prepend'` follow moves. * @default 'items' */ targetOrder?: TransferTargetOrder | undefined; /** * Fires on either list scroll with geometry for infinite-scroll loading. * @default undefined — no-op. */ onListScroll?: ((detail: TransferListScrollDetail) => void) | undefined; /** * Heading text for derived `TransferListHeader`s. Ignored when `children` given. * @default { source: 'Available items', target: 'Selected items' } */ titles?: TransferTitles | undefined; /** * Text for derived `TransferEmpty` states (`{query}` replaced with search text). * @default { empty: 'Nothing here.', noMatch: 'No match for "{query}".' } */ emptyText?: TransferEmptyText | undefined; /** * Whether derived `TransferControls` include a move-all trigger (off by default due to destructiveness). * @default false */ showMoveAll?: boolean | undefined; /** * Renders derived `TransferItem` content instead of `label`. Ignored when `children` given. * @default undefined — each item renders its `label`. */ renderItem?: ((item: TransferItemData) => React.ReactNode) | undefined; /** * Spacing rung the skin packs the rows by, fed to both derived lists. * @default undefined — the skin's own default spacing. */ density?: 'compact' | (string & {}) | undefined; /** * Corner rung the skin rounds the lists by, fed to both derived lists. * @default undefined — the skin's own default corner. */ shape?: 'square' | 'rounded' | (string & {}) | undefined; /** * Size rung the skin scales the lists by, fed to both derived lists. * @default undefined — the skin's own default size. */ size?: 'sm' | 'lg' | (string & {}) | undefined; } /** Text for empty states. See {@link TransferProps.emptyText}. */ export interface TransferEmptyText { /** Shown when side holds no items. @default 'Nothing here.' */ empty?: string | undefined; /** Shown when search filters all items (`{query}` replaced). @default 'No match for "{query}".' */ noMatch?: string | undefined; } /** List headings for derived anatomy. See {@link TransferProps.titles}. */ export interface TransferTitles { /** Source (available) list heading. @default 'Available items' */ source?: string | undefined; /** Target (selected) list heading. @default 'Selected items' */ target?: string | undefined; } /** @internal Wiring passed from a {@link TransferList} to the {@link TransferItem}s inside it. Not part of the public API. */ export interface TransferListContextValue { side: TransferSide; } /** Props for {@link TransferList}, the `role="listbox"` element for one side. */ export interface TransferListProps extends Omit, 'onChange' | 'defaultValue'> { /** Which of the two sides this list renders the items for. */ side: TransferSide; /** * Renders derived `TransferItem` content instead of `label`. Ignored when `children` given. * @default undefined — each item renders its `label`. */ renderItem?: ((item: TransferItemData) => React.ReactNode) | undefined; /** * Custom `TransferItem`s to override the default rendering of items. * @default undefined — one `TransferItem` per visible item, using `label` as content. */ children?: React.ReactNode | undefined; /** * Spacing rung the skin packs this list by, mirrored to `data-density`. * @default undefined — the skin's own default spacing. */ density?: 'compact' | (string & {}) | undefined; /** * Corner rung the skin rounds this list by, mirrored to `data-shape`. * @default undefined — the skin's own default corner. */ shape?: 'square' | 'rounded' | (string & {}) | undefined; /** * Size rung the skin scales this list by, mirrored to `data-size`. * @default undefined — the skin's own default size. */ size?: 'sm' | 'lg' | (string & {}) | undefined; } /** Props for {@link TransferListHeader}, the heading naming one list. */ export interface TransferListHeaderProps extends PrimitivePropsWithRef<'div'> { /** Which side this header names. Must match the `side` of the list it sits beside. */ side: TransferSide; } /** Props for {@link TransferListSearch}, the filter input for one list. */ export interface TransferListSearchProps extends Omit, 'value' | 'defaultValue' | 'type'> { /** Which of the two sides this input filters the items of. */ side: TransferSide; } /** Props for a single {@link TransferItem} option. */ export interface TransferItemProps extends PrimitivePropsWithRef<'div'> { /** The id of the `TransferItemData` entry this option renders. */ value: string; /** * Excludes option from marking, navigation, and moves. * @default false */ disabled?: boolean | undefined; /** * Text typeahead matches against, published as `data-text-value`. * @default undefined — falls back to rendered text content. */ textValue?: string | undefined; } /** Props for {@link TransferControls}, the named group holding the move triggers. */ export interface TransferControlsProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link TransferMoveTrigger}, moves marked items. */ export interface TransferMoveTriggerProps extends PrimitivePropsWithRef<'button'> { /** Direction: `'toTarget'` adds to value, `'toSource'` removes. */ direction: TransferDirection; } /** Props for {@link TransferMoveAllTrigger}, moves all visible items. */ export interface TransferMoveAllTriggerProps extends PrimitivePropsWithRef<'button'> { /** Direction: `'toTarget'` adds to value, `'toSource'` removes. */ direction: TransferDirection; } /** Props for {@link TransferEmpty}, shown when side has nothing to render. */ export interface TransferEmptyProps extends Omit, 'children'> { /** Which side ('source' or 'target') this empty state applies to. */ side: TransferSide; /** * Message; pass function to differentiate empty vs no-results (published as `data-reason` / `data-query`). * @default undefined — renders element only. */ children?: React.ReactNode | ((context: TransferEmptyContext) => React.ReactNode) | undefined; }