UNPKG

@carbon/react

Version:

React components for the Carbon Design System

157 lines (155 loc) 6.57 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_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;