You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.6 KiB
161 lines
4.6 KiB
12 months ago
|
/**
|
||
|
* @file
|
||
|
* Customization of navigation.
|
||
|
*/
|
||
|
|
||
|
((Drupal, once, tabbable) => {
|
||
|
/**
|
||
|
* Checks if navWrapper contains "is-active" class.
|
||
|
*
|
||
|
* @param {Element} navWrapper
|
||
|
* Header navigation.
|
||
|
*
|
||
|
* @return {boolean}
|
||
|
* True if navWrapper contains "is-active" class, false if not.
|
||
|
*/
|
||
|
function isNavOpen(navWrapper) {
|
||
|
return navWrapper.classList.contains('is-active');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Opens or closes the header navigation.
|
||
|
*
|
||
|
* @param {object} props
|
||
|
* Navigation props.
|
||
|
* @param {boolean} state
|
||
|
* State which to transition the header navigation menu into.
|
||
|
*/
|
||
|
function toggleNav(props, state) {
|
||
|
const value = !!state;
|
||
|
props.navButton.setAttribute('aria-expanded', value);
|
||
|
props.body.classList.toggle('is-overlay-active', value);
|
||
|
props.body.classList.toggle('is-fixed', value);
|
||
|
props.navWrapper.classList.toggle('is-active', value);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialize the header navigation.
|
||
|
*
|
||
|
* @param {object} props
|
||
|
* Navigation props.
|
||
|
*/
|
||
|
function init(props) {
|
||
|
props.navButton.setAttribute('aria-controls', props.navWrapperId);
|
||
|
props.navButton.setAttribute('aria-expanded', 'false');
|
||
|
|
||
|
props.navButton.addEventListener('click', () => {
|
||
|
toggleNav(props, !isNavOpen(props.navWrapper));
|
||
|
});
|
||
|
|
||
|
// Close any open sub-navigation first, then close the header navigation.
|
||
|
document.addEventListener('keyup', (e) => {
|
||
|
if (e.key === 'Escape') {
|
||
|
if (props.olives.areAnySubNavsOpen()) {
|
||
|
props.olives.closeAllSubNav();
|
||
|
} else {
|
||
|
toggleNav(props, false);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
props.overlay.addEventListener('click', () => {
|
||
|
toggleNav(props, false);
|
||
|
});
|
||
|
|
||
|
props.overlay.addEventListener('touchstart', () => {
|
||
|
toggleNav(props, false);
|
||
|
});
|
||
|
|
||
|
// Focus trap. This is added to the header element because the navButton
|
||
|
// element is not a child element of the navWrapper element, and the keydown
|
||
|
// event would not fire if focus is on the navButton element.
|
||
|
props.header.addEventListener('keydown', (e) => {
|
||
|
if (e.key === 'Tab' && isNavOpen(props.navWrapper)) {
|
||
|
const tabbableNavElements = tabbable.tabbable(props.navWrapper);
|
||
|
tabbableNavElements.unshift(props.navButton);
|
||
|
const firstTabbableEl = tabbableNavElements[0];
|
||
|
const lastTabbableEl =
|
||
|
tabbableNavElements[tabbableNavElements.length - 1];
|
||
|
|
||
|
if (e.shiftKey) {
|
||
|
if (
|
||
|
document.activeElement === firstTabbableEl &&
|
||
|
!props.olives.isDesktopNav()
|
||
|
) {
|
||
|
lastTabbableEl.focus();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
} else if (
|
||
|
document.activeElement === lastTabbableEl &&
|
||
|
!props.olives.isDesktopNav()
|
||
|
) {
|
||
|
firstTabbableEl.focus();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Remove overlays when browser is resized and desktop nav appears.
|
||
|
window.addEventListener('resize', () => {
|
||
|
if (props.olives.isDesktopNav()) {
|
||
|
toggleNav(props, false);
|
||
|
props.body.classList.remove('is-overlay-active');
|
||
|
props.body.classList.remove('is-fixed');
|
||
|
}
|
||
|
|
||
|
// Ensure that all sub-navigation menus close when the browser is resized.
|
||
|
Drupal.olives.closeAllSubNav();
|
||
|
});
|
||
|
|
||
|
// If hyperlink links to an anchor in the current page, close the
|
||
|
// mobile menu after the click.
|
||
|
props.navWrapper.addEventListener('click', (e) => {
|
||
|
if (
|
||
|
e.target.matches(
|
||
|
`[href*="${window.location.pathname}#"], [href*="${window.location.pathname}#"] *, [href^="#"], [href^="#"] *`,
|
||
|
)
|
||
|
) {
|
||
|
toggleNav(props, false);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialize the navigation.
|
||
|
*
|
||
|
* @type {Drupal~behavior}
|
||
|
*
|
||
|
* @prop {Drupal~behaviorAttach} attach
|
||
|
* Attach context and settings for navigation.
|
||
|
*/
|
||
|
Drupal.behaviors.olivesNavigation = {
|
||
|
attach(context) {
|
||
|
const headerId = 'header';
|
||
|
const header = once('navigation', `#${headerId}`, context).shift();
|
||
|
const navWrapperId = 'header-nav';
|
||
|
|
||
|
if (header) {
|
||
|
const navWrapper = header.querySelector(`#${navWrapperId}`);
|
||
|
const { olives } = Drupal;
|
||
|
const navButton = context.querySelector(
|
||
|
'[data-drupal-selector="mobile-nav-button"]',
|
||
|
);
|
||
|
const body = document.body;
|
||
|
const overlay = context.querySelector(
|
||
|
'[data-drupal-selector="header-nav-overlay"]',
|
||
|
);
|
||
|
|
||
|
init({
|
||
|
olives,
|
||
|
header,
|
||
|
navWrapperId,
|
||
|
navWrapper,
|
||
|
navButton,
|
||
|
body,
|
||
|
overlay,
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
})(Drupal, once, tabbable);
|