diff --git a/CODEOWNERS b/CODEOWNERS index d72f876b01..e0222be92e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -42,6 +42,7 @@ /libs/blocks/merch-card-collection/ @adobecom/tacocat /libs/blocks/merch-offers/ @adobecom/tacocat /libs/blocks/mnemonic-list/ @adobecom/tacocat +/libs/blocks/notification/ @elan-tbx @Sartxi @ryanmparrish /libs/blocks/ost/ @adobecom/tacocat /libs/blocks/pdf-vewer/ @meganthecoder @JasonHowellSlavin @Brandon32 /libs/blocks/quiz/ @colloyd @sabyamon @fullcolorcoder @JackySun9 diff --git a/libs/blocks/notification/notification.css b/libs/blocks/notification/notification.css new file mode 100644 index 0000000000..a67d29f246 --- /dev/null +++ b/libs/blocks/notification/notification.css @@ -0,0 +1,561 @@ +.notification { + --min-block-size: 160px; + --min-block-size-ribbon: 50px; + --min-block-size-pill: 72px; + --margin-inline-ribbon: 30px; + --max-inline-size-image: 75px; + --max-inline-size-icon: 231px; + --inline-size-image: auto; + --inline-size-pill: 85%; + --border-block-size: 10px; + --close-size: 20px; + --icon-size: 56px; + --icon-size-s: 40px; + --icon-size-xs: 24px; + --icon-size-l: 64px; + --pill-radius: 16px; + + display: flex; + inline-size: 100%; + position: relative; + align-items: center; + justify-content: center; + flex-wrap: wrap; + overflow: hidden; + min-block-size: var(--min-block-size); +} + +.notification.ribbon { + min-block-size: var(--min-block-size-ribbon); +} + +.dark .notification, +.notification.dark { + color: var(--color-white); +} + +.notification p { + margin: 0; + inline-size: 100%; +} + +.notification [class*="heading-"] { + margin-block-end: var(--spacing-xxs); +} + +.notification [class*="heading-"] strong { + font-weight: unset; +} + +.notification [class*="heading-"] + p { + margin-block-end: var(--spacing-s); +} + +.notification.ribbon.space-between [class*="heading-"] + p { + margin-block-end: 0; +} + +.notification .foreground.container { + display: flex; + position: relative; + align-items: flex-start; + flex-direction: column; + gap: var(--spacing-xs); + margin-block: var(--spacing-s); + box-sizing: border-box; + justify-content: flex-start; +} + +.notification.ribbon .foreground.container { + inline-size: 100%; + margin-inline: var(--margin-inline-ribbon); + margin-block: 0; + padding-block: var(--spacing-s); +} + +.notification .foreground.container [data-align=center], +.notification.center .foreground.container, +.notification.center .foreground.container > * { + text-align: center; + justify-content: center; +} + +.notification.pill .foreground.container { + padding-inline: var(--spacing-xs) var(--spacing-xxs); + padding-block: var(--spacing-xs) var(--spacing-xxs); + margin: 0; + inline-size: 100%; +} + +.notification.ribbon.xxs-padding .foreground.container { + padding-block: var(--spacing-xxs); +} + +.notification.ribbon.xs-padding .foreground.container { + padding-block: var(--spacing-xs); +} + +.notification:is(.ribbon, .pill) .close { + position: absolute; + inset-inline: auto var(--spacing-xxs); + inset-block: var(--spacing-xxs) auto; + block-size: var(--close-size); + inline-size: var(--close-size); + cursor: pointer; + margin: auto; + appearance: none; + border: none; + background: transparent; + padding: 0; +} + +.notification .close .path { + fill: var(--text-color); +} + +.dark .notification .close .path, +.notification.dark .close .path { + fill: var(--color-white); +} + +.notification .border { + display: block; + block-size: var(--border-block-size); + inline-size: 100%; +} + +.notification .action-area { + gap: var(--spacing-s); + display: flex; + align-items: center; +} + +.notification .background img { + min-block-size: unset; +} + +.notification .foreground.container .text { + display: flex; + flex-wrap: wrap; + max-inline-size: none; + padding-block-start: 0; + padding-block-end: 0; +} + +.notification.pill .foreground.container .text { + flex-direction: column; + align-items: flex-start; + text-align: start; + inline-size: 100%; +} + +.notification.ribbon.space-between .foreground.container .text { + flex-wrap: nowrap; + inline-size: 100%; +} + +.notification.ribbon.space-between .foreground.container .copy-wrap { + margin-inline-end: var(--spacing-s); +} + +.notification .foreground.container .image { + position: relative; + display: flex; + inline-size: var(--inline-size-image); + max-inline-size: var(--max-inline-size-image); + margin: 0; + order: -1; +} + +.notification .foreground.container > div { + flex-grow: 1; + flex-basis: 100%; + min-inline-size: 0; +} + +.notification .foreground.container .text a { + white-space: nowrap; +} + +.notification .icon-area img { + display: flex; + align-items: center; + inline-size: auto; +} + +.notification.ribbon .icon-area img { + max-block-size: var(--icon-size); +} + +.notification:is(.ribbon.s-icon, .pill) .icon-area img { + max-block-size: var(--icon-size-s); +} + +.notification.xs-icon:is(.ribbon, .pill) .icon-area img { + max-block-size: var(--icon-size-xs); +} + +.notification.ribbon.l-icon .icon-area img { + max-block-size: var(--icon-size-l); +} + +.notification .text [class*="heading-"] + .action-area { + margin-block-start: var(--spacing-xs); +} + +.notification.center .foreground.container .action-area { + justify-content: center; +} + +.notification .foreground.container .icon-area { + block-size: auto; + max-inline-size: none; + margin-block-end: var(--spacing-xs); + flex-shrink: 0; + display: flex; + gap: var(--spacing-xs); +} + +.notification.center .foreground.container .icon-area { + justify-content: center; +} + +.notification.pill .foreground.container .icon-area { + margin-inline-end: 0; + margin-block-end: var(--spacing-xs); + inline-size: auto; +} + +.notification.ribbon.space-between .foreground.container .icon-area { + align-items: center; + inline-size: auto; + margin-inline-end: var(--spacing-xs); + margin-block-end: 0; +} + +.notification .foreground.container .image :is(picture, video), +.notification .foreground.container .image picture img { + inline-size: 100%; + display: flex; +} + +.notification .foreground.container .text a:not(.con-button) { + inline-size: auto; + font-weight: normal; +} + +.notification .foreground.container .text .action-area > a { + margin-inline-end: 0; +} + +.notification .foreground.container .text .heading-l { + margin-block-end: var(--spacing-xxs); +} + +.notification .foreground.container:not(.no-image) .text .body-s.action-area, +.notification .foreground.container:not(.no-image) .text .body-m.action-area { + margin-block-end: 0; +} + +.notification.center .icon-area picture { + display: flex; + justify-content: center; +} + +.notification.pill { + border-radius: var(--pill-radius); + inline-size: calc(100% - var(--spacing-m)); + margin-inline: auto; +} + +.notification.pill .foreground.container .action-area { + justify-content: flex-end; + flex-wrap: wrap; +} + +.notification.ribbon.space-between .foreground.container .action-area { + flex-wrap: wrap; + align-self: center; + justify-content: flex-end; +} + +.notification .flexible-inner { + inline-size: 100%; +} + +.notification.pill .foreground.container .text > :not(.action-area) { + padding-inline-end: var(--spacing-xxs); + inline-size: calc(100% - var(--spacing-xxs)); +} + +@media screen and (min-width: 600px) { + .notification { + --max-inline-size-image: 188px; + --max-inline-size-banner: 800px; + --min-inline-size-flexible: 300px; + --inline-size-image: 35%; + --full-width: 1200px; + --padding-inline-flexible: 80px; + } + + .notification:not(.pill, .ribbon) .foreground.container { + max-inline-size: var(--max-inline-size-banner); + } + + .notification .foreground.container { + align-items: center; + flex-direction: row; + margin-block: 0; + margin-inline: auto; + padding-block: var(--spacing-s); + padding-inline: 0; + gap: var(--spacing-s); + } + + .notification:is(.full-width, .ribbon) .foreground.container { + max-inline-size: var(--full-width); + margin-inline: var(--grid-margins-width); + } + + .notification .foreground.container .image { + margin: 0; + padding: 0; + order: unset; + } + + .notification .foreground.container .text.image { + justify-content: flex-start; + } + + .notification .background { + overflow: hidden; + position: absolute; + inset: 0; + } + + .notification .foreground.container .text { + margin-block-end: 0; + padding-inline-end: 0; + } + + .notification .foreground.container .text + .image { + margin-inline-end: 0; + } + + .notification .foreground.container .icon-area { + inline-size: auto; + margin-inline-end: var(--spacing-xs); + margin-block-end: 0; + } + + .notification.ribbon .close { + inset-inline: auto var(--spacing-s); + inset-block: 0; + } + + .notification.ribbon .foreground.container .text { + flex-flow: row nowrap; + align-items: center; + } + + .notification.ribbon .action-area { + inline-size: auto; + } + + .notification.ribbon .foreground.container .icon-area { + flex-shrink: 1; + } + + .notification.ribbon .copy-wrap { + margin-inline-end: var(--spacing-s); + } + + .notification.space-between .copy-wrap { + flex-basis: 100%; + } + + .notification.ribbon .copy-wrap :last-child { + margin-block-end: 0; + } + + .notification.center .copy-wrap { + text-align: start; + } + + .notification.pill .foreground.container { + padding: var(--spacing-s); + } + + .notification.pill:not(.flexible) .foreground.container { + inline-size: var(--grid-container-width); + } + + .notification.pill .foreground.container .text { + align-items: center; + text-align: center; + } + + .notification.pill .foreground.container .action-area { + justify-content: center; + } + + .notification.ribbon.space-between .foreground.container .action-area { + flex-wrap: unset; + } + + .notification.pill.flexible { + pointer-events: none; + } + + .notification .flexible-inner { + position: relative; + margin: auto; + pointer-events: auto; + inline-size: auto; + padding-inline: var(--padding-inline-flexible); + overflow: hidden; + min-inline-size: var(--min-inline-size-flexible); + border-radius: var(--pill-radius); + } + + .notification.pill .foreground.container .text > :not(.action-area) { + padding-inline-end: unset; + inline-size: unset; + } +} + +@media screen and (min-width: 1200px) { + .notification { + --inline-size-image: 20%; + --inline-size-image-10: 30%; + --max-inline-size-image-10: 300px; + --inline-size-image-full: 33.333%; + --max-inline-size-image-full: 400px; + --pill-radius: 36px; + } + + .notification:not(.pill, .ribbon) .foreground.container { + inline-size: calc(var(--grid-container-width) * (8 / 12)); + margin-inline: var(--grid-margins-width-8); + gap: var(--spacing-m); + } + + .notification:is(.full-width, .ribbon) .foreground.container { + gap: var(--spacing-xl); + } + + .notification:is(.full-width, .max-width-10-desktop) .foreground.container { + inline-size: unset; + } + + .notification.max-width-10-desktop .foreground.container { + margin-inline: var(--grid-margins-width-10); + gap: var(--spacing-l); + } + + .notification.full-width .foreground.container { + margin-inline: var(--grid-margins-width); + } + + .notification .foreground.container > div { + object-fit: cover; + padding-inline-start: 0; + } + + .notification .foreground.container .icon-area { + max-inline-size: var(--max-inline-size-icon); + margin-inline-end: var(--spacing-s); + } + + .notification.ribbon .foreground.container .icon-area { + flex-shrink: 0; + } + + .notification .foreground.container .image { + inline-size: var(--inline-size-image); + } + + .notification.max-width-10-desktop .foreground.container .image { + inline-size: var(--inline-size-image); + max-inline-size: var(--max-inline-size-image-10); + } + + .notification.full-width .foreground.container .image { + inline-size: var(--inline-size-image-full); + max-inline-size: var(--max-inline-size-image-full); + } + + .notification .foreground.container .text + .image { + margin-inline-end: 0; + } + + .notification.pill { + min-block-size: var(--min-block-size-pill); + inline-size: var(--inline-size-pill); + margin-inline: auto; + } + + .notification.pill [class*="heading-"], + .notification.pill p { + flex-shrink: 0; + margin-block-end: 0; + } + + .notification.pill .foreground.container .text [class*="heading-"] { + margin-inline-end: var(--spacing-xxs); + margin-block-end: 0; + } + + .notification.pill p { + inline-size: auto; + } + + .notification.pill .close { + inset-inline: auto var(--spacing-s); + inset-block: 0; + } + + .notification.pill .foreground.container { + padding-block: var(--spacing-xs); + padding-inline: var(--spacing-m); + margin: 0; + } + + .notification.pill.flexible .foreground.container { + padding-inline: 0; + } + + .notification.pill .foreground.container .icon-area { + margin-inline-end: var(--spacing-xs); + margin-block-end: 0; + } + + .notification.pill .icon-area img { + max-block-size: var(--icon-size-s); + } + + .notification.pill .foreground.container .action-area { + margin-inline-start: var(--spacing-s); + } + + .notification.pill .foreground.container .text { + flex-flow: row nowrap; + align-items: center; + justify-content: center; + } + + .notification.pill.flexible .flexible-inner { + border-radius: var(--pill-radius); + } + + .notification.pill .copy-wrap { + display: flex; + flex-wrap: wrap; + align-items: baseline; + text-align: start; + } + + .notification.ribbon.space-between .foreground.container .icon-area { + margin-inline-end: var(--spacing-s); + } +} diff --git a/libs/blocks/notification/notification.js b/libs/blocks/notification/notification.js new file mode 100644 index 0000000000..4a65bcb266 --- /dev/null +++ b/libs/blocks/notification/notification.js @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* +* Notification - v1.0 +*/ + +import { decorateBlockText, decorateBlockBg, decorateTextOverrides } from '../../utils/decorate.js'; +import { createTag } from '../../utils/utils.js'; + +const variants = ['banner', 'ribbon', 'pill']; +const sizes = ['small', 'medium', 'large']; +const [banner, ribbon, pill] = variants; +const [small, medium, large] = sizes; +const defaultSize = medium; +const defaultVariant = banner; +const blockConfig = { + [banner]: { + [small]: ['s', 's', 's', 'm'], + [medium]: ['m', 'm', 'm', 'm'], + [large]: ['l', 'l', 'l', 'l'], + }, + [ribbon]: { + [small]: ['s', 's', 's', 'm'], + [medium]: ['m', 'm', 'm', 'l'], + [large]: ['l', 'l', 'l', 'l'], + }, + [pill]: { + [small]: ['s', 's', 's', 'm'], + [medium]: ['m', 'm', 'm', 'l'], + [large]: ['l', 'm', 'm', 'l'], + }, +}; + +const closeSvg = ``; + +function getOpts(el) { + const optRows = [...el.querySelectorAll(':scope > div:nth-of-type(n+3)')]; + if (!optRows.length) return {}; + optRows.forEach((row) => row.remove()); + const camel = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase()); + const fmt = (child) => child.textContent.toLowerCase().replace('\n', '').trim(); + return optRows.reduce((a, c) => ({ ...a, [camel(fmt(c.children[0]))]: fmt(c.children[1]) }), {}); +} + +function getBlockData(el) { + const variant = variants.find((varClass) => el.classList.contains(varClass)) || defaultVariant; + const size = sizes.find((sizeClass) => el.classList.contains(sizeClass)) || defaultSize; + const fontSizes = [...blockConfig[variant][size]]; + if (el.classList.contains('s-button')) fontSizes.splice(3, 1, 'm'); + return { fontSizes, options: { ...getOpts(el) } }; +} + +function wrapCopy(foreground) { + const text = foreground.querySelector('.text'); + if (!text) return; + const heading = text?.querySelector('h1, h2, h3, h4, h5, h6'); + const icon = heading?.previousElementSibling; + const body = heading?.nextElementSibling?.classList.contains('action-area') ? '' : heading?.nextElementSibling; + const copy = createTag('div', { class: 'copy-wrap' }, [heading, body]); + text?.insertBefore(copy, icon?.nextSibling || text.children[0]); +} + +function decorateClose(el) { + const btn = createTag('button', { 'aria-label': 'close', class: 'close' }, closeSvg); + btn.addEventListener('click', () => (el.style.display = 'none')); + el.appendChild(btn); +} + +function decorateFlexible(el) { + const innards = [ + el.querySelector('.background'), + el.querySelector('.foreground'), + el.querySelector('.close'), + ]; + const inner = createTag('div', { class: 'flexible-inner' }, innards); + el.appendChild(inner); +} + +function decorateLayout(el) { + const [background, ...rest] = el.querySelectorAll(':scope > div'); + const foreground = rest.pop(); + if (background) decorateBlockBg(el, background); + foreground?.classList.add('foreground', 'container'); + const text = foreground?.querySelector('h1, h2, h3, h4, h5, h6, p')?.closest('div'); + text?.classList.add('text'); + const iconArea = text?.querySelector('p picture')?.closest('p'); + iconArea?.classList.add('icon-area'); + const fgMedia = foreground?.querySelector(':scope > div:not(.text) :is(img, video, a[href*=".mp4"])')?.closest('div'); + const bgMedia = el.querySelector(':scope > div:not(.foreground) :is(img, video, a[href*=".mp4"])')?.closest('div'); + const media = fgMedia ?? bgMedia; + media?.classList.toggle('image', media && !media.classList.contains('text')); + foreground?.classList.toggle('no-image', !media && !iconArea); + if (el.matches(`:is(.${pill}, .${ribbon}):not(.no-closure)`)) decorateClose(el); + if (el.matches(`.${pill}.flexible`)) decorateFlexible(el); + return foreground; +} + +export default function init(el) { + el.classList.add('con-block'); + const { fontSizes, options } = getBlockData(el); + const blockText = decorateLayout(el); + decorateBlockText(blockText, fontSizes); + if (options.borderBottom) { + el.append(createTag('div', { style: `background: ${options.borderBottom};`, class: 'border' })); + } + decorateTextOverrides(el); + el.querySelectorAll('a:not([class])').forEach((staticLink) => staticLink.classList.add('static')); + if (el.matches(`:is(.${ribbon}, .${pill})`)) wrapCopy(blockText); +} diff --git a/libs/utils/decorate.js b/libs/utils/decorate.js index 6754dd4faa..19f3845ec0 100644 --- a/libs/utils/decorate.js +++ b/libs/utils/decorate.js @@ -69,7 +69,8 @@ export function decorateBlockText(el, config = ['m', 's', 'm'], type = null) { .forEach((text) => text.classList.add(`body-${config[1]}`)); } } - decorateButtons(el); + const buttonSize = config.length > 3 ? `button-${config[3]}` : ''; + decorateButtons(el, buttonSize); if (type === 'merch') decorateIconStack(el); } diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 2302d3472c..50c8cc0206 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -56,6 +56,7 @@ const MILO_BLOCKS = [ 'mobile-app-banner', 'modal', 'modal-metadata', + 'notification', 'pdf-viewer', 'quote', 'read-more', diff --git a/test/blocks/notification/mocks/body.html b/test/blocks/notification/mocks/body.html new file mode 100644 index 0000000000..f1fa56ca5e --- /dev/null +++ b/test/blocks/notification/mocks/body.html @@ -0,0 +1,370 @@ +
notification
+Body M 18/27 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eius. See terms.
+ +notification (dark)
+Body M 18/27 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eius. See terms.
+ +notification – with `border-bottom` option in additional row
+Body M 18/27 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eius. See terms.
+ +notification (ribbon)
+ + +notification (pill)
+notification (pill, dark)
+notification (pill, no closure)
+notification (pill)
+notification (pill, dark, flexible)
+notification (pill, dark, flexible, s-button)
+ + +