@carbon/react
Version:
React components for the Carbon Design System
907 lines (891 loc) • 31.5 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
import cx from 'classnames';
import Downshift, { useCombobox, useMultipleSelection } from 'downshift';
import isEqual from 'react-fast-compare';
import PropTypes from 'prop-types';
import React, { forwardRef, useContext, useRef, useState, useMemo, useCallback, useLayoutEffect, useEffect, cloneElement } from 'react';
import { defaultFilterItems } from './filter.js';
import { sortingPropTypes } from './MultiSelectPropTypes.js';
import ListBox from '../ListBox/index.js';
import Checkbox from '../Checkbox/Checkbox.js';
import '../Checkbox/Checkbox.Skeleton.js';
import ListBoxSelection from '../ListBox/next/ListBoxSelection.js';
import ListBoxTrigger from '../ListBox/next/ListBoxTrigger.js';
import { Space, Enter, Delete, Escape, Tab, Home, End } from '../../internal/keyboard/keys.js';
import { match } from '../../internal/keyboard/match.js';
import { defaultItemToString } from './tools/itemToString.js';
import mergeRefs from '../../tools/mergeRefs.js';
import { deprecate } from '../../prop-types/deprecate.js';
import { useId } from '../../internal/useId.js';
import { defaultSortItems, defaultCompareItems } from './tools/sorting.js';
import { usePrefix } from '../../internal/usePrefix.js';
import '../FluidForm/FluidForm.js';
import { FormContext } from '../FluidForm/FormContext.js';
import { useSelection } from '../../internal/Selection.js';
import { useFloating, autoUpdate, flip, size, hide } from '@floating-ui/react';
import { AILabel } from '../AILabel/index.js';
import { isComponentElement } from '../../internal/utils.js';
import { ListBoxTypePropType, ListBoxSizePropType } from '../ListBox/ListBoxPropTypes.js';
const {
InputBlur,
InputKeyDownEnter,
ItemClick,
MenuMouseLeave,
InputKeyDownArrowUp,
InputKeyDownArrowDown,
ItemMouseMove,
InputClick,
ToggleButtonClick,
FunctionToggleMenu,
InputChange,
InputKeyDownEscape,
FunctionSetHighlightedIndex
} = useCombobox.stateChangeTypes;
const {
SelectedItemKeyDownBackspace,
SelectedItemKeyDownDelete,
DropdownKeyDownBackspace,
FunctionRemoveSelectedItem
} = useMultipleSelection.stateChangeTypes;
/**
* Message ids that will be passed to translateWithId().
* Combination of message ids from ListBox/next/ListBoxSelection.js and
* ListBox/next/ListBoxTrigger.js, but we can't access those values directly
* because those components aren't Typescript. (If you try, TranslationKey
* ends up just being defined as "string".)
*/
const FilterableMultiSelect = /*#__PURE__*/forwardRef(function FilterableMultiSelect({
autoAlign = false,
className: containerClassName,
clearSelectionDescription = 'Total items selected: ',
clearSelectionText = 'To clear selection, press Delete or Backspace',
compareItems = defaultCompareItems,
decorator,
direction = 'bottom',
disabled = false,
downshiftProps,
filterItems = defaultFilterItems,
helperText,
hideLabel,
id,
initialSelectedItems = [],
invalid,
invalidText,
items,
itemToElement: ItemToElement,
// needs to be capitalized for react to render it correctly
itemToString = defaultItemToString,
light,
locale = 'en',
onInputValueChange,
open = false,
onChange,
onMenuChange,
placeholder,
readOnly,
titleText,
type,
selectionFeedback = 'top-after-reopen',
selectedItems: selected,
size: size$1,
sortItems = defaultSortItems,
translateWithId,
useTitleInItem,
warn,
warnText,
slug,
inputProps
}, ref) {
const {
isFluid
} = useContext(FormContext);
const isFirstRender = useRef(true);
const [isFocused, setIsFocused] = useState(false);
const [isOpen, setIsOpen] = useState(!!open);
const [prevOpen, setPrevOpen] = useState(!!open);
const [inputValue, setInputValue] = useState('');
const [topItems, setTopItems] = useState(initialSelectedItems ?? []);
const [inputFocused, setInputFocused] = useState(false);
const filteredItems = useMemo(() => filterItems(items, {
itemToString,
inputValue
}), [items, inputValue, itemToString, filterItems]);
const nonSelectAllItems = useMemo(() => filteredItems.filter(item => !item.isSelectAll), [filteredItems]);
const selectAll = filteredItems.some(item => item.isSelectAll);
const {
selectedItems: controlledSelectedItems,
onItemChange,
clearSelection,
toggleAll
} = useSelection({
disabled,
initialSelectedItems,
onChange,
selectedItems: selected,
selectAll,
filteredItems
});
const selectAllStatus = useMemo(() => {
const selectable = nonSelectAllItems.filter(item => !item.disabled);
const nonSelectedCount = selectable.filter(item => !controlledSelectedItems.some(sel => isEqual(sel, item))).length;
const totalCount = selectable.length;
return {
checked: totalCount > 0 && nonSelectedCount === 0,
indeterminate: nonSelectedCount > 0 && nonSelectedCount < totalCount
};
}, [controlledSelectedItems, nonSelectAllItems]);
const handleSelectAllClick = useCallback(() => {
const selectable = nonSelectAllItems.filter(i => !i.disabled);
const {
checked,
indeterminate
} = selectAllStatus;
// clear all options if select-all state is checked or indeterminate
if (checked || indeterminate) {
const remainingSelectedItems = controlledSelectedItems.filter(sel => !filteredItems.some(e => isEqual(e, sel)));
toggleAll(remainingSelectedItems);
// select all options if select-all state is empty
} else {
const toSelect = selectable.filter(e => !controlledSelectedItems.some(sel => isEqual(sel, e)));
toggleAll([...controlledSelectedItems, ...toSelect]);
}
}, [nonSelectAllItems, selectAllStatus, controlledSelectedItems, toggleAll]);
const {
refs,
floatingStyles,
middlewareData
} = useFloating(autoAlign ? {
placement: direction,
// The floating element is positioned relative to its nearest
// containing block (usually the viewport). It will in many cases also
// “break” the floating element out of a clipping ancestor.
// https://floating-ui.com/docs/misc#clipping
strategy: 'fixed',
// Middleware order matters, arrow should be last
middleware: [flip({
crossAxis: false
}), size({
apply({
rects,
elements
}) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`
});
}
}), hide()],
whileElementsMounted: autoUpdate
} : {});
useLayoutEffect(() => {
if (autoAlign) {
const updatedFloatingStyles = {
...floatingStyles,
visibility: middlewareData.hide?.referenceHidden ? 'hidden' : 'visible'
};
Object.keys(updatedFloatingStyles).forEach(style => {
if (refs.floating.current) {
refs.floating.current.style[style] = updatedFloatingStyles[style];
}
});
}
}, [autoAlign, floatingStyles, refs.floating, middlewareData, open]);
const textInput = useRef(null);
const filterableMultiSelectInstanceId = useId();
const prefix = usePrefix();
if (prevOpen !== open) {
setIsOpen(open);
setPrevOpen(open);
}
// memoize sorted items to reduce unnecessary expensive sort on rerender
const sortedItems = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const selectAllItem = items.find(item => item.isSelectAll);
const selectableRealItems = nonSelectAllItems.filter(item => !item.disabled);
// Sort only non-select-all items, select-all item must stay at the top
const sortedReal = sortItems(nonSelectAllItems, {
selectedItems: {
top: controlledSelectedItems,
fixed: [],
'top-after-reopen': topItems
}[selectionFeedback],
itemToString,
compareItems,
locale
});
// Only show select-all-item if there exist non-disabled filtered items to select
if (selectAllItem && selectableRealItems.length > 0) {
return [selectAllItem, ...sortedReal];
}
return sortedReal;
}, [items, inputValue, controlledSelectedItems, topItems, selectionFeedback, itemToString, compareItems, locale, sortItems, nonSelectAllItems]);
const inline = type === 'inline';
const showWarning = !invalid && warn;
const wrapperClasses = cx(`${prefix}--multi-select__wrapper`, `${prefix}--multi-select--filterable__wrapper`, `${prefix}--list-box__wrapper`, containerClassName, {
[`${prefix}--multi-select__wrapper--inline`]: inline,
[`${prefix}--list-box__wrapper--inline`]: inline,
[`${prefix}--multi-select__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid,
[`${prefix}--list-box--up`]: direction === 'top',
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator,
[`${prefix}--autoalign`]: autoAlign
});
const helperId = !helperText ? undefined : `filterablemultiselect-helper-text-${filterableMultiSelectInstanceId}`;
const labelId = `${id}-label`;
const titleClasses = cx({
[`${prefix}--label`]: true,
[`${prefix}--label--disabled`]: disabled,
[`${prefix}--visually-hidden`]: hideLabel
});
const helperClasses = cx({
[`${prefix}--form__helper-text`]: true,
[`${prefix}--form__helper-text--disabled`]: disabled
});
const inputClasses = cx({
[`${prefix}--text-input`]: true,
[`${prefix}--text-input--empty`]: !inputValue,
[`${prefix}--text-input--light`]: light
});
const helper = helperText ? /*#__PURE__*/React.createElement("div", {
id: helperId,
className: helperClasses
}, helperText) : null;
const menuId = `${id}__menu`;
const inputId = `${id}-input`;
useEffect(() => {
if (!isOpen) {
setTopItems(controlledSelectedItems);
}
}, [controlledSelectedItems, isOpen, setTopItems]);
const validateHighlightFocus = () => {
if (controlledSelectedItems.length > 0) {
setHighlightedIndex(0);
}
};
function handleMenuChange(forceIsOpen) {
if (!readOnly) {
const nextIsOpen = forceIsOpen ?? !isOpen;
setIsOpen(nextIsOpen);
validateHighlightFocus();
}
}
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
if (open) {
onMenuChange?.(isOpen);
}
} else {
onMenuChange?.(isOpen);
}
}, [isOpen, onMenuChange, open]);
useEffect(() => {
const handleClickOutside = event => {
const target = event.target;
const wrapper = document.getElementById(id)?.closest(`.${prefix}--multi-select__wrapper`);
// If click is outside our component and menu is open or input is focused
if (wrapper && !wrapper.contains(target)) {
if (isOpen || inputFocused) {
setIsOpen(false);
setInputFocused(false);
setInputValue('');
}
}
};
if (inputFocused || isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, inputFocused]);
const {
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
setHighlightedIndex,
getItemProps,
openMenu,
isOpen: isMenuOpen
} = useCombobox({
isOpen,
items: sortedItems,
// defaultHighlightedIndex: 0, // after selection, highlight the first item.
itemToString,
id,
labelId,
menuId,
inputId,
inputValue,
stateReducer,
isItemDisabled(item, _index) {
return item?.disabled;
}
});
function stateReducer(state, actionAndChanges) {
const {
type,
props,
changes
} = actionAndChanges;
const {
highlightedIndex
} = changes;
if (changes.isOpen && !isOpen) {
setTopItems(controlledSelectedItems);
}
switch (type) {
case InputKeyDownEnter:
if (sortedItems.length === 0) {
return changes;
}
if (changes.selectedItem && changes.selectedItem.disabled !== true) {
if (changes.selectedItem.isSelectAll) {
handleSelectAllClick();
} else {
onItemChange(changes.selectedItem);
}
}
setHighlightedIndex(changes.selectedItem);
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case ItemClick:
if (changes.selectedItem.isSelectAll) {
handleSelectAllClick();
} else {
onItemChange(changes.selectedItem);
}
setHighlightedIndex(changes.selectedItem);
return changes;
case InputBlur:
case InputKeyDownEscape:
setIsOpen(false);
return changes;
case FunctionToggleMenu:
case ToggleButtonClick:
validateHighlightFocus();
if (changes.isOpen && !changes.selectedItem) {
return {
...changes
};
}
return {
...changes,
highlightedIndex: controlledSelectedItems.length > 0 ? 0 : -1
};
case InputChange:
if (onInputValueChange) {
onInputValueChange(changes.inputValue);
}
setInputValue(changes.inputValue ?? '');
setIsOpen(true);
return {
...changes,
highlightedIndex: 0
};
case InputClick:
setIsOpen(changes.isOpen || false);
validateHighlightFocus();
if (changes.isOpen && !changes.selectedItem) {
return {
...changes
};
}
return {
...changes,
isOpen: false,
highlightedIndex: controlledSelectedItems.length > 0 ? 0 : -1
};
case MenuMouseLeave:
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case InputKeyDownArrowUp:
case InputKeyDownArrowDown:
if (InputKeyDownArrowDown === type && !isOpen) {
setIsOpen(true);
return {
...changes,
highlightedIndex: 0
};
}
if (highlightedIndex > -1) {
const itemArray = document.querySelectorAll(`li.${prefix}--list-box__menu-item[role="option"]`);
props.scrollIntoView(itemArray[highlightedIndex]);
}
if (highlightedIndex === -1) {
return {
...changes,
highlightedIndex: 0
};
}
return changes;
case ItemMouseMove:
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case FunctionSetHighlightedIndex:
if (!isOpen) {
return {
...changes,
highlightedIndex: 0
};
} else {
return {
...changes,
highlightedIndex: props.items.indexOf(highlightedIndex)
};
}
default:
return changes;
}
}
const {
getDropdownProps
} = useMultipleSelection({
activeIndex: highlightedIndex,
initialSelectedItems,
selectedItems: controlledSelectedItems,
onStateChange(changes) {
switch (changes.type) {
case SelectedItemKeyDownBackspace:
case SelectedItemKeyDownDelete:
case DropdownKeyDownBackspace:
case FunctionRemoveSelectedItem:
{
clearSelection();
break;
}
}
},
...downshiftProps
});
useEffect(() => {
if (isOpen && !isMenuOpen) {
openMenu();
}
});
function clearInputValue(event) {
const value = textInput.current?.value;
if (value?.length === 1 || event && 'key' in event && match(event, Escape)) {
setInputValue('');
} else {
setInputValue(value ?? '');
}
if (textInput.current) {
textInput.current.focus();
}
}
// AILabel always size `mini`
const candidate = slug ?? decorator;
const candidateIsAILabel = isComponentElement(candidate, AILabel);
const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/cloneElement(candidate, {
size: 'mini'
}) : null;
// exclude the select-all item from the count
const selectedItemsLength = controlledSelectedItems.filter(item => !item.isSelectAll).length;
const className = cx(`${prefix}--multi-select`, `${prefix}--combo-box`, `${prefix}--multi-select--filterable`, {
[`${prefix}--multi-select--invalid`]: invalid,
[`${prefix}--multi-select--invalid--focused`]: invalid && inputFocused,
[`${prefix}--multi-select--open`]: isOpen,
[`${prefix}--multi-select--inline`]: inline,
[`${prefix}--multi-select--selected`]: controlledSelectedItems?.length > 0,
[`${prefix}--multi-select--filterable--input-focused`]: inputFocused,
[`${prefix}--multi-select--readonly`]: readOnly,
[`${prefix}--multi-select--selectall`]: selectAll
});
const labelProps = getLabelProps();
const buttonProps = getToggleButtonProps({
disabled,
onClick: () => {
handleMenuChange(!isOpen);
textInput.current?.focus();
},
// When we moved the "root node" of Downshift to the <input> for
// ARIA 1.2 compliance, we unfortunately hit this branch for the
// "mouseup" event that downshift listens to:
// https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
//
// As a result, it will reset the state of the component and so we
// stop the event from propagating to prevent this. This allows the
// toggleMenu behavior for the toggleButton to correctly open and
// close the menu.
onMouseUp(event) {
if (isOpen) {
event.stopPropagation();
}
}
});
const inputProp = getInputProps(getDropdownProps({
'aria-controls': isOpen ? menuId : undefined,
'aria-describedby': helperText ? helperId : undefined,
'aria-haspopup': 'listbox',
// Remove excess aria `aria-labelledby`. HTML <label for>
// provides this aria information.
'aria-labelledby': undefined,
disabled,
placeholder,
preventKeyAction: isOpen,
...inputProps,
onClick: () => handleMenuChange(true),
onKeyDown(event) {
const $input = event.target;
const $value = $input.value;
if (match(event, Space)) {
event.stopPropagation();
}
if (match(event, Enter)) {
handleMenuChange(true);
}
if (!disabled) {
if (match(event, Delete) || match(event, Escape)) {
if (isOpen) {
handleMenuChange(true);
clearInputValue(event);
event.stopPropagation();
} else if (!isOpen) {
clearInputValue(event);
clearSelection();
event.stopPropagation();
}
}
}
if (match(event, Tab)) {
handleMenuChange(false);
}
if (match(event, Home)) {
$input.setSelectionRange(0, 0);
}
if (match(event, End)) {
$input.setSelectionRange($value.length, $value.length);
}
},
onFocus: () => setInputFocused(true),
onBlur: () => {
setInputFocused(false);
setInputValue('');
}
}));
// Memoize the value of getMenuProps to avoid an infinite loop
const menuProps = useMemo(() => getMenuProps({
ref: autoAlign ? refs.setFloating : null
}, {
suppressRefError: true
}), [autoAlign, getMenuProps, refs.setFloating]);
const handleFocus = evt => {
if (evt?.target.classList.contains(`${prefix}--tag__close-icon`) || evt?.target.classList.contains(`${prefix}--list-box__selection`)) {
setIsFocused(false);
} else {
setIsFocused(evt?.type === 'focus' ? true : false);
}
};
const mergedRef = mergeRefs(textInput, inputProp.ref);
const readOnlyEventHandlers = readOnly ? {
onClick: evt => {
// NOTE: does not prevent click
evt.preventDefault();
// focus on the element as per readonly input behavior
if (mergedRef.current !== undefined) {
mergedRef.current.focus();
}
},
onKeyDown: evt => {
const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter'];
// This prevents the select from opening for the above keys
if (selectAccessKeys.includes(evt.key)) {
evt.preventDefault();
}
}
} : {};
const clearSelectionContent = controlledSelectedItems.length > 0 ? `${clearSelectionDescription} ${controlledSelectedItems.length}. ${clearSelectionText}.` : `${clearSelectionDescription} 0.`;
return /*#__PURE__*/React.createElement("div", {
className: wrapperClasses
}, titleText ? /*#__PURE__*/React.createElement("label", _extends({
className: titleClasses
}, labelProps), titleText, /*#__PURE__*/React.createElement("span", {
className: `${prefix}--visually-hidden`
}, clearSelectionContent)) : null, /*#__PURE__*/React.createElement(ListBox, {
onFocus: isFluid ? handleFocus : undefined,
onBlur: isFluid ? handleFocus : undefined,
className: className,
disabled: disabled,
light: light,
ref: ref,
id: id,
invalid: invalid,
invalidText: invalidText,
warn: warn,
warnText: warnText,
isOpen: !readOnly && isOpen,
size: size$1
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--list-box__field`,
ref: autoAlign ? refs.setReference : null
}, controlledSelectedItems.length > 0 && /*#__PURE__*/React.createElement(ListBoxSelection, {
readOnly: readOnly,
clearSelection: () => {
clearSelection();
if (textInput.current) {
textInput.current.focus();
}
},
selectionCount: selectedItemsLength,
translateWithId: translateWithId,
disabled: disabled
}), /*#__PURE__*/React.createElement("input", _extends({
className: inputClasses
}, inputProp, {
ref: mergedRef
}, readOnlyEventHandlers, {
readOnly: readOnly
})), invalid && /*#__PURE__*/React.createElement(WarningFilled, {
className: `${prefix}--list-box__invalid-icon`
}), showWarning && /*#__PURE__*/React.createElement(WarningAltFilled, {
className: `${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`
}), inputValue && /*#__PURE__*/React.createElement(ListBoxSelection, {
clearSelection: clearInputValue,
disabled: disabled,
translateWithId: translateWithId,
readOnly: readOnly,
onMouseUp: event => {
// If we do not stop this event from propagating,
// it seems like Downshift takes our event and
// prevents us from getting `onClick` /
// `clearSelection` from the underlying <button> in
// ListBoxSelection
event.stopPropagation();
}
}), /*#__PURE__*/React.createElement(ListBoxTrigger, _extends({}, buttonProps, {
isOpen: isOpen,
translateWithId: translateWithId
}))), slug ? normalizedDecorator : decorator ? /*#__PURE__*/React.createElement("div", {
className: `${prefix}--list-box__inner-wrapper--decorator`
}, normalizedDecorator) : '', /*#__PURE__*/React.createElement(ListBox.Menu, menuProps, isOpen ? sortedItems.map((item, index) => {
let isChecked;
let isIndeterminate = false;
if (item.isSelectAll) {
isChecked = selectAllStatus.checked;
isIndeterminate = selectAllStatus.indeterminate;
} else {
isChecked = controlledSelectedItems.filter(selected => isEqual(selected, item)).length > 0;
}
const itemProps = getItemProps({
item,
['aria-selected']: isChecked
});
const itemText = itemToString(item);
// The initial implementation using <Downshift> would place the disabled attribute
// on disabled menu items. Conversely, useCombobox places aria-disabled instead.
// To avoid any potential breaking changes, we avoid placing aria-disabled and
// instead match the old behavior of placing the disabled attribute.
const disabled = itemProps['aria-disabled'];
const {
'aria-disabled': unusedAriaDisabled,
// eslint-disable-line @typescript-eslint/no-unused-vars
...modifiedItemProps
} = itemProps;
return /*#__PURE__*/React.createElement(ListBox.MenuItem, _extends({
key: itemProps.id,
"aria-label": itemText,
isActive: isChecked && !item['isSelectAll'],
isHighlighted: highlightedIndex === index,
title: itemText,
disabled: disabled
}, modifiedItemProps), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--checkbox-wrapper`
}, /*#__PURE__*/React.createElement(Checkbox, {
id: `${itemProps.id}-item`,
labelText: ItemToElement ? /*#__PURE__*/React.createElement(ItemToElement, _extends({
key: itemProps.id
}, item)) : itemText,
checked: isChecked,
title: useTitleInItem ? itemText : undefined,
indeterminate: isIndeterminate,
disabled: disabled,
tabIndex: -1
})));
}) : null)), !inline && !invalid && !warn ? helper : null);
});
FilterableMultiSelect.displayName = 'FilterableMultiSelect';
FilterableMultiSelect.propTypes = {
/**
* Deprecated, aria-label is no longer needed
* Specify a label to be read by screen readers on the container node
*/
['aria-label']: deprecate(PropTypes.string, 'ariaLabel / aria-label props are no longer required for FilterableMultiSelect'),
/**
* Deprecated, please use `aria-label` instead.
* Specify a label to be read by screen readers on the container note.
*/
ariaLabel: deprecate(PropTypes.string, 'ariaLabel / aria-label props are no longer required for FilterableMultiSelect'),
/**
* **Experimental**: Will attempt to automatically align the floating
* element to avoid collisions with the viewport and being clipped by
* ancestor elements. Requires React v17+
* @see https://github.com/carbon-design-system/carbon/issues/18714
*/
autoAlign: PropTypes.bool,
/**
* Specify the text that should be read for screen readers that describes total items selected
*/
clearSelectionDescription: PropTypes.string,
/**
* Specify the text that should be read for screen readers to clear selection.
*/
clearSelectionText: PropTypes.string,
/**
* **Experimental**: Provide a decorator component to be rendered inside the `FilterableMultiSelect` component
*/
decorator: PropTypes.node,
/**
* Provide a method that filters the dropdown options based on the current input. Overriding this
* prop means that you have to handle the filtering logic when the user types in the text input.
* Otherwise, a default built-in filtering function will be used.
*/
filterItems: PropTypes.func,
/**
* Specify the direction of the multiselect dropdown. Can be either top or bottom.
*/
direction: PropTypes.oneOf(['top', 'bottom']),
/**
* Disable the control
*/
disabled: PropTypes.bool,
/**
* Additional props passed to Downshift.
*
* **Use with caution:** anything you define here overrides the components'
* internal handling of that prop. Downshift APIs and internals are subject to
* change, and in some cases they can not be shimmed by Carbon to shield you
* from potentially breaking changes.
*/
// @ts-ignore
downshiftProps: PropTypes.shape(Downshift.propTypes),
/**
* Specify whether the title text should be hidden or not
*/
hideLabel: PropTypes.bool,
/**
* Specify a custom `id`
*/
id: PropTypes.string.isRequired,
/**
* Allow users to pass in arbitrary items from their collection that are
* pre-selected
*/
initialSelectedItems: PropTypes.array,
/**
* Is the current selection invalid?
*/
invalid: PropTypes.bool,
/**
* If invalid, what is the error?
*/
invalidText: PropTypes.node,
/**
* Function to render items as custom components instead of strings.
* Defaults to null and is overridden by a getter
*/
itemToElement: PropTypes.func,
/**
* Helper function passed to downshift that allows the library to render a
* given item to a string label. By default, it extracts the `label` field
* from a given item to serve as the item label in the list.
*/
itemToString: PropTypes.func,
/**
* We try to stay as generic as possible here to allow individuals to pass
* in a collection of whatever kind of data structure they prefer
*/
items: PropTypes.array.isRequired,
/**
* `true` to use the light version.
*/
light: deprecate(PropTypes.bool, 'The `light` prop for `FilterableMultiSelect` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'),
/**
* Specify the locale of the control. Used for the default `compareItems`
* used for sorting the list of items in the control.
*/
locale: PropTypes.string,
/**
* `onChange` is a utility for this controlled component to communicate to a
* consuming component what kind of internal state changes are occurring.
*/
onChange: PropTypes.func,
/**
* `onInputValueChange` is a utility for this controlled component to communicate to
* the currently typed input.
*/
onInputValueChange: PropTypes.func,
/**
* `onMenuChange` is a utility for this controlled component to communicate to a
* consuming component that the menu was opened(`true`)/closed(`false`).
*/
onMenuChange: PropTypes.func,
/**
* Initialize the component with an open(`true`)/closed(`false`) menu.
*/
open: PropTypes.bool,
/**
* Generic `placeholder` that will be used as the textual representation of
* what this field is for
*/
placeholder: PropTypes.string,
/**
* Specify feedback (mode) of the selection.
* `top`: selected item jumps to top
* `fixed`: selected item stays at it's position
* `top-after-reopen`: selected item jump to top after reopen dropdown
*/
selectionFeedback: PropTypes.oneOf(['top', 'fixed', 'top-after-reopen']),
/**
* Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
*/
size: ListBoxSizePropType,
slug: deprecate(PropTypes.node, 'The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead.'),
...sortingPropTypes,
/**
* Provide text to be used in a `<label>` element that is tied to the
* combobox via ARIA attributes.
*/
titleText: PropTypes.node,
/**
* Callback function for translating ListBoxMenuIcon SVG title
*/
translateWithId: PropTypes.func,
type: ListBoxTypePropType,
/**
* Specify title to show title on hover
*/
useTitleInItem: PropTypes.bool,
/**
* Specify whether the control is currently in warning state
*/
warn: PropTypes.bool,
/**
* Provide the text that is displayed when the control is in warning state
*/
warnText: PropTypes.node,
/**
* Specify native input attributes to place on the `<input>`, like maxLength.
* These are passed to downshift's getInputProps() and will override the
* internal input props.
* https://github.com/downshift-js/downshift?tab=readme-ov-file#getinputprops
*/
inputProps: PropTypes.object
};
export { FilterableMultiSelect };