import { AriaAttributes, CSSProperties, ReactElement } from 'react'; import { AiMarkWithTooltipOrPopoverProps, CheckState, LayoutUtilProps, Size } from '../../../types'; import { SelectFieldGroupByValue, SelectFieldOption } from '../SelectField/types'; import { ChipProps } from '../../../components/Chip'; /** * Partial chip props that can be returned from the `getChipProps` callback. * Supports `icon` or `avatar` (mutually exclusive) and all other chip props as optional overrides. * Core props (`label`, `onClose`, `className`, `title`, `size`) are managed by the component and excluded from this type. */ export type MultiSelectFieldChipProps = Partial> & ({ icon?: ChipProps["icon"]; avatar?: never; } | { icon?: never; avatar?: ChipProps["avatar"]; }); /** * Configuration options for the MultiSelectField cache. * @property enabled - Whether caching is enabled. Defaults to true. * @property maxSize - Maximum number of cached entries before clearing the cache. Defaults to 15. */ export type MultiSelectFieldCacheOptions = { enabled?: boolean; maxSize?: number; }; /** * Imperative handle for the MultiSelectField component. * @property clearCache - Clears the options cache. * @property invalidate - Clears the cache and triggers a fresh options load from the data source. */ export type MultiSelectFieldHandle = { clearCache: () => void; invalidate: () => void; }; export type MultiSelectFieldGroupByValue = SelectFieldGroupByValue; export type MultiSelectFieldOption = SelectFieldOption; export type MultiSelectFieldGroupedOption = MultiSelectFieldOption & { group: MultiSelectFieldGroupByValue; }; export type MultiSelectFieldUngroupedOption = Omit & { group?: never; }; export type MultiSelectFieldOptionsResult = MultiSelectFieldOption[] | Promise; export type MultiSelectFieldEagerLoader = (searchValue: string) => MultiSelectFieldOptionsResult; export type MultiSelectFieldOffsetLazyResult = { options: MultiSelectFieldUngroupedOption[]; hasMore?: boolean; }; export type MultiSelectFieldOffsetLazyLoader = (searchValue: string, offset: number, limit: number) => MultiSelectFieldOffsetLazyResult | Promise; export type MultiSelectFieldPageLazyResult = { options: MultiSelectFieldUngroupedOption[]; hasMore?: boolean; }; export type MultiSelectFieldPageLazyLoader = (searchValue: string, pageNumber: number, pageSize: number) => MultiSelectFieldPageLazyResult | Promise; export type MultiSelectFieldGroupLazyResult = { options: MultiSelectFieldGroupedOption[]; hasMore?: boolean; }; export type MultiSelectFieldGroupLazyLoader = (searchValue: string, previousGroupKey: MultiSelectFieldGroupByValue | null) => MultiSelectFieldGroupLazyResult | Promise; export type MultiSelectFieldSearchProps = { /** * The current search value. * Not a preferred usage. You likely don't need to control the searchValue yourself. */ searchValue?: string; /** * Callback when the search value changes. * Not a preferred usage. Lean on the searchValue in the loadOptions function instead. */ onSearchChange?: (searchValue: string) => void; /** * The number of milliseconds to debounce the search input. */ debounceMs?: number; }; export type MultiSelectFieldPinnedOptionsSection = { options: MultiSelectFieldOption[] | ((searchValue: string) => MultiSelectFieldOption[] | Promise); label: string; /** * Whether to re-call the loader when search value changes. * Only applies when `options` is a function. Defaults to true. * When false, the loader is called once and the result is reused for all search values. * * You may wish to use this, for example, if you are loading a set of "favorites" options that should not be re-loaded when the search value changes. */ searchReactive?: boolean; /** * Maximum number of search results to cache per section. * Only applies when `options` is a function and `searchReactive` is true. * Oldest entries are evicted when the limit is reached. Defaults to 15. */ cacheSize?: number; }; /** * There are two ways to configure pinned options. * 1. A labeled pinned options object (e.g. "Favorites", "Recent", "AI Suggestions", etc.) * 2. An array of labeled pinned options objects (e.g. [{"label": "Favorites", "options": [...]}]) */ export type MultiSelectFieldPinnedOptions = MultiSelectFieldPinnedOptionsSection | MultiSelectFieldPinnedOptionsSection[]; type MultiSelectFieldCommonProps = { /** * The id of the multi-select field. */ id?: string; /** * The label of the multi-select field. */ label: string; /** * The placeholder of the multi-select field. */ placeholder?: string; /** * The size of the multi-select field. */ size?: Extract; /** * The selected options. Must be controlled state. */ value: MultiSelectFieldOption[]; /** * The callback to call when the selected options change. * @param options - The new array of selected options. */ onSelectedOptionsChange: (options: MultiSelectFieldOption[]) => void; /** * Defines the initial loading behavior of the options. Controls when loadOptions is called. * @default "auto" * @description "auto" default behavior, currently equivalent to "immediate". * @description "immediate" will load the initial options when the component is mounted. * @description "open" will load the initial options if/when the user opens the dropdown. */ initialLoad?: "auto" | "immediate" | "open"; /** * The options to pin to the top of the list. */ pinned?: MultiSelectFieldPinnedOptions; /** * Configuration for caching loadOptions results. * Caching is enabled by default. Set `{ enabled: false }` to disable. */ cache?: MultiSelectFieldCacheOptions; /** * The way to display the menu. * @default "auto" * @description "auto" will display the menu as a popover on mobile and a dialog on desktop. * @description "popover" will always display the menu as a popover. * @description "dialog" will always display the menu as a dialog. */ displayMenuAs?: "auto" | "popover" | "dialog"; /** * Error state for the field. Pass `true` to indicate error styling without a message. * Pass a string, string[], or ReactElement (deprecated) for error messages. */ error?: boolean | string | ReactElement | string[]; /** * Visually hides the label while keeping it accessible to screen readers. * @default false */ hideLabel?: boolean; /** * Hint text displayed below the input field. */ hint?: ReactElement | string; /** * Description text displayed below the input field. */ description?: ReactElement | string; /** * @deprecated No longer used. Error messages always use `aria-live="assertive"`. */ errorAriaLive?: AriaAttributes["aria-live"]; /** * Warning message(s) to display. Supports a single string or an array of strings. */ warning?: string | string[]; /** * Whether the field is required. Shows a red asterisk (*) next to the label. */ required?: boolean; /** * AI mark configuration to display next to the label. * Can be a boolean to show a simple AI mark, or an object with tooltip/popover configuration. */ labelAiMark?: AiMarkWithTooltipOrPopoverProps["aiMark"]; /** * Whether the field is disabled. * When disabled, the input is still focusable but menu items cannot be selected. */ disabled?: boolean; /** * Whether the field is read-only. * When read-only, the input is still focusable but menu items cannot be selected. */ readOnly?: boolean; /** * Content to display before the input field. */ prefix?: string | ReactElement; /** * Content to display after the input field. */ suffix?: string | ReactElement; /** * Configuration for the "Select All" option at the top of the list. * When provided, a "Select All" option will be shown while the search input is empty. * * Select All and Select Filtered are mutually exclusive: * - Select All is shown when the search input is empty. * - Select Filtered (if provided) is shown when a search term is active. */ selectAll?: { /** * The label to display for the "Select All" option. * @default "Select All" */ label?: string; /** * Callback when the "Select All" option is clicked. * The parent component is responsible for handling the select/deselect all logic. */ onClick: () => void; /** * Check state for the "Select All" option. * @default false */ checkState: boolean | CheckState; }; /** * Function that receives the current search value and returns configuration for * the "Select Filtered" option at the top of the list. * * When provided and a search term is active, a "Select Filtered" option will be shown * instead of "Select All". This allows selecting all options matching the current filter. * * Select All and Select Filtered are mutually exclusive: * - Select All (if provided) is shown when the search input is empty. * - Select Filtered is shown when a search term is active. * * @param searchValue - The current search input value * @returns Configuration object with label, onClick, and checkState * * @example * selectFiltered={(searchValue) => ({ * label: `Select items matching "${searchValue}"`, * onClick: () => handleSelectFiltered(searchValue), * checkState: allFilteredSelected ? "checked" : "unchecked", * })} */ selectFiltered?: (searchValue: string) => { /** The label to display. @default `Select items matching "${searchValue}"` */ label?: string; /** Callback when clicked. */ onClick: () => void; /** Check state for the option. */ checkState: boolean | CheckState; }; /** * When true, restricts the field to a single row height. * Overflow chips will be collapsed into a "+N" indicator. * @default false */ singleRow?: boolean; /** * Maximum number of chips to display before showing a "+N" indicator. * Applies regardless of fixedHeight setting. * @default 10 */ maxChips?: number; /** * Custom CSS class name for the wrapper element. */ className?: string; /** * Custom inline styles for the wrapper element. */ style?: CSSProperties; /** * Whether to virtualize the dropdown list using windowed rendering. * Enable this for large option sets to improve performance by only rendering visible items. * @default false */ virtualize?: boolean; /** * Whether to disable the search input. * When true, the input is replaced with a non-editable select trigger and the ARIA pattern * changes from combobox to listbox. * @default false */ disableSearch?: boolean; /** * Callback to customize chip props for each selected option. * Allows customizing chip appearance (color, icon, avatar, etc.) per option. * Core props (label, onClose, className, title, size) are managed by the component and excluded from this type. * @param option - The selected option the chip represents * @returns Partial chip props to merge onto the chip */ getChipProps?: (option: MultiSelectFieldOption) => MultiSelectFieldChipProps; } & MultiSelectFieldSearchProps & LayoutUtilProps; type MultiSelectFieldGroupingProps = { /** * Function to convert a group value to a display label. * Only used when options have a `group` property. * @param groupValue - The group value from the option's `group` property * @returns The formatted group label */ groupToString?: (groupValue: MultiSelectFieldGroupByValue) => string; /** * Custom comparator function to sort groups. * Receives two group values and returns a number indicating sort order. * Without this, groups appear in the order they are first encountered. * @param a - First group value to compare * @param b - Second group value to compare * @returns Negative if a < b, positive if a > b, zero if equal */ groupSorter?: (a: MultiSelectFieldGroupByValue, b: MultiSelectFieldGroupByValue) => number; }; type MultiSelectFieldNonGroupingProps = { /** * Incompatible with non-group lazy loading. */ groupToString?: never; /** * Incompatible with non-group lazy loading. */ groupSorter?: never; }; export type MultiSelectFieldPropsLazyPage = MultiSelectFieldCommonProps & { /** * Lazy loading mode using page-based pagination. * Options will be loaded on demand when the user scrolls to the bottom of the list. * This mode only supports flat options (i.e. cannot be used with grouped options). */ lazy: "page"; lazyOptions?: { pageSize?: number; }; /** * Function to load the options. */ loadOptions: MultiSelectFieldPageLazyLoader; } & MultiSelectFieldNonGroupingProps; export type MultiSelectFieldPropsLazyOffset = MultiSelectFieldCommonProps & { /** * Lazy loading mode using offset-based pagination. * Options will be loaded on demand when the user scrolls to the bottom of the list. * This mode only supports flat options (i.e. cannot be used with grouped options). */ lazy: "offset"; lazyOptions?: { limit?: number; }; /** * Function to load the options. */ loadOptions: MultiSelectFieldOffsetLazyLoader; } & MultiSelectFieldNonGroupingProps; export type MultiSelectFieldPropsLazyGroup = MultiSelectFieldCommonProps & { /** * Lazy loading mode using incremental group loading. * Groups will be loaded on demand when the user scrolls to the bottom of the list. * This mode supports grouped options. */ lazy: "group"; lazyOptions?: object; /** * Function to load the options. */ loadOptions: MultiSelectFieldGroupLazyLoader; } & MultiSelectFieldGroupingProps; export type MultiSelectFieldPropsEager = MultiSelectFieldCommonProps & { /** * Whether the options are lazy loaded. If true, the options will be loaded on demand when the user scrolls to the bottom of the list. * @default false */ lazy?: false; /** * Function to load the options. */ loadOptions: MultiSelectFieldEagerLoader; } & MultiSelectFieldGroupingProps; export type MultiSelectFieldPropsLazy = MultiSelectFieldPropsLazyPage | MultiSelectFieldPropsLazyOffset | MultiSelectFieldPropsLazyGroup; export type MultiSelectFieldProps = MultiSelectFieldPropsLazy | MultiSelectFieldPropsEager; export {};