UNPKG

@carbon/react

Version:

React components for the Carbon Design System

361 lines (359 loc) 15.8 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_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_ListBoxPropTypes = require("../ListBox/ListBoxPropTypes.js"); const require_FormContext = require("../FluidForm/FormContext.js"); const require_index$2 = require("../ListBox/index.js"); const require_mergeRefs = require("../../tools/mergeRefs.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"); //#region src/components/Dropdown/Dropdown.tsx /** * Copyright IBM Corp. 2022, 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 { ItemMouseMove, MenuMouseLeave, ToggleButtonBlur, FunctionCloseMenu } = downshift.useSelect.stateChangeTypes; /** * Custom state reducer for `useSelect` in Downshift, providing control over * state changes. * * This function is called each time `useSelect` updates its internal state or * triggers `onStateChange`. It allows for fine-grained control of state * updates by modifying or overriding the default changes from Downshift's * reducer. * https://github.com/downshift-js/downshift/tree/master/src/hooks/useSelect#statereducer * * @param {Object} state - The current full state of the Downshift component. * @param {Object} actionAndChanges - Contains the action type and proposed * changes from the default Downshift reducer. * @param {Object} actionAndChanges.changes - Suggested state changes. * @param {string} actionAndChanges.type - The action type for the state * change (e.g., item selection). * @returns {Object} - The modified state based on custom logic or default * changes if no custom logic applies. */ function stateReducer(state, actionAndChanges) { const { changes, type } = actionAndChanges; switch (type) { case ItemMouseMove: return state; case MenuMouseLeave: if (changes.highlightedIndex === state.highlightedIndex) return state; return changes; case ToggleButtonBlur: case FunctionCloseMenu: return { ...changes, selectedItem: state.selectedItem }; default: return changes; } } const Dropdown = react.default.forwardRef(({ autoAlign = false, className: containerClassName, decorator, disabled = false, direction = "bottom", items: itemsProp, label, ["aria-label"]: ariaLabel, ariaLabel: deprecatedAriaLabel, itemToString = require_defaultItemToString.defaultItemToString, itemToElement = null, renderSelectedItem, type = "default", size, onChange, id, titleText = "", hideLabel, helperText = "", translateWithId, light, invalid, invalidText, warn, warnText, initialSelectedItem, selectedItem: controlledSelectedItem, downshiftProps, readOnly, slug, ...other }, ref) => { const enableFloatingStyles = require_index.useFeatureFlag("enable-v12-dynamic-floating-styles"); const { refs, floatingStyles, middlewareData } = (0, _floating_ui_react.useFloating)(enableFloatingStyles || autoAlign ? { placement: direction, strategy: "fixed", middleware: [ (0, _floating_ui_react.size)({ apply({ rects, elements }) { Object.assign(elements.floating.style, { width: `${rects.reference.width}px` }); } }), autoAlign && (0, _floating_ui_react.flip)(), autoAlign && (0, _floating_ui_react.hide)() ], whileElementsMounted: _floating_ui_react.autoUpdate } : {}); (0, react.useEffect)(() => { if (enableFloatingStyles || 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]; }); } }, [ floatingStyles, autoAlign, refs.floating ]); const prefix = require_usePrefix.usePrefix(); const { isFluid } = (0, react.useContext)(require_FormContext.FormContext); const onSelectedItemChange = (0, react.useCallback)(({ selectedItem }) => { if (onChange) onChange({ selectedItem: selectedItem ?? null }); }, [onChange]); const isItemDisabled = (0, react.useCallback)((item) => { return item !== null && typeof item === "object" && "disabled" in item && item.disabled === true; }, []); const onHighlightedIndexChange = (0, react.useCallback)((changes) => { const { highlightedIndex } = changes; if (highlightedIndex !== void 0 && highlightedIndex > -1) { const highlightedItem = document.querySelectorAll(`li.${prefix}--list-box__menu-item[role="option"]`)[highlightedIndex]; if (highlightedItem) highlightedItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); } }, [prefix]); const items = (0, react.useMemo)(() => itemsProp, [itemsProp]); const selectProps = (0, react.useMemo)(() => ({ items, itemToString, initialSelectedItem, onSelectedItemChange, stateReducer, isItemDisabled, onHighlightedIndexChange, ...downshiftProps }), [ items, itemToString, initialSelectedItem, onSelectedItemChange, stateReducer, isItemDisabled, onHighlightedIndexChange, downshiftProps ]); if (controlledSelectedItem !== void 0) selectProps.selectedItem = controlledSelectedItem; const { isOpen, getToggleButtonProps, getLabelProps, getMenuProps, getItemProps, selectedItem, highlightedIndex } = (0, downshift.useSelect)(selectProps); const inline = type === "inline"; const normalizedProps = require_useNormalizedInputProps.useNormalizedInputProps({ id, readOnly, disabled: disabled ?? false, invalid: invalid ?? false, invalidText, warn: warn ?? false, warnText }); const [isFocused, setIsFocused] = (0, react.useState)(false); const className = (0, classnames.default)(`${prefix}--dropdown`, { [`${prefix}--dropdown--invalid`]: normalizedProps.invalid, [`${prefix}--dropdown--warning`]: normalizedProps.warn, [`${prefix}--dropdown--open`]: isOpen, [`${prefix}--dropdown--focus`]: isFocused, [`${prefix}--dropdown--inline`]: inline, [`${prefix}--dropdown--disabled`]: normalizedProps.disabled, [`${prefix}--dropdown--light`]: light, [`${prefix}--dropdown--readonly`]: readOnly, [`${prefix}--dropdown--${size}`]: size, [`${prefix}--list-box--up`]: direction === "top", [`${prefix}--autoalign`]: autoAlign }); const titleClasses = (0, classnames.default)(`${prefix}--label`, { [`${prefix}--label--disabled`]: normalizedProps.disabled, [`${prefix}--visually-hidden`]: hideLabel }); const helperClasses = (0, classnames.default)(`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: normalizedProps.disabled }); const wrapperClasses = (0, classnames.default)(`${prefix}--dropdown__wrapper`, `${prefix}--list-box__wrapper`, containerClassName, { [`${prefix}--dropdown__wrapper--inline`]: inline, [`${prefix}--list-box__wrapper--inline`]: inline, [`${prefix}--dropdown__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 toggleButtonProps = getToggleButtonProps({ "aria-label": ariaLabel || deprecatedAriaLabel }); const helper = helperText && !isFluid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { id: normalizedProps.helperId, className: helperClasses, children: helperText }) : null; const handleFocus = (evt) => { setIsFocused(evt.type === "focus" && !selectedItem); }; const buttonRef = (0, react.useRef)(null); const mergedRef = require_mergeRefs.mergeRefs(toggleButtonProps.ref, ref, buttonRef); const [currTimer, setCurrTimer] = (0, react.useState)(); const [isTyping, setIsTyping] = (0, react.useState)(false); const onKeyDownHandler = (0, react.useCallback)((evt) => { if (![ "ArrowDown", "ArrowUp", " ", "Enter" ].includes(evt.key)) { setIsTyping(true); if (currTimer) clearTimeout(currTimer); setCurrTimer(setTimeout(() => { setIsTyping(false); }, 3e3)); } else if (isTyping && evt.key === " ") { if (currTimer) clearTimeout(currTimer); setCurrTimer(setTimeout(() => { setIsTyping(false); }, 3e3)); } if (["ArrowDown"].includes(evt.key)) setIsFocused(false); if (["Enter"].includes(evt.key) && !selectedItem && !isOpen) setIsFocused(true); if (toggleButtonProps.onKeyDown && (evt.key !== "ArrowUp" || isOpen && evt.key === "ArrowUp")) toggleButtonProps.onKeyDown(evt); }, [ isTyping, currTimer, toggleButtonProps ]); const readOnlyEventHandlers = (0, react.useMemo)(() => { if (readOnly) return { onClick: (evt) => { evt.preventDefault(); buttonRef.current?.focus(); }, onKeyDown: (evt) => { if ([ "ArrowDown", "ArrowUp", " ", "Enter" ].includes(evt.key)) evt.preventDefault(); } }; else return { onKeyDown: onKeyDownHandler }; }, [readOnly, onKeyDownHandler]); const menuProps = (0, react.useMemo)(() => getMenuProps({ ref: enableFloatingStyles || autoAlign ? refs.setFloating : null }), [ autoAlign, getMenuProps, refs.setFloating, enableFloatingStyles ]); const candidate = slug ?? decorator; const normalizedDecorator = require_utils.isComponentElement(candidate, require_index$1.AILabel) ? (0, react.cloneElement)(candidate, { size: "mini" }) : candidate; const allLabelProps = getLabelProps(); const labelProps = (0, react.isValidElement)(titleText) ? { id: allLabelProps.id } : allLabelProps; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: wrapperClasses, ...other, children: [ titleText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { className: titleClasses, ...labelProps, children: titleText }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_index$2.default, { onFocus: handleFocus, onBlur: handleFocus, size, className, invalid: normalizedProps.invalid, invalidText, invalidTextId: normalizedProps.invalidId, warn: normalizedProps.warn, warnText, warnTextId: normalizedProps.warnId, light, isOpen, ref: enableFloatingStyles || autoAlign ? refs.setReference : null, id, children: [ normalizedProps.invalid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.WarningFilled, { className: `${prefix}--list-box__invalid-icon` }), normalizedProps.warn && /* @__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)("button", { type: "button", className: `${prefix}--list-box__field`, disabled: normalizedProps.disabled, "aria-disabled": readOnly ? true : void 0, "aria-describedby": !inline && !normalizedProps.invalid && !normalizedProps.warn && helper ? normalizedProps.helperId : normalizedProps.invalid ? normalizedProps.invalidId : normalizedProps.warn ? normalizedProps.warnId : void 0, title: selectedItem && itemToString !== void 0 ? itemToString(selectedItem) : require_defaultItemToString.defaultItemToString(label), ...toggleButtonProps, ...readOnlyEventHandlers, ref: mergedRef, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${prefix}--list-box__label`, children: selectedItem ? renderSelectedItem ? renderSelectedItem(selectedItem) : itemToString(selectedItem) : label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$2.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$2.default.Menu, { ...menuProps, children: isOpen && items.map((item, index) => { const itemProps = getItemProps({ item, index }); const title = itemToString(item); return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_index$2.default.MenuItem, { isActive: selectedItem === item, isHighlighted: highlightedIndex === index, title, disabled: itemProps["aria-disabled"], ...itemProps, children: [itemToElement ? itemToElement(item) : itemToString(item), selectedItem === item && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.Checkmark, { className: `${prefix}--list-box__menu-item__selected-icon` })] }, itemProps.id); }) }) ] }), !inline && !isFluid && !normalizedProps.validation && helper ] }); }); Dropdown.displayName = "Dropdown"; Dropdown.propTypes = { ["aria-label"]: prop_types.default.string, ariaLabel: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-label`."), autoAlign: prop_types.default.bool, className: prop_types.default.string, 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, initialSelectedItem: prop_types.default.oneOfType([ prop_types.default.object, prop_types.default.string, prop_types.default.number ]), 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 `Dropdown` has been deprecated in favor of the new `Layer` component. It will be removed in the next major release."), onChange: prop_types.default.func, readOnly: prop_types.default.bool, renderSelectedItem: prop_types.default.func, selectedItem: prop_types.default.oneOfType([ prop_types.default.object, prop_types.default.string, prop_types.default.number ]), size: require_ListBoxPropTypes.ListBoxSizePropType, slug: require_deprecate.deprecate(prop_types.default.node, "The `slug` prop for `Dropdown` has been deprecated in favor of the new `decorator` prop. It will be removed in the next major release."), titleText: prop_types.default.node.isRequired, translateWithId: prop_types.default.func, type: require_ListBoxPropTypes.ListBoxTypePropType, warn: prop_types.default.bool, warnText: prop_types.default.node }; //#endregion exports.default = Dropdown;