@carbon/react
Version:
React components for the Carbon Design System
941 lines (922 loc) • 32.6 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 cx from 'classnames';
import { useCombobox } from 'downshift';
import PropTypes from 'prop-types';
import React, { forwardRef, useRef, useEffect, useState, useContext, useCallback, useMemo, cloneElement } from 'react';
import '../Text/index.js';
import { WarningFilled, WarningAltFilled, Checkmark } from '@carbon/icons-react';
import isEqual from 'react-fast-compare';
import ListBox from '../ListBox/index.js';
import ListBoxSelection from '../ListBox/next/ListBoxSelection.js';
import ListBoxTrigger from '../ListBox/next/ListBoxTrigger.js';
import { Space, Enter, Escape, Home, End } from '../../internal/keyboard/keys.js';
import { match } from '../../internal/keyboard/match.js';
import { useId } from '../../internal/useId.js';
import mergeRefs from '../../tools/mergeRefs.js';
import { deprecate } from '../../prop-types/deprecate.js';
import { usePrefix } from '../../internal/usePrefix.js';
import '../FluidForm/FluidForm.js';
import { FormContext } from '../FluidForm/FormContext.js';
import { useFloating, autoUpdate, flip, hide } from '@floating-ui/react';
import { useFeatureFlag } from '../FeatureFlags/index.js';
import { AILabel } from '../AILabel/index.js';
import { isComponentElement } from '../../internal/utils.js';
import { ListBoxSizePropType } from '../ListBox/ListBoxPropTypes.js';
import { Text } from '../Text/Text.js';
const {
InputBlur,
InputKeyDownEnter,
FunctionToggleMenu,
ToggleButtonClick,
ItemMouseMove,
InputKeyDownArrowUp,
InputKeyDownArrowDown,
MenuMouseLeave,
ItemClick,
FunctionSelectItem
} = useCombobox.stateChangeTypes;
const defaultItemToString = item => {
if (typeof item === 'string') {
return item;
}
if (typeof item === 'number') {
return `${item}`;
}
if (item !== null && typeof item === 'object' && 'label' in item && typeof item['label'] === 'string') {
return item['label'];
}
return '';
};
const defaultShouldFilterItem = () => true;
const autocompleteCustomFilter = ({
item,
inputValue
}) => {
if (inputValue === null || inputValue === '') {
return true; // Show all items if there's no input
}
const lowercaseItem = item.toLowerCase();
const lowercaseInput = inputValue.toLowerCase();
return lowercaseItem.startsWith(lowercaseInput);
};
const getInputValue = ({
initialSelectedItem,
itemToString,
selectedItem,
prevSelectedItem
}) => {
// If there's a current selection (even if it's an object or string), use it.
if (selectedItem !== null && typeof selectedItem !== 'undefined') {
return itemToString(selectedItem);
}
// On the very first render (when no previous value exists), use
// `initialSelectedItem`.
if (typeof prevSelectedItem === 'undefined' && initialSelectedItem !== null && typeof initialSelectedItem !== 'undefined') {
return itemToString(initialSelectedItem);
}
// Otherwise (i.e., after the user has cleared the selection), return an empty
// string.
return '';
};
const findHighlightedIndex = ({
items,
itemToString = defaultItemToString
}, inputValue) => {
if (!inputValue) {
return -1;
}
const searchValue = inputValue.toLowerCase();
for (let i = 0; i < items.length; i++) {
const item = itemToString(items[i]).toLowerCase();
if (!items[i]['disabled'] && item.indexOf(searchValue) !== -1) {
return i;
}
}
return -1;
};
/**
* 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 ComboBox = /*#__PURE__*/forwardRef((props, ref) => {
const prevInputLengthRef = useRef(0);
const inputRef = useRef(null);
const {
['aria-label']: ariaLabel = 'Choose an item',
ariaLabel: deprecatedAriaLabel,
autoAlign = false,
className: containerClassName,
decorator,
direction = 'bottom',
disabled = false,
downshiftActions,
downshiftProps,
helperText,
id,
initialSelectedItem,
invalid,
invalidText,
items,
itemToElement = null,
itemToString = defaultItemToString,
light,
onChange,
onInputChange,
onToggleClick,
placeholder,
readOnly,
selectedItem: selectedItemProp,
shouldFilterItem = defaultShouldFilterItem,
size,
titleText,
translateWithId,
typeahead = false,
warn,
warnText,
allowCustomValue = false,
slug,
inputProps,
...rest
} = props;
const enableFloatingStyles = useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign;
const {
refs,
floatingStyles,
middlewareData
} = useFloating(enableFloatingStyles ? {
placement: direction,
strategy: 'fixed',
middleware: autoAlign ? [flip(), hide()] : undefined,
whileElementsMounted: autoUpdate
} : {});
const parentWidth = refs?.reference?.current?.clientWidth;
useEffect(() => {
if (enableFloatingStyles) {
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];
}
});
if (parentWidth && refs.floating.current) {
refs.floating.current.style.width = parentWidth + 'px';
}
}
}, [enableFloatingStyles, floatingStyles, refs.floating, parentWidth]);
const [inputValue, setInputValue] = useState(getInputValue({
initialSelectedItem,
itemToString,
selectedItem: selectedItemProp
}));
const [typeaheadText, setTypeaheadText] = useState('');
useEffect(() => {
if (typeahead) {
if (inputValue.length >= prevInputLengthRef.current) {
if (inputValue) {
const filteredItems = items.filter(item => autocompleteCustomFilter({
item: itemToString(item),
inputValue: inputValue
}));
if (filteredItems.length > 0) {
const suggestion = itemToString(filteredItems[0]);
setTypeaheadText(suggestion.slice(inputValue.length));
} else {
setTypeaheadText('');
}
} else {
setTypeaheadText('');
}
} else {
setTypeaheadText('');
}
prevInputLengthRef.current = inputValue.length;
}
}, [typeahead, inputValue, items, itemToString, autocompleteCustomFilter]);
const isManualClearingRef = useRef(false);
const [isClearing, setIsClearing] = useState(false);
const prefix = usePrefix();
const {
isFluid
} = useContext(FormContext);
const textInput = useRef(null);
const comboBoxInstanceId = useId();
const [isFocused, setIsFocused] = useState(false);
const prevInputValue = useRef(inputValue);
const prevSelectedItemProp = useRef(selectedItemProp);
useEffect(() => {
isManualClearingRef.current = isClearing;
// Reset flag after render cycle
if (isClearing) {
setIsClearing(false);
}
}, [isClearing]);
// fully controlled combobox: handle changes to selectedItemProp
useEffect(() => {
if (prevSelectedItemProp.current !== selectedItemProp) {
const currentInputValue = getInputValue({
initialSelectedItem,
itemToString,
selectedItem: selectedItemProp,
prevSelectedItem: prevSelectedItemProp.current
});
// selectedItem has been updated externally, need to update state and call onChange
if (inputValue !== currentInputValue) {
setInputValue(currentInputValue);
onChange({
selectedItem: selectedItemProp,
inputValue: currentInputValue
});
}
prevSelectedItemProp.current = selectedItemProp;
}
}, [selectedItemProp]);
const filterItems = (items, itemToString, inputValue) => items.filter(item => typeahead ? autocompleteCustomFilter({
item: itemToString(item),
inputValue
}) : shouldFilterItem ? shouldFilterItem({
item,
itemToString,
inputValue
}) : defaultShouldFilterItem());
// call onInputChange whenever inputValue is updated
useEffect(() => {
if (prevInputValue.current !== inputValue) {
prevInputValue.current = inputValue;
onInputChange && onInputChange(inputValue);
}
}, [inputValue]);
const handleSelectionClear = () => {
if (textInput?.current) {
textInput.current.focus();
}
};
const filteredItems = inputValue => filterItems(items, itemToString, inputValue || null);
const indexToHighlight = inputValue => findHighlightedIndex({
...props,
items: filteredItems(inputValue)
}, inputValue);
const stateReducer = useCallback((state, actionAndChanges) => {
const {
type,
changes
} = actionAndChanges;
const {
highlightedIndex
} = changes;
switch (type) {
case InputBlur:
{
if (allowCustomValue && highlightedIndex == '-1') {
const customValue = inputValue;
changes.selectedItem = customValue;
if (onChange) {
onChange({
selectedItem: inputValue,
inputValue
});
}
return changes;
}
if (state.inputValue && highlightedIndex == '-1' && changes.selectedItem) {
return {
...changes,
inputValue: itemToString(changes.selectedItem)
};
}
if (state.inputValue && highlightedIndex == '-1' && !allowCustomValue && !changes.selectedItem) {
return {
...changes,
inputValue: ''
};
}
return changes;
}
case InputKeyDownEnter:
if (!allowCustomValue) {
if (state.highlightedIndex !== -1) {
const filteredList = filterItems(items, itemToString, inputValue);
const highlightedItem = filteredList[state.highlightedIndex];
if (highlightedItem && !highlightedItem.disabled) {
return {
...changes,
selectedItem: highlightedItem,
inputValue: itemToString(highlightedItem)
};
}
} else {
const autoIndex = indexToHighlight(inputValue);
if (autoIndex !== -1) {
const matchingItem = items[autoIndex];
if (matchingItem && !matchingItem.disabled) {
return {
...changes,
selectedItem: matchingItem,
inputValue: itemToString(matchingItem)
};
}
}
// If no matching item is found and there is an existing
// selection, clear the selection.
if (state.selectedItem !== null) {
return {
...changes,
selectedItem: null,
inputValue
};
}
}
}
// For `allowCustomValue` or if no matching item is found, keep the
// menu open.
return {
...changes,
isOpen: true
};
case FunctionToggleMenu:
case ToggleButtonClick:
if (!changes.isOpen && state.inputValue && highlightedIndex === -1 && !allowCustomValue) {
return {
...changes,
inputValue: '' // Clear the input
};
}
if (changes.isOpen && !changes.selectedItem) {
return {
...changes
};
}
return changes;
case MenuMouseLeave:
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case InputKeyDownArrowUp:
case InputKeyDownArrowDown:
if (highlightedIndex === -1) {
return {
...changes,
highlightedIndex: 0
};
}
return changes;
case ItemMouseMove:
return {
...changes,
highlightedIndex: state.highlightedIndex
};
default:
return changes;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[allowCustomValue, inputValue, onChange]);
const handleToggleClick = isOpen => event => {
if (onToggleClick) {
onToggleClick(event);
}
if (readOnly) {
// Prevent the list from opening if readOnly is true
event.preventDownshiftDefault = true;
event?.persist?.();
return;
}
if (event.target === textInput.current && isOpen) {
event.preventDownshiftDefault = true;
event?.persist?.();
}
};
const showWarning = !invalid && warn;
const className = cx(`${prefix}--combo-box`, {
[`${prefix}--combo-box--invalid--focused`]: invalid && isFocused,
[`${prefix}--list-box--up`]: direction === 'top',
[`${prefix}--combo-box--warning`]: showWarning,
[`${prefix}--combo-box--readonly`]: readOnly,
[`${prefix}--autoalign`]: enableFloatingStyles
});
const titleClasses = cx(`${prefix}--label`, {
[`${prefix}--label--disabled`]: disabled
});
const helperTextId = `combobox-helper-text-${comboBoxInstanceId}`;
const warnTextId = `combobox-warn-text-${comboBoxInstanceId}`;
const invalidTextId = `combobox-invalid-text-${comboBoxInstanceId}`;
const helperClasses = cx(`${prefix}--form__helper-text`, {
[`${prefix}--form__helper-text--disabled`]: disabled
});
const wrapperClasses = cx(`${prefix}--list-box__wrapper`, [containerClassName, {
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator
}]);
const inputClasses = cx(`${prefix}--text-input`, {
[`${prefix}--text-input--empty`]: !inputValue,
[`${prefix}--combo-box--input--focus`]: isFocused
});
// needs to be Capitalized for react to render it correctly
const ItemToElement = itemToElement;
// AILabel always size `mini`
const candidate = slug ?? decorator;
const candidateIsAILabel = isComponentElement(candidate, AILabel);
const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/cloneElement(candidate, {
size: 'mini'
}) : null;
const {
// Prop getters
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
getToggleButtonProps,
// State
isOpen,
highlightedIndex,
selectedItem,
// Actions
closeMenu,
openMenu,
reset,
selectItem,
setHighlightedIndex,
setInputValue: downshiftSetInputValue,
toggleMenu
} = useCombobox({
items: filterItems(items, itemToString, inputValue),
inputValue: inputValue,
itemToString: item => {
return itemToString(item);
},
onInputValueChange({
inputValue
}) {
const normalizedInput = inputValue || '';
setInputValue(normalizedInput);
setHighlightedIndex(indexToHighlight(normalizedInput));
},
onHighlightedIndexChange: ({
highlightedIndex
}) => {
if (highlightedIndex > -1 && typeof window !== undefined) {
const itemArray = document.querySelectorAll(`li.${prefix}--list-box__menu-item[role="option"]`);
const highlightedItem = itemArray[highlightedIndex];
if (highlightedItem) {
highlightedItem.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}
},
initialSelectedItem: initialSelectedItem,
inputId: id,
stateReducer,
isItemDisabled(item, _index) {
return item?.disabled;
},
...downshiftProps,
onStateChange: ({
type,
selectedItem: newSelectedItem
}) => {
downshiftProps?.onStateChange?.({
type,
selectedItem: newSelectedItem
});
if (isManualClearingRef.current) {
return;
}
if ((type === ItemClick || type === FunctionSelectItem || type === InputKeyDownEnter) && typeof newSelectedItem !== 'undefined' && !isEqual(selectedItemProp, newSelectedItem)) {
onChange({
selectedItem: newSelectedItem
});
}
}
});
useEffect(() => {
// Used to expose the downshift actions to consumers for use with downshiftProps
// An odd pattern, here we mutate the value stored in the ref provided from the consumer.
// A riff of https://gist.github.com/gaearon/1a018a023347fe1c2476073330cc5509
if (downshiftActions) {
downshiftActions.current = {
closeMenu,
openMenu,
reset,
selectItem,
setHighlightedIndex,
setInputValue: downshiftSetInputValue,
toggleMenu
};
}
}, [closeMenu, openMenu, reset, selectItem, setHighlightedIndex, downshiftSetInputValue, toggleMenu]);
const buttonProps = getToggleButtonProps({
disabled: disabled || readOnly,
onClick: handleToggleClick(isOpen),
// 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 if the menu is already open.
// This allows the toggleMenu behavior for the toggleButton to correctly open and
// close the menu.
onMouseUp(event) {
if (isOpen) {
event.stopPropagation();
}
}
});
const handleFocus = evt => {
setIsFocused(evt.type === 'focus');
if (!inputRef.current?.value && evt.type === 'blur') {
selectItem(null);
}
};
const readOnlyEventHandlers = readOnly ? {
onKeyDown: evt => {
// This prevents the select from opening for the above keys
if (evt.key !== 'Tab') {
evt.preventDefault();
}
},
onClick: evt => {
// Prevent the default behavior which would open the list
evt.preventDefault();
// Focus on the element as per readonly input behavior
evt.currentTarget.focus();
}
} : {};
// The input should be described by the appropriate message text id
// when both the message is supplied *and* when the component is in
// the matching state (invalid, warn, etc).
const ariaDescribedBy = invalid && invalidText && invalidTextId || warn && warnText && warnTextId || helperText && !isFluid && helperTextId || undefined;
// Memoize the value of getMenuProps to avoid an infinite loop
const menuProps = useMemo(() => getMenuProps({
ref: enableFloatingStyles ? refs.setFloating : null
}), [enableFloatingStyles, deprecatedAriaLabel, ariaLabel, getMenuProps, refs.setFloating]);
useEffect(() => {
if (textInput.current) {
if (inputRef.current && typeaheadText) {
const selectionStart = inputValue.length;
const selectionEnd = selectionStart + typeaheadText.length;
inputRef.current.value = inputValue + typeaheadText;
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
}
}
}, [inputValue, typeaheadText]);
return /*#__PURE__*/React.createElement("div", {
className: wrapperClasses
}, titleText && /*#__PURE__*/React.createElement(Text, _extends({
as: "label",
className: titleClasses
}, getLabelProps()), titleText), /*#__PURE__*/React.createElement(ListBox, {
onFocus: handleFocus,
onBlur: handleFocus,
className: className,
disabled: disabled,
invalid: invalid,
invalidText: invalidText,
invalidTextId: invalidTextId,
isOpen: isOpen,
light: light,
size: size,
warn: warn,
ref: enableFloatingStyles ? refs.setReference : null,
warnText: warnText,
warnTextId: warnTextId
}, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--list-box__field`
}, /*#__PURE__*/React.createElement("input", _extends({
disabled: disabled,
className: inputClasses,
type: "text",
tabIndex: 0,
"aria-haspopup": "listbox",
title: textInput?.current?.value
}, getInputProps({
'aria-label': titleText ? undefined : deprecatedAriaLabel || ariaLabel,
'aria-controls': isOpen ? undefined : menuProps.id,
placeholder,
value: inputValue,
...inputProps,
onChange: e => {
const newValue = e.target.value;
setInputValue(newValue);
downshiftSetInputValue(newValue);
},
ref: mergeRefs(textInput, ref, inputRef),
onKeyDown: event => {
if (match(event, Space)) {
event.stopPropagation();
}
if (match(event, Enter) && (!inputValue || allowCustomValue)) {
toggleMenu();
if (highlightedIndex !== -1) {
selectItem(filterItems(items, itemToString, inputValue)[highlightedIndex]);
}
// Since `onChange` does not normally fire when the menu is closed, we should
// manually fire it when `allowCustomValue` is provided, the menu is closing,
// and there is a value.
if (allowCustomValue && isOpen && inputValue && highlightedIndex === -1) {
onChange({
selectedItem: null,
inputValue
});
}
event.preventDownshiftDefault = true;
event?.persist?.();
}
if (match(event, Escape) && inputValue) {
if (event.target === textInput.current && isOpen) {
toggleMenu();
event.preventDownshiftDefault = true;
event?.persist?.();
}
}
if (match(event, Home) && event.code !== 'Numpad7') {
event.target.setSelectionRange(0, 0);
}
if (match(event, End) && event.code !== 'Numpad1') {
event.target.setSelectionRange(event.target.value.length, event.target.value.length);
}
if (event.altKey && event.key == 'ArrowDown') {
event.preventDownshiftDefault = true;
if (!isOpen) {
toggleMenu();
}
}
if (event.altKey && event.key == 'ArrowUp') {
event.preventDownshiftDefault = true;
if (isOpen) {
toggleMenu();
}
}
if (!inputValue && highlightedIndex == -1 && event.key == 'Enter') {
if (!isOpen) toggleMenu();
selectItem(null);
event.preventDownshiftDefault = true;
if (event.currentTarget.ariaExpanded === 'false') openMenu();
}
if (typeahead && event.key === 'Tab') {
// event.preventDefault();
const matchingItem = items.find(item => itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase()));
if (matchingItem) {
const newValue = itemToString(matchingItem);
downshiftSetInputValue(newValue);
selectItem(matchingItem);
}
}
}
}), rest, readOnlyEventHandlers, {
readOnly: readOnly,
"aria-describedby": ariaDescribedBy
})), 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: () => {
setIsClearing(true); // This updates the state which syncs to the ref
setInputValue('');
onChange({
selectedItem: null
});
selectItem(null);
handleSelectionClear();
},
translateWithId: translateWithId,
disabled: disabled || readOnly,
onClearSelection: handleSelectionClear,
selectionCount: 0
}), /*#__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 ? filterItems(items, itemToString, inputValue).map((item, index) => {
const isObject = item !== null && typeof item === 'object';
const title = isObject && 'text' in item && itemToElement ? item.text?.toString() : itemToString(item);
const itemProps = getItemProps({
item,
index
});
// 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,
isActive: selectedItem === item,
isHighlighted: highlightedIndex === index,
title: title,
disabled: disabled
}, modifiedItemProps), ItemToElement ? /*#__PURE__*/React.createElement(ItemToElement, _extends({
key: itemProps.id
}, item)) : itemToString(item), selectedItem === item && /*#__PURE__*/React.createElement(Checkmark, {
className: `${prefix}--list-box__menu-item__selected-icon`
}));
}) : null)), helperText && !invalid && !warn && !isFluid && /*#__PURE__*/React.createElement(Text, {
as: "div",
id: helperTextId,
className: helperClasses
}, helperText));
});
ComboBox.displayName = 'ComboBox';
ComboBox.propTypes = {
/**
* Specify whether or not the ComboBox should allow a value that is
* not in the list to be entered in the input
*/
allowCustomValue: PropTypes.bool,
/**
* 'aria-label' of the ListBox component.
* Specify a label to be read by screen readers on the container node
*/
['aria-label']: PropTypes.string,
/**
* Deprecated, please use `aria-label` instead.
* Specify a label to be read by screen readers on the container note.
* 'aria-label' of the ListBox component.
*/
ariaLabel: deprecate(PropTypes.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'),
/**
* **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,
/**
* An optional className to add to the container node
*/
className: PropTypes.string,
/**
* **Experimental**: Provide a decorator component to be rendered inside the `ComboBox` component
*/
decorator: PropTypes.node,
/**
* Specify the direction of the combobox dropdown. Can be either top or bottom.
*/
direction: PropTypes.oneOf(['top', 'bottom']),
/**
* Specify if the control should be disabled, or not
*/
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.
*/
downshiftProps: PropTypes.object,
/**
* Provide a ref that will be mutated to contain an object of downshift
* action functions. These can be called to change the internal state of the
* downshift useCombobox hook.
*
* **Use with caution:** calling these actions modifies the internal state of
* downshift. It may conflict with or override the state management used within
* Combobox. 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.
*/
downshiftActions: PropTypes.exact({
current: PropTypes.any
}),
/**
* Provide helper text that is used alongside the control label for
* additional help
*/
helperText: PropTypes.node,
/**
* Specify a custom `id` for the input
*/
id: PropTypes.string.isRequired,
/**
* Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
* from their collection that are pre-selected
*/
initialSelectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.number]),
/**
* Specify if the currently selected value is invalid.
*/
invalid: PropTypes.bool,
/**
* Message which is displayed if the value is invalid.
*/
invalidText: PropTypes.node,
/**
* Optional 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,
/**
* should use "light theme" (white background)?
*/
light: deprecate(PropTypes.bool, 'The `light` prop for `Combobox` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'),
/**
* `onChange` is a utility for this controlled component to communicate to a
* consuming component when a specific dropdown item is selected.
* `({ selectedItem }) => void`
* @param {{ selectedItem }}
*/
onChange: PropTypes.func.isRequired,
/**
* Callback function to notify consumer when the text input changes.
* This provides support to change available items based on the text.
* `(inputText) => void`
* @param {string} inputText
*/
onInputChange: PropTypes.func,
/**
* Callback function that fires when the combobox menu toggle is clicked
* `(evt) => void`
* @param {MouseEvent} event
*/
onToggleClick: PropTypes.func,
/**
* Used to provide a placeholder text node before a user enters any input.
* This is only present if the control has no items selected
*/
placeholder: PropTypes.string,
/**
* Is the ComboBox readonly?
*/
readOnly: PropTypes.bool,
/**
* For full control of the selection
*/
selectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.number]),
/**
* Specify your own filtering logic by passing in a `shouldFilterItem`
* function that takes in the current input and an item and passes back
* whether or not the item should be filtered.
* this prop will be ignored if `typeahead` prop is enabled
*/
shouldFilterItem: PropTypes.func,
/**
* 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.'),
/**
* Provide text to be used in a `<label>` element that is tied to the
* combobox via ARIA attributes.
*/
titleText: PropTypes.node,
/**
* Specify a custom translation function that takes in a message identifier
* and returns the localized string for the message
*/
translateWithId: PropTypes.func,
/**
* **Experimental**: will enable autocomplete and typeahead for the input field
*/
typeahead: 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 { ComboBox as default };