@massds/mayflower-react
Version:
React versions of Mayflower design system UI components
326 lines (324 loc) • 12.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 _hooks = require("../HeaderNav/hooks.js");
var _useWindowWidth = _interopRequireDefault(require("../hooks/use-window-width.js"));
var _getFallbackComponent = _interopRequireDefault(require("../utilities/getFallbackComponent.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
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); }
var HeaderMainNavContext = exports.HeaderMainNavContext = /*#__PURE__*/_react["default"].createContext();
var HeaderMainNav = exports.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
}))
);
})))));
};
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 HeaderNavItem = exports.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;
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
});
// This is the same logic as twig for when covid background displays.
var isCovid = text.toLowerCase().includes('covid');
var topNavLinkclasses = (0, _classnames["default"])('ma__main-nav__top-link', {
' cv-alternate-style': isCovid
});
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 && $dropdownLinks.length;
var focusIndexInDropdown = Array.from($dropdownLinks).findIndex(function (link) {
return link === $focusedElement;
});
// Easy access to the key that was pressed.
var key = e.key,
code = e.code;
var 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(function (link) {
return 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) {
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: "menuitem",
className: classes,
tabIndex: "-1"
}, hasSubNav ? /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].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["default"].createElement("span", {
className: "visually-hidden show-label"
}, "Show the sub topics of "), text), /*#__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", {
onClick: onButtonLinkClick,
href: item.href,
role: "menuitem",
className: "ma__main-nav__link"
}, item.text))
);
})))) : /*#__PURE__*/_react["default"].createElement("a", {
href: href,
className: topNavLinkclasses,
tabIndex: "0"
}, text));
});
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,
active: _propTypes["default"].bool,
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])
} : {};