Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 200 additions & 9 deletions blocks/footer/footer.css
Original file line number Diff line number Diff line change
@@ -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);
}
}
145 changes: 137 additions & 8 deletions blocks/footer/footer.js
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ textarea,
select,
button {
border: 0;
background: none;
font: inherit;
}

Expand Down