From 6a603bafb7fc5063c6ae81e4d2c2893d9ecb3659 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 19 Dec 2023 15:14:42 +0100 Subject: [PATCH] initial header --- blocks/grid/grid.css | 1 + blocks/grid/grid.js | 11 +- blocks/header/header.css | 276 ++----------------------------- blocks/header/header.js | 150 +---------------- blocks/navigation/navigation.css | 5 + blocks/navigation/navigation.js | 3 + scripts/lib-franklin.js | 39 +++-- scripts/scripts.js | 4 +- styles/styles.css | 12 +- 9 files changed, 66 insertions(+), 435 deletions(-) create mode 100644 blocks/navigation/navigation.css create mode 100644 blocks/navigation/navigation.js diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index d7f192a4..ba453ff5 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -21,6 +21,7 @@ .section.grid .element img { width: 100%; + height: auto; } .section.grid .element p { diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index fafdfefc..65ff9a07 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -9,13 +9,10 @@ export default function decorate(block) { const rowTemplate = elements.find((e) => e.dataset.gridRows)?.dataset.gridRows; if (columnTemplate || rowTemplate) { const variables = {}; - if (columnTemplate) { - variables['grid-template-columns'] = columnTemplate; - } - if (rowTemplate) { - variables['grid-template-rows'] = rowTemplate; - } - addCssVariables(block, variables); + addCssVariables(block, { + 'grid-template-columns': columnTemplate, + 'grid-template-rows': rowTemplate, + }); } elements.forEach((e) => { diff --git a/blocks/header/header.css b/blocks/header/header.css index 4c2b46d5..09581712 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -1,275 +1,19 @@ -/* header and nav layout */ -header .nav-wrapper { - background-color: var(--background-color); - width: 100%; - z-index: 2; - position: fixed; -} - -header nav { - box-sizing: border-box; - display: grid; - grid-template: - 'hamburger brand tools' var(--nav-height) - 'sections sections sections' 1fr / auto 1fr auto; - align-items: center; - gap: 0 2em; - margin: auto; - max-width: 1264px; - height: var(--nav-height); - padding: 0 1rem; - font-family: var(--body-font-family); -} - -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: 100vh; -} - -@media (min-width: 600px) { - header nav { - padding: 0 2rem; - } -} - -@media (min-width: 900px) { - header nav { - display: flex; - justify-content: space-between; - } - - header nav[aria-expanded="true"] { - min-height: 0; - overflow: visible; - } -} - -header nav p { - margin: 0; - line-height: 1; -} - -header nav a:any-link { - color: currentcolor; -} - -/* hamburger */ -header nav .nav-hamburger { - grid-area: hamburger; - height: 22px; - display: flex; - align-items: center; +header a:any-link { + color: inherit; } -header nav .nav-hamburger button { - height: 22px; +header p, +header .section.grid .element p { margin: 0; - border: 0; - border-radius: 0; padding: 0; - background-color: var(--background-color); - color: inherit; - overflow: initial; - text-overflow: initial; - white-space: initial; -} - -header nav .nav-hamburger-icon, -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { - box-sizing: border-box; - display: block; - position: relative; - width: 20px; -} - -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::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; -} - -header nav[aria-expanded="false"] .nav-hamburger-icon::before { - top: -6px; -} - -header nav[aria-expanded="false"] .nav-hamburger-icon::after { - top: 6px; -} - -header nav[aria-expanded="true"] .nav-hamburger-icon { - height: 22px; -} - -header nav[aria-expanded="true"] .nav-hamburger-icon::before, -header nav[aria-expanded="true"] .nav-hamburger-icon::after { - top: 3px; - left: 1px; - transform: rotate(45deg); - transform-origin: 2px 1px; - width: 24px; - height: 2px; - border-radius: 2px; } -header nav[aria-expanded="true"] .nav-hamburger-icon::after { - top: unset; - bottom: 3px; - transform: rotate(-45deg); -} - -@media (min-width: 900px) { - header nav .nav-hamburger { - display: none; - visibility: hidden; - } -} - -/* brand */ -header .nav-brand { - grid-area: brand; - flex-basis: 128px; - font-size: var(--heading-font-size-s); - font-weight: 700; - line-height: 1; -} - -header nav .nav-brand img { - width: 128px; - height: auto; -} - -/* sections */ -header nav .nav-sections { - grid-area: sections; - flex: 1 1 auto; - display: none; - visibility: hidden; - background-color: var(--overlay-color); -} - -header nav[aria-expanded="true"] .nav-sections { - display: block; - visibility: visible; - align-self: start; -} - -header nav .nav-sections ul { - list-style: none; - padding-left: 0; - font-size: var(--body-font-size-s); - font-weight: 500; -} - -header nav .nav-sections ul > li { - font-weight: 700; -} - -header nav .nav-sections ul > li > ul { - margin-top: 0; -} - -header nav .nav-sections ul > li > ul > li { - font-weight: 500; -} - -@media (min-width: 900px) { - header nav .nav-sections { - display: block; - visibility: visible; - white-space: nowrap; - } - - header nav[aria-expanded="true"] .nav-sections { - align-self: unset; - } - - header nav .nav-sections .nav-drop { - position: relative; - padding-right: 16px; - cursor: pointer; - } - - header nav .nav-sections .nav-drop::after { - content: ''; - display: inline-block; - position: absolute; - top: .5em; - right: 2px; - transform: rotate(135deg); - width: 6px; - height: 6px; - border: 2px solid currentcolor; - border-radius: 0 1px 0 0; - border-width: 2px 2px 0 0; - } - - header nav .nav-sections .nav-drop[aria-expanded="true"]::after { - top: unset; - bottom: .5em; - transform: rotate(315deg); - } - - header nav .nav-sections ul { - display: flex; - gap: 2em; - margin: 0; - font-size: var(--body-font-size-xs); - } - - header nav .nav-sections > ul > li { - flex: 0 1 auto; - position: relative; - font-weight: 500; - } - - header nav .nav-sections > ul > li > ul { - display: none; - position: relative; - } - - header nav .nav-sections > ul > li[aria-expanded="true"] > ul { - display: block; - position: absolute; - left: -1em; - width: 200px; - margin-top: 12px; - padding: 1em; - background-color: var(--highlight-background-color); - white-space: initial; - } - - header nav .nav-sections > ul > li > ul::before { - content: ''; - position: absolute; - top: -8px; - left: 8px; - width: 0; - height: 0; - border-left: 8px solid transparent; - border-right: 8px solid transparent; - border-bottom: 8px solid var(--highlight-background-color); - } - - header nav .nav-sections > ul > li > ul > li { - padding: 8px 0; - } +a.button:any-link { + background-color: transparent; + color: inherit; + font-weight: normal; } -/* tools */ -header nav .nav-tools { - grid-area: tools; +a.button:any-link .icon { + vertical-align: middle; } diff --git a/blocks/header/header.js b/blocks/header/header.js index 9159e0d7..c8512c3f 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,145 +1,11 @@ -import { getMetadata, decorateIcons } from '../../scripts/lib-franklin.js'; +import { + loadBlocks, +} from '../../scripts/lib-franklin.js'; +import { + decorateMain, +} from '../../scripts/scripts.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'); - 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', 'tools']; - 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 = ``; - 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); - const navWrapper = document.createElement('div'); - navWrapper.className = 'nav-wrapper'; - navWrapper.append(nav); - block.append(navWrapper); - } + decorateMain(block); + loadBlocks(block); } diff --git a/blocks/navigation/navigation.css b/blocks/navigation/navigation.css new file mode 100644 index 00000000..132aa196 --- /dev/null +++ b/blocks/navigation/navigation.css @@ -0,0 +1,5 @@ +.navigation ul { + list-style: none; + display: flex; + gap: var(--padding-horizontal); +} \ No newline at end of file diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js new file mode 100644 index 00000000..46969a3c --- /dev/null +++ b/blocks/navigation/navigation.js @@ -0,0 +1,3 @@ +export default async function decorate(/* block */) { + // nop +} diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index a5e00564..7b6cf0cf 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -179,7 +179,7 @@ export async function decorateIcons(element) { if (!ICONS_CACHE[iconName]) { ICONS_CACHE[iconName] = true; try { - const response = await fetch(`${window.hlx.codeBasePath}/icons/${iconName}.svg`); + const response = await fetch(`${window.hlx.iconsPath}/${iconName}.svg`); if (!response.ok) { ICONS_CACHE[iconName] = false; return; @@ -337,7 +337,7 @@ export function addCssVariables(element, variables) { const style = document.createElement('style'); style.textContent = `#${element.id} { - ${Object.keys(variables).map((k) => `--${k}: ${variables[k]};`).join(' ')} + ${Object.keys(variables).map((k) => variables[k] && `--${k}: ${variables[k]};`).join(' ')} }`; element.parentNode.insertBefore(style, element); @@ -376,14 +376,10 @@ export function decorateSection(section) { } if (section.dataset.textColor || section.dataset.background) { - const variables = {}; - if (section.dataset.textColor) { - variables['text-color'] = section.dataset.textColor; - } - if (section.dataset.background) { - variables['background-color'] = section.dataset.background; - } - addCssVariables(section, variables); + addCssVariables(section, { + 'text-color': section.dataset.textColor, + 'background-color': section.dataset.background, + }); } } @@ -667,11 +663,23 @@ export async function waitForLCP(lcpBlocks) { * @param {Element} header header element * @returns {Promise} */ -export function loadHeader(header) { - const headerBlock = buildBlock('header', ''); - header.append(headerBlock); - decorateBlock(headerBlock); - return loadBlock(headerBlock); +export async function loadHeader(header) { + const navMeta = getMetadata('header'); + const navPath = navMeta ? new URL(navMeta).pathname : '/header'; + const resp = await fetch(`${navPath}.plain.html`); + + addCssVariables(header, { + 'background-color': getMetadata('header-background'), + 'header-height': getMetadata('header-height'), + 'text-color': getMetadata('header-text-color'), + }); + + if (resp.ok) { + const html = await resp.text(); + header.innerHTML = html; + header.dataset.blockName = 'header'; + await loadBlock(header); + } } /** @@ -695,6 +703,7 @@ export function setup() { window.hlx.codeBasePath = ''; window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; window.hlx.patchBlockConfig = []; + window.hlx.iconsPath = '/assets/icons'; const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); if (scriptEl) { diff --git a/scripts/scripts.js b/scripts/scripts.js index bd9af7ce..05fd988d 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -103,6 +103,9 @@ export function decorateMain(main) { async function loadEager(doc) { document.documentElement.lang = 'en'; decorateTemplateAndTheme(); + + loadHeader(doc.querySelector('header')); + const main = doc.querySelector('main'); if (main) { decorateMain(main); @@ -132,7 +135,6 @@ async function loadLazy(doc) { const element = hash ? doc.getElementById(hash.substring(1)) : false; if (hash && element) element.scrollIntoView(); - loadHeader(doc.querySelector('header')); loadFooter(doc.querySelector('footer')); loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); diff --git a/styles/styles.css b/styles/styles.css index b3b7d4f3..8c3f54a5 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -37,8 +37,7 @@ --heading-font-size-s: 20px; --heading-font-size-xs: 18px; - /* nav height */ - --nav-height: 64px; + /* mind the gaps */ --padding-horizontal: 32px; --padding-vertical: 16px; @@ -78,7 +77,13 @@ body.appear { } header { - height: var(--nav-height); + height: var(--header-height, 120px); + background-color: var(--background-color); + color: var(--text-color); +} + +header svg { + fill: var(--text-color); } h1, h2, h3, @@ -88,7 +93,6 @@ h4, h5, h6 { line-height: 1.25; margin-top: 1em; margin-bottom: .5em; - scroll-margin: calc(var(--nav-height) + 1em); } h1 { font-size: var(--heading-font-size-xxl) }