diff --git a/blocks/header/header.css b/blocks/header/header.css index f414ca7..3dc9ea8 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,272 +1,615 @@ -/* header and nav layout */ -header .nav-wrapper { - position: fixed; - z-index: 2; - width: 100%; - background-color: var(--color-bg); +body > header { + height: var(--nav-height); + background-color: var(--color-black); + color: var(--color-bg); } -header nav { +header .header { display: grid; grid-template: - 'hamburger brand tools' var(--nav-height) - 'sections sections sections' 1fr / auto 1fr auto; + 'hamburger logo' auto + 'search search' auto / 44px 1fr; 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: var(--space-l) var(--space-s); + max-width: var(--site-width); + margin: 0 auto; + padding: var(--space-s) var(--space-l); } -header nav[aria-expanded='true'] { +header .header[data-nav] { grid-template: - 'hamburger brand' var(--nav-height) - 'sections sections' 1fr - 'tools tools' var(--nav-height) / auto 1fr; - overflow-y: auto; - min-height: 100dvh; + '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 >= 900px) { - header .nav-wrapper { - position: relative; +@media (width >= 400px) { + header .header { + grid-template: + 'hamburger logo .' auto + 'search search search' auto / 44px 1fr 44px; } +} - header nav { - display: flex; - justify-content: space-between; - gap: 0 var(--space-l); - max-width: 1264px; - padding: 0 var(--space-l); +@media (width >= 900px) { + header .header { + padding: var(--space-s); } +} - 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 1fr auto; + gap: 0 var(--space-m); + padding-bottom: 0; } } -header nav p { +header .header p { margin: 0; - line-height: 1; } -header nav a:any-link { - color: currentcolor; +header .header a { + color: inherit; + text-decoration: none; } -/* hamburger */ -header nav .nav-hamburger { +header .header ul { + list-style: none; + margin: 0; + padding: 0; +} + +/* HEADER LOGO */ + +header .header-logo { + grid-area: logo; + justify-self: center; + padding: var(--space-s) 0; +} + +header .header-logo img, +header .header-logo svg { + display: block; + max-height: 40px; + width: auto; +} + +/* HEADER SEARCH */ + +header .header-search { + grid-area: search; +} + +header [data-nav] .header-search { + display: none; +} + +header .header-search form { display: flex; - grid-area: hamburger; - align-items: center; - height: 22px; + align-items: stretch; + overflow: hidden; + border-radius: var(--radius-s); + background-color: var(--color-bg); + color: var(--color-text); } -header nav .nav-hamburger button { - overflow: initial; - height: 22px; - margin: 0; - border: 0; +header .header-search input { + flex: 1; + min-width: 0; + border: none; + padding: var(--space-xs) var(--space-s); + background: none; + color: inherit; +} + +header .header-search input:focus { + outline: none; +} + +header .header-search button { + height: 44px; + width: 44px; border-radius: 0; padding: 0; - background-color: var(--color-bg); +} + +header .header-search button .icon img, +header .header-search button .icon svg { + display: block; + height: 1.25em; + width: 1.25em; +} + +/* stylelint-disable no-descending-specificity */ + +/* HEADER HAMBURGER */ + +header .header-hamburger { + grid-area: hamburger; +} + +header .header-hamburger button { + display: flex; + align-items: center; + justify-content: center; + height: 44px; + width: 44px; + border: none; + padding: 0; + background: none; color: inherit; - text-overflow: initial; - white-space: initial; + cursor: pointer; + transition: transform 0.2s; +} + +header .header-hamburger button:hover, +header .header-hamburger button:focus-visible { + transform: scale(1.2); } -header nav .nav-hamburger-icon, -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { +header .header-hamburger .icon-hamburger, +header .header-hamburger .icon-hamburger::before, +header .header-hamburger .icon-hamburger::after { display: block; + height: 4px; + width: 28px; + background-color: currentcolor; + transition: transform 0.2s, background-color 0.2s; +} + +header .header-hamburger .icon-hamburger { position: relative; - box-sizing: border-box; - width: 20px; } -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { +header .header-hamburger .icon-hamburger::before, +header .header-hamburger .icon-hamburger::after { content: ''; position: absolute; - background: currentcolor; -} - -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; + left: 0; } -header nav[aria-expanded='false'] .nav-hamburger-icon::before { - top: -6px; +header .header-hamburger .icon-hamburger::before { + top: -10px; } -header nav[aria-expanded='false'] .nav-hamburger-icon::after { - top: 6px; +header .header-hamburger .icon-hamburger::after { + top: 10px; } -header nav[aria-expanded='true'] .nav-hamburger-icon { - height: 22px; +header .header-hamburger button[aria-expanded='true'] .icon-hamburger { + background-color: transparent; } -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-hamburger button[aria-expanded='true'] .icon-hamburger::before { + transform: translateY(10px) rotate(45deg); } -header nav[aria-expanded='true'] .nav-hamburger-icon::after { - top: unset; - bottom: 3px; - transform: rotate(-45deg); +header .header-hamburger button[aria-expanded='true'] .icon-hamburger::after { + transform: translateY(-10px) rotate(-45deg); } -@media (width >= 900px) { - header nav .nav-hamburger { +@media (width >= 1200px) { + header .header-hamburger { display: none; - visibility: hidden; } } -/* brand */ -header .nav-brand { - flex-basis: 128px; - grid-area: brand; - font-size: var(--heading-s); - font-weight: 700; - line-height: 1; +/* HEADER NAV */ + +header .header-nav { + display: none; + grid-area: nav; } -header nav .nav-brand img { - height: auto; - width: 128px; +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); } -/* sections */ -header nav .nav-sections { +/* top-level list items */ +header .header-nav > ul > li > ul { display: none; - flex: 1 1 auto; - grid-area: sections; - visibility: hidden; } -header nav[aria-expanded='true'] .nav-sections { +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; - align-self: start; - visibility: visible; } -header nav .nav-sections ul { - list-style: none; - padding-left: 0; - font-size: var(--text-m); +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > button { + display: none; } -header nav .nav-sections ul > li { - font-weight: 400; +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul { + display: block; } -header nav .nav-sections ul > li > ul { - margin-top: 0; +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul:not(.submenu-multi) > li:not([class]) { + padding: 0.8em 0; } -header nav .nav-sections ul > li > ul > li { - font-weight: 400; +header .header-nav[data-submenu='expanded'] > ul > li[data-active] > ul > li a { + display: block; } -@media (width >= 900px) { - header nav .nav-sections { +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; - visibility: visible; - white-space: nowrap; + position: relative; + overflow: visible; + background: none; + color: var(--color-bg); } - header nav[aria-expanded='true'] .nav-sections { - align-self: unset; + 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 nav .nav-sections .nav-drop { + 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; - padding-right: var(--space-s); - cursor: pointer; + color: var(--color-brand); } - header nav .nav-sections .nav-drop::after { + header .header-nav > ul > li > button[aria-expanded='true']::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); + 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 nav .nav-sections .nav-drop[aria-expanded='true']::after { - top: unset; - bottom: 0.5em; - transform: rotate(315deg); + header .header-nav[data-submenu='expanded'] > ul > li { + display: block; } - header nav .nav-sections ul { + header .header-nav[data-submenu='expanded'] > ul > li[data-active] > button { display: flex; - gap: var(--space-m); - margin: 0; } - header nav .nav-sections .default-content-wrapper > ul > li { - flex: 0 1 auto; - position: relative; + /* 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 nav .nav-sections .default-content-wrapper > ul > li > ul { + 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; - position: relative; } - header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded='true'] > ul { + /* multi-nav split pane */ + header .header-nav > ul > li > button[aria-expanded='true'] + ul.submenu-multi { display: block; position: absolute; - top: 150%; - left: -24px; - width: 200px; - padding: var(--space-s); + 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); - white-space: initial; } - header nav .nav-sections .default-content-wrapper > ul > li > ul::before { - content: ''; + /* 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; - 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; + inset: 0 0 0 240px; + margin: var(--space-s); + margin-right: 0; + border-left: 1px solid var(--color-border); } - header nav .nav-sections .default-content-wrapper > ul > li > ul > li { - padding: var(--space-xs) 0; + 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); } -/* tools */ -header nav .nav-tools { - grid-area: tools; +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 c7d6ea2..c373678 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,171 +1,358 @@ -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(); - } - } +/** + * 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 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); - } +/** + * 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 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'); - } -} +/** + * 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) { + 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; + + 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.setAttribute('aria-haspopup', true); + button.textContent = label; + const chevron = document.createElement('span'); + 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