blocks/header/header.js (107 lines of code) (raw):

import { getMetadata, decorateIcons, wrapImgsInLinks } from '../../scripts/lib-franklin.js'; // media query match that indicates mobile/tablet width const isDesktop = window.matchMedia('(min-width: 1200px)'); function closeOnEscape(e) { if (e.code === 'Escape') { const nav = document.getElementById('nav'); const navSections = nav.querySelector('.nav-sections'); const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]'); if (navSectionExpanded && isDesktop.matches) { // eslint-disable-next-line no-use-before-define toggleAllNavSections(navSections); navSectionExpanded.focus(); } else if (!isDesktop.matches) { // eslint-disable-next-line no-use-before-define toggleMenu(nav, navSections); nav.querySelector('button').focus(); } } } function openOnKeydown(e) { const focused = document.activeElement; const isNavDrop = focused.className === 'nav-drop'; if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) { const dropExpanded = focused.getAttribute('aria-expanded') === 'true'; // eslint-disable-next-line no-use-before-define toggleAllNavSections(focused.closest('.nav-sections')); focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true'); } } function focusNavSection() { document.activeElement.addEventListener('keydown', openOnKeydown); } /** * Toggles all nav sections * @param {Element} sections The container element * @param {Boolean} expanded Whether the element should be expanded or collapsed */ function toggleAllNavSections(sections, expanded = false) { sections.querySelectorAll('.nav-sections > ul > li').forEach((section) => { section.setAttribute('aria-expanded', expanded); }); } /** * Toggles the entire nav * @param {Element} nav The container element * @param {Element} navSections The nav sections within the container element * @param {*} forceExpanded Optional param to force nav expand behavior when not null */ function toggleMenu(nav, navSections, forceExpanded = null) { const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true'; const button = nav.querySelector('.nav-hamburger button'); document.body.style.overflowY = (expanded || isDesktop.matches) ? '' : 'hidden'; nav.setAttribute('aria-expanded', expanded ? 'false' : 'true'); toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true'); button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation'); // enable nav dropdown keyboard accessibility const navDrops = navSections.querySelectorAll('.nav-drop'); if (isDesktop.matches) { navDrops.forEach((drop) => { if (!drop.hasAttribute('tabindex')) { drop.setAttribute('role', 'button'); drop.setAttribute('tabindex', 0); drop.addEventListener('focus', focusNavSection); } }); } else { navDrops.forEach((drop) => { drop.removeAttribute('role'); drop.removeAttribute('tabindex'); drop.removeEventListener('focus', focusNavSection); }); } // enable menu collapse on escape keypress if (!expanded || isDesktop.matches) { // collapse menu on escape press window.addEventListener('keydown', closeOnEscape); } else { window.removeEventListener('keydown', closeOnEscape); } } /** * decorates the header, mainly the nav * @param {Element} block The header block element */ export default async function decorate(block) { // fetch nav content const navMeta = getMetadata('nav'); const navPath = navMeta ? new URL(navMeta).pathname : '/nav'; const resp = await fetch(`${navPath}.plain.html`); if (resp.ok) { const html = await resp.text(); // decorate nav DOM const nav = document.createElement('nav'); nav.id = 'nav'; nav.innerHTML = html; const classes = ['brand', 'sections']; classes.forEach((c, i) => { const section = nav.children[i]; if (section) section.classList.add(`nav-${c}`); }); const navSections = nav.querySelector('.nav-sections'); if (navSections) { navSections.querySelectorAll(':scope > ul > li').forEach((navSection) => { if (navSection.querySelector('ul')) navSection.classList.add('nav-drop'); navSection.addEventListener('click', () => { if (isDesktop.matches) { const expanded = navSection.getAttribute('aria-expanded') === 'true'; toggleAllNavSections(navSections); navSection.setAttribute('aria-expanded', expanded ? 'false' : 'true'); } }); }); } // hamburger for mobile const hamburger = document.createElement('div'); hamburger.classList.add('nav-hamburger'); hamburger.innerHTML = `<button type="button" aria-controls="nav" aria-label="Open navigation"> <span class="nav-hamburger-icon"></span> </button>`; hamburger.addEventListener('click', () => toggleMenu(nav, navSections)); nav.prepend(hamburger); nav.setAttribute('aria-expanded', 'false'); // prevent mobile nav behavior on window resize toggleMenu(nav, navSections, isDesktop.matches); isDesktop.addEventListener('change', () => toggleMenu(nav, navSections, isDesktop.matches)); decorateIcons(nav); wrapImgsInLinks(nav); const navWrapper = document.createElement('div'); navWrapper.className = 'nav-wrapper'; navWrapper.append(nav); block.append(navWrapper); } }