From f9aed0b1ca8e09cb15a81513dd215ab280a6cf4b Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Mon, 18 Dec 2023 16:41:47 +0100 Subject: [PATCH 1/5] first version of flexible column block --- blocks/columns/columns.css | 61 ++++++++++++++++++++++++++++---------- blocks/columns/columns.js | 34 +++++++++++---------- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index b6cdee4e..472e99b9 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -7,18 +7,6 @@ width: 100%; } -.columns > div > div { - order: 1; -} - -.columns > div > .columns-img-col { - order: 0; -} - -.columns > div > .columns-img-col img { - display: block; -} - @media (min-width: 900px) { .columns > div { align-items: center; @@ -26,8 +14,51 @@ gap: 32px; } - .columns > div > div { - flex: 1; - order: unset; + .columns > div > div:nth-child(1) { + flex: var(--column0-flex, 0); + } + + .columns > div > div:nth-child(2) { + flex: var(--column1-flex, 0); + } + + .columns > div > div:nth-child(3) { + flex: var(--column2-flex, 0); + } + + .columns > div > div:nth-child(4) { + flex: var(--column3-flex, 0); + } + + .columns > div > div:nth-child(5) { + flex: var(--column4-flex, 0); + } + + .columns > div > div:nth-child(6) { + flex: var(--column5-flex, 0); + } + + .columns > div > div:nth-child(7) { + flex: var(--column6-flex, 0); + } + + .columns > div > div:nth-child(8) { + flex: var(--column7-flex, 0); + } + + .columns > div > div:nth-child(9) { + flex: var(--column8-flex, 0); + } + + .columns > div > div:nth-child(10) { + flex: var(--column9-flex, 0); + } + + .columns > div > div:nth-child(11) { + flex: var(--column10-flex, 0); + } + + .columns > div > div:nth-child(12) { + flex: var(--column11-flex, 0); } } diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js index 9b78c812..77432c47 100644 --- a/blocks/columns/columns.js +++ b/blocks/columns/columns.js @@ -1,18 +1,22 @@ export default function decorate(block) { - const cols = [...block.firstElementChild.children]; - block.classList.add(`columns-${cols.length}-cols`); + const id = `gen${crypto.randomUUID().split('-')[0]}`; + block.id = id; + + const columns = block.querySelectorAll(':scope > div > div'); + const columnCount = columns.length; + // following line regex matches partition sizes separated by dashes like 1-2-3 + const columnPartionRegex = /^\d{1,}(?:-\d{1,})*$/; + const columnPartions = [...block.classList].find((c) => columnPartionRegex.test(c))?.split('-') || []; + + let variables = ''; + for(let i = 0; i < columnCount; i+=1) { + const partition = columnPartions.length > i ? columnPartions[i] : 1; + variables += `--column${i}-flex: ${partition};`; + } - // setup image columns - [...block.children].forEach((row) => { - [...row.children].forEach((col) => { - const pic = col.querySelector('picture'); - if (pic) { - const picWrapper = pic.closest('div'); - if (picWrapper && picWrapper.children.length === 1) { - // picture is only content in column - picWrapper.classList.add('columns-img-col'); - } - } - }); - }); + const style = document.createElement('style'); + style.textContent = `#${id} { + ${variables} + }`; + block.parentNode.insertBefore(style, block); } From fb399b36b7d5a47d35a88dba05c51932f7aed5d1 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 19 Dec 2023 07:54:06 +0100 Subject: [PATCH 2/5] fix linting --- blocks/columns/columns.js | 6 +++--- package-lock.json | 4 ++-- package.json | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js index 77432c47..22747504 100644 --- a/blocks/columns/columns.js +++ b/blocks/columns/columns.js @@ -1,15 +1,15 @@ export default function decorate(block) { const id = `gen${crypto.randomUUID().split('-')[0]}`; block.id = id; - + const columns = block.querySelectorAll(':scope > div > div'); const columnCount = columns.length; // following line regex matches partition sizes separated by dashes like 1-2-3 const columnPartionRegex = /^\d{1,}(?:-\d{1,})*$/; const columnPartions = [...block.classList].find((c) => columnPartionRegex.test(c))?.split('-') || []; - + let variables = ''; - for(let i = 0; i < columnCount; i+=1) { + for (let i = 0; i < columnCount; i += 1) { const partition = columnPartions.length > i ? columnPartions[i] : 1; variables += `--column${i}-flex: ${partition};`; } diff --git a/package-lock.json b/package-lock.json index 84797a11..c23bc9b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adobe/helix-project-boilerplate", - "version": "1.0.0", + "version": "1.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@adobe/helix-project-boilerplate", - "version": "1.0.0", + "version": "1.2.2", "license": "Apache License 2.0", "devDependencies": { "@babel/core": "7.21.0", diff --git a/package.json b/package.json index 112c3dc9..4587406e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "lint:js": "eslint .", "lint:css": "stylelint blocks/**/*.css styles/*.css", "lint": "npm run lint:js && npm run lint:css", + "lint:fix": "npm run lint:js -- --fix && npm run lint:css -- --fix", "semantic-release": "semantic-release --debug" }, "repository": { From a8af4d0fc3501cc02af605585c358b28d3511533 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 19 Dec 2023 10:15:06 +0100 Subject: [PATCH 3/5] initial version of grid --- blocks/columns/columns.css | 3 +- blocks/columns/columns.js | 18 +++----- blocks/grid/grid.css | 28 ++++++++++++ blocks/grid/grid.js | 33 ++++++++++++++ scripts/lib-franklin.js | 91 ++++++++++++++++++++++++-------------- scripts/scripts.js | 27 +++++++++++ styles/styles.css | 23 ++-------- 7 files changed, 160 insertions(+), 63 deletions(-) create mode 100644 blocks/grid/grid.css create mode 100644 blocks/grid/grid.js diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index 472e99b9..4b09aeea 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -11,7 +11,8 @@ .columns > div { align-items: center; flex-direction: unset; - gap: 32px; + gap: var(--padding-vertical) var(--padding-horizontal); + padding: var(--padding-vertical) var(--padding-horizontal); } .columns > div > div:nth-child(1) { diff --git a/blocks/columns/columns.js b/blocks/columns/columns.js index 22747504..823c6c18 100644 --- a/blocks/columns/columns.js +++ b/blocks/columns/columns.js @@ -1,22 +1,18 @@ -export default function decorate(block) { - const id = `gen${crypto.randomUUID().split('-')[0]}`; - block.id = id; +import { + addCssVariables, +} from '../../scripts/lib-franklin.js'; +export default function decorate(block) { const columns = block.querySelectorAll(':scope > div > div'); const columnCount = columns.length; // following line regex matches partition sizes separated by dashes like 1-2-3 const columnPartionRegex = /^\d{1,}(?:-\d{1,})*$/; const columnPartions = [...block.classList].find((c) => columnPartionRegex.test(c))?.split('-') || []; - let variables = ''; + const variables = {}; for (let i = 0; i < columnCount; i += 1) { const partition = columnPartions.length > i ? columnPartions[i] : 1; - variables += `--column${i}-flex: ${partition};`; + variables[`column${i}-flex`] = partition; } - - const style = document.createElement('style'); - style.textContent = `#${id} { - ${variables} - }`; - block.parentNode.insertBefore(style, block); + addCssVariables(block, variables); } diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css new file mode 100644 index 00000000..3e2378b0 --- /dev/null +++ b/blocks/grid/grid.css @@ -0,0 +1,28 @@ +.section.grid { + display: grid; + grid-template-columns: var(--grid-template-columns); + grid-template-rows: var(--grid-template-rows); +} + +.section.grid .element { + grid-column: var(--grid-column-start-position, auto) var(--grid-column-end-position, auto); + width: 100%; + max-width: unset; + background-color: var(--background-color, transparent); + color: var(--text-color); + margin: auto; +} + +.section.grid .element .image-wrapper { + margin: 0; + padding: 0; +} + +.section.grid .element img { + width: 100%; +} + +.section.grid .element p { + margin: 0; + padding: var(--padding-vertical) var(--padding-horizontal); +} diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js new file mode 100644 index 00000000..dd2aea2e --- /dev/null +++ b/blocks/grid/grid.js @@ -0,0 +1,33 @@ +import { + addCssVariables, +} from '../../scripts/lib-franklin.js'; + +export default function decorate(block) { + const elements = [...block.querySelectorAll(':scope > div')]; + + const columnTemplate = elements.find((e) => e.dataset.gridColumns)?.dataset.gridColumns; + const rowTemplate = elements.find((e) => e.dataset.gridRows)?.dataset.gridRows; + if (columnTemplate || rowTemplate) { + const variables = {}; + if (columnTemplate) { + variables['grid-template-columns'] = columnTemplate; + } + if (columnTemplate) { + variables['grid-template-rows'] = rowTemplate; + } + addCssVariables(block, variables); + } + + elements.forEach((e) => { + e.classList.add('element'); + + const [[startColumnPosition, startRowPosition], [endColumnPosition, endRowPosition]] = e.dataset.gridPosition.split(/\s*\/\s*/).map((p) => p.split(/\s*-\s*/)); + + addCssVariables(e, { + 'grid-column-start-position': startColumnPosition, + 'grid-row-start-position': startRowPosition, + 'grid-column-end-position': endColumnPosition, + 'grid-row-end-position': endRowPosition, + }); + }); +} diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 80b042e0..a5e00564 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -329,43 +329,70 @@ export function readBlockConfig(block) { return config; } +export function addCssVariables(element, variables) { + if (!element.id) { + const id = `gen${crypto.randomUUID().split('-')[0]}`; + element.id = id; + } + + const style = document.createElement('style'); + style.textContent = `#${element.id} { + ${Object.keys(variables).map((k) => `--${k}: ${variables[k]};`).join(' ')} + }`; + + element.parentNode.insertBefore(style, element); +} + +export function decorateSection(section) { + const wrappers = []; + let defaultContent = false; + [...section.children].forEach((e) => { + if (e.tagName === 'DIV' || !defaultContent) { + const wrapper = document.createElement('div'); + wrappers.push(wrapper); + defaultContent = e.tagName !== 'DIV'; + if (defaultContent) wrapper.classList.add('default-content-wrapper'); + } + wrappers[wrappers.length - 1].append(e); + }); + wrappers.forEach((wrapper) => section.append(wrapper)); + section.classList.add('section'); + section.dataset.sectionStatus = 'initialized'; + section.style.display = 'none'; + + /* process section metadata */ + const sectionMeta = section.querySelector('div.section-metadata'); + if (sectionMeta) { + const meta = readBlockConfig(sectionMeta); + Object.keys(meta).forEach((key) => { + if (key === 'style') { + const styles = meta.style.split(',').map((style) => toClassName(style.trim())); + styles.forEach((style) => section.classList.add(style)); + } else { + section.dataset[toCamelCase(key)] = meta[key]; + } + }); + sectionMeta.parentNode.remove(); + } + + 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); + } +} + /** * Decorates all sections in a container element. * @param {Element} main The container element */ export function decorateSections(main) { - main.querySelectorAll(':scope > div').forEach((section) => { - const wrappers = []; - let defaultContent = false; - [...section.children].forEach((e) => { - if (e.tagName === 'DIV' || !defaultContent) { - const wrapper = document.createElement('div'); - wrappers.push(wrapper); - defaultContent = e.tagName !== 'DIV'; - if (defaultContent) wrapper.classList.add('default-content-wrapper'); - } - wrappers[wrappers.length - 1].append(e); - }); - wrappers.forEach((wrapper) => section.append(wrapper)); - section.classList.add('section'); - section.dataset.sectionStatus = 'initialized'; - section.style.display = 'none'; - - /* process section metadata */ - const sectionMeta = section.querySelector('div.section-metadata'); - if (sectionMeta) { - const meta = readBlockConfig(sectionMeta); - Object.keys(meta).forEach((key) => { - if (key === 'style') { - const styles = meta.style.split(',').map((style) => toClassName(style.trim())); - styles.forEach((style) => section.classList.add(style)); - } else { - section.dataset[toCamelCase(key)] = meta[key]; - } - }); - sectionMeta.parentNode.remove(); - } - }); + main.querySelectorAll(':scope > div').forEach((section) => decorateSection(section)); } /** diff --git a/scripts/scripts.js b/scripts/scripts.js index f6d84985..bd9af7ce 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -5,6 +5,7 @@ import { loadFooter, decorateButtons, decorateIcons, + decorateSection, decorateSections, decorateBlocks, decorateTemplateAndTheme, @@ -30,6 +31,22 @@ function buildHeroBlock(main) { } } +function buildGrid(main) { + const gridElements = main.querySelectorAll('.section[data-grid-position]'); + gridElements.forEach((e) => { + e.classList.remove('section'); + delete e.dataset.sectionStatus; + e.removeAttribute('style'); + }); + const grid = document.createElement('div'); + grid.classList.add('block', 'grid'); + grid.dataset.blockName = 'grid'; + grid.dataset.blockStatus = 'initialized'; + main.insertBefore(grid, gridElements[0]); + decorateSection(grid); + grid.replaceChildren(...gridElements); +} + /** * load fonts.css and set a session storage flag */ @@ -55,6 +72,14 @@ function buildAutoBlocks(main) { } } +function decorateImages(main) { + main.querySelectorAll('picture').forEach((i) => { + if (i.parentElement.tagName === 'P' && i.parentElement.children.length === 1) { + i.parentElement.classList.add('image-wrapper'); + } + }); +} + /** * Decorates the main element. * @param {Element} main The main element @@ -67,6 +92,8 @@ export function decorateMain(main) { buildAutoBlocks(main); decorateSections(main); decorateBlocks(main); + decorateImages(main); + buildGrid(main); } /** diff --git a/styles/styles.css b/styles/styles.css index a0d8b819..b3b7d4f3 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -39,6 +39,8 @@ /* nav height */ --nav-height: 64px; + --padding-horizontal: 32px; + --padding-vertical: 16px; } @@ -229,23 +231,6 @@ main img { } main .section { - padding: 64px 16px; -} - -@media (min-width: 600px) { - main .section { - padding: 64px 32px; - } -} - -@media (min-width: 900px) { - .section > div { - max-width: 1200px; - margin: auto; - } -} - -/* section metadata */ -main .section.highlight { - background-color: var(--highlight-background-color); + background-color: var(--background-color, transparent); + color: var(--text-color); } From f9668ab7ccbbda5b7eaa63ce18725f8f1a5ab7bc Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 19 Dec 2023 11:32:09 +0100 Subject: [PATCH 4/5] minor fixes --- blocks/grid/grid.css | 7 ++++--- blocks/grid/grid.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 3e2378b0..d7f192a4 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -1,11 +1,12 @@ .section.grid { display: grid; - grid-template-columns: var(--grid-template-columns); - grid-template-rows: var(--grid-template-rows); + grid-template-columns: var(--grid-template-columns, unset); + grid-template-rows: var(--grid-template-rows, unset); } .section.grid .element { - grid-column: var(--grid-column-start-position, auto) var(--grid-column-end-position, auto); + grid-column: var(--grid-column-start-position, auto) / var(--grid-column-end-position, auto); + grid-row: var(--grid-row-start-position, auto) / var(--grid-row-end-position, auto); width: 100%; max-width: unset; background-color: var(--background-color, transparent); diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index dd2aea2e..fafdfefc 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -12,7 +12,7 @@ export default function decorate(block) { if (columnTemplate) { variables['grid-template-columns'] = columnTemplate; } - if (columnTemplate) { + if (rowTemplate) { variables['grid-template-rows'] = rowTemplate; } addCssVariables(block, variables); From 49b6041aeddcb084f81fde86f185f9d024971b9b Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 19 Dec 2023 15:14:42 +0100 Subject: [PATCH 5/5] initial header --- blocks/grid/grid.css | 1 + blocks/grid/grid.js | 12 +- 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(+), 436 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..5a9fc3f8 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -8,14 +8,10 @@ export default function decorate(block) { const columnTemplate = elements.find((e) => e.dataset.gridColumns)?.dataset.gridColumns; 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) }