From c1c2871c38255046d773bc0ba1cbd8552f803d47 Mon Sep 17 00:00:00 2001 From: Putra Bonaccorsi Date: Thu, 5 Oct 2023 12:48:44 -0400 Subject: [PATCH] feat(microsite): Add Solution Header and Solution Footer blocks used within custom campaign/microsites (#344) * feat(solution-header): add block and scroll to behavior * feat(solution-header): adjust scroll to behavior * feat(solution-header): add nav list * feat(solution-header): add cleanup * feat(solution-header): add solution footer block * feat(solution-footer): add responsive styling * feat(solution-footer): add more styles and fixes * feat(solution-header): check for when the append is fired * feat(solution-header): add spacing fixes based on feedback * feat(solution-header): fix lint issue --- blocks/columns/columns.css | 1 + blocks/cta/cta.css | 12 +- blocks/solution-footer/solution-footer.css | 134 ++++++++++ blocks/solution-footer/solution-footer.js | 77 ++++++ blocks/solution-header/solution-header.css | 287 +++++++++++++++++++++ blocks/solution-header/solution-header.js | 127 +++++++++ scripts/lib-franklin.js | 64 ++++- scripts/scripts.js | 5 +- styles/styles.css | 1 + 9 files changed, 695 insertions(+), 13 deletions(-) create mode 100644 blocks/solution-footer/solution-footer.css create mode 100644 blocks/solution-footer/solution-footer.js create mode 100644 blocks/solution-header/solution-header.css create mode 100644 blocks/solution-header/solution-header.js diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index c568e7a7..2d674a10 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -63,6 +63,7 @@ main .columns-container h4 { font-weight: var(--font-weight-light); line-height: var(--line-height-160); letter-spacing: var(--letter-spacing-1); + padding-inline-start: var(--spacer-element-02); } .columns.terms div, .columns.terms div strong, diff --git a/blocks/cta/cta.css b/blocks/cta/cta.css index 74a86cbb..83cb735b 100644 --- a/blocks/cta/cta.css +++ b/blocks/cta/cta.css @@ -18,14 +18,9 @@ .section.cta-container .cta.block > div, .section.cta-container .cta.block .cta__inner { text-align: center; - max-width: var(--text-max-container); margin: 0 auto; } -.cta.block .button-group p { - margin: 0; -} - .section.cta-container .cta.block h2 { margin: unset; font-size: 32px; @@ -39,6 +34,13 @@ line-height: 160%; text-align: center; letter-spacing: 0.01em; + max-width: var(--text-max-container); + margin: 0 auto; +} + +.section.cta-container .cta.block p picture { + display: block; + line-height: 0; } .section.cta-container .cta.block p.only-picture:last-child { diff --git a/blocks/solution-footer/solution-footer.css b/blocks/solution-footer/solution-footer.css new file mode 100644 index 00000000..b349ba1c --- /dev/null +++ b/blocks/solution-footer/solution-footer.css @@ -0,0 +1,134 @@ +/* Section - Solution Footer */ +main .section.solution-footer-container { + background-color: var(--neutral-carbon); + color: var(--neutral-white); +} + +main .section.solution-footer-container > div:last-child { + padding: var(--spacer-element-08) 0; +} + +/* Block - Solution Footer */ +.solution-footer { + position: relative; + display: flex; + flex-direction: column; +} + +.solution-footer .icon { + display: block; + line-height: 0; +} + +.solution-footer svg { + width: auto; + height: 38px; +} + +.solution-footer svg #text * { + fill: var(--neutral-white); +} + +.solution-footer .default-content-wrapper { + width: 100%; + margin-top: var(--spacer-element-08); + padding-top: var(--spacer-element-07); + border-top: 1px solid var(--neutral-grey-tint140); +} + +.solution-footer ul { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--gap-16); + list-style: none; + margin: 0; + padding-top: var(--spacer-element-03); + padding-inline-start: 0; +} + +.solution-footer ul li { + font-size: var(--font-size-14); + color: var(--neutral-grey-tint100); + margin: 0; +} + +.solution-footer ul li .cookie-consent { + color: var(--neutral-white); +} + +/* stylelint-disable no-descending-specificity */ +.solution-footer ul li a, +.solution-footer ul li a:not(.button):any-link { + color: var(--neutral-white); + border-color: transparent; +} + +.solution-footer ul li a:hover, +.solution-footer ul li a:not(.button):any-link:hover { + background: none; + color: var(--neutral-white); + border-bottom: 1px solid var(--neutral-white); + border-image-source: unset; + -webkit-text-fill-color: var(--neutral-white); +} +/* stylelint-enable no-descending-specificity */ + +.solution-footer .default-content-wrapper ul + p { + margin: 0 0 0 var(--spacer-element-08); + position: absolute; + top: 0; + right: 0; +} + +/* Tablet */ +@media only screen and (min-width: 768px) { + .solution-footer { + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + } + + .solution-footer ul { + display: flex; + justify-content: flex-end; + flex-direction: row; + align-items: center; + gap: var(--gap-24); + } + + .solution-footer .default-content-wrapper { + display: flex; + align-items: center; + flex-grow: 0; + width: auto; + margin-top: 0; + padding-top: 0; + border-top: 0; + } + + .solution-footer .default-content-wrapper ul + p { + position: relative; + } +} + +/* Desktop */ +@media (min-width: 1200px) { + main .section.solution-footer-container > div:last-child { + padding: var(--spacer-element-10) 0; + } + + .solution-footer svg { + height: 48px; + } + + .solution-footer ul li { + font-size: var(--font-size-16); + } + + .solution-footer .default-content-wrapper ul + p { + margin: 0 0 0 var(--spacer-element-10); + line-height: 0; + } +} \ No newline at end of file diff --git a/blocks/solution-footer/solution-footer.js b/blocks/solution-footer/solution-footer.js new file mode 100644 index 00000000..3e81bba1 --- /dev/null +++ b/blocks/solution-footer/solution-footer.js @@ -0,0 +1,77 @@ +import { decorateMain } from '../../scripts/scripts.js'; +import { loadBlocks, decorateButtons } from '../../scripts/lib-franklin.js'; + +/** + * Loads a fragment. + * @param {string} path - The path to the fragment + * @returns {HTMLElement} - The root element of the fragment + */ +async function loadFragment(path) { + try { + const url = new URL(path, window.location.origin); // Parse the URL + + if (url.pathname.startsWith('/')) { + const resp = await fetch(`${url.pathname}.plain.html`); + if (resp.ok) { + const main = document.createElement('div'); + main.innerHTML = await resp.text(); + // Decorate main element and load additional blocks + decorateMain(main); + await loadBlocks(main); + return main; + } + throw new Error('Failed to fetch fragment'); + } else { + throw new Error('Invalid path'); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Error loading fragment: ${error}`); + } + return null; +} + +export default async function decorate(block) { + // Get the block name attribute from the block element + const blockName = block.getAttribute('data-block-name'); + if (!blockName) { + return; + } + + // Decorate buttons within the block, ignoring class decoration + decorateButtons(block, { decorateClasses: false }); + + // Get the last child element of the block + const lastRow = [...block.children][1]; + + if (lastRow) { + const link = lastRow.querySelector('a'); + // Extract the href attribute from the link, if it exists + const { href } = link || {}; + const fragment = await loadFragment(href); + + if (fragment) { + // Extract the section element from the loaded fragment + const fragmentSection = fragment.querySelector(':scope .section'); + const { classList } = fragmentSection || []; + if (classList) { + // Add classes from the fragment's section element to the last row + lastRow.classList.add(...classList); + // Replace the last row content with fragment section content + lastRow.replaceWith(...fragmentSection.childNodes); + } + } + + // Handle click events on footer base links + block.addEventListener('click', (e) => { + const { target } = e; + // Check if the clicked element is a footer base link with an empty href attribute + if (target.tagName === 'A' && target.getAttribute('href') === '') { + e.preventDefault(); + // Call the OneTrust function to toggle info display + // eslint-disable-next-line no-undef + OneTrust.ToggleInfoDisplay(); + } + }); + } +} diff --git a/blocks/solution-header/solution-header.css b/blocks/solution-header/solution-header.css new file mode 100644 index 00000000..1cdb6fea --- /dev/null +++ b/blocks/solution-header/solution-header.css @@ -0,0 +1,287 @@ +/* Block - Solution Header */ +.solution-header-wrapper { + opacity: 0; + position: fixed; + border-bottom: 1px solid var(--neutral-sand); + width: 100%; + height: 140px; + background-color: var(--neutral-bone); + left: 0; + top: 0; + transition: all .5s ease; + z-index: 99999; +} + +.solution-header { + opacity: 0; + max-width: var(--secton-max-container); + height: 100%; + margin: 0 auto; +} + +.header-visible .solution-header-wrapper, +.header-visible .solution-header[data-block-status="loaded"] { + opacity: 1; +} + +.solution-header .solution-header__inner { + position: relative; +} + +.solution-header p { + margin: 0; +} + +.solution-header .icon { + display: block; + line-height: 0; +} + +.solution-header .icon svg { + width: auto; + height: 38px; +} + +.solution-header .solution-header__inner .solution-header__col-1 { + margin: var(--spacer-element-07); +} + +.solution-header .solution-header__inner .solution-header__col-2 { + overflow-x: auto; + height: 100%; + margin: var(--spacer-element-07) 0 calc(var(--spacer-element-07) * -1) 0; + border-top: 1px solid var(--neutral-sand); +} + +.solution-header .solution-header__inner .solution-header__col-3 { + display: none; +} + +.solution-header .solution-header__inner .solution-header__col-3 p:not(.button-container) { + display: none; + visibility: hidden; +} + +.solution-header ul { + display: flex; + align-items: flex-end; + justify-content: flex-start; + height: 100%; + margin: 0; +} + +.solution-header ul li { + font-family: var(--sans-serif-font-medium); + list-style: none; + height: 100%; + padding-left: var(--spacer-element-07); +} + +.solution-header ul li:last-child { + padding-right: var(--spacer-element-07); +} + +.solution-header ul li a { + display: flex; + align-items: center; + height: 100%; + white-space: nowrap; + color: var(--neutral-carbon); + font-size: var(--font-size-16); + font-weight: 500; + text-decoration: none; + padding: var(--spacer-element-05) 0; + background-image: var(--gradient-left-right); + background-size: 0 var(--spacer-element-02), auto; + background-repeat: no-repeat; + background-position: center bottom; + transition: all .2s ease-out; +} + +.solution-header ul li a:hover, +.solution-header ul li a.active { + background-size: 100% var(--spacer-element-02), auto; + z-index: 6; +} + +/* Sticky - Solution Header */ +body.microsites.header-visible main { + padding-top: 140px; +} + +.solution-header-wrapper.is-sticky { + background-color: var(--neutral-carbon); + border-bottom: 1px solid var(--neutral-carbon); + color: var(--neutral-white); + padding: 0; + height: var(--spacer-layout-05); +} + +.solution-header-wrapper.is-sticky .solution-header__inner { + display: flex; + align-items: flex-end; + height: 100%; + transition: padding-bottom .5s ease; +} + +.solution-header-wrapper.is-sticky .solution-header .icon { + display: block; + width: var(--spacer-layout-03); + height: var(--spacer-layout-03); + clip-path: circle(50% at 50% 50%); +} + +.solution-header-wrapper.is-sticky .solution-header .icon svg { + height: var(--spacer-layout-03); +} + +.solution-header-wrapper.is-sticky .solution-header .solution-header__col-1 { + margin: var(--spacer-element-05) var(--spacer-element-07); +} + +.solution-header-wrapper.is-sticky .solution-header .solution-header__col-2 { + border-color: transparent; + margin: 0; + border-left: 1px solid var(--neutral-grey-tint140); +} + +.solution-header-wrapper.is-sticky .solution-header ul li a { + color: var(--neutral-white); + padding: var(--spacer-element-05) 0; +} + +/* Tablet */ +@media only screen and (min-width: 768px) { + /* Block - Solution Header */ + .solution-header ul { + justify-content: center; + } + + .solution-header .solution-header__inner .solution-header__col-1 { + margin: var(--spacer-element-07) var(--spacer-element-08); + } + + .solution-header .solution-header__inner .solution-header__col-3 { + display: block; + max-width: 225px; + position: absolute; + top: calc(var(--spacer-element-02) * -1); + right: var(--spacer-layout-036); + } + + /* Sticky - Solution Header */ + .solution-header-wrapper.is-sticky .solution-header .solution-header__inner .solution-header__col-1 { + margin: var(--spacer-element-05) var(--spacer-element-08); + } + + .solution-header-wrapper.is-sticky .solution-header .solution-header__inner .solution-header__col-3 { + display: none; + } +} + +/* Desktop */ +@media (min-width: 1200px) { + /* Block - Solution Header */ + .solution-header { + margin: 0 auto; + width: 100%; + } + + .solution-header .solution-header__inner { + display: flex; + height: 100%; + align-items: flex-end; + justify-content: space-between; + flex-shrink: 0; + gap: var(--spacer-layout-04); + padding-top: 0; + padding-bottom: var(--spacer-element-08); + } + + .solution-header ul { + justify-content: flex-start; + } + + .solution-header ul li { + padding-left: var(--spacer-element-08); + } + + .solution-header ul li:first-child { + padding-left: 0; + } + + .solution-header ul li:last-child { + padding-right: 0; + } + + .solution-header .icon svg { + height: var(--spacer-layout-04); + } + + .solution-header .solution-header__inner .solution-header__col-1 { + margin: 0; + } + + .solution-header .solution-header__inner .solution-header__col-2 { + overflow-x: auto; + max-width: 650px; + height: 100%; + margin: 0 0 calc(var(--spacer-element-08) * -1) 0; + border-top: 0; + } + + .solution-header .solution-header__inner .solution-header__col-3 { + position: relative; + top: inherit; + right: inherit; + } + + .solution-header ul li a { + padding: var(--spacer-layout-036) 0; + } + + /* Sticky - Solution Header */ + body.microsites.header-visible main { + padding-top: var(--nav-height-desktop); + } + + .solution-header-wrapper { + height: var(--nav-height-desktop); + } + + .solution-header-wrapper.is-sticky { + height: var(--sticky-nav-height); + } + + .solution-header-wrapper.is-sticky .solution-header__inner { + padding-bottom: var(--spacer-element-05); + } + + .solution-header-wrapper.is-sticky .solution-header ul li a { + padding: var(--spacer-element-07) 0; + } + + .solution-header-wrapper.is-sticky .solution-header .solution-header__inner .solution-header__col-1 { + margin: 0; + } + + .solution-header-wrapper.is-sticky .solution-header .solution-header__inner .solution-header__col-2 { + border-color: transparent; + margin: 0 0 calc(var(--spacer-element-05) * -1) 0; + } + + .solution-header-wrapper.is-sticky .solution-header .solution-header__inner .solution-header__col-3 { + display: block; + } + + .solution-header-wrapper.is-sticky .solution-header .icon { + display: block; + width: var(--spacer-element-09); + height: var(--spacer-element-09); + clip-path: circle(50% at 50% 50%); + } + + .solution-header-wrapper.is-sticky .solution-header .icon svg { + height: var(--spacer-element-09); + } +} \ No newline at end of file diff --git a/blocks/solution-header/solution-header.js b/blocks/solution-header/solution-header.js new file mode 100644 index 00000000..7544da80 --- /dev/null +++ b/blocks/solution-header/solution-header.js @@ -0,0 +1,127 @@ +export default function decorate(block) { + const blockName = block.getAttribute('data-block-name'); + if (!blockName) { + return; + } + + [...block.children].forEach((element) => { + element.classList.add(`${blockName}__inner`); + + // Find all the div elements within the inner content class + const innerElements = element.querySelectorAll(`.${blockName}__inner div`); + + // Add the class to column and append a number to each of these div elements + let counter = 1; + innerElements.forEach((divElement) => { + const newClass = `${blockName}__col-${counter}`; + divElement.classList.add(`${blockName}__col`, newClass); + counter += 1; + }); + }); + + // Add page scroll listener to know when header turns to sticky + const header = block.parentNode; + window.addEventListener('scroll', () => { + const scrollAmount = window.scrollY; + if (scrollAmount > header.offsetHeight) { + header.classList.add('is-sticky'); + } else { + header.classList.remove('is-sticky'); + } + }); + + // Define a function to generate and append a semantic navigation component + function generateAnchorLinkNav() { + const sectionsWithTitles = document.querySelectorAll('.section[data-title]'); + const navItemsContainer = document.querySelector(`.${blockName} .${blockName}__col-2`); + + // Create an unordered list for the navigation + const navList = document.createElement('ul'); + navList.classList.add('nav-list'); // Add a class for styling or accessibility + + sectionsWithTitles.forEach((section) => { + // Get the value of the data-title attribute + const sectionTitle = section.getAttribute('data-title'); + + // Create a list item for each section + const listItem = document.createElement('li'); + listItem.classList.add('nav-item'); // Add a class for styling or accessibility + + // Create an anchor link element + const anchorLink = document.createElement('a'); + anchorLink.textContent = sectionTitle; + const achorLinkID = section.getAttribute('id'); + anchorLink.href = `#${achorLinkID}`; + + // Append the anchor link to the list item + listItem.appendChild(anchorLink); + + // Append the list item to the navigation list + navList.appendChild(listItem); + }); + + // Append the navigation list to the nav items container + navItemsContainer.appendChild(navList); + } + + // Call the function to generate and append the semantic navigation component + generateAnchorLinkNav(); + + // Define the list of navigation links + const navigationLinks = document.querySelectorAll(`.${blockName}__col-2 ul li a`); + + // Extract section IDs from navigation links + const sectionIds = Array.from(navigationLinks).map((link) => link.getAttribute('href').substring(1)); + + // Define the Intersection Observer options + const observerOptions = { + root: null, + rootMargin: '0px', + threshold: 0.5, // Trigger when 50% of the element is in the viewport + }; + + // Create Intersection Observer callback function + const handleIntersection = (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const targetId = entry.target.getAttribute('id'); + const correspondingLink = document.querySelector(`.${blockName}__col-2 ul li a[href="#${targetId}"]`); + + // Remove 'active' class from all links + navigationLinks.forEach((link) => { + link.classList.remove('active'); + }); + + // Add 'active' class to the corresponding link + correspondingLink.classList.add('active'); + } + }); + }; + + // Create Intersection Observer instance + const observer = new IntersectionObserver(handleIntersection, observerOptions); + + // Add observer to each section + sectionIds.forEach((sectionId) => { + const section = document.getElementById(sectionId); + if (section) { + observer.observe(section); + } + }); + + // Smooth scroll to anchor when a navigation link is clicked + navigationLinks.forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = link.getAttribute('href').substring(1); + const targetSection = document.getElementById(targetId); + + if (targetSection) { + window.scrollTo({ + top: targetSection.offsetTop, + behavior: 'smooth', + }); + } + }); + }); +} diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 18ef04ac..8cc14357 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -123,6 +123,23 @@ export function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } +/** + * Convert text to sentence case. + * @param {string} text property + * @returns {string} The sentence case value(s) + */ +export function toSentenceCase(text) { + // Split the text by hyphens or other non-word characters + const words = text.split(/[-\s]+/); + + // Capitalize the first letter of each word and convert the rest to lowercase + // eslint-disable-next-line max-len + const sentenceCaseWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); + + // Join the words back together with spaces + return sentenceCaseWords.join(' '); +} + /** * Replace icons with inline SVG and prefix with codeBasePath. * @param {Element} element @@ -264,17 +281,30 @@ export function decorateSections(main) { const sectionMeta = section.querySelector('div.section-metadata'); if (sectionMeta) { const meta = readBlockConfig(sectionMeta); + let styles; 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)); - } if (key === 'theme') { - section.setAttribute('data-theme', meta.theme); - } else { - section.dataset[toCamelCase(key)] = meta[key]; + switch (key) { + case 'style': + styles = meta.style.split(',').map((style) => toClassName(style.trim())); + styles.forEach((style) => section.classList.add(style)); + break; + case 'theme': + section.setAttribute('data-theme', meta.theme); + break; + case 'id': + section.setAttribute('id', toClassName(meta.id)); + if (key === 'title') { + section.setAttribute('data-title', meta.title); + } else { + section.setAttribute('data-title', toSentenceCase(meta.id)); + } + break; + default: + section.dataset[toCamelCase(key)] = meta[key]; } }); + sectionMeta.parentNode.remove(); } }); @@ -820,6 +850,26 @@ export function loadHeader(header) { return loadBlock(headerBlock); } +/** + * loads a block named 'solution-header' into header + */ + +export function loadSolutionHeader(header) { + const solutionHeaderBlock = document.querySelector('.solution-header-wrapper'); + + if (solutionHeaderBlock) { + header.append(solutionHeaderBlock); + + // Create a promise that resolves when the next animation frame is available + const waitForAnimationFrame = () => new Promise(requestAnimationFrame); + + // Wait for the next animation frame before adding the class + waitForAnimationFrame().then(() => { + document.querySelector('body').classList.add('header-visible'); + }); + } +} + /** * loads a block named 'footer' into footer */ diff --git a/scripts/scripts.js b/scripts/scripts.js index 6b359651..37bc5f9e 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -2,6 +2,7 @@ import { sampleRUM, buildBlock, loadHeader, + loadSolutionHeader, loadFooter, decorateButtons, decorateIcons, @@ -923,10 +924,12 @@ async function loadLazy(doc) { const element = hash ? main.querySelector(hash) : false; if (hash && element) element.scrollIntoView(); - if (!locationCheck('block-library') && !locationCheck('quick-links')) { + if (!locationCheck('block-library') && !locationCheck('quick-links') && !locationCheck('campaigns')) { loadHeader(doc.querySelector('header')); loadFooter(doc.querySelector('footer')); await buildBreadcrumb(); + } else if (locationCheck('campaigns')) { + loadSolutionHeader(doc.querySelector('header')); } loadCSS(`${window.hlx.codeBasePath}/styles/lazy-styles.css`); diff --git a/styles/styles.css b/styles/styles.css index a8f65e73..c5a60166 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -135,6 +135,7 @@ --neutral-160: #454544; --neutral-grey-tint100: #B9B5AE; --neutral-grey-tint120: #8B8883; + --neutral-grey-tint140: #5C5A57; /* Feedback */ --feedback-green: #059641;