@carbon/react
Version:
React components for the Carbon Design System
361 lines (359 loc) • 15.8 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_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;