diff --git a/libs/blocks/brick/brick.css b/libs/blocks/brick/brick.css new file mode 100644 index 0000000000..89459a2fb6 --- /dev/null +++ b/libs/blocks/brick/brick.css @@ -0,0 +1,245 @@ +.brick { + position: relative; + display: flex; + text-size-adjust: none; + min-height: 300px; +} + +.brick, +.brick.click a.foreground { + color: inherit; +} + +.brick.light, +.brick.light.click a.foreground { + color: var(--text-color); +} + +.brick.dark, +.dark.brick.click a.foreground { + color: var(--color-white); +} + +.brick .background { + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; + overflow: hidden; +} + +.brick .foreground { + position: relative; + display: flex; + flex-grow: 1; + padding: var(--spacing-m); +} + +.brick.rounded-corners, +.brick.rounded-corners .background, +.brick.rounded-corners .foreground { + border-radius: var(--spacing-xs); +} + +.brick.align-center .foreground, +.brick.align-center .foreground .action-area, +.brick.center .foreground, +.brick.center .foreground .action-area { + align-items: center; + text-align: center; + justify-content: center; +} + +.brick.center .foreground { + align-items: flex-start; +} + +.brick .background div, +.brick .background p, +.brick .background picture { + height: 100%; + margin: 0; + padding: 0; +} + +.brick .background p, +.brick .background picture { + display: block; +} + +.brick .background img { + object-fit: contain; + object-position: bottom center; + width: 100%; + height: 100%; +} + +.brick .mobile-only, +.brick .tablet-only, +.brick .desktop-only { + display: none; +} + +.brick .foreground p { + padding: 0; + margin: 0; +} + +.brick .foreground div > * { + margin-top: var(--spacing-xs); +} + +.brick .foreground p:first-child, +.brick .foreground p.icon-area, +.brick .foreground p.icon-area + p { + margin-top: 0; +} + +.brick .foreground p.icon-area { + display: inline-block; + margin-bottom: var(--spacing-s); +} + +.brick .foreground p.action-area { + display: flex; + flex-wrap: wrap; + gap: 24px; + margin-top: var(--spacing-s); +} + +.brick .icon-stack-area li picture { + display: flex; + margin: 0; + padding: 0; + flex-shrink: 0; +} + +.brick .foreground .icon-area picture { + display: flex; +} + +.brick .icon-stack-area li img { + width: var(--icon-size-s); + height: auto; +} + +.brick .foreground .icon-area img { + height: var(--icon-size-l); + width: auto; +} + +.brick.click > a { + text-decoration: none; +} + +.brick .icon-stack-area { + display: flex; + flex-flow: row wrap; + flex-direction: column; + gap: var(--spacing-xs); + width: 100%; + margin: 0; + padding: 0; + list-style-type: none; +} + +.brick.center .icon-stack-area, +.brick.align-center .icon-stack-area { + display: inline-flex; + width: auto; +} + +.brick .icon-stack-area li, +.brick .icon-stack-area li a { + display: flex; + align-items: center; + gap: var(--spacing-xs); + text-align: left; +} + +.brick .foreground a:not([class]), +.brick .foreground span.first-link { + font-weight: 700; +} + +[dir="rtl"] .brick .icon-stack-area li, +[dir="rtl"] .brick .icon-stack-area li a { + text-align: right; +} + +.brick.click a.foreground .first-link:not([class*="button"]) { + color: var(--link-color); + text-decoration: none; +} + +.brick.click:hover a.foreground .first-link:not([class*="button"]) { + text-decoration: underline; + color: var(--link-hover-color); +} + +.static-links .brick.click a.foreground .first-link:not([class*="button"]), +.static-links .brick.click a.foreground a:not([class*="button"]), +.brick.static-links.click a.foreground .first-link:not([class*="button"]), +.brick.static-links.click a.foreground a:not([class*="button"]) { + color: inherit; + text-decoration: underline; +} + +.brick.click:hover .first-link.con-button.blue, +.brick.click:active .first-link.con-button.blue { + background: var(--color-accent-hover); + border-color: var(--color-accent-hover); + color: var(--color-white); +} + +.brick.click:hover .first-link.con-button, +.brick.click:active .first-link.con-button, +.brick.light.click:hover .first-link.con-button, +.brick.light.click:active .first-link.con-button, +.light .brick.click:hover .first-link.con-button, +.light .brick.click:active .first-link.con-button { + background-color: var(--color-black); + border-color: var(--color-black); + color: var(--color-white); +} + +.dark .brick.click:hover .first-link.con-button, +.brick.dark.click:active .first-link.con-button { + background-color: var(--color-white); + color: var(--color-black); + text-decoration: none; +} + + +@media screen and (max-width: 600px) { + .brick .mobile-only { + display: block; + } +} + +@media screen and (min-width: 600px) { + .brick { + min-height: 384px; + } + + .brick .foreground { + padding: var(--spacing-l); + } +} + +@media screen and (min-width: 600px) and (max-width: 1199px) { + .brick .tablet-only { + display: block; + } +} + +@media screen and (min-width: 1200px) { + .brick .desktop-only { + display: block; + } + + .brick.large { + min-height: 500px; + } +} diff --git a/libs/blocks/brick/brick.js b/libs/blocks/brick/brick.js new file mode 100644 index 0000000000..90799735e7 --- /dev/null +++ b/libs/blocks/brick/brick.js @@ -0,0 +1,96 @@ +import { decorateTextOverrides, decorateBlockText, decorateBlockBg, decorateIconStack, decorateButtons } from '../../utils/decorate.js'; +import { createTag } from '../../utils/utils.js'; + +const blockTypeSizes = { + large: ['xxl', 'm', 'l'], + default: ['xl', 'm', 'l'], +}; +const objFitOptions = ['fill', 'contain', 'cover', 'none', 'scale-down']; + +function getBlockSize(el) { + const sizes = Object.keys(blockTypeSizes); + const size = sizes.find((s) => el.classList.contains(`${s}`)) || 'default'; + return blockTypeSizes[size]; +} + +function handleSupplementalText(foreground) { + if (!foreground.querySelector('.action-area')) return; + const nextP = foreground.querySelector('.action-area + p'); + const lastP = foreground.querySelector('.action-area ~ p:last-child'); + if (nextP) nextP.className = ''; + if (lastP) lastP.className = 'supplemental-text'; +} + +function setObjectFitAndPos(text, pic, bgEl) { + const backgroundConfig = text.split(',').map((c) => c.toLowerCase().trim()); + const fitOption = objFitOptions.filter((c) => backgroundConfig.includes(c)); + const focusOption = backgroundConfig.filter((c) => !fitOption.includes(c)); + if (fitOption) [pic.querySelector('img').style.objectFit] = fitOption; + bgEl.innerHTML = ''; + bgEl.append(pic); + bgEl.append(document.createTextNode(focusOption.join(','))); +} + +function handleObjectFit(bgRow) { + const bgConfig = bgRow.querySelectorAll('div'); + [...bgConfig].forEach((r) => { + const pic = r.querySelector('picture'); + if (!pic) return; + let text = ''; + const pchild = [...r.querySelectorAll('p:not(:empty)')].filter((p) => p.innerHTML.trim() !== ''); + if (pchild.length > 2) text = pchild[1]?.textContent.trim(); + if (!text && r.textContent) text = r.textContent; + if (!text) return; + setObjectFitAndPos(text, pic, r); + }); +} + +function handleClickableBrick(el, foreground) { + if (!el.classList.contains('click')) return; + const links = foreground.querySelectorAll('a'); + if (links.length !== 1) { el.classList.remove('click'); return; } + const a = links[0]; + const linkDiv = createTag('span', { class: [...a.classList, 'first-link'].join(' ') }, a.innerHTML); + a.replaceWith(linkDiv, a); + a.className = 'foreground'; + el.appendChild(a); + a.innerHTML = foreground.innerHTML; + foreground.remove(); +} + +function decorateSupplementalText(el) { + const supplementalEl = el.querySelector('.foreground p.supplemental-text'); + if (!supplementalEl) return; + supplementalEl.className = 'body-xs supplemental-text'; +} + +function decorateBricks(el) { + if (!el.classList.contains('light')) el.classList.add('dark'); + const elems = el.querySelectorAll(':scope > div'); + if (elems.length > 1) { + handleObjectFit(elems[elems.length - 2]); + decorateBlockBg(el, elems[elems.length - 2], { useHandleFocalpoint: true }); + } + if (elems.length > 2) { + el.querySelector('.background').style.background = elems[0].textContent; + elems[0].remove(); + } + const foreground = elems[elems.length - 1]; + foreground.classList.add('foreground'); + const hasIconArea = foreground.querySelector('p')?.querySelector('img'); + if (hasIconArea) foreground.querySelector('p').classList.add('icon-area'); + const blockFormatting = getBlockSize(el); + decorateButtons(foreground, 'button-l'); + decorateBlockText(foreground, blockFormatting); + decorateIconStack(el); + el.querySelector('.icon-stack-area')?.classList.add('body-xs'); + handleSupplementalText(foreground); + handleClickableBrick(el, foreground); + return foreground; +} + +export default async function init(el) { + decorateBricks(el); + decorateTextOverrides(el); + decorateSupplementalText(el); +} diff --git a/libs/blocks/section-metadata/section-metadata.css b/libs/blocks/section-metadata/section-metadata.css index e63a464342..20cda08030 100644 --- a/libs/blocks/section-metadata/section-metadata.css +++ b/libs/blocks/section-metadata/section-metadata.css @@ -189,10 +189,32 @@ main > .section[class*='-up'] > .content { margin: 0; } +.section.masonry-layout { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: var(--spacing-s); + padding-left: var(--grid-margins-width); + padding-right: var(--grid-margins-width); +} + +.section.masonry-layout > div[class*='grid-'], +.section.masonry-layout > div[class*='grid-'] > div.fragment, +.section.masonry-layout > div[class*='grid-'] > div.fragment > div.section { + display: grid; +} + @media screen and (min-width: 600px) and (max-width: 1200px) { .section.five-up { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } + + .section.masonry-layout { + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + } + + .section.masonry-layout .grid-full-width:first-child { + grid-column: 1 / -1; + } } @media screen and (min-width: 720px) { @@ -269,4 +291,22 @@ main > .section[class*='-up'] > .content { padding-left: var(--grid-margins-width-10); padding-right: var(--grid-margins-width-10); } + + .section.masonry-layout { + grid-template-columns: repeat(12, 1fr); + } + + .section.masonry-layout .grid-full-width {grid-column: span 12; } + .section.masonry-layout .grid-half-width {grid-column: span 6; } + .section.masonry-layout .grid-span-1 {grid-column: span 1; } + .section.masonry-layout .grid-span-2 {grid-column: span 2; } + .section.masonry-layout .grid-span-3 {grid-column: span 3; } + .section.masonry-layout .grid-span-4 {grid-column: span 4; } + .section.masonry-layout .grid-span-5 {grid-column: span 5; } + .section.masonry-layout .grid-span-6 {grid-column: span 6; } + .section.masonry-layout .grid-span-7 {grid-column: span 7; } + .section.masonry-layout .grid-span-8 {grid-column: span 8; } + .section.masonry-layout .grid-span-9 {grid-column: span 9; } + .section.masonry-layout .grid-span-10 {grid-column: span 10; } + .section.masonry-layout .grid-span-11 {grid-column: span 11; } } diff --git a/libs/blocks/section-metadata/section-metadata.js b/libs/blocks/section-metadata/section-metadata.js index 52db480f58..7dcd143755 100644 --- a/libs/blocks/section-metadata/section-metadata.js +++ b/libs/blocks/section-metadata/section-metadata.js @@ -27,6 +27,17 @@ export async function handleStyle(text, section) { section.classList.add(...styles); } +function handleMasonry(text, section) { + section.classList.add(...['masonry-layout', 'masonry-up']); + const divs = section.querySelectorAll(":scope > div:not([class*='metadata'])"); + const spans = []; + text.split('\n').forEach((line) => spans.push(...line.trim().split(','))); + [...divs].forEach((div, i) => { + const spanWidth = spans[i] ? spans[i] : 'span 4'; + div.classList.add(`grid-${spanWidth.trim().replace(' ', '-')}`); + }); +} + function handleLayout(text, section) { if (!(text || section)) return; const layoutClass = `grid-template-columns-${text.replaceAll(' | ', '-')}`; @@ -49,4 +60,5 @@ export default async function init(el) { if (metadata.style) await handleStyle(metadata.style.text, section); if (metadata.background) handleBackground(metadata, section); if (metadata.layout) handleLayout(metadata.layout.text, section); + if (metadata.masonry) handleMasonry(metadata.masonry.text, section); } diff --git a/libs/utils/utils.js b/libs/utils/utils.js index d1e9ae405d..c70f5dd769 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -13,6 +13,7 @@ const MILO_BLOCKS = [ 'article-header', 'aside', 'author-header', + 'brick', 'bulk-publish', 'caas', 'caas-config', diff --git a/test/blocks/brick/brick.test.js b/test/blocks/brick/brick.test.js new file mode 100644 index 0000000000..81282d6130 --- /dev/null +++ b/test/blocks/brick/brick.test.js @@ -0,0 +1,87 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import { getLocale, setConfig } from '../../../libs/utils/utils.js'; + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +const { default: init } = await import('../../../libs/blocks/brick/brick.js'); +const { default: getFragment } = await import('../../../libs/blocks/fragment/fragment.js'); + +const locales = { '': { ietf: 'en-US', tk: 'hah7vzn.css' } }; +const config = { + imsClientId: 'milo', + codeRoot: '/libs', + contentRoot: `${window.location.origin}${getLocale(locales).prefix}`, + locales, +}; +setConfig(config); + +describe('basic brick', () => { + const bricks = document.querySelectorAll('.brick'); + bricks.forEach((brick) => { + init(brick); + }); + const fullCopy = document.querySelector('#full-copy'); + + it('has a heading-xxl for large brick', () => { + const heading = document.body.querySelector('.large .heading-xxl'); + expect(heading).to.exist; + }); + + it('has a body-m', () => { + const bodyCopy = document.body.querySelector('.brick .foreground .body-m'); + expect(bodyCopy).to.exist; + }); + + it('has a detail-l', () => { + const bodyCopy = document.body.querySelector('.brick .foreground .detail-l'); + expect(bodyCopy).to.exist; + }); + + it('has text overrides', () => { + const heading = document.body.querySelector('#textOverride .foreground .heading-l'); + const bodyCopy = document.body.querySelector('#textOverride .foreground .body-l'); + expect(heading).to.exist; + expect(bodyCopy).to.exist; + }); + + it('has clickable brick', () => { + const a = document.body.querySelector('.click a'); + expect(a.classList.contains('foreground')).to.be.true; + }); + + it('supports gradient color', () => { + const bgColor = document.body.querySelector('.brick .background'); + expect(bgColor.style.background).to.not.be.null; + }); + + it('has background image', () => { + const bgImage = document.body.querySelector('.brick .background'); + expect(bgImage.querySelector('picture')).to.exist; + }); + + it('has icon stack', () => { + expect(fullCopy.querySelector('.icon-stack-area')).to.exist; + }); + + it('has supplemental text with small font', () => { + expect(fullCopy.querySelector('.supplemental-text.body-xs')).to.exist; + }); + + it('has supplemental text with small font in override', () => { + expect(bricks[0].querySelector('.supplemental-text.body-xs')).to.exist; + }); + + it('renders in fragments', async () => { + const fragmentLink = document.body.querySelector('.fragment-link'); + await getFragment(fragmentLink); + const fragment = document.querySelector('.fragment'); + expect(fragment).to.exist; + }); + + it('fragment labels grid in section', async () => { + const fragment = document.querySelector('.fragment'); + init(fragment); + const fullGrids = fragment.closest('.section.masonry-layout').querySelectorAll('.large'); + expect(fullGrids.length > 2).to.be.true; + }); +}); diff --git a/test/blocks/brick/mocks/body.html b/test/blocks/brick/mocks/body.html new file mode 100644 index 0000000000..7820dbf2ef --- /dev/null +++ b/test/blocks/brick/mocks/body.html @@ -0,0 +1,489 @@ +
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XXL 44/55 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more Learn + more

+

Supplemental text

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, center, contain

+
+
+

+ + + +

+

Bottom, center, contain

+
+
+

+ + + +

+

Bottom, center, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, center, contain

+
+
+

+ + + +

+

Bottom, center, contain

+
+
+

+ + + +

+

Bottom, center, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XXL 44/55 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+ +
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+

+ + + +

+

Bottom, right, contain

+
+
+
+
+

Heading XL 36/45 Lorem ipsum.

+

Body M regular (18/27) Lorem ipsum dolor sit amet.

+

Learn more

+
+
+
+ Fragment +
+
+
+
+
+
+

+ + + +

+

bottom, center, contain

+
+
+

+ + + +

+

bottom, center, contain

+
+
+

+ + + +

+

bottom, center, contain

+
+
+
+
+

+ + + +

+

THE CREATIVITY CONFERENCE

+

Adobe MAX. It’s live now.

+

Grab your front-row seat at the creative event of the year.

+
    +
  • + + + Adobe Photoshop +
  • +
  • + + + Adobe Photoshop +
  • +
  • + + + Adobe Photoshop +
  • +
+

Illustrator Photoshop

+

Text Link

+

Supplemental text

+
+
+
+
+
diff --git a/test/blocks/brick/mocks/fragments/body.plain.html b/test/blocks/brick/mocks/fragments/body.plain.html new file mode 100644 index 0000000000..f120fab783 --- /dev/null +++ b/test/blocks/brick/mocks/fragments/body.plain.html @@ -0,0 +1,62 @@ +
+
+
+
+

+ + + +

+

bottom, center, contain

+
+
+

+ + + +

+

bottom, center, contain

+
+
+

+ + + +

+

bottom, center, contain

+
+
+
+
+

+ + + +

+

THE CREATIVITY CONFERENCE

+

Adobe MAX. It’s live now.

+

Grab your front-row seat at the creative event of the year.

+
    +
  • + + + Adobe Photoshop +
  • +
  • + + + Adobe Photoshop +
  • +
  • + + + Adobe Photoshop +
  • +
+

Illustrator Photoshop

+

Text Link

+

Supplemental text

+
+
+
+
diff --git a/test/blocks/section-metadata/mocks/body.html b/test/blocks/section-metadata/mocks/body.html index 6df377b541..9903e8fe5e 100644 --- a/test/blocks/section-metadata/mocks/body.html +++ b/test/blocks/section-metadata/mocks/body.html @@ -82,6 +82,22 @@ +
+
Block 1
+
Block 2
+
Block 3
+
Block 4
+
+
+
style
+
Darkest, XXL Spacing
+
+
+
masonry
+
full width, span 4, span 4, span 4
+
+
+
diff --git a/test/blocks/section-metadata/section-meta.test.js b/test/blocks/section-metadata/section-meta.test.js index 605a299d13..e87da3ec35 100644 --- a/test/blocks/section-metadata/section-meta.test.js +++ b/test/blocks/section-metadata/section-meta.test.js @@ -49,6 +49,13 @@ describe('Section Metdata', () => { expect(sec.classList.contains('grid-template-columns-1-2')).to.be.true; }); + it('Adds class based on masonry input', async () => { + const sec = document.querySelector('.section.masonrysec'); + const sm = sec.querySelector('.section-metadata'); + await init(sm); + expect(sec.classList.contains('masonry-layout')).to.be.true; + }); + it('gets section metadata', () => { const metadata = getMetadata(document.querySelector('.section.color .section-metadata')); expect(metadata.background.text).to.equal('rgb(239, 239, 239)');