diff --git a/hlx_statics/blocks/logo-showcase/logo-showcase.css b/hlx_statics/blocks/logo-showcase/logo-showcase.css new file mode 100644 index 00000000..1b71b36e --- /dev/null +++ b/hlx_statics/blocks/logo-showcase/logo-showcase.css @@ -0,0 +1,296 @@ +main div.logo-showcase-wrapper:has(.logo-showcase.background-color-white), +main div.logo-showcase-wrapper div.logo-showcase.background-color-white { + background-color: white; +} + +main div.logo-showcase-wrapper:has(.logo-showcase.background-color-gray), +main div.logo-showcase-wrapper div.logo-showcase.background-color-gray { + background-color: rgb(245, 245, 245); +} + +main div.logo-showcase-wrapper:has(.logo-showcase.background-color-navy), +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy { + background-color: rgb(15, 55, 95); +} + +main div.logo-showcase-wrapper { + width: 100%; + background: rgb(245, 245, 245); +} + +main div.logo-showcase-wrapper div.logo-showcase { + max-width: 1280px; + box-sizing: border-box; + margin: auto; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + grid-template-areas: + "media content" + "media tabs"; + gap: 48px; +} + +main div.logo-showcase-wrapper div.logo-showcase-tablist { + grid-area: tabs; + display: flex; + flex-wrap: wrap; + gap: 32px; + align-items: flex-start; +} + +main div.logo-showcase-wrapper div.logo-showcase-tabpanel { + display: contents; +} + +main div.logo-showcase-wrapper div.logo-showcase-tabpanel[hidden] { + display: none; +} + +main div.logo-showcase-wrapper div.logo-showcase-media-panel { + grid-area: media; + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 350px; + padding: 0 8px; + box-sizing: border-box; + overflow: hidden; +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel { + grid-area: content; + display: flex; + flex-direction: column; + justify-content: center; +} + +main div.logo-showcase-wrapper div.logo-showcase-media-panel.is-text-fallback { + align-items: flex-start; + justify-content: center; + padding: 0 32px; + overflow: auto; +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel h1.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase-content-panel h2.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase-content-panel h3.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase-media-text h1.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase-media-text h2.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase-media-text h3.logo-showcase-heading { + margin: 0; + font-size: 36px; + line-height: 1.15; + color: rgb(44, 44, 44); +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel p.spectrum-Body, +main div.logo-showcase-wrapper div.logo-showcase-media-text p.spectrum-Body { + margin: 0; + color: rgb(44, 44, 44); +} + +main div.logo-showcase-wrapper div.logo-showcase-media-text { + display: flex; + flex-direction: column; + gap: 22px; + width: 100%; + text-align: left; +} + +main div.logo-showcase-wrapper div.logo-showcase-media-panel picture, +main div.logo-showcase-wrapper div.logo-showcase-media-panel img { + display: block; + max-width: 100%; + max-height: 400px; + width: auto; + height: auto; + object-fit: contain; +} + +main div.logo-showcase-wrapper div.logo-showcase-media-panel video, +main div.logo-showcase-wrapper div.logo-showcase-media-panel .video-container { + max-width: 100%; + max-height: 350px; +} + +main div.logo-showcase-wrapper div.logo-showcase-media-panel.has-video .video-container { + width: 100%; + max-width: calc(350px * 16 / 9); +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel > div { + display: flex; + flex-direction: column; + gap: 22px; +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel p.button-container { + margin-top: 8px; +} + +main div.logo-showcase-wrapper div.logo-showcase-button-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 8px; +} + +main div.logo-showcase-wrapper div.logo-showcase-button-container p.button-container { + margin: 0; +} + +main div.logo-showcase-wrapper button.logo-showcase-nav-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + min-width: 72px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + transition: background-color 160ms ease, color 160ms ease, filter 160ms ease; +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item { + color: white; +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item:hover:not(.active), +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item:focus-visible:not(.active) { + background-color: rgba(255, 255, 255, 0.08); +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item.active, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item.active:hover { + background-color: rgba(255, 255, 255, 0.16); +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy button.logo-showcase-nav-item:not(:has(.logo-showcase-nav-logo)) { + padding: 5px; + border-radius: 4px; +} + +main div.logo-showcase-wrapper button.logo-showcase-nav-item:focus-visible { + outline: 2px solid rgb(20, 115, 230); + outline-offset: 4px; + border-radius: 4px; +} + +main div.logo-showcase-wrapper div.logo-showcase-nav-logo { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 45px; +} + +main div.logo-showcase-wrapper div.logo-showcase-nav-logo picture, +main div.logo-showcase-wrapper div.logo-showcase-nav-logo img { + display: block; + max-width: 100%; + object-fit: contain; + filter: grayscale(1) opacity(0.45); + transition: filter 0.2s ease; +} + +main div.logo-showcase-wrapper button.logo-showcase-nav-item.active div.logo-showcase-nav-logo picture, +main div.logo-showcase-wrapper button.logo-showcase-nav-item.active div.logo-showcase-nav-logo img, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:hover div.logo-showcase-nav-logo picture, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:hover div.logo-showcase-nav-logo img, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:focus-visible div.logo-showcase-nav-logo picture, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:focus-visible div.logo-showcase-nav-logo img { + filter: none; +} + +main div.logo-showcase-wrapper span.logo-showcase-nav-name { + font-size: 14px; + line-height: 1.2; + color: rgb(110, 110, 110); + transition: color 0.2s ease; +} + +main div.logo-showcase-wrapper button.logo-showcase-nav-item.active span.logo-showcase-nav-name, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:hover span.logo-showcase-nav-name, +main div.logo-showcase-wrapper button.logo-showcase-nav-item:focus-visible span.logo-showcase-nav-name { + color: rgb(44, 44, 44); +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy h1.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy h2.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy h3.logo-showcase-heading, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy .logo-showcase-content-panel p.spectrum-Body, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy div.logo-showcase-media-text p.spectrum-Body, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy span.logo-showcase-nav-name, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy .logo-showcase-content-panel a, +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy .spectrum-Link--quiet { + color: white; +} + +main div.logo-showcase-wrapper div.logo-showcase.background-color-navy .spectrum-Button--secondary { + color: white; + border-color: white; +} + +@media screen and (max-width: 1024px) { + main div.logo-showcase-wrapper div.logo-showcase { + padding: 64px 24px; + grid-template-columns: 1fr; + grid-template-areas: + "media" + "content" + "tabs"; + gap: 40px; + } + + main div.logo-showcase-wrapper div.logo-showcase-content-panel { + padding: 0 25px; + } + + main div.logo-showcase-wrapper div.logo-showcase-media-panel picture, + main div.logo-showcase-wrapper div.logo-showcase-media-panel img, + main div.logo-showcase-wrapper div.logo-showcase-media-panel video, + main div.logo-showcase-wrapper div.logo-showcase-media-panel .video-container { + max-width: 100%; + max-height: 400px; + } + + main div.logo-showcase-wrapper h1.logo-showcase-heading, + main div.logo-showcase-wrapper h2.logo-showcase-heading, + main div.logo-showcase-wrapper h3.logo-showcase-heading, + main div.logo-showcase-wrapper div.logo-showcase-media-text h1.logo-showcase-heading, + main div.logo-showcase-wrapper div.logo-showcase-media-text h2.logo-showcase-heading, + main div.logo-showcase-wrapper div.logo-showcase-media-text h3.logo-showcase-heading { + font-size: 28px; + } +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel h1.logo-showcase-heading .anchor-link { + visibility: hidden; +} + +main div.logo-showcase-wrapper div.logo-showcase-content-panel h1.logo-showcase-heading:hover .anchor-link { + visibility: visible; +} + +@media screen and (max-width: 640px) { + main div.logo-showcase-wrapper div.logo-showcase { + padding: 48px 16px; + } + + main div.logo-showcase-wrapper div.logo-showcase-media-panel { + min-height: 200px; + } + + main div.logo-showcase-wrapper div.logo-showcase-tablist { + gap: 35px; + justify-content: space-between; + } + + main div.logo-showcase-wrapper button.logo-showcase-nav-item { + min-width: 64px; + flex: 1 1 calc(50% - 20px); + } +} diff --git a/hlx_statics/blocks/logo-showcase/logo-showcase.js b/hlx_statics/blocks/logo-showcase/logo-showcase.js new file mode 100644 index 00000000..fea33061 --- /dev/null +++ b/hlx_statics/blocks/logo-showcase/logo-showcase.js @@ -0,0 +1,348 @@ +import { + createTag, + decorateButtons, + removeEmptyPTags, + decorateAnchorLink, +} from '../../scripts/lib-adobeio.js'; +import { + createOptimizedPicture, + decorateLightOrDark, +} from '../../scripts/lib-helix.js'; +import { + applyVideoContainer, + getVideoTitle, + parseVideoSource, +} from '../../scripts/video.js'; + +function isAnimatedGif(src) { + try { + const url = new URL(src, window.location.href); + return url.pathname.toLowerCase().endsWith('.gif'); + } catch { + return false; + } +} + +function optimizeImages(container) { + container.querySelectorAll('picture > img').forEach((img) => { + const pic = img.closest('picture'); + if (!pic || pic.dataset.optimized) return; + if (isAnimatedGif(img.src)) return; + const optimized = createOptimizedPicture(img.src, img.alt); + optimized.dataset.optimized = 'true'; + pic.replaceWith(optimized); + }); +} + +function ensureYoutubeJsApi(iframe) { + const src = iframe.getAttribute('src'); + if (!src) return; + const url = new URL(src, window.location.href); + if (!/youtube(-nocookie)?\.com$/.test(url.hostname.replace(/^www\./, ''))) return; + if (!url.searchParams.has('enablejsapi')) { + url.searchParams.set('enablejsapi', '1'); + iframe.setAttribute('src', url.toString()); + } +} + +function postYoutubeCommand(iframe, func) { + iframe.contentWindow?.postMessage(JSON.stringify({ event: 'command', func, args: [] }), '*'); +} + +function pauseIframeVideo(iframe) { + postYoutubeCommand(iframe, 'pauseVideo'); + if (!iframe.dataset.logoShowcaseLoadBound) { + iframe.dataset.logoShowcaseLoadBound = 'true'; + iframe.addEventListener('load', () => { + if (iframe.dataset.logoShowcasePendingPause === 'true') postYoutubeCommand(iframe, 'pauseVideo'); + }); + } + iframe.dataset.logoShowcasePendingPause = 'true'; +} + +function playIframeVideo(iframe) { + iframe.dataset.logoShowcasePendingPause = 'false'; + postYoutubeCommand(iframe, 'playVideo'); +} + +function prepareMedia(mediaDiv, block) { + if (!mediaDiv) return null; + if (block.classList.contains('video')) { + const videoSource = parseVideoSource(mediaDiv); + if (videoSource) { + const wrapper = createTag('div'); + const title = getVideoTitle(videoSource.url, videoSource.linkText); + applyVideoContainer(wrapper, { + url: videoSource.url, + title, + autoplay: true, + muted: true, + loop: true, + controls: block.classList.contains('controls'), + }); + wrapper.setAttribute('role', 'region'); + wrapper.setAttribute('aria-label', title || 'Video'); + wrapper.querySelectorAll('iframe').forEach((iframe) => ensureYoutubeJsApi(iframe)); + return wrapper; + } + } + return mediaDiv.cloneNode(true); +} + +function hasVisualMedia(mediaDiv, block) { + if (!mediaDiv) return false; + if (block.classList.contains('video') && parseVideoSource(mediaDiv)) return true; + return !!mediaDiv.querySelector('picture, img, video, .video-container'); +} + +function decorateTypography(container) { + container.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach((heading) => { + heading.classList.add('spectrum-Heading', 'spectrum-Heading--sizeL', 'logo-showcase-heading'); + decorateAnchorLink(heading); + }); + container.querySelectorAll('p').forEach((p) => { p.classList.add('spectrum-Body', 'spectrum-Body--sizeM') }); +} + +function preparePartnerMedia(mediaDiv, text, block) { + if (hasVisualMedia(mediaDiv, block)) { + return { media: prepareMedia(mediaDiv, block), text, isTextFallback: false }; + } + if (mediaDiv?.textContent?.trim()) { + const mediaText = mediaDiv.cloneNode(true); + decorateTypography(mediaText); + mediaText.classList.add('logo-showcase-media-text'); + return { media: mediaText, text, isTextFallback: true }; + } + if (text?.textContent?.trim()) { + const fallbackText = text.cloneNode(true); + fallbackText.classList.add('logo-showcase-media-text'); + return { media: fallbackText, text: null, isTextFallback: true }; + } + return { media: null, text, isTextFallback: false }; +} + +function isCtaLink(link, paragraph) { + if (link.closest('.button-container') || link.parentElement?.tagName === 'STRONG' || link.classList.contains('button')) { + return true; + } + const childNodes = [...paragraph.childNodes]; + const links = [...paragraph.querySelectorAll('a')]; + const nodes = childNodes.filter((n) => n.nodeType !== Node.TEXT_NODE || n.textContent.trim()); + + if (links.length === 1 && nodes.length === 1 && (nodes[0] === link || nodes[0]?.contains?.(link))) { + return true; + } + if (link !== links.at(-1)) return false; + + const linkIndex = childNodes.indexOf(link); + return linkIndex > -1 && childNodes.slice(0, linkIndex).some((n) => n.nodeName === 'BR'); +} + +function decorateContent(content) { + decorateTypography(content); + const buttonGroup = createTag('div', { class: 'logo-showcase-button-container' }); + + [...content.querySelectorAll('p')].forEach((paragraph) => { + if (paragraph.classList.contains('button-container')) { + buttonGroup.append(paragraph); + return; + } + + [...paragraph.querySelectorAll('a')].filter((link) => isCtaLink(link, paragraph)).forEach((link) => { + const childNodes = [...paragraph.childNodes]; + childNodes.slice(0, childNodes.indexOf(link)).reverse().forEach((node) => { + if (node.nodeName === 'BR' || (node.nodeType === Node.TEXT_NODE && !node.textContent.trim())) node.remove(); + }); + const buttonParagraph = createTag('p', { class: 'button-container' }); + buttonParagraph.append(link.parentElement?.tagName === 'STRONG' ? link.parentElement : link); + buttonGroup.append(buttonParagraph); + }); + + if (!paragraph.textContent.trim() && !paragraph.querySelector('img,picture')) paragraph.remove(); + }); + + if (buttonGroup.childElementCount) content.append(buttonGroup); + decorateButtons(content); +} + +function getNavItemLabel(partner, index) { + if (partner.label) { + return partner.label; + } + const img = partner.logo?.querySelector('img'); + if (img?.alt) { + return img.alt.trim(); + } + return `Partner ${index + 1}`; +} + +let logoShowcaseInstanceId = 0; + +function buildTab(partner, index, idPrefix, onSelect) { + const label = getNavItemLabel(partner, index); + const item = createTag('button', { + class: 'logo-showcase-nav-item', + type: 'button', + role: 'tab', + id: `${idPrefix}-tab-${index}`, + 'aria-controls': `${idPrefix}-panel-${index}`, + 'aria-selected': index ? 'false' : 'true', + tabindex: index ? '-1' : '0', + 'aria-label': label, + }); + if (!index) item.classList.add('active'); + + const logo = createTag('div', { class: 'logo-showcase-nav-logo' }); + if (partner.logo) logo.append(partner.logo); + + const name = createTag('span', { class: 'logo-showcase-nav-name spectrum-Body spectrum-Body--sizeM' }); + name.textContent = partner.label || ''; + + if (partner.logo && partner.label) item.append(logo, name); + else item.append(partner.logo ? logo : name); + + item.addEventListener('click', () => onSelect(index)); + return item; +} + +function bindTablistKeyboard(tablist, activate) { + tablist.addEventListener('keydown', (event) => { + const tabs = [...tablist.querySelectorAll('[role="tab"]')]; + const currentIndex = tabs.indexOf(document.activeElement); + if (currentIndex === -1) return; + + let nextIndex = -1; + if (event.key === 'ArrowRight') { + nextIndex = (currentIndex + 1) % tabs.length; + } else if (event.key === 'ArrowLeft') { + nextIndex = (currentIndex - 1 + tabs.length) % tabs.length; + } else if (event.key === 'Home') { + nextIndex = 0; + } else if (event.key === 'End') { + nextIndex = tabs.length - 1; + } else if (event.key === ' ' || event.key === 'Spacebar' || event.key === 'Enter') { + event.preventDefault(); + activate(currentIndex); + return; + } else { + return; + } + + event.preventDefault(); + tabs[nextIndex].focus(); + }); +} + +export default async function decorate(block) { + block.setAttribute('daa-lh', 'logo-showcase'); + decorateLightOrDark(block); + removeEmptyPTags(block); + + const isVideo = block.classList.contains('video'); + const rows = [...block.children].filter((child) => child.tagName === 'DIV'); + if (!rows.length) return; + + rows.forEach((row) => decorateLightOrDark(row)); + + const partners = rows.map((row) => { + const [media, content, selector] = row.children; + const text = content?.cloneNode(true); + if (text) decorateContent(text); + const { media: mediaContent, text: panelText, isTextFallback } = preparePartnerMedia(media, text, block); + const selectorParagraphs = [...selector?.children || []].filter((el) => el.tagName === 'P'); + return { + media: mediaContent, + text: panelText, + isTextFallback, + label: selectorParagraphs[1]?.textContent?.trim() || selector?.textContent?.trim(), + logo: selector?.querySelector('picture')?.cloneNode(true), + }; + }); + + logoShowcaseInstanceId += 1; + const idPrefix = `logo-showcase-${logoShowcaseInstanceId}`; + + const tablist = createTag('div', { + class: 'logo-showcase-tablist', + role: 'tablist', + 'aria-label': 'Partners', + }); + + const tabPanels = partners.map((partner, index) => { + const panel = createTag('div', { + class: 'logo-showcase-tabpanel', + role: 'tabpanel', + id: `${idPrefix}-panel-${index}`, + 'aria-labelledby': `${idPrefix}-tab-${index}`, + }); + if (index) { + panel.hidden = true; + } + + const mediaPanel = createTag('div', { class: 'logo-showcase-media-panel' }); + if (isVideo) { + mediaPanel.classList.add('has-video'); + } + if (partner.isTextFallback) { + mediaPanel.classList.add('is-text-fallback'); + } + if (partner.media) { + mediaPanel.append(partner.media); + } + const contentPanel = createTag('div', { class: 'logo-showcase-content-panel' }); + if (partner.isTextFallback && !partner.text) { + contentPanel.classList.add('is-empty'); + } + if (partner.text) { + contentPanel.append(partner.text); + } + + panel.append(mediaPanel, contentPanel); + return panel; + }); + + tabPanels.forEach((panel, i) => { + if (i === 0) return; + panel.querySelectorAll('video').forEach((v) => { + v.pause?.() + }); + panel.querySelectorAll('iframe').forEach((iframe) => { + pauseIframeVideo(iframe) + }); + }); + + const setActive = (index) => { + tabPanels.forEach((panel, i) => { + const becomingActive = i === index; + panel.hidden = !becomingActive; + panel.querySelectorAll('video').forEach((v) => { + if (becomingActive) { + v.play?.().catch(() => { }); + } + else { + v.pause?.(); + } + }); + panel.querySelectorAll('iframe').forEach((iframe) => { + if (becomingActive) { + playIframeVideo(iframe); + } + else { + pauseIframeVideo(iframe); + } + }); + }); + tablist.querySelectorAll('[role="tab"]').forEach((tab, i) => { + const active = i === index; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', active ? 'true' : 'false'); + tab.setAttribute('tabindex', active ? '0' : '-1'); + }); + }; + + partners.forEach((partner, index) => { tablist.append(buildTab(partner, index, idPrefix, setActive)) }); + bindTablistKeyboard(tablist, setActive); + + block.replaceChildren(tablist, ...tabPanels); + optimizeImages(block); +} diff --git a/hlx_statics/styles/styles.css b/hlx_statics/styles/styles.css index d6e9d76d..f59f332e 100644 --- a/hlx_statics/styles/styles.css +++ b/hlx_statics/styles/styles.css @@ -330,6 +330,7 @@ main div.nested-code-wrapper, main div.mini-resource-card-wrapper, main div.product-card-wrapper, main div.profile-card-wrapper, +main div.logo-showcase-wrapper, main div.tab-wrapper, main div.table-wrapper, main div.text-wrapper,