@massds/mayflower-react
Version:
React versions of Mayflower design system UI components
387 lines (323 loc) • 13.9 kB
JavaScript
"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])
} : {};