@carbon/react
Version:
React components for the Carbon Design System
469 lines (467 loc) • 20.4 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_usePrefix = require("../../internal/usePrefix.js");
const require_keys = require("../../internal/keyboard/keys.js");
const require_match = require("../../internal/keyboard/match.js");
const require_useIsomorphicEffect = require("../../internal/useIsomorphicEffect.js");
const require_useId = require("../../internal/useId.js");
const require_noopFn = require("../../internal/noopFn.js");
const require_deprecate = require("../../prop-types/deprecate.js");
const require_defaultItemToString = require("../../internal/defaultItemToString.js");
const require_utils = require("../../internal/utils.js");
const require_index = require("../FeatureFlags/index.js");
const require_useNormalizedInputProps = require("../../internal/useNormalizedInputProps.js");
const require_index$1 = require("../AILabel/index.js");
const require_index$2 = require("../Checkbox/index.js");
const require_ListBoxPropTypes = require("../ListBox/ListBoxPropTypes.js");
const require_FormContext = require("../FluidForm/FormContext.js");
const require_index$3 = require("../ListBox/index.js");
const require_mergeRefs = require("../../tools/mergeRefs.js");
const require_MultiSelectPropTypes = require("./MultiSelectPropTypes.js");
const require_sorting = require("./tools/sorting.js");
const require_Selection = require("../../internal/Selection.js");
let classnames = require("classnames");
classnames = require_runtime.__toESM(classnames);
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let react_jsx_runtime = require("react/jsx-runtime");
let _carbon_icons_react = require("@carbon/icons-react");
let _floating_ui_react = require("@floating-ui/react");
let downshift = require("downshift");
let react_fast_compare = require("react-fast-compare");
react_fast_compare = require_runtime.__toESM(react_fast_compare);
//#region src/components/MultiSelect/MultiSelect.tsx
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const { ItemClick, ToggleButtonBlur, ToggleButtonKeyDownArrowDown, ToggleButtonKeyDownArrowUp, ToggleButtonKeyDownEnter, ToggleButtonKeyDownEscape, ToggleButtonKeyDownSpaceButton, ItemMouseMove, MenuMouseLeave, ToggleButtonClick, ToggleButtonKeyDownPageDown, ToggleButtonKeyDownPageUp, FunctionSetHighlightedIndex } = downshift.useSelect.stateChangeTypes;
const MultiSelect = react.default.forwardRef(({ autoAlign = false, className: containerClassName, decorator, id, items, itemToElement, itemToString = require_defaultItemToString.defaultItemToString, titleText = false, hideLabel, helperText, label, type = "default", size, disabled = false, initialSelectedItems = [], sortItems = require_sorting.defaultSortItems, compareItems = require_sorting.defaultCompareItems, clearSelectionText = "To clear selection, press Delete or Backspace", clearAnnouncement = "all items have been cleared", clearSelectionDescription = "Total items selected: ", light, invalid = false, invalidText, warn = false, warnText, useTitleInItem, translateWithId, downshiftProps, open = false, selectionFeedback = "top-after-reopen", onChange, onMenuChange, direction = "bottom", selectedItems: selected, readOnly, locale = "en", slug }, ref) => {
const filteredItems = (0, react.useMemo)(() => {
return items.filter((item) => {
if (typeof item === "object" && item !== null) {
for (const key in item) if (Object.hasOwn(item, key) && item[key] === void 0) return false;
}
return true;
});
}, [items]);
const selectAll = filteredItems.some((item) => item.isSelectAll);
const prefix = require_usePrefix.usePrefix();
const { isFluid } = (0, react.useContext)(require_FormContext.FormContext);
const multiSelectInstanceId = require_useId.useId();
const prevOpenPropRef = (0, react.useRef)(open);
const [inputFocused, setInputFocused] = (0, react.useState)(false);
const [isOpen, setIsOpen] = (0, react.useState)(open || false);
const [topItems, setTopItems] = (0, react.useState)([]);
const [itemsCleared, setItemsCleared] = (0, react.useState)(false);
const enableFloatingStyles = require_index.useFeatureFlag("enable-v12-dynamic-floating-styles") || autoAlign;
const { refs, floatingStyles, middlewareData } = (0, _floating_ui_react.useFloating)(enableFloatingStyles ? {
placement: direction,
strategy: "fixed",
middleware: [
autoAlign && (0, _floating_ui_react.flip)({ crossAxis: false }),
(0, _floating_ui_react.size)({ apply({ rects, elements }) {
Object.assign(elements.floating.style, { width: `${rects.reference.width}px` });
} }),
autoAlign && (0, _floating_ui_react.hide)()
],
whileElementsMounted: _floating_ui_react.autoUpdate
} : {});
require_useIsomorphicEffect.default(() => {
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];
});
}
}, [
enableFloatingStyles,
floatingStyles,
refs.floating,
middlewareData,
open
]);
const { selectedItems: controlledSelectedItems, onItemChange, clearSelection } = require_Selection.useSelection({
disabled,
initialSelectedItems,
onChange,
selectedItems: selected,
selectAll,
filteredItems
});
const sortOptions = {
selectedItems: controlledSelectedItems,
itemToString,
compareItems,
locale
};
const { getToggleButtonProps, getLabelProps, getMenuProps, getItemProps, selectedItem, highlightedIndex, setHighlightedIndex } = (0, downshift.useSelect)({
stateReducer,
isOpen,
itemToString: (filteredItems) => {
return Array.isArray(filteredItems) && filteredItems.map((item) => {
return itemToString(item);
}).join(", ") || "";
},
selectedItem: controlledSelectedItems,
items: filteredItems,
isItemDisabled(item) {
return item?.disabled;
},
...downshiftProps
});
const toggleButtonProps = getToggleButtonProps({
onFocus: () => {
setInputFocused(true);
},
onBlur: () => {
setInputFocused(false);
},
onKeyDown: (e) => {
if (!disabled) {
if ((require_match.match(e, require_keys.Delete) || require_match.match(e, require_keys.Escape)) && !isOpen) {
clearSelection();
e.stopPropagation();
}
if (!isOpen && require_match.match(e, require_keys.Delete) && selectedItems.length > 0) setItemsCleared(true);
if ((require_match.match(e, require_keys.Space) || require_match.match(e, require_keys.ArrowDown) || require_match.match(e, require_keys.Enter)) && !isOpen) {
setHighlightedIndex(0);
setItemsCleared(false);
setIsOpenWrapper(true);
}
if (require_match.match(e, require_keys.ArrowDown) && selectedItems.length === 0) setInputFocused(false);
if (require_match.match(e, require_keys.Escape) && isOpen) setInputFocused(true);
if (require_match.match(e, require_keys.Enter) && isOpen) setInputFocused(true);
}
}
});
const toggleButtonRef = (0, react.useRef)(null);
const mergedRef = require_mergeRefs.mergeRefs(toggleButtonProps.ref, ref, toggleButtonRef);
const selectedItems = selectedItem;
/**
* wrapper function to forward changes to consumer
*/
const setIsOpenWrapper = (open) => {
setIsOpen(open);
if (onMenuChange) onMenuChange(open);
};
(0, react.useEffect)(() => {
if (prevOpenPropRef.current !== open) {
setIsOpen(open);
onMenuChange?.(open);
prevOpenPropRef.current = open;
}
}, [open, onMenuChange]);
const normalizedProps = require_useNormalizedInputProps.useNormalizedInputProps({
id,
disabled,
readOnly,
invalid,
warn
});
const inline = type === "inline";
const showWarning = normalizedProps.warn;
const showHelperText = !normalizedProps.warn && !normalizedProps.invalid && helperText;
const wrapperClasses = (0, classnames.default)(`${prefix}--multi-select__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 && normalizedProps.invalid,
[`${prefix}--list-box__wrapper--inline--invalid`]: inline && normalizedProps.invalid,
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && normalizedProps.invalid,
[`${prefix}--list-box__wrapper--slug`]: slug,
[`${prefix}--list-box__wrapper--decorator`]: decorator
});
const titleClasses = (0, classnames.default)(`${prefix}--label`, {
[`${prefix}--label--disabled`]: disabled,
[`${prefix}--visually-hidden`]: hideLabel
});
const helperId = !helperText ? void 0 : `multiselect-helper-text-${multiSelectInstanceId}`;
const fieldLabelId = `multiselect-field-label-${multiSelectInstanceId}`;
const helperClasses = (0, classnames.default)(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: disabled });
const className = (0, classnames.default)(`${prefix}--multi-select`, {
[`${prefix}--multi-select--invalid`]: normalizedProps.invalid,
[`${prefix}--multi-select--invalid--focused`]: inputFocused && normalizedProps.invalid,
[`${prefix}--multi-select--warning`]: showWarning,
[`${prefix}--multi-select--inline`]: inline,
[`${prefix}--multi-select--selected`]: selectedItems && selectedItems.length > 0,
[`${prefix}--list-box--up`]: direction === "top",
[`${prefix}--multi-select--readonly`]: readOnly,
[`${prefix}--autoalign`]: enableFloatingStyles,
[`${prefix}--multi-select--selectall`]: selectAll
});
if (selectionFeedback === "fixed") sortOptions.selectedItems = [];
else if (selectionFeedback === "top-after-reopen") sortOptions.selectedItems = topItems;
function stateReducer(state, actionAndChanges) {
const { changes, props, type } = actionAndChanges;
const { highlightedIndex } = changes;
if (changes.isOpen && !isOpen) setTopItems(controlledSelectedItems);
switch (type) {
case ToggleButtonKeyDownSpaceButton:
case ToggleButtonKeyDownEnter:
if (changes.selectedItem === void 0) break;
if (Array.isArray(changes.selectedItem)) break;
onItemChange(changes.selectedItem);
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case ToggleButtonBlur:
case ToggleButtonKeyDownEscape:
setIsOpenWrapper(false);
break;
case ToggleButtonClick:
setIsOpenWrapper(changes.isOpen || false);
return {
...changes,
highlightedIndex: controlledSelectedItems.length > 0 ? 0 : void 0
};
case ItemClick:
setHighlightedIndex(changes.selectedItem);
onItemChange(changes.selectedItem);
return {
...changes,
highlightedIndex: state.highlightedIndex
};
case MenuMouseLeave: return {
...changes,
highlightedIndex: state.highlightedIndex
};
case FunctionSetHighlightedIndex: if (!isOpen) return {
...changes,
highlightedIndex: 0
};
else return {
...changes,
highlightedIndex: filteredItems.indexOf(highlightedIndex)
};
case ToggleButtonKeyDownArrowDown:
case ToggleButtonKeyDownArrowUp:
case ToggleButtonKeyDownPageDown:
case ToggleButtonKeyDownPageUp:
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
};
}
return changes;
}
const multiSelectFieldWrapperClasses = (0, classnames.default)(`${prefix}--list-box__field--wrapper`, { [`${prefix}--list-box__field--wrapper--input-focused`]: inputFocused });
const readOnlyEventHandlers = readOnly ? {
onClick: (evt) => {
evt.preventDefault();
if (toggleButtonRef.current) toggleButtonRef.current.focus();
},
onKeyDown: (evt) => {
if ([
"ArrowDown",
"ArrowUp",
" ",
"Enter"
].includes(evt.key)) evt.preventDefault();
}
} : {};
const candidate = slug ?? decorator;
const normalizedDecorator = require_utils.isComponentElement(candidate, require_index$1.AILabel) ? (0, react.cloneElement)(candidate, { size: "mini" }) : candidate;
const itemsSelectedText = selectedItems.length > 0 && selectedItems.map((item) => item?.text);
const selectedItemsLength = selectAll ? selectedItems.filter((item) => !item.isSelectAll).length : selectedItems.length;
const menuProps = (0, react.useMemo)(() => getMenuProps({
ref: enableFloatingStyles ? refs.setFloating : null,
hidden: !isOpen
}), [
enableFloatingStyles,
getMenuProps,
isOpen,
refs.setFloating
]);
const allLabelProps = getLabelProps();
const labelProps = (0, react.isValidElement)(titleText) ? { id: allLabelProps.id } : allLabelProps;
const getSelectionStats = (0, react.useCallback)((selectedItems, filteredItems) => {
return {
hasIndividualSelections: selectedItems.some((selected) => !selected.isSelectAll),
nonSelectAllSelectedCount: selectedItems.filter((selected) => !selected.isSelectAll).length,
totalSelectableCount: filteredItems.filter((item) => !item.isSelectAll && !item.disabled).length
};
}, [selectedItems, filteredItems]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: wrapperClasses,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
className: titleClasses,
...labelProps,
children: [titleText && titleText, selectedItems.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
className: `${prefix}--visually-hidden`,
children: [
clearSelectionDescription,
" ",
selectedItems.length,
" ",
itemsSelectedText,
",",
clearSelectionText
]
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_index$3.default, {
type,
size,
className,
disabled,
light,
invalid: normalizedProps.invalid,
invalidText,
warn: normalizedProps.warn,
warnText,
isOpen,
id,
children: [
normalizedProps.invalid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningFilled, { className: `${prefix}--list-box__invalid-icon` }),
showWarning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningAltFilled, { className: `${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning` }),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: multiSelectFieldWrapperClasses,
ref: enableFloatingStyles ? refs.setReference : null,
children: [
selectedItems.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$3.default.Selection, {
readOnly,
clearSelection: !disabled && !readOnly ? clearSelection : require_noopFn.noopFn,
selectionCount: selectedItemsLength,
translateWithId,
disabled
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
type: "button",
className: `${prefix}--list-box__field`,
disabled,
"aria-disabled": disabled || readOnly,
"aria-describedby": !inline && showHelperText ? helperId : void 0,
...toggleButtonProps,
ref: mergedRef,
...readOnlyEventHandlers,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
id: fieldLabelId,
className: `${prefix}--list-box__label`,
children: label
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$3.default.MenuIcon, {
isOpen,
translateWithId
})]
}),
slug ? normalizedDecorator : decorator ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--list-box__inner-wrapper--decorator`,
children: normalizedDecorator
}) : ""
]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$3.default.Menu, {
...menuProps,
children: isOpen && sortItems(filteredItems, sortOptions).map((item, index) => {
const { hasIndividualSelections, nonSelectAllSelectedCount, totalSelectableCount } = getSelectionStats(selectedItems, filteredItems);
const isChecked = item.isSelectAll ? nonSelectAllSelectedCount === totalSelectableCount && totalSelectableCount > 0 : selectedItems.some((selected) => (0, react_fast_compare.default)(selected, item));
const isIndeterminate = item.isSelectAll && hasIndividualSelections && nonSelectAllSelectedCount < totalSelectableCount;
const itemProps = getItemProps({
item,
["aria-selected"]: isChecked
});
const itemText = itemToString(item);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$3.default.MenuItem, {
isActive: isChecked && !item["isSelectAll"],
"aria-label": itemText,
"aria-checked": isIndeterminate ? "mixed" : isChecked,
isHighlighted: highlightedIndex === index,
title: itemText,
disabled: itemProps["aria-disabled"],
...itemProps,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--checkbox-wrapper`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$2.default, {
id: `${itemProps.id}__checkbox`,
labelText: itemToElement ? itemToElement(item) : itemText,
checked: isChecked,
title: useTitleInItem ? itemText : void 0,
indeterminate: isIndeterminate,
disabled
})
})
}, itemProps.id);
})
}),
itemsCleared && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
"aria-live": "assertive",
"aria-label": clearAnnouncement
})
]
}),
!inline && showHelperText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
id: helperId,
className: helperClasses,
children: helperText
})
]
});
});
MultiSelect.displayName = "MultiSelect";
MultiSelect.propTypes = {
...require_MultiSelectPropTypes.sortingPropTypes,
autoAlign: prop_types.default.bool,
className: prop_types.default.string,
clearSelectionDescription: prop_types.default.string,
clearSelectionText: prop_types.default.string,
compareItems: prop_types.default.func,
decorator: prop_types.default.node,
direction: prop_types.default.oneOf(["top", "bottom"]),
disabled: prop_types.default.bool,
downshiftProps: prop_types.default.object,
helperText: prop_types.default.node,
hideLabel: prop_types.default.bool,
id: prop_types.default.string.isRequired,
initialSelectedItems: prop_types.default.array,
invalid: prop_types.default.bool,
invalidText: prop_types.default.node,
itemToElement: prop_types.default.func,
itemToString: prop_types.default.func,
items: prop_types.default.array.isRequired,
label: prop_types.default.node.isRequired,
light: require_deprecate.deprecate(prop_types.default.bool, "The `light` prop for `MultiSelect` has been deprecated in favor of the new `Layer` component. It will be removed in the next major release."),
locale: prop_types.default.string,
onChange: prop_types.default.func,
onMenuChange: prop_types.default.func,
open: prop_types.default.bool,
readOnly: prop_types.default.bool,
selectedItems: prop_types.default.array,
selectionFeedback: prop_types.default.oneOf([
"top",
"fixed",
"top-after-reopen"
]),
size: require_ListBoxPropTypes.ListBoxSizePropType,
slug: require_deprecate.deprecate(prop_types.default.node, "The `slug` prop has been deprecated and will be removed in the next major version. Use the decorator prop instead."),
sortItems: prop_types.default.func,
titleText: prop_types.default.node,
translateWithId: prop_types.default.func,
type: require_ListBoxPropTypes.ListBoxTypePropType,
useTitleInItem: prop_types.default.bool,
warn: prop_types.default.bool,
warnText: prop_types.default.node
};
//#endregion
exports.MultiSelect = MultiSelect;