@carbon/react
Version:
React components for the Carbon Design System
310 lines (308 loc) • 11.9 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_Text = require("../Text/Text.js");
const require_keys = require("../../internal/keyboard/keys.js");
const require_match = require("../../internal/keyboard/match.js");
const require_useId = require("../../internal/useId.js");
const require_defaultItemToString = require("../../internal/defaultItemToString.js");
const require_useMergedRefs = require("../../internal/useMergedRefs.js");
const require_MenuContext = require("./MenuContext.js");
const require_useLayoutDirection = require("../LayoutDirection/useLayoutDirection.js");
const require_Menu = require("./Menu.js");
const require_useControllableState = require("../../internal/useControllableState.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");
//#region src/components/Menu/MenuItem.tsx
/**
* Copyright IBM Corp. 2023, 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 MenuItem = (0, react.forwardRef)(function MenuItem({ children, className, dangerDescription = "danger", disabled, kind = "default", label, onClick, renderIcon: IconElement, shortcut, ...rest }, forwardRef) {
const [submenuOpen, setSubmenuOpen] = (0, react.useState)(false);
const [rtl, setRtl] = (0, react.useState)(false);
const { refs, floatingStyles, context: floatingContext } = (0, _floating_ui_react.useFloating)({
open: submenuOpen,
onOpenChange: setSubmenuOpen,
placement: rtl ? "left-start" : "right-start",
whileElementsMounted: _floating_ui_react.autoUpdate,
middleware: [(0, _floating_ui_react.offset)({
mainAxis: -6,
crossAxis: -6
})],
strategy: "fixed"
});
const { getReferenceProps, getFloatingProps } = (0, _floating_ui_react.useInteractions)([(0, _floating_ui_react.useHover)(floatingContext, {
delay: 100,
enabled: true,
handleClose: (0, _floating_ui_react.safePolygon)({ requireIntent: false })
})]);
const prefix = require_usePrefix.usePrefix();
const context = (0, react.useContext)(require_MenuContext.MenuContext);
const menuItem = (0, react.useRef)(null);
const ref = require_useMergedRefs.useMergedRefs([
forwardRef,
menuItem,
refs.setReference
]);
const hasChildren = react.default.Children.toArray(children).length > 0;
const isDisabled = disabled && !hasChildren;
const isDanger = kind === "danger" && !hasChildren;
function registerItem() {
context.dispatch({
type: "registerItem",
payload: {
ref: menuItem,
disabled: disabled ?? false
}
});
}
function openSubmenu() {
if (!menuItem.current) return;
setSubmenuOpen(true);
}
function closeSubmenu() {
setSubmenuOpen(false);
}
function handleClick(e) {
if (!isDisabled) if (hasChildren) openSubmenu();
else {
context.state.requestCloseRoot(e);
if (onClick) onClick(e);
}
}
const pendingKeyboardClick = (0, react.useRef)(false);
const keyboardClickEvent = (e) => require_match.match(e, require_keys.Enter) || require_match.match(e, require_keys.Space);
function handleKeyDown(e) {
if (hasChildren && require_match.match(e, require_keys.ArrowRight)) {
openSubmenu();
requestAnimationFrame(() => {
refs.floating.current?.focus();
});
e.stopPropagation();
e.preventDefault();
}
pendingKeyboardClick.current = keyboardClickEvent(e);
if (rest.onKeyDown) rest.onKeyDown(e);
}
function handleKeyUp(e) {
if (pendingKeyboardClick.current && keyboardClickEvent(e)) handleClick(e);
pendingKeyboardClick.current = false;
}
const classNames = (0, classnames.default)(className, `${prefix}--menu-item`, {
[`${prefix}--menu-item--disabled`]: isDisabled,
[`${prefix}--menu-item--danger`]: isDanger
});
(0, react.useEffect)(() => {
registerItem();
}, []);
const { direction } = require_useLayoutDirection.useLayoutDirection();
(0, react.useEffect)(() => {
if (document?.dir === "rtl" || direction === "rtl") setRtl(true);
else setRtl(false);
}, [direction]);
(0, react.useEffect)(() => {
if (IconElement && !context.state.hasIcons) context.dispatch({ type: "enableIcons" });
}, [
IconElement,
context.state.hasIcons,
context
]);
(0, react.useEffect)(() => {
Object.keys(floatingStyles).forEach((style) => {
if (refs.floating.current && style !== "position") refs.floating.current.style[style] = floatingStyles[style];
});
}, [floatingStyles, refs.floating]);
const assistiveId = require_useId.useId("danger-description");
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_floating_ui_react.FloatingFocusManager, {
context: floatingContext,
order: ["reference", "floating"],
modal: false,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
role: "menuitem",
...rest,
ref,
className: classNames,
tabIndex: !disabled ? 0 : -1,
"aria-disabled": isDisabled ?? void 0,
"aria-haspopup": hasChildren ?? void 0,
"aria-expanded": hasChildren ? submenuOpen : void 0,
onClick: handleClick,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
title: label,
...getReferenceProps(),
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--menu-item__selection-icon`,
children: rest["aria-checked"] && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.Checkmark, {})
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--menu-item__icon`,
children: IconElement && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconElement, {})
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--menu-item__label`,
children: label
}),
isDanger && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
id: assistiveId,
className: `${prefix}--visually-hidden`,
children: dangerDescription
}),
shortcut && !hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--menu-item__shortcut`,
children: shortcut
}),
hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--menu-item__shortcut`,
children: rtl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.CaretLeft, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.CaretRight, {})
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Menu.Menu, {
label,
open: submenuOpen,
onClose: () => {
closeSubmenu();
menuItem.current?.focus();
},
ref: refs.setFloating,
...getFloatingProps(),
children
})] })
]
})
});
});
MenuItem.propTypes = {
children: prop_types.default.node,
className: prop_types.default.string,
dangerDescription: prop_types.default.string,
disabled: prop_types.default.bool,
kind: prop_types.default.oneOf(["default", "danger"]),
label: prop_types.default.string.isRequired,
onClick: prop_types.default.func,
renderIcon: prop_types.default.oneOfType([prop_types.default.func, prop_types.default.object]),
shortcut: prop_types.default.string
};
const MenuItemSelectable = (0, react.forwardRef)(function MenuItemSelectable({ className, defaultSelected, label, onChange, selected, ...rest }, forwardRef) {
const prefix = require_usePrefix.usePrefix();
const context = (0, react.useContext)(require_MenuContext.MenuContext);
const [checked, setChecked] = require_useControllableState.useControllableState({
value: selected,
onChange,
defaultValue: defaultSelected ?? false
});
function handleClick() {
setChecked(!checked);
}
(0, react.useEffect)(() => {
if (!context.state.hasSelectableItems) context.dispatch({ type: "enableSelectableItems" });
}, [context.state.hasSelectableItems, context]);
const classNames = (0, classnames.default)(className, `${prefix}--menu-item-selectable--selected`);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
...rest,
ref: forwardRef,
label,
className: classNames,
role: "menuitemcheckbox",
"aria-checked": checked,
onClick: handleClick
});
});
MenuItemSelectable.propTypes = {
className: prop_types.default.string,
defaultSelected: prop_types.default.bool,
label: prop_types.default.string.isRequired,
onChange: prop_types.default.func,
selected: prop_types.default.bool
};
const MenuItemGroup = (0, react.forwardRef)(function MenuItemGroup({ children, className, label, ...rest }, forwardRef) {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: (0, classnames.default)(className, `${require_usePrefix.usePrefix()}--menu-item-group`),
role: "none",
ref: forwardRef,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
...rest,
role: "group",
"aria-label": label,
children
})
});
});
MenuItemGroup.propTypes = {
children: prop_types.default.node,
className: prop_types.default.string,
label: prop_types.default.string.isRequired
};
const MenuItemRadioGroup = (0, react.forwardRef)(function MenuItemRadioGroup({ className, defaultSelectedItem, items, itemToString = require_defaultItemToString.defaultItemToString, label, onChange, selectedItem, ...rest }, forwardRef) {
const prefix = require_usePrefix.usePrefix();
const context = (0, react.useContext)(require_MenuContext.MenuContext);
const [selection, setSelection] = require_useControllableState.useControllableState({
value: selectedItem,
onChange,
defaultValue: defaultSelectedItem ?? {}
});
function handleClick(item) {
setSelection(item);
}
(0, react.useEffect)(() => {
if (!context.state.hasSelectableItems) context.dispatch({ type: "enableSelectableItems" });
}, [context.state.hasSelectableItems, context]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
className: (0, classnames.default)(className, `${prefix}--menu-item-radio-group`),
role: "none",
ref: forwardRef,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
...rest,
role: "group",
"aria-label": label,
children: items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
label: itemToString(item),
role: "menuitemradio",
"aria-checked": item === selection,
onClick: () => {
handleClick(item);
}
}, i))
})
});
});
MenuItemRadioGroup.propTypes = {
className: prop_types.default.string,
defaultSelectedItem: prop_types.default.any,
itemToString: prop_types.default.func,
items: prop_types.default.array,
label: prop_types.default.string.isRequired,
onChange: prop_types.default.func,
selectedItem: prop_types.default.any
};
const MenuItemDivider = (0, react.forwardRef)(function MenuItemDivider({ className, ...rest }, forwardRef) {
const classNames = (0, classnames.default)(className, `${require_usePrefix.usePrefix()}--menu-item-divider`);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
...rest,
className: classNames,
role: "separator",
ref: forwardRef
});
});
MenuItemDivider.propTypes = { className: prop_types.default.string };
//#endregion
exports.MenuItem = MenuItem;
exports.MenuItemDivider = MenuItemDivider;
exports.MenuItemGroup = MenuItemGroup;
exports.MenuItemRadioGroup = MenuItemRadioGroup;
exports.MenuItemSelectable = MenuItemSelectable;