uswds
Version:
Open source UI components and visual style guide for U.S. government websites
121 lines (98 loc) • 3.76 kB
JavaScript
const assign = require('object-assign');
const forEach = require('array-foreach');
const behavior = require('../utils/behavior');
const select = require('../utils/select');
const FocusTrap = require('../utils/focus-trap');
const accordion = require('./accordion');
const { CLICK } = require('../events');
const { prefix: PREFIX } = require('../config');
const NAV = `.${PREFIX}-nav`;
const NAV_LINKS = `${NAV} a`;
const OPENERS = `.${PREFIX}-menu-btn`;
const CLOSE_BUTTON = `.${PREFIX}-nav-close`;
const OVERLAY = `.${PREFIX}-overlay`;
const CLOSERS = `${CLOSE_BUTTON}, .${PREFIX}-overlay`;
const TOGGLES = [NAV, OVERLAY].join(', ');
const ACTIVE_CLASS = 'usa-mobile_nav-active';
const VISIBLE_CLASS = 'is-visible';
let navigation;
const isActive = () => document.body.classList.contains(ACTIVE_CLASS);
const toggleNav = function (active) {
const { body } = document;
const safeActive = typeof active === 'boolean' ? active : !isActive();
body.classList.toggle(ACTIVE_CLASS, safeActive);
forEach(select(TOGGLES), el => el.classList.toggle(VISIBLE_CLASS, safeActive));
navigation.focusTrap.update(safeActive);
const closeButton = body.querySelector(CLOSE_BUTTON);
const menuButton = body.querySelector(OPENERS);
if (safeActive && closeButton) {
// The mobile nav was just activated, so focus on the close button,
// which is just before all the nav elements in the tab order.
closeButton.focus();
} else if (!safeActive && document.activeElement === closeButton && menuButton) {
// The mobile nav was just deactivated, and focus was on the close
// button, which is no longer visible. We don't want the focus to
// disappear into the void, so focus on the menu button if it's
// visible (this may have been what the user was just focused on,
// if they triggered the mobile nav by mistake).
menuButton.focus();
}
return safeActive;
};
const resize = () => {
const closer = document.body.querySelector(CLOSE_BUTTON);
if (isActive() && closer && closer.getBoundingClientRect().width === 0) {
// The mobile nav is active, but the close box isn't visible, which
// means the user's viewport has been resized so that it is no longer
// in mobile mode. Let's make the page state consistent by
// deactivating the mobile nav.
navigation.toggleNav.call(closer, false);
}
};
const onMenuClose = () => navigation.toggleNav.call(navigation, false);
navigation = behavior({
[CLICK]: {
[OPENERS]: toggleNav,
[CLOSERS]: toggleNav,
[NAV_LINKS]() {
// A navigation link has been clicked! We want to collapse any
// hierarchical navigation UI it's a part of, so that the user
// can focus on whatever they've just selected.
// Some navigation links are inside accordions; when they're
// clicked, we want to collapse those accordions.
const acc = this.closest(accordion.ACCORDION);
if (acc) {
accordion.getButtons(acc).forEach(btn => accordion.hide(btn));
}
// If the mobile navigation menu is active, we want to hide it.
if (isActive()) {
navigation.toggleNav.call(navigation, false);
}
},
},
}, {
init() {
const trapContainer = document.querySelector(NAV);
if (trapContainer) {
navigation.focusTrap = FocusTrap(trapContainer, {
Escape: onMenuClose,
});
}
resize();
window.addEventListener('resize', resize, false);
},
teardown() {
window.removeEventListener('resize', resize, false);
},
focusTrap: null,
toggleNav,
});
/**
* TODO for 2.0, remove this statement and export `navigation` directly:
*
* module.exports = behavior({...});
*/
module.exports = assign(
el => navigation.on(el),
navigation
);