UNPKG

@massds/mayflower-react

Version:

React versions of Mayflower design system UI components

307 lines (306 loc) 11.5 kB
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } import React from "react"; import classNames from "classnames"; import propTypes from "prop-types"; import { useHeaderNavKeydown, useHeaderNavMouseEvents, useHeaderNavButtonEffects, useHeaderMainNav } from "../HeaderNav/hooks.mjs"; import useWindowWidth from "../hooks/use-window-width.mjs"; import getFallbackComponent from "../utilities/getFallbackComponent.mjs"; export const HeaderMainNavContext = /*#__PURE__*/React.createContext(); export const HeaderMainNav = _ref => { let NavItem = _ref.NavItem, _ref$items = _ref.items, items = _ref$items === void 0 ? [] : _ref$items; const RenderedNavItem = getFallbackComponent(NavItem, HeaderNavItem); const mainNavRef = React.useRef(); // All items passed will become part of HeaderMainNav's context. const state = useHeaderMainNav(items); return /*#__PURE__*/React.createElement(HeaderMainNavContext.Provider, { value: state }, /*#__PURE__*/React.createElement("div", { className: "ma__header__main-nav" }, /*#__PURE__*/React.createElement("div", { className: "ma__main-nav" }, /*#__PURE__*/React.createElement("ul", { ref: mainNavRef, role: "menubar", className: "ma__main-nav__items js-main-nav" }, items.map((item, itemIndex) => /*#__PURE__*/ // eslint-disable-next-line react/no-array-index-key React.createElement(RenderedNavItem, _extends({ key: "main-nav-navitem--" + itemIndex }, item, { index: itemIndex, mainNav: mainNavRef }))))))); }; HeaderMainNav.propTypes = process.env.NODE_ENV !== "production" ? { /** An uninstantiated component which handles displaying individual menu items within the menu. */ NavItem: propTypes.elementType, /** An array of items used to create the menu. */ items: propTypes.arrayOf(propTypes.shape({ href: propTypes.string, text: propTypes.string, // Active main nav item eccentuated with an styled underline active: propTypes.bool, subNav: propTypes.arrayOf(propTypes.shape({ href: propTypes.string, text: propTypes.string })) })) } : {}; export const HeaderNavItem = /*#__PURE__*/React.memo(_ref2 => { let 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; const mainContext = React.useContext(HeaderMainNavContext); const windowWidth = useWindowWidth(); const itemRef = React.useRef(); const buttonRef = React.useRef(); const contentRef = React.useRef(); const breakpoint = 840; const items = mainContext.items, hide = mainContext.hide, show = mainContext.show, setIsOpen = mainContext.setIsOpen, setButtonExpanded = mainContext.setButtonExpanded; const state = items[index]; const buttonExpanded = state.buttonExpanded, isItemOpen = state.isOpen; const hasSubNav = subNav && subNav.length > 0; const classes = classNames('ma__main-nav__item js-main-nav-toggle', { 'has-subnav': hasSubNav, 'is-active': active }); const contentClasses = classNames('ma__main-nav__subitems js-main-nav-content', { 'is-open': isItemOpen, 'is-closed': !isItemOpen }); // This is the same logic as twig for when covid background displays. const isCovid = text.toLowerCase().includes('covid'); const topNavLinkclasses = classNames('ma__main-nav__top-link', { ' cv-alternate-style': isCovid }); const onMouseEnter = React.useCallback(() => { show({ index: index }); }, [show, index]); const onMouseLeave = React.useCallback(() => { hide(); }, [hide]); const onButtonLinkClick = React.useCallback(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]); const onKeyDown = React.useCallback(e => { const item = itemRef.current; const $parent = mainNav.current; if (item && windowWidth && $parent) { // Grab all the DOM info we need... const hasFocus = 'has-focus'; const $link = item; const $topLevelLinks = $parent.querySelectorAll('.ma__main-nav__top-link'); const $focusedElement = document.activeElement; const menuFlipped = windowWidth < breakpoint; const $otherLinks = Array.from($parent.childNodes).filter(child => item !== child); // relevant if open.. const $topLevelItem = $focusedElement.closest('.ma__main-nav__item'); const $topLevelLink = $topLevelItem.querySelector('.ma__main-nav__top-link'); const $dropdownLinks = $link.querySelectorAll('.ma__main-nav__subitem .ma__main-nav__link'); const dropdownLinksLength = $dropdownLinks && $dropdownLinks.length; let focusIndexInDropdown = Array.from($dropdownLinks).findIndex(link => link === $focusedElement); // Easy access to the key that was pressed. const key = e.key, code = e.code; const action = { tab: key === 'Tab', // tab esc: key === 'Esc' || key === 'Escape', // esc left: key === 'Left' || key === 'ArrowLeft', // left arrow right: key === 'Right' || key === 'ArrowRight', // right arrow up: key === 'Up' || key === 'ArrowUp', // up arrow down: key === 'Down' || key === 'ArrowDown', // down arrow space: key === ' ' || code === 'Space', // space enter: key === 'Enter' // enter }; // Default behavior is prevented for all actions except 'tab'. if (action.esc || action.left || action.right || action.up || action.down) { e.preventDefault(); } if (action.enter || action.space) { $link.classList.add(hasFocus); $otherLinks.forEach(link => link.classList.remove(hasFocus)); } if (action.tab && dropdownLinksLength === focusIndexInDropdown + 1) { if (isItemOpen) { hide(); } $topLevelLink.setAttribute('aria-expanded', 'false'); $link.classList.remove(hasFocus); return; } // Navigate into or within a submenu using the up/down arrow keys. if ((action.up || action.down) && dropdownLinksLength > 0) { // Open submenu if it's not open already. if (!isItemOpen && !$link.classList.contains(hasFocus)) { show({ index: index }); if (action.up) { focusIndexInDropdown = dropdownLinksLength - 1; } else { focusIndexInDropdown = 0; } $dropdownLinks[focusIndexInDropdown].focus(); } else { // Adjust index of active menu item based on performed action. 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.esc || 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) { let idx = Array.from($topLevelLinks).findIndex(link => link === $topLevelLink); const prev = action.left || action.up; const 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. useHeaderNavKeydown(itemRef.current, onKeyDown); // Adds mouse events. useHeaderNavMouseEvents(itemRef.current, onMouseEnter, onMouseLeave); // Adds button events. useHeaderNavButtonEffects(buttonRef.current, onButtonLinkClick); return /*#__PURE__*/React.createElement("li", { ref: itemRef, role: "menuitem", className: classes, tabIndex: "-1" }, hasSubNav ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", { ref: buttonRef, type: "button", id: "button" + index, className: "ma__main-nav__top-link", "aria-haspopup": "true", tabIndex: "0", "aria-expanded": buttonExpanded }, /*#__PURE__*/React.createElement("span", { className: "visually-hidden show-label" }, "Show the sub topics of "), text), /*#__PURE__*/React.createElement("div", { ref: contentRef, className: contentClasses }, /*#__PURE__*/React.createElement("ul", { id: id || "menu" + index, role: "menu", "aria-labelledby": "button" + index, className: "ma__main-nav__container" }, subNav.map((item, itemIndex) => /*#__PURE__*/ // eslint-disable-next-line react/no-array-index-key React.createElement("li", { key: "main-nav-subitem--" + index + "-" + itemIndex, role: "none", className: "ma__main-nav__subitem" }, /*#__PURE__*/React.createElement("a", { onClick: onButtonLinkClick, href: item.href, role: "menuitem", className: "ma__main-nav__link" }, item.text)))))) : /*#__PURE__*/React.createElement("a", { href: href, className: topNavLinkclasses, tabIndex: "0" }, text)); }); HeaderNavItem.propTypes = process.env.NODE_ENV !== "production" ? { id: propTypes.string, hide: propTypes.func, show: propTypes.func, href: propTypes.string, text: propTypes.string, active: propTypes.bool, mainNav: propTypes.shape({ /* eslint-disable-next-line react/forbid-prop-types */ current: propTypes.object // Element doesn't exist for SSR, so we check for it. }), subNav: propTypes.arrayOf(propTypes.shape({ href: propTypes.string, text: propTypes.string })), index: propTypes.oneOfType([propTypes.number, propTypes.string]) } : {};