UNPKG

@carbon/react

Version:

React components for the Carbon Design System

240 lines (225 loc) 9.08 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var iconsReact = require('@carbon/icons-react'); var cx = require('classnames'); var React = require('react'); var PropTypes = require('prop-types'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var AriaPropTypes = require('../../prop-types/AriaPropTypes.js'); var usePrefix = require('../../internal/usePrefix.js'); var deprecate = require('../../prop-types/deprecate.js'); var events = require('../../tools/events.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); 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 = React.useContext(usePrefix.PrefixContext); const [expanded, setExpanded] = React.useState(false); const menuButtonRef = React.useRef(null); const subMenusRef = React.useRef(null); const itemRefs = React.useRef([]); const mergedButtonRef = 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 => { // Handle enter or space key for toggling the expanded state of the menu. if (match.matches(event, [keys.Enter, 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 => { // Close the menu on blur when the related target is not a sibling menu item // or a child in a submenu 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 => { // Handle ESC keydown for closing the expanded menu. if (match.matches(event, [keys.Escape]) && expanded) { event.stopPropagation(); event.preventDefault(); setExpanded(false); // Return focus to menu button when the user hits ESC. if (menuButtonRef.current) { menuButtonRef.current.focus(); } } }; const hasActiveDescendant = childrenArg => React.Children.toArray(childrenArg).some(child => { if (! /*#__PURE__*/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 (/*#__PURE__*/React.isValidElement(item)) { return /*#__PURE__*/React.cloneElement(item, { ref: handleItemRef(index) }); } return item; }; const accessibilityLabel = { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy }; const itemClassName = cx__default["default"]({ [`${prefix}--header__submenu`]: true, [`${customClassName}`]: !!customClassName }); const isActivePage = isActive ? isActive : isCurrentPage; const linkClassName = cx__default["default"]({ [`${prefix}--header__menu-item`]: true, [`${prefix}--header__menu-title`]: true, // We set the current class only if `isActive` is passed in and we do // not have an `aria-current="page"` set for the breadcrumb item [`${prefix}--header__menu-item--current`]: isActivePage || hasActiveDescendant(children) && !expanded }); // Notes on eslint comments and based on the examples in: // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-1/menubar-1.html# // - The focus is handled by the <a> menuitem, onMouseOver is for mouse // users // - aria-haspopup can definitely have the value "menu" // - aria-expanded is on their example node with role="menuitem" // - href can be set to javascript:void(0), ideally this will be a button return /*#__PURE__*/React__default["default"].createElement("li", _rollupPluginBabelHelpers["extends"]({}, rest, { className: itemClassName, onKeyDown: events.composeEventHandlers([onKeyDown, handleMenuClose]), onClick: events.composeEventHandlers([onClick, handleOnClick]), onBlur: events.composeEventHandlers([onBlur, handleOnBlur]), ref: ref }), /*#__PURE__*/React__default["default"].createElement("a", _rollupPluginBabelHelpers["extends"]({ // eslint-disable-line jsx-a11y/role-supports-aria-props,jsx-a11y/anchor-is-valid "aria-haspopup": "menu" // eslint-disable-line jsx-a11y/aria-proptypes , "aria-expanded": expanded, className: linkClassName, href: "#", onKeyDown: handleOnKeyDown, ref: mergedButtonRef, tabIndex: 0 }, accessibilityLabel), menuLinkName, MenuContent ? /*#__PURE__*/React__default["default"].createElement(MenuContent, null) : /*#__PURE__*/React__default["default"].createElement(iconsReact.ChevronDown, { className: `${prefix}--header__menu-arrow` })), /*#__PURE__*/React__default["default"].createElement("ul", _rollupPluginBabelHelpers["extends"]({}, accessibilityLabel, { ref: subMenusRef, className: `${prefix}--header__menu` }), React.Children.map(children, renderMenuItem))); }); HeaderMenu.displayName = 'HeaderMenu'; HeaderMenu.propTypes = { /** * Required props for the accessibility label of the menu */ ...AriaPropTypes.AriaLabelPropType, /** * Optionally provide a custom class to apply to the underlying `<li>` node */ className: PropTypes__default["default"].string, /** * Provide a custom ref handler for the menu button */ focusRef: PropTypes__default["default"].func, /** * Applies selected styles to the item if a user sets this to true and `aria-current !== 'page'`. */ isActive: PropTypes__default["default"].bool, /** * Applies selected styles to the item if a user sets this to true and `aria-current !== 'page'`. * @deprecated Please use `isActive` instead. This will be removed in the next major release. */ isCurrentPage: deprecate["default"](PropTypes__default["default"].bool, 'The `isCurrentPage` prop for `HeaderMenu` has ' + 'been deprecated. Please use `isActive` instead. This will be removed in the next major release.'), /** * Provide a label for the link text */ menuLinkName: PropTypes__default["default"].string.isRequired, /** * Optionally provide an onBlur handler that is called when the underlying * button fires it's onblur event */ onBlur: PropTypes__default["default"].func, /** * Optionally provide an onClick handler that is called when the underlying * button fires it's onclick event */ onClick: PropTypes__default["default"].func, /** * Optionally provide an onKeyDown handler that is called when the underlying * button fires it's onkeydown event */ onKeyDown: PropTypes__default["default"].func, /** * Optional component to render instead of string */ renderMenuContent: PropTypes__default["default"].func, /** * Optionally provide a tabIndex for the underlying menu button */ tabIndex: PropTypes__default["default"].number }; exports.HeaderMenu = HeaderMenu; exports["default"] = HeaderMenu;