From d77b24fb3970a3ca2fe14bfbfd2ad58168dff4cc Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 26 Jun 2026 12:55:02 -0600 Subject: [PATCH] fix: default form override --- blocks/footer/footer.css | 209 +++++++++++++++++++++++++++++++++++++-- blocks/footer/footer.js | 145 +++++++++++++++++++++++++-- styles/styles.css | 1 + 3 files changed, 338 insertions(+), 17 deletions(-) diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index b01e819..cfe0514 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,20 +1,211 @@ -footer { - background-color: var(--color-bg-light); - font-size: var(--text-s); +body > footer { + overflow: clip; + background-color: var(--color-gray-300); } -footer .footer > div { +footer .footer { + display: grid; + grid-template-areas: + 'nav' + 'social' + 'copyright'; + grid-template-columns: 1fr; + gap: var(--space-s); max-width: var(--site-width); - margin: auto; - padding: var(--space-l) var(--space-m) var(--space-m); + margin: 0 auto; + padding: var(--space-l); +} + +@media (width >=900px) { + footer .footer { + gap: var(--space-m); + } } footer .footer p { margin: 0; } -@media (width >= 900px) { - footer .footer > div { - padding: var(--space-l) var(--space-l) var(--space-m); +footer .footer a { + color: inherit; +} + +footer .footer ul { + list-style: none; + margin: 0; + padding: 0; +} + +footer .footer summary { + list-style: none; + display: grid; + grid-template-columns: 1fr 44px; + align-items: center; + gap: 1ch; + padding: 1.2em 0; + font-family: var(--heading-font-family); + cursor: pointer; +} + +footer .footer summary::-webkit-details-marker { + display: none; +} + +footer .footer summary:hover, +footer .footer summary:focus-visible, +footer .footer details[open] summary { + text-decoration: underline; +} + +footer .footer summary .icon { + justify-self: center; +} + +footer .footer details[open] .icon img, +footer .footer details[open] .icon svg { + transform: rotate(-180deg); +} + +footer .footer details>ul { + display: block; + overflow: hidden; + height: 0; + padding: 0.8em 0; + transition: height 0.2s ease; +} + +footer .footer details>ul>li { + padding: 0.6em 0; +} + +@media (width >=600px) { + footer .footer summary { + grid-template-columns: 1fr; + cursor: default; + pointer-events: none; + } + + footer .footer summary:hover, + footer .footer summary:focus-visible, + footer .footer details[open] summary { + text-decoration: none; + } + + footer .footer summary .icon { + display: none; + } + + footer .footer details>ul { + overflow: visible; + height: auto; + padding: 0; + transition: none; + } + + footer .footer details>ul>li { + padding: 0.4em 0; + } +} + +/* footer nav */ +footer .footer-nav { + grid-area: nav; +} + +@media (width >=600px) { + footer .footer-nav { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + gap: var(--space-s); + } +} + +@media (width >=900px) { + footer .footer-nav { + grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr)); + } +} + +/* footer social */ +footer .footer-social { + grid-area: social; + font-size: 24px; +} + +/* stylelint-disable-next-line no-descending-specificity */ +footer .footer-social ul { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1ch; +} + +footer .footer-social a { + display: flex; + align-items: center; + justify-content: center; + height: 44px; + width: 44px; +} + +@media (width >=600px) { + footer .footer-social ul { + justify-content: flex-end; + } +} + +/* footer copyright */ +footer .footer-copyright { + grid-area: copyright; + position: relative; + padding-top: var(--space-l); + color: var(--color-link); + isolation: isolate; +} + +footer .footer-copyright::after { + content: ''; + position: absolute; + top: 0; + left: calc(50% - 50vw); + bottom: calc(-1 * var(--space-l)); + z-index: -1; + width: 100vw; + background-color: var(--color-bg); +} + +/* stylelint-disable-next-line no-descending-specificity */ +footer .footer-copyright li { + padding: 0.8em 0; +} + +footer .footer-copyright #cookie { + padding: 0; + color: inherit; +} + +footer .footer-copyright a:hover, +footer .footer-copyright a:focus-visible, +footer .footer-copyright #cookie:hover, +footer .footer-copyright #cookie:focus-visible { + text-decoration: underline; +} + +@media (width >=600px) { + footer .footer-copyright ul { + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + footer .footer-copyright li { + padding: 0; + } + + footer .footer-copyright li + li::before { + content: '|'; + margin-right: 1ch; + padding-left: 1ch; + color: var(--color-muted); } } diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index ff5708a..3b0704f 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -1,20 +1,149 @@ -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'; /** - * loads and decorates the footer - * @param {Element} block The footer block element + * Returns a named footer section from within the block. + * @param {Element} block - The footer block element + * @param {string} sectionName - The section name, matching the footer-{name} class + * @returns {Element|null} The section element, or `null` if not found + */ +function getSection(block, sectionName) { + return block.querySelector(`.footer-${sectionName}`); +} + +/** + * Wraps each direct h2+ul pair within a container in a details/summary. + * @param {Element} container - The element whose direct h2+ul children to transform + */ +function decorateDetails(container) { + const mq = window.matchMedia('(width >= 600px)'); + const created = []; + + container.querySelectorAll(':scope > h2').forEach((heading) => { + const list = heading.nextElementSibling; + if (!list || list.tagName !== 'UL') return; + const details = document.createElement('details'); + const summary = document.createElement('summary'); + summary.textContent = heading.textContent; + const chevron = document.createElement('span'); + chevron.classList.add('icon', 'icon-chevron'); + summary.append(chevron); + details.append(summary, list); + heading.replaceWith(details); + created.push({ details, list, summary }); + + summary.addEventListener('click', (e) => { + e.preventDefault(); + if (mq.matches) return; + if (details.open) { + list.style.height = `${list.scrollHeight}px`; + list.getBoundingClientRect(); + list.style.height = '0'; + list.addEventListener('transitionend', () => { + details.removeAttribute('open'); + }, { once: true }); + } else { + details.setAttribute('open', ''); + const targetHeight = list.scrollHeight; + list.style.height = '0'; + list.getBoundingClientRect(); + list.style.height = `${targetHeight}px`; + } + }); + }); + + const handleBreakpoint = ({ matches }) => { + created.forEach(({ details, list, summary }) => { + if (matches) { + details.setAttribute('open', ''); + summary.setAttribute('tabindex', '-1'); + } else { + details.removeAttribute('open'); + summary.removeAttribute('tabindex'); + } + list.style.height = ''; + }); + }; + + mq.addEventListener('change', handleBreakpoint); + handleBreakpoint(mq); +} + +/** + * Replaces the section div with a nav landmark. + * @param {Element} section - The footer-nav section element to promote + */ +function decorateNav(section) { + const content = section.querySelector('div'); + if (!content) return; + decorateDetails(content); + const nav = document.createElement('nav'); + nav.setAttribute('aria-label', 'Footer'); // TODO: localization + nav.classList.add(...section.classList); + nav.append(...content.children); + section.replaceWith(nav); +} + +/** + * Converts the "Cookie Settings" bare text item into a button for OneTrust SDK hookup. + * @param {Element} section - The footer-copyright section element + */ +function decorateCopyright(section) { + section.querySelectorAll('li').forEach((li) => { + if (li.querySelector('a[href]')) return; + const text = li.textContent.trim(); + if (!text) return; + const btn = document.createElement('button'); + btn.id = 'cookie'; + btn.type = 'button'; + btn.textContent = text; + // TODO: wire up OneTrust consent SDK + li.replaceChildren(btn); + }); +} + +/** + * Adds accessible labels to icon-only social links, derived from each link's URL. + * @param {Element} section - The social section element + */ +function decorateSocial(section) { + section.querySelectorAll('a[href]').forEach((link) => { + const { hostname } = new URL(link.href); + const host = hostname.replace(/^www\./, ''); + const name = host.split('.')[0]; + const label = name.charAt(0).toUpperCase() + name.slice(1); + link.setAttribute('aria-label', label); + }); +} + +/** + * Loads and decorates the footer. + * @param {Element} block - The footer block element */ export default async function decorate(block) { - // load footer as fragment const footerMeta = getMetadata('footer'); const footerPath = footerMeta ? new URL(footerMeta, window.location).pathname : '/footer'; const fragment = await loadFragment(footerPath); - // decorate footer DOM block.textContent = ''; - const footer = document.createElement('div'); - while (fragment.firstElementChild) footer.append(fragment.firstElementChild); + block.append(...fragment.children); + + const sections = ['nav', 'social', 'copyright']; + sections.forEach((s, i) => { + const section = block.children[i]; + if (section) section.classList.add(`footer-${s}`); + }); + + const nav = getSection(block, 'nav'); + if (nav) decorateNav(nav); + + const social = getSection(block, 'social'); + if (social) decorateSocial(social); + + const copyright = getSection(block, 'copyright'); + if (copyright) decorateCopyright(copyright); - block.append(footer); + decorateExternalLinks(block); + decorateIcons(block); } diff --git a/styles/styles.css b/styles/styles.css index 538c6fe..dcd900f 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -295,6 +295,7 @@ textarea, select, button { border: 0; + background: none; font: inherit; }