UNPKG

@massds/mayflower-react

Version:

React versions of Mayflower design system UI components

387 lines (323 loc) 13.9 kB
"use strict"; exports.__esModule = true; exports.HeaderNavItem = exports.HeaderMainNavContext = exports.HeaderMainNav = void 0; var _react = _interopRequireDefault(require("react")); var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _IconArrowbent = _interopRequireDefault(require("../Icon/IconArrowbent.js")); var _hooks = require("../HeaderNav/hooks.js"); var _useWindowWidth = _interopRequireDefault(require("../hooks/use-window-width.js")); var _getFallbackComponent = _interopRequireDefault(require("../utilities/getFallbackComponent.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var HeaderMainNavContext = /*#__PURE__*/_react["default"].createContext(); exports.HeaderMainNavContext = HeaderMainNavContext; var HeaderMainNav = function HeaderMainNav(_ref) { var NavItem = _ref.NavItem, _ref$items = _ref.items, items = _ref$items === void 0 ? [] : _ref$items; var RenderedNavItem = (0, _getFallbackComponent["default"])(NavItem, HeaderNavItem); var mainNavRef = _react["default"].useRef(); // All items passed will become part of HeaderMainNav's context. var state = (0, _hooks.useHeaderMainNav)(items); return /*#__PURE__*/_react["default"].createElement(HeaderMainNavContext.Provider, { value: state }, /*#__PURE__*/_react["default"].createElement("div", { className: "ma__header__main-nav" }, /*#__PURE__*/_react["default"].createElement("div", { className: "ma__main-nav" }, /*#__PURE__*/_react["default"].createElement("ul", { ref: mainNavRef, role: "menubar", className: "ma__main-nav__items js-main-nav" }, items.map(function (item, itemIndex) { return ( /*#__PURE__*/ // eslint-disable-next-line react/no-array-index-key _react["default"].createElement(RenderedNavItem, _extends({ key: "main-nav-navitem--" + itemIndex }, item, { index: itemIndex, mainNav: mainNavRef })) ); }))))); }; exports.HeaderMainNav = HeaderMainNav; HeaderMainNav.propTypes = process.env.NODE_ENV !== "production" ? { /** An uninstantiated component which handles displaying individual menu items within the menu. */ NavItem: _propTypes["default"].elementType, /** An array of items used to create the menu. */ items: _propTypes["default"].arrayOf(_propTypes["default"].shape({ href: _propTypes["default"].string, text: _propTypes["default"].string, // Active main nav item eccentuated with an styled underline active: _propTypes["default"].bool, subNav: _propTypes["default"].arrayOf(_propTypes["default"].shape({ href: _propTypes["default"].string, text: _propTypes["default"].string })) })) } : {}; var MemoArrowBent = /*#__PURE__*/_react["default"].memo(_IconArrowbent["default"]); var HeaderNavItem = /*#__PURE__*/_react["default"].memo(function (_ref2) { var href = _ref2.href, text = _ref2.text, active = _ref2.active, _ref2$subNav = _ref2.subNav, subNav = _ref2$subNav === void 0 ? [] : _ref2$subNav, index = _ref2.index, mainNav = _ref2.mainNav, id = _ref2.id; var mainContext = _react["default"].useContext(HeaderMainNavContext); var windowWidth = (0, _useWindowWidth["default"])(); var itemRef = _react["default"].useRef(); var buttonRef = _react["default"].useRef(); var contentRef = _react["default"].useRef(); var breakpoint = 840; // This is the same logic as twig for when covid background displays. var isCovid = text.toLowerCase().includes('covid'); var items = mainContext.items, hide = mainContext.hide, show = mainContext.show, setIsOpen = mainContext.setIsOpen, setButtonExpanded = mainContext.setButtonExpanded; var state = items[index]; var buttonExpanded = state.buttonExpanded, isItemOpen = state.isOpen; var hasSubNav = subNav && subNav.length > 0; var classes = (0, _classnames["default"])('ma__main-nav__item js-main-nav-toggle', { 'has-subnav': hasSubNav, 'is-active': active }); var contentClasses = (0, _classnames["default"])('ma__main-nav__subitems js-main-nav-content', { 'is-open': isItemOpen, 'is-closed': !isItemOpen }); var onMouseEnter = _react["default"].useCallback(function () { show({ index: index }); }, [show, index]); var onMouseLeave = _react["default"].useCallback(function () { hide(); }, [hide]); var onButtonLinkClick = _react["default"].useCallback(function (e) { if (windowWidth) { // mobile if (windowWidth <= breakpoint) { e.preventDefault(); // add open class to this item setIsOpen({ index: index, status: true }); show({ index: index }); setButtonExpanded({ index: index, status: true }); } else { if (isItemOpen) { hide({ index: index }); } setButtonExpanded({ index: index, status: false }); if (!isItemOpen) { show({ index: index }); setButtonExpanded({ index: index, status: true }); } } } }, [show, windowWidth, isItemOpen, setIsOpen, setButtonExpanded, index]); var onKeyDown = _react["default"].useCallback(function (e) { var item = itemRef.current; var $parent = mainNav.current; if (item && windowWidth && $parent) { // Grab all the DOM info we need... var hasFocus = 'has-focus'; var $link = item; var $topLevelLinks = $parent.querySelectorAll('.ma__main-nav__top-link'); var $focusedElement = document.activeElement; var menuFlipped = windowWidth < breakpoint; var $otherLinks = Array.from($parent.childNodes).filter(function (child) { return item !== child; }); // relevant if open.. var $topLevelItem = $focusedElement.closest('.ma__main-nav__item'); var $topLevelLink = $topLevelItem.querySelector('.ma__main-nav__top-link'); var $dropdownLinks = $link.querySelectorAll('.ma__main-nav__subitem .ma__main-nav__link'); var dropdownLinksLength = $dropdownLinks.length; var focusIndexInDropdown = Array.from($dropdownLinks).findIndex(function (link) { return link === $focusedElement; }); // Easy access to the key that was pressed. var keycode = e.keyCode; var action = { skip: keycode === 9, // tab close: keycode === 27, // esc left: keycode === 37, // left arrow right: keycode === 39, // right arrow up: keycode === 38, // up arrow down: keycode === 40, // down arrow space: keycode === 32, // space enter: keycode === 13 // enter }; // Default behavior is prevented for all actions except 'skip'. if (action.close || action.left || action.right || action.up || action.down) { e.preventDefault(); } if (action.enter || action.space) { $link.classList.add(hasFocus); $otherLinks.forEach(function (link) { return link.classList.remove(hasFocus); }); } if (action.skip && dropdownLinksLength === focusIndexInDropdown + 1) { if (isItemOpen) { hide(); } $topLevelLink.setAttribute('aria-expanded', 'false'); $link.classList.remove(hasFocus); return; } // Navigate into or within a submenu. This is needed on up/down actions // (unless the menu is flipped and closed) and when using the right arrow // while the menu is flipped and submenu is closed. if ((action.up || action.down) && !(menuFlipped && !isItemOpen) || action.right && menuFlipped && !isItemOpen) { // Open pull down menu if necessary. if (!isItemOpen && !$link.classList.contains(hasFocus)) { show({ index: index }); } // Adjust index of active menu item based on performed action. focusIndexInDropdown += action.up ? -1 : 1; // If the menu is flipped, skip the last item in each submenu. Otherwise, // skip the first item. This is done by repeating the index adjustment. if (menuFlipped) { if (focusIndexInDropdown === dropdownLinksLength - 1) { focusIndexInDropdown += action.up ? -1 : 1; } } else if (focusIndexInDropdown === 0 || focusIndexInDropdown >= dropdownLinksLength) { focusIndexInDropdown += action.up ? -1 : 1; } // Wrap around if at the end of the submenu. focusIndexInDropdown = (focusIndexInDropdown % dropdownLinksLength + dropdownLinksLength) % dropdownLinksLength; $dropdownLinks[focusIndexInDropdown].focus(); } // Close menu and return focus to menubar if (action.close || menuFlipped && action.left) { if (isItemOpen) { hide(); } $link.classList.remove(hasFocus); $topLevelLink.focus(); } // Navigate between submenus. This is needed for left/right actions in // normal layout, or up/down actions in flipped layout (when nav is closed). if ((action.left || action.right) && !menuFlipped || (action.up || action.down) && menuFlipped && !isItemOpen) { var idx = Array.from($topLevelLinks).findIndex(function (link) { return link === $topLevelLink; }); var prev = action.left || action.up; var linkCount = $topLevelLinks.length; // hide content // If menubar focus // - Change menubar item // // If dropdown focus // - Open previous pull down menu and select first item if (isItemOpen) { hide(); } $link.classList.remove(hasFocus); // Get previous item if left arrow, next item if right arrow. idx += prev ? -1 : 1; // Wrap around if at the end of the set of menus. idx = (idx % linkCount + linkCount) % linkCount; $topLevelLinks[idx].focus(); } } }, [index, itemRef, windowWidth, mainNav, isItemOpen]); // Adds keyboard support. (0, _hooks.useHeaderNavKeydown)(itemRef.current, onKeyDown); // Adds mouse events. (0, _hooks.useHeaderNavMouseEvents)(itemRef.current, onMouseEnter, onMouseLeave); // Adds button events. (0, _hooks.useHeaderNavButtonEffects)(buttonRef.current, onButtonLinkClick); return /*#__PURE__*/_react["default"].createElement("li", { ref: itemRef, role: "none", className: classes, tabIndex: "-1" }, isCovid ? /*#__PURE__*/_react["default"].createElement("a", { role: "menuitem", href: href, className: "ma__main-nav__top-link cv-alternate-style", tabIndex: "0" }, text) : /*#__PURE__*/_react["default"].createElement("button", { ref: buttonRef, type: "button", role: "menuitem", id: "button" + index, className: "ma__main-nav__top-link", "aria-haspopup": "true", tabIndex: "0", "aria-expanded": buttonExpanded }, /*#__PURE__*/_react["default"].createElement("span", { className: "visually-hidden show-label" }, "Show the sub topics of "), text), hasSubNav && /*#__PURE__*/_react["default"].createElement("div", { ref: contentRef, className: contentClasses }, /*#__PURE__*/_react["default"].createElement("ul", { id: id || "menu" + index, role: "menu", "aria-labelledby": "button" + index, className: "ma__main-nav__container" }, subNav.map(function (item, itemIndex) { return ( /*#__PURE__*/ // eslint-disable-next-line react/no-array-index-key _react["default"].createElement("li", { key: "main-nav-subitem--" + index + "-" + itemIndex, role: "none", className: "ma__main-nav__subitem" }, /*#__PURE__*/_react["default"].createElement("a", { "aria-expanded": buttonExpanded, onClick: onButtonLinkClick, role: "menuitem", href: item.href, className: "ma__main-nav__link" }, item.text)) ); }), href && /*#__PURE__*/_react["default"].createElement("li", { role: "none", className: "ma__main-nav__subitem--main" }, /*#__PURE__*/_react["default"].createElement("a", { "aria-expanded": buttonExpanded, onClick: onButtonLinkClick, role: "menuitem", href: href, className: "ma__main-nav__link" }, /*#__PURE__*/_react["default"].createElement(MemoArrowBent, null), /*#__PURE__*/_react["default"].createElement("span", null, /*#__PURE__*/_react["default"].createElement("span", { className: "visually-hidden" }, "See all topics under "), text)))))); }); exports.HeaderNavItem = HeaderNavItem; HeaderNavItem.propTypes = process.env.NODE_ENV !== "production" ? { id: _propTypes["default"].string, hide: _propTypes["default"].func, show: _propTypes["default"].func, href: _propTypes["default"].string, text: _propTypes["default"].string, mainNav: _propTypes["default"].shape({ /* eslint-disable-next-line react/forbid-prop-types */ current: _propTypes["default"].object // Element doesn't exist for SSR, so we check for it. }), subNav: _propTypes["default"].arrayOf(_propTypes["default"].shape({ href: _propTypes["default"].string, text: _propTypes["default"].string })), index: _propTypes["default"].oneOfType([_propTypes["default"].number, _propTypes["default"].string]) } : {};