UNPKG

@carbon/react

Version:

React components for the Carbon Design System

469 lines (467 loc) 20.4 kB
/** * 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;