@carbon/react
Version:
React components for the Carbon Design System
157 lines (155 loc) • 6.57 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_deprecate = require("../../prop-types/deprecate.js");
const require_useMergedRefs = require("../../internal/useMergedRefs.js");
const require_events = require("../../tools/events.js");
const require_AriaPropTypes = require("../../prop-types/AriaPropTypes.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");
//#region src/components/UIShell/HeaderMenu.tsx
/**
* Copyright IBM Corp. 2016, 2025
*
* 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 frFn = react.forwardRef;
const HeaderMenu = frFn((props, ref) => {
const { isActive, isCurrentPage, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: customClassName, children, renderMenuContent: MenuContent, menuLinkName, focusRef, onBlur, onClick, onKeyDown, ...rest } = props;
const prefix = (0, react.useContext)(require_usePrefix.PrefixContext);
const [expanded, setExpanded] = (0, react.useState)(false);
const menuButtonRef = (0, react.useRef)(null);
const subMenusRef = (0, react.useRef)(null);
const itemRefs = (0, react.useRef)([]);
const mergedButtonRef = require_useMergedRefs.useMergedRefs([
ref,
focusRef,
menuButtonRef
]);
/**
* Toggle the expanded state of the menu on click.
*/
const handleOnClick = (e) => {
if (!subMenusRef.current || e.target instanceof Node && !subMenusRef.current.contains(e.target)) e.preventDefault();
setExpanded((prev) => !prev);
};
/**
* Keyboard event handler for the entire menu.
*/
const handleOnKeyDown = (event) => {
if (require_match.matches(event, [require_keys.Enter, require_keys.Space])) {
event.stopPropagation();
event.preventDefault();
setExpanded((prev) => !prev);
return;
}
};
/**
* Handle our blur event from our underlying menuitems. Will mostly be used
* for closing our menu in response to a user clicking off or tabbing out of
* the menu or menubar.
*/
const handleOnBlur = (event) => {
const siblingItemBlurredTo = itemRefs.current.find((element) => element === event.relatedTarget);
const childItemBlurredTo = subMenusRef.current?.contains(event.relatedTarget);
if (!siblingItemBlurredTo && !childItemBlurredTo) setExpanded(false);
};
/**
* Handles individual menuitem refs. We assign them to a class instance
* property so that we can properly manage focus of our children.
*/
const handleItemRef = (index) => (node) => {
itemRefs.current[index] = node;
};
const handleMenuClose = (event) => {
if (require_match.matches(event, [require_keys.Escape]) && expanded) {
event.stopPropagation();
event.preventDefault();
setExpanded(false);
if (menuButtonRef.current) menuButtonRef.current.focus();
}
};
const hasActiveDescendant = (childrenArg) => react.Children.toArray(childrenArg).some((child) => {
if (!(0, react.isValidElement)(child)) return false;
const { isActive, isCurrentPage, children } = child.props;
return isActive || isCurrentPage || Array.isArray(children) && hasActiveDescendant(children);
});
/**
* We capture the `ref` for each child inside of `this.items` to properly
* manage focus. In addition to this focus management, all items receive a
* `tabIndex: -1` so the user won't hit a large number of items in their tab
* sequence when they might not want to go through all the items.
*/
const renderMenuItem = (item, index) => {
if ((0, react.isValidElement)(item)) return (0, react.cloneElement)(item, { ref: handleItemRef(index) });
return item;
};
const accessibilityLabel = {
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledBy
};
const itemClassName = (0, classnames.default)({
[`${prefix}--header__submenu`]: true,
[`${customClassName}`]: !!customClassName
});
const isActivePage = isActive ? isActive : isCurrentPage;
const linkClassName = (0, classnames.default)({
[`${prefix}--header__menu-item`]: true,
[`${prefix}--header__menu-title`]: true,
[`${prefix}--header__menu-item--current`]: isActivePage || hasActiveDescendant(children) && !expanded
});
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
...rest,
className: itemClassName,
onKeyDown: require_events.composeEventHandlers([onKeyDown, handleMenuClose]),
onClick: require_events.composeEventHandlers([onClick, handleOnClick]),
onBlur: require_events.composeEventHandlers([onBlur, handleOnBlur]),
ref,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
"aria-haspopup": "menu",
"aria-expanded": expanded,
className: linkClassName,
href: "#",
onKeyDown: handleOnKeyDown,
ref: mergedButtonRef,
tabIndex: 0,
...accessibilityLabel,
children: [menuLinkName, MenuContent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuContent, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.ChevronDown, { className: `${prefix}--header__menu-arrow` })]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
...accessibilityLabel,
ref: subMenusRef,
className: `${prefix}--header__menu`,
children: react.Children.map(children, renderMenuItem)
})]
});
});
HeaderMenu.displayName = "HeaderMenu";
HeaderMenu.propTypes = {
...require_AriaPropTypes.AriaLabelPropType,
className: prop_types.default.string,
focusRef: prop_types.default.func,
isActive: prop_types.default.bool,
isCurrentPage: require_deprecate.deprecate(prop_types.default.bool, "The `isCurrentPage` prop for `HeaderMenu` has been deprecated. Please use `isActive` instead. This will be removed in the next major release."),
menuLinkName: prop_types.default.string.isRequired,
onBlur: prop_types.default.func,
onClick: prop_types.default.func,
onKeyDown: prop_types.default.func,
renderMenuContent: prop_types.default.func,
tabIndex: prop_types.default.number
};
//#endregion
exports.default = HeaderMenu;