From 6417b22552084e4ea295dd990be4a94a4249c7e1 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Mon, 22 Jun 2026 15:58:45 -0600 Subject: [PATCH 1/4] feat: beginning of new header block --- blocks/header/header.css | 369 ++++++++++++++++++--------------------- blocks/header/header.js | 253 ++++++++++++--------------- 2 files changed, 283 insertions(+), 339 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index f414ca7..67a7ef1 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,272 +1,243 @@ -/* header and nav layout */ -header .nav-wrapper { - position: fixed; - z-index: 2; - width: 100%; +body > header { + border-bottom: 1px solid var(--color-border); background-color: var(--color-bg); + background-image: linear-gradient(var(--color-bg) 83px, var(--color-gray-200) 83px); } -header nav { +@media (width >= 1200px) { + body > header { + background-image: linear-gradient(var(--color-gray-200) 56px, var(--color-bg) 56px); + } +} + +header .header { display: grid; grid-template: - 'hamburger brand tools' var(--nav-height) - 'sections sections sections' 1fr / auto 1fr auto; + 'hamburger logo .' 68px + 'search search search' auto + 'nav nav nav' auto + 'language language language' auto + / 44px 1fr 44px; align-items: center; - gap: 0 var(--space-m); - box-sizing: border-box; - height: var(--nav-height); - max-width: 1248px; - margin: auto; - padding: 0 var(--space-m); - font-family: var(--body-font-family); + gap: calc(2 * var(--space-xs)) 0; + max-width: var(--site-width); + margin: 0 auto; + padding: var(--space-xs); + font-size: var(--text-s); } -header nav[aria-expanded='true'] { - grid-template: - 'hamburger brand' var(--nav-height) - 'sections sections' 1fr - 'tools tools' var(--nav-height) / auto 1fr; - overflow-y: auto; - min-height: 100dvh; +@media (width >= 600px) { + header .header { + padding: var(--space-xs) var(--space-s); + } } @media (width >= 900px) { - header .nav-wrapper { - position: relative; - } - - header nav { - display: flex; - justify-content: space-between; - gap: 0 var(--space-l); - max-width: 1264px; - padding: 0 var(--space-l); + header .header { + padding: var(--space-xs) var(--space-m); } +} - header nav[aria-expanded='true'] { - overflow: visible; - min-height: 0; +@media (width >= 1200px) { + header .header { + grid-template: + 'logo search search' auto + 'nav nav language' auto / auto auto auto; + gap: calc(2 * var(--space-s)) var(--space-m); + padding: var(--space-s) var(--space-m); } } -header nav p { +header .header p { margin: 0; - line-height: 1; } -header nav a:any-link { - color: currentcolor; -} - -/* hamburger */ -header nav .nav-hamburger { - display: flex; - grid-area: hamburger; - align-items: center; - height: 22px; +header .header a { + color: inherit; } -header nav .nav-hamburger button { - overflow: initial; - height: 22px; +header .header ul { + list-style: none; margin: 0; - border: 0; - border-radius: 0; padding: 0; - background-color: var(--color-bg); - color: inherit; - text-overflow: initial; - white-space: initial; } -header nav .nav-hamburger-icon, -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { - display: block; - position: relative; - box-sizing: border-box; - width: 20px; +/* header logo */ +header .header-logo { + grid-area: logo; + justify-self: center; } -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { - content: ''; - position: absolute; - background: currentcolor; +header .header-logo img, +header .header-logo svg { + max-height: 68px; + width: auto; } -header nav[aria-expanded='false'] .nav-hamburger-icon, -header nav[aria-expanded='false'] .nav-hamburger-icon::before, -header nav[aria-expanded='false'] .nav-hamburger-icon::after { - height: 2px; - border-radius: 2px; - background: currentcolor; -} - -header nav[aria-expanded='false'] .nav-hamburger-icon::before { - top: -6px; -} +@media (width >= 1200px) { + header .header-logo { + padding: var(--space-xs) 0; + } -header nav[aria-expanded='false'] .nav-hamburger-icon::after { - top: 6px; + header .header-logo img, + header .header-logo svg { + max-height: 40px; + } } -header nav[aria-expanded='true'] .nav-hamburger-icon { - height: 22px; +/* header nav */ +header .header-nav { + display: none; + grid-area: nav; } -header nav[aria-expanded='true'] .nav-hamburger-icon::before, -header nav[aria-expanded='true'] .nav-hamburger-icon::after { - top: 3px; - left: 1px; - height: 2px; - width: 24px; - border-radius: 2px; - transform: rotate(45deg); - transform-origin: 2px 1px; +header .header-nav > ul { + display: flex; + align-items: center; + gap: var(--space-m); } -header nav[aria-expanded='true'] .nav-hamburger-icon::after { - top: unset; - bottom: 3px; - transform: rotate(-45deg); +header .header-nav > ul > li { + position: relative; } -@media (width >= 900px) { - header nav .nav-hamburger { - display: none; - visibility: hidden; - } +header .header-nav button { + display: flex; + align-items: center; + gap: 0.5ch; + border: none; + background: none; + cursor: pointer; } -/* brand */ -header .nav-brand { - flex-basis: 128px; - grid-area: brand; - font-size: var(--heading-s); - font-weight: 700; - line-height: 1; +header .header-nav button, +header .header-nav > ul > li a { + padding: 1em 0; } -header nav .nav-brand img { - height: auto; - width: 128px; +header .header-nav button[aria-expanded='true'], +header .header-nav button:hover, +header .header-nav button:focus-visible { + text-decoration: underline; } -/* sections */ -header nav .nav-sections { +header .header-nav li > ul { display: none; - flex: 1 1 auto; - grid-area: sections; - visibility: hidden; } -header nav[aria-expanded='true'] .nav-sections { +header .header-nav button[aria-expanded='true'] + ul { display: block; - align-self: start; - visibility: visible; + position: absolute; + top: 100%; + left: calc(-1 * var(--space-s)); + width: max-content; + min-width: 100%; + padding: var(--space-s); + box-shadow: var(--shadow-s); + background-color: var(--color-bg-subtle); } -header nav .nav-sections ul { - list-style: none; - padding-left: 0; - font-size: var(--text-m); +header .header-nav button[aria-expanded='true'] .icon img, +header .header-nav button[aria-expanded='true'] .icon svg { + transform: rotate(-180deg); } -header nav .nav-sections ul > li { - font-weight: 400; +header .header-nav li > ul li + li { + margin-top: var(--space-s); } -header nav .nav-sections ul > li > ul { - margin-top: 0; +@media (width >= 1200px) { + header .header-nav { + display: block; + } } -header nav .nav-sections ul > li > ul > li { - font-weight: 400; +/* header language */ +header .header-language { + display: none; + grid-area: language; + position: relative; } -@media (width >= 900px) { - header nav .nav-sections { +@media (width >= 1200px) { + header .header-language { display: block; - visibility: visible; - white-space: nowrap; } +} - header nav[aria-expanded='true'] .nav-sections { - align-self: unset; - } +/* header search */ +header .header-search { + grid-area: search; + position: relative; +} - header nav .nav-sections .nav-drop { - position: relative; - padding-right: var(--space-s); - cursor: pointer; - } +/* header hamburger */ +header .header-hamburger { + grid-area: hamburger; +} - header nav .nav-sections .nav-drop::after { - content: ''; - display: inline-block; - position: absolute; - top: 0.5em; - right: 2px; - height: 6px; - width: 6px; - border: 2px solid currentcolor; - border-radius: 0 1px 0 0; - border-width: 2px 2px 0 0; - transform: rotate(135deg); - } +/* stylelint-disable-next-line no-descending-specificity */ +header .header-hamburger button { + display: flex; + align-items: center; + justify-content: center; + height: 44px; + width: 44px; + border: none; + padding: 0; + background: none; + cursor: pointer; + transition: transform 0.2s; +} - header nav .nav-sections .nav-drop[aria-expanded='true']::after { - top: unset; - bottom: 0.5em; - transform: rotate(315deg); - } +header .header-hamburger button:hover, +header .header-hamburger button:focus-visible { + transform: scale(1.2); +} - header nav .nav-sections ul { - display: flex; - gap: var(--space-m); - margin: 0; - } +header .header-hamburger .icon-hamburger, +header .header-hamburger .icon-hamburger::before, +header .header-hamburger .icon-hamburger::after { + display: block; + height: 2px; + width: 24px; + background-color: currentcolor; + transition: transform 0.2s, background-color 0.2s; +} - header nav .nav-sections .default-content-wrapper > ul > li { - flex: 0 1 auto; - position: relative; - } +header .header-hamburger .icon-hamburger { + position: relative; +} - header nav .nav-sections .default-content-wrapper > ul > li > ul { - display: none; - position: relative; - } +header .header-hamburger .icon-hamburger::before, +header .header-hamburger .icon-hamburger::after { + content: ''; + position: absolute; + left: 0; +} - header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded='true'] > ul { - display: block; - position: absolute; - top: 150%; - left: -24px; - width: 200px; - padding: var(--space-s); - background-color: var(--color-bg-light); - white-space: initial; - } +header .header-hamburger .icon-hamburger::before { + top: -8px; +} - header nav .nav-sections .default-content-wrapper > ul > li > ul::before { - content: ''; - position: absolute; - top: -8px; - left: 16px; - height: 0; - width: 0; - border-left: 8px solid transparent; - border-bottom: 8px solid var(--color-bg-light); - border-right: 8px solid transparent; - } +header .header-hamburger .icon-hamburger::after { + top: 8px; +} - header nav .nav-sections .default-content-wrapper > ul > li > ul > li { - padding: var(--space-xs) 0; - } +header .header-hamburger button[aria-expanded='true'] .icon-hamburger { + background-color: transparent; +} + +header .header-hamburger button[aria-expanded='true'] .icon-hamburger::before { + transform: translateY(8px) rotate(45deg); +} + +header .header-hamburger button[aria-expanded='true'] .icon-hamburger::after { + transform: translateY(-8px) rotate(-45deg); } -/* tools */ -header nav .nav-tools { - grid-area: tools; +@media (width >= 1200px) { + header .header-hamburger { + display: none; + } } diff --git a/blocks/header/header.js b/blocks/header/header.js index c7d6ea2..5610fc4 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,171 +1,144 @@ -import { getMetadata } from '../../scripts/aem.js'; +import { getMetadata, decorateIcons } from '../../scripts/aem.js'; +import { decorateExternalLinks } from '../../scripts/scripts.js'; import { loadFragment } from '../fragment/fragment.js'; -// media query match that indicates mobile/tablet width -const isDesktop = window.matchMedia('(min-width: 900px)'); - -function closeOnEscape(e) { - if (e.code === 'Escape') { - const nav = document.getElementById('nav'); - const navSections = nav.querySelector('.nav-sections'); - if (!navSections) return; - 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 closeOnFocusLost(e) { - const nav = e.currentTarget; - if (!nav.contains(e.relatedTarget)) { - const navSections = nav.querySelector('.nav-sections'); - if (!navSections) return; - const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]'); - if (navSectionExpanded && isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(navSections, false); - } else if (!isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleMenu(nav, navSections, false); - } - } +/** + * Returns a named header section from within the block. + * @param {Element} block - The header block element + * @param {string} sectionName - The section name, matching the header-{name} class + * @returns {Element|null} The section element, or `null` if not found + */ +function getSection(block, sectionName) { + return block.querySelector(`.header-${sectionName}`); } -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'); +/** + * Wraps the logo image in its anchor if an href is present. + * @param {Element} section - The logo section element + */ +function decorateLogo(section) { + const img = section.querySelector('img, svg'); + if (!img) return; + const link = section.querySelector('a[href]'); + if (link) { + const picture = img.closest('picture') || img; + link.textContent = ''; + link.append(picture); + section.textContent = ''; + section.append(link); } } -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 + * Replaces the section div with a semantic nav element and decorates top-level items. + * @param {Element} section - The nav section element to promote */ -function toggleAllNavSections(sections, expanded = false) { - if (!sections) return; - sections.querySelectorAll('.nav-sections .default-content-wrapper > ul > li').forEach((section) => { - section.setAttribute('aria-expanded', expanded); +function decorateNav(section) { + const list = section.querySelector('ul'); + if (!list) return; + + list.querySelectorAll(':scope > li').forEach((item) => { + const p = item.querySelector(':scope > p'); + const submenu = item.querySelector(':scope > ul'); + if (!p) return; + + if (submenu) { + const button = document.createElement('button'); + button.type = 'button'; + button.setAttribute('aria-expanded', false); + button.textContent = p.textContent.trim(); + const chevron = document.createElement('span'); + chevron.classList.add('icon', 'icon-chevron'); + button.append(chevron); + button.addEventListener('click', () => { + const expanded = button.getAttribute('aria-expanded') === 'true'; + button.closest('ul').querySelectorAll('button[aria-expanded="true"]').forEach((open) => { + open.setAttribute('aria-expanded', false); + }); + if (!expanded) button.setAttribute('aria-expanded', true); + }); + p.replaceWith(button); + } else { + const link = p.querySelector('a'); + if (link) p.replaceWith(link); + } + }); + + const nav = document.createElement('nav'); + nav.classList.add(...section.classList); + nav.append(list); + section.replaceWith(nav); + + document.addEventListener('click', (e) => { + if (nav.contains(e.target)) return; + nav.querySelectorAll('button[aria-expanded="true"]').forEach((open) => { + open.setAttribute('aria-expanded', false); + }); + }); + + document.addEventListener('keydown', (e) => { + if (e.key !== 'Escape') return; + const open = nav.querySelector('button[aria-expanded="true"]'); + if (!open) return; + open.setAttribute('aria-expanded', false); + open.focus(); }); } /** - * 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 + * Builds the hamburger section. + * @returns {Element} - The hamburger section element */ -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 - if (navSections) { - const navDrops = navSections.querySelectorAll('.nav-drop'); - if (isDesktop.matches) { - navDrops.forEach((drop) => { - if (!drop.hasAttribute('tabindex')) { - drop.setAttribute('tabindex', 0); - drop.addEventListener('focus', focusNavSection); - } - }); - } else { - navDrops.forEach((drop) => { - drop.removeAttribute('tabindex'); - drop.removeEventListener('focus', focusNavSection); - }); - } - } +function buildHamburger() { + const wrapper = document.createElement('div'); + wrapper.classList.add('section', 'header-hamburger'); - // enable menu collapse on escape keypress - if (!expanded || isDesktop.matches) { - // collapse menu on escape press - window.addEventListener('keydown', closeOnEscape); - // collapse menu on focus lost - nav.addEventListener('focusout', closeOnFocusLost); - } else { - window.removeEventListener('keydown', closeOnEscape); - nav.removeEventListener('focusout', closeOnFocusLost); - } + const button = document.createElement('button'); + button.type = 'button'; + button.setAttribute('aria-expanded', false); + button.setAttribute('aria-controls', 'nav'); + button.setAttribute('aria-label', 'Menu'); // TODO: localization + + const icon = document.createElement('span'); + icon.classList.add('icon-hamburger'); + button.append(icon); + + button.addEventListener('click', () => { + const expanded = button.getAttribute('aria-expanded') === 'true'; + button.setAttribute('aria-expanded', !expanded); + }); + + wrapper.append(button); + return wrapper; } /** - * loads and decorates the header, mainly the nav - * @param {Element} block The header block element + * Loads and decorates the header, including the nav. + * @param {Element} block - The header block element */ export default async function decorate(block) { - // load nav as fragment const navMeta = getMetadata('nav'); const navPath = navMeta ? new URL(navMeta, window.location).pathname : '/nav'; const fragment = await loadFragment(navPath); - // decorate nav DOM block.textContent = ''; - const nav = document.createElement('nav'); - nav.id = 'nav'; - while (fragment.firstElementChild) nav.append(fragment.firstElementChild); + block.append(...fragment.children); - const classes = ['brand', 'sections', 'tools']; - classes.forEach((c, i) => { - const section = nav.children[i]; - if (section) section.classList.add(`nav-${c}`); + const sections = ['logo', 'nav', 'language', 'search']; + sections.forEach((s, i) => { + const section = block.children[i]; + if (section) section.classList.add(`header-${s}`); }); - const navBrand = nav.querySelector('.nav-brand'); - const brandLink = navBrand.querySelector('.button'); - if (brandLink) { - brandLink.className = ''; - brandLink.closest('.button-container').className = ''; - } + const logo = getSection(block, 'logo'); + if (logo) decorateLogo(logo); - const navSections = nav.querySelector('.nav-sections'); - if (navSections) { - navSections.querySelectorAll(':scope .default-content-wrapper > 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'); - } - }); - }); - } + const nav = getSection(block, 'nav'); + if (nav) decorateNav(nav); + + const hamburger = buildHamburger(); + block.prepend(hamburger); - // hamburger for mobile - const hamburger = document.createElement('div'); - hamburger.classList.add('nav-hamburger'); - hamburger.innerHTML = ``; - 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)); - - const navWrapper = document.createElement('div'); - navWrapper.className = 'nav-wrapper'; - navWrapper.append(nav); - block.append(navWrapper); + decorateExternalLinks(block); + decorateIcons(block); } From 31919c6c0e95c07626eafe9144e8aab38409162c Mon Sep 17 00:00:00 2001 From: fkakatie Date: Mon, 22 Jun 2026 16:00:09 -0600 Subject: [PATCH 2/4] feat: external link decoration --- scripts/scripts.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/scripts.js b/scripts/scripts.js index dfc85b8..6799e90 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -126,6 +126,19 @@ function decorateButtons(main) { } } +/** + * Sets target and rel on links whose hostname differs from the current page. + * @param {Element} container - The element to search for links within + */ +export function decorateExternalLinks(container) { + container.querySelectorAll('a[href]').forEach((link) => { + if (new URL(link.href).hostname !== window.location.hostname) { + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + } + }); +} + /** * Decorates the main element. * @param {Element} main The main element @@ -187,16 +200,17 @@ async function loadLazy(doc) { * Loads everything that happens a lot later, * without impacting the user experience. */ -function loadDelayed() { +function loadDelayed(doc) { // eslint-disable-next-line import/no-cycle window.setTimeout(() => import('./delayed.js'), 3000); // load anything that can be postponed to the latest here + decorateExternalLinks(doc.querySelector('main')); } async function loadPage() { await loadEager(document); await loadLazy(document); - loadDelayed(); + loadDelayed(document); } loadPage(); From cdf1f1ad760a84a20a0b8ce27a39e4010f9d7d06 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Mon, 22 Jun 2026 22:09:37 -0600 Subject: [PATCH 3/4] feat: icons for header --- icons/arrow.svg | 3 +++ icons/chevron.svg | 3 +++ icons/globe.svg | 3 +++ icons/search.svg | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 icons/arrow.svg create mode 100644 icons/chevron.svg create mode 100644 icons/globe.svg diff --git a/icons/arrow.svg b/icons/arrow.svg new file mode 100644 index 0000000..e7f7d3b --- /dev/null +++ b/icons/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/chevron.svg b/icons/chevron.svg new file mode 100644 index 0000000..d18fcc9 --- /dev/null +++ b/icons/chevron.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/globe.svg b/icons/globe.svg new file mode 100644 index 0000000..af71e8c --- /dev/null +++ b/icons/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/search.svg b/icons/search.svg index 637c677..7841c2c 100644 --- a/icons/search.svg +++ b/icons/search.svg @@ -1,5 +1,5 @@ - From 4293dfa075878adf98f9ec7f700623652f4dc92f Mon Sep 17 00:00:00 2001 From: fkakatie Date: Mon, 22 Jun 2026 22:11:12 -0600 Subject: [PATCH 4/4] feat: header block --- blocks/header/header.css | 588 ++++++++++++++++++++++++++++++++------- blocks/header/header.js | 236 +++++++++++++++- styles/styles.css | 40 ++- 3 files changed, 742 insertions(+), 122 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index 67a7ef1..3dc9ea8 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,40 +1,45 @@ body > header { - border-bottom: 1px solid var(--color-border); - background-color: var(--color-bg); - background-image: linear-gradient(var(--color-bg) 83px, var(--color-gray-200) 83px); -} - -@media (width >= 1200px) { - body > header { - background-image: linear-gradient(var(--color-gray-200) 56px, var(--color-bg) 56px); - } + height: var(--nav-height); + background-color: var(--color-black); + color: var(--color-bg); } header .header { display: grid; grid-template: - 'hamburger logo .' 68px - 'search search search' auto - 'nav nav nav' auto - 'language language language' auto - / 44px 1fr 44px; + 'hamburger logo' auto + 'search search' auto / 44px 1fr; align-items: center; - gap: calc(2 * var(--space-xs)) 0; + gap: var(--space-l) var(--space-s); max-width: var(--site-width); margin: 0 auto; - padding: var(--space-xs); - font-size: var(--text-s); + padding: var(--space-s) var(--space-l); } -@media (width >= 600px) { +header .header[data-nav] { + grid-template: + 'hamburger logo .' auto + 'nav nav nav' 1fr + 'language language language' auto / 44px 1fr 44px; + gap: var(--space-s); + position: fixed; + inset: 0; + z-index: 2; + max-height: 100dvh; + background-image: linear-gradient(var(--color-black) 104px, var(--color-bg) 104px); +} + +@media (width >= 400px) { header .header { - padding: var(--space-xs) var(--space-s); + grid-template: + 'hamburger logo .' auto + 'search search search' auto / 44px 1fr 44px; } } @media (width >= 900px) { header .header { - padding: var(--space-xs) var(--space-m); + padding: var(--space-s); } } @@ -42,9 +47,9 @@ header .header { header .header { grid-template: 'logo search search' auto - 'nav nav language' auto / auto auto auto; - gap: calc(2 * var(--space-s)) var(--space-m); - padding: var(--space-s) var(--space-m); + 'nav nav language' auto / auto 1fr auto; + gap: 0 var(--space-m); + padding-bottom: 0; } } @@ -54,6 +59,7 @@ header .header p { header .header a { color: inherit; + text-decoration: none; } header .header ul { @@ -62,121 +68,75 @@ header .header ul { padding: 0; } -/* header logo */ +/* HEADER LOGO */ + header .header-logo { grid-area: logo; justify-self: center; + padding: var(--space-s) 0; } header .header-logo img, header .header-logo svg { - max-height: 68px; + display: block; + max-height: 40px; width: auto; } -@media (width >= 1200px) { - header .header-logo { - padding: var(--space-xs) 0; - } +/* HEADER SEARCH */ - header .header-logo img, - header .header-logo svg { - max-height: 40px; - } +header .header-search { + grid-area: search; } -/* header nav */ -header .header-nav { +header [data-nav] .header-search { display: none; - grid-area: nav; } -header .header-nav > ul { +header .header-search form { display: flex; - align-items: center; - gap: var(--space-m); -} - -header .header-nav > ul > li { - position: relative; + align-items: stretch; + overflow: hidden; + border-radius: var(--radius-s); + background-color: var(--color-bg); + color: var(--color-text); } -header .header-nav button { - display: flex; - align-items: center; - gap: 0.5ch; +header .header-search input { + flex: 1; + min-width: 0; border: none; + padding: var(--space-xs) var(--space-s); background: none; - cursor: pointer; -} - -header .header-nav button, -header .header-nav > ul > li a { - padding: 1em 0; + color: inherit; } -header .header-nav button[aria-expanded='true'], -header .header-nav button:hover, -header .header-nav button:focus-visible { - text-decoration: underline; +header .header-search input:focus { + outline: none; } -header .header-nav li > ul { - display: none; +header .header-search button { + height: 44px; + width: 44px; + border-radius: 0; + padding: 0; } -header .header-nav button[aria-expanded='true'] + ul { +header .header-search button .icon img, +header .header-search button .icon svg { display: block; - position: absolute; - top: 100%; - left: calc(-1 * var(--space-s)); - width: max-content; - min-width: 100%; - padding: var(--space-s); - box-shadow: var(--shadow-s); - background-color: var(--color-bg-subtle); -} - -header .header-nav button[aria-expanded='true'] .icon img, -header .header-nav button[aria-expanded='true'] .icon svg { - transform: rotate(-180deg); -} - -header .header-nav li > ul li + li { - margin-top: var(--space-s); -} - -@media (width >= 1200px) { - header .header-nav { - display: block; - } -} - -/* header language */ -header .header-language { - display: none; - grid-area: language; - position: relative; + height: 1.25em; + width: 1.25em; } -@media (width >= 1200px) { - header .header-language { - display: block; - } -} +/* stylelint-disable no-descending-specificity */ -/* header search */ -header .header-search { - grid-area: search; - position: relative; -} +/* HEADER HAMBURGER */ -/* header hamburger */ header .header-hamburger { grid-area: hamburger; } -/* stylelint-disable-next-line no-descending-specificity */ header .header-hamburger button { display: flex; align-items: center; @@ -186,6 +146,7 @@ header .header-hamburger button { border: none; padding: 0; background: none; + color: inherit; cursor: pointer; transition: transform 0.2s; } @@ -199,8 +160,8 @@ header .header-hamburger .icon-hamburger, header .header-hamburger .icon-hamburger::before, header .header-hamburger .icon-hamburger::after { display: block; - height: 2px; - width: 24px; + height: 4px; + width: 28px; background-color: currentcolor; transition: transform 0.2s, background-color 0.2s; } @@ -217,11 +178,11 @@ header .header-hamburger .icon-hamburger::after { } header .header-hamburger .icon-hamburger::before { - top: -8px; + top: -10px; } header .header-hamburger .icon-hamburger::after { - top: 8px; + top: 10px; } header .header-hamburger button[aria-expanded='true'] .icon-hamburger { @@ -229,11 +190,11 @@ header .header-hamburger button[aria-expanded='true'] .icon-hamburger { } header .header-hamburger button[aria-expanded='true'] .icon-hamburger::before { - transform: translateY(8px) rotate(45deg); + transform: translateY(10px) rotate(45deg); } header .header-hamburger button[aria-expanded='true'] .icon-hamburger::after { - transform: translateY(-8px) rotate(-45deg); + transform: translateY(-10px) rotate(-45deg); } @media (width >= 1200px) { @@ -241,3 +202,414 @@ header .header-hamburger button[aria-expanded='true'] .icon-hamburger::after { display: none; } } + +/* HEADER NAV */ + +header .header-nav { + display: none; + grid-area: nav; +} + +header .header-nav[data-expanded] { + display: block; + align-self: start; + overflow-y: auto; + max-height: calc(100% - var(--space-s)); + margin-top: var(--space-s); + background-color: var(--color-bg); + color: var(--color-text); +} + +/* top-level list items */ +header .header-nav > ul > li > ul { + display: none; +} + +header .header-nav > ul > li > button, +header .header-nav > ul > li > a { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1ch; + width: 100%; + border: none; + padding: 1.2em 0; + background: none; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + transition: color 0.2s; +} + +header .header-nav > ul > li > button:hover, +header .header-nav > ul > li > button:focus-visible, +header .header-nav > ul > li > a:hover, +header .header-nav > ul > li > a:focus-visible { + color: var(--color-link); +} + +header .header-nav > ul > li > button:focus-visible { + outline: 1px solid var(--color-border); +} + +/* mobile push panel */ +header .header-nav[data-submenu='expanded'] > ul > li { + display: none; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] { + display: block; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > button { + display: none; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul { + display: block; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul:not(.submenu-multi) > li:not([class]) { + padding: 0.8em 0; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul > li a { + display: block; +} + +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul > li a:hover { + text-decoration: underline; +} + +/* submenu back button */ +header .header-nav .submenu-back button { + display: flex; + align-items: center; + gap: 1ch; + width: 100%; + border: none; + border-bottom: 1px solid var(--color-border); + padding: 1.2em 0; + background: none; + color: inherit; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; +} + +header .header-nav .submenu-back button:hover, +header .header-nav .submenu-back button:focus-visible { + color: var(--color-link); +} + +header .header-nav .submenu-back button:focus-visible { + outline: 1px solid var(--color-border); +} + +/* flat submenu items */ +header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) { + border-bottom: 1px solid var(--color-border); +} + +header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a { + display: block; + padding: var(--space-s) var(--space-m); +} + +header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a:hover, +header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a:focus-visible { + background-color: var(--color-bg-light); +} + +/* multi-nav accordion */ +header .header-nav .submenu-multi > li:not(.submenu-back) > button { + display: flex; + align-items: center; + justify-content: space-between; + overflow: hidden; + width: 100%; + border: none; + padding: 1.2em 0; + background: none; + color: inherit; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + transition: color 0.2s; +} + +header .header-nav .submenu-multi > li:not(.submenu-back) > button .icon { + padding: 0 1ch; +} + +header .header-nav .submenu-multi > li:not(.submenu-back) > button:hover, +header .header-nav .submenu-multi > li:not(.submenu-back) > button:focus-visible { + color: var(--color-link); +} + +header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] { + outline: 1px solid var(--color-border); +} + +header .header-nav .submenu-multi > li > button[aria-expanded='true'] .icon img, +header .header-nav .submenu-multi > li > button[aria-expanded='true'] .icon svg { + transform: rotate(-180deg); +} + +/* category sub-list */ +header .header-nav .submenu-multi > li > button + ul { + display: none; +} + +header .header-nav .submenu-multi > li > button[aria-expanded='true'] + ul { + display: block; +} + +header .header-nav .submenu-multi > li > button[aria-expanded='true'] + ul > li > a { + display: block; + padding: 0.8em 0; +} + +@media (width >= 1200px) { + header .header-nav { + display: block; + position: relative; + overflow: visible; + background: none; + color: var(--color-bg); + } + + header .header-nav > ul { + display: flex; + align-items: center; + gap: 0.6em; + } + + header .header-nav > ul > li { + position: static; + } + + header .header-nav > ul > li .icon { + transform: rotate(90deg); + } + + header .header-nav > ul > li > button, + header .header-nav > ul > li > a { + justify-content: flex-start; + gap: 1ch; + width: auto; + padding: 0.6em; + color: inherit; + font-weight: normal; + text-transform: none; + transition: background-color 0.2s, color 0.2s; + } + + header .header-nav > ul > li > button:hover, + header .header-nav > ul > li > button:focus-visible, + header .header-nav > ul > li > a:hover, + header .header-nav > ul > li > a:focus-visible { + background-color: var(--color-muted); + color: var(--color-brand); + } + + header .header-nav > ul > li > button[aria-expanded='true'] { + position: relative; + color: var(--color-brand); + } + + header .header-nav > ul > li > button[aria-expanded='true']::after { + content: ''; + position: absolute; + inset: auto 0 0; + height: 5px; + border-radius: var(--radius-m) var(--radius-m) 0 0; + background: currentcolor; + } + + header .header-nav > ul > li > button[aria-expanded='true'] .icon img, + header .header-nav > ul > li > button[aria-expanded='true'] .icon svg { + transform: rotate(90deg); + } + + header .header-nav[data-submenu='expanded'] > ul > li { + display: block; + } + + header .header-nav[data-submenu='expanded'] > ul > li[data-active] > button { + display: flex; + } + + /* flat dropdown */ + header .header-nav > ul > li > button[aria-expanded='true'] + ul:not(.submenu-multi) { + display: block; + column-fill: auto; + column-width: 240px; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1; + height: 240px; + padding: var(--space-s) 0; + box-shadow: var(--shadow-s); + background-color: var(--color-bg); + color: var(--color-text); + } + + header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul:not(.submenu-multi) > li:not([class]) { + padding: 0; + } + + header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) { + border: none; + break-inside: avoid; + } + + header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a { + padding: var(--space-xs) 0; + } + + header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul > li a { + padding: 0.8em 1.2em; + transition: background-color 0.2s, color 0.2s; + } + + header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul > li a:hover { + background-color: var(--color-bg-light); + color: var(--color-link); + text-decoration: none; + } + + header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a:hover, + header .header-nav .submenu:not(.submenu-multi) > li:not(.submenu-back) > a:focus-visible { + background: none; + text-decoration: underline; + } + + header .header-nav .submenu-back { + display: none; + } + + /* multi-nav split pane */ + header .header-nav > ul > li > button[aria-expanded='true'] + ul.submenu-multi { + display: block; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1; + min-height: 240px; + box-shadow: var(--shadow-s); + background-color: var(--color-bg); + color: var(--color-text); + } + + header .header-nav .submenu-multi { + padding: var(--space-s) 0; + } + + /* left rail */ + header .header-nav .submenu-multi > li:not(.submenu-back) { + width: 240px; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button { + width: 100%; + border-bottom: 1px solid var(--color-border); + padding: 0.8em 1.2em; + font-weight: normal; + text-transform: uppercase; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] { + border-bottom: 2px solid var(--color-brand); + font-weight: bold; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button .icon { + display: none; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='false']:hover, + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='false']:focus-visible { + background-color: var(--color-bg-light); + } + + /* right pane */ + header .header-nav .submenu-multi > li:not(.submenu-back) > button + ul { + display: none; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] + ul { + display: block; + column-fill: auto; + column-width: 240px; + position: absolute; + inset: 0 0 0 240px; + margin: var(--space-s); + margin-right: 0; + border-left: 1px solid var(--color-border); + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] + ul > li { + break-inside: avoid; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] + ul > li:first-of-type { + text-transform: uppercase; + } + + header .header-nav .submenu-multi > li:not(.submenu-back) > button[aria-expanded='true'] + ul > li > a { + padding: 0.8em 1.2em; + } +} + +/* HEADER LANGUAGE SELECTOR */ + +header .header-language { + display: none; + grid-area: language; + align-self: start; + color: var(--color-text); + text-transform: uppercase; +} + +header .header[data-nav] .header-language { + display: block; +} + +header .header-language ul { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +header .header-language a { + display: flex; + align-items: center; + gap: 0.5ch; +} + +header .header-language .icon img, +header .header-language .icon svg { + display: block; + height: 1.25em; + width: 1.25em; +} + +@media (width >= 1200px) { + header .header-language { + display: flex; + align-self: center; + } + + header .header-language ul { + flex-direction: row; + gap: var(--space-s); + } + + header .header-language a { + color: var(--color-bg); + } +} diff --git a/blocks/header/header.js b/blocks/header/header.js index 5610fc4..c373678 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -30,7 +30,7 @@ function decorateLogo(section) { } /** - * Replaces the section div with a semantic nav element and decorates top-level items. + * Replaces the section div with a semantic nav element and decorates all nav items. * @param {Element} section - The nav section element to promote */ function decorateNav(section) { @@ -42,22 +42,103 @@ function decorateNav(section) { const submenu = item.querySelector(':scope > ul'); if (!p) return; + const label = p.textContent.trim(); + if (submenu) { + // top-level trigger button const button = document.createElement('button'); button.type = 'button'; button.setAttribute('aria-expanded', false); - button.textContent = p.textContent.trim(); + button.setAttribute('aria-haspopup', true); + button.textContent = label; const chevron = document.createElement('span'); - chevron.classList.add('icon', 'icon-chevron'); + chevron.classList.add('icon', 'icon-chevron', 'chevron-right'); button.append(chevron); + p.replaceWith(button); + + // back button injected as first child of every submenu + const backLi = document.createElement('li'); + backLi.classList.add('submenu-back'); + const backBtn = document.createElement('button'); + backBtn.type = 'button'; + const backArrow = document.createElement('span'); + backArrow.classList.add('icon', 'icon-arrow', 'arrow-left'); + const backLabel = document.createElement('span'); + backLabel.textContent = label; + backBtn.append(backArrow, backLabel); + backLi.append(backBtn); + submenu.prepend(backLi); + + // detect multi-nav: any submenu li that has both a

and a