diff --git a/packages/web-components/src/components/promo-banner/__stories__/README.stories.mdx b/packages/web-components/src/components/promo-banner/__stories__/README.stories.mdx new file mode 100644 index 00000000000..89d640e030a --- /dev/null +++ b/packages/web-components/src/components/promo-banner/__stories__/README.stories.mdx @@ -0,0 +1,17 @@ +import { Meta, Props, Story, Canvas, Description } from '@storybook/addon-docs'; +import { cdnJs } from '../../../globals/internal/storybook-cdn'; +import '../index.ts'; + + + +# Promo Banner + + + + + + + +### `` attributes, properties, and events + + diff --git a/packages/web-components/src/components/promo-banner/__stories__/promo-banner.stories.ts b/packages/web-components/src/components/promo-banner/__stories__/promo-banner.stories.ts new file mode 100644 index 00000000000..b13a7d9921d --- /dev/null +++ b/packages/web-components/src/components/promo-banner/__stories__/promo-banner.stories.ts @@ -0,0 +1,121 @@ +/** + * @license + * + * Copyright IBM Corp. 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { boolean, select, text } from '@storybook/addon-knobs'; +import { html } from 'lit'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import '../index'; +import '../../cta/index'; +import '../../image/index'; +import '../../table-of-contents/index'; +import { types } from '../../../component-mixins/cta/cta'; + +import readme from './README.stories.mdx'; + +const incompatibleTypes = ['video', 'email', 'schedule', 'chat', 'call']; +const ctaTypes = Object.values(types).filter( + (val) => !!val && !incompatibleTypes.includes(val) +); + +export default { + title: 'Components/Promo Banner', + parameters: { + ...readme.parameters, + knobs: { + escapeHTML: false, + PromoBanner: () => { + const heading = text( + 'Heading (HTML Enabled)', + '
Try a demo of WatsonX
' + ); + const body = text( + 'Body Text (HTML Enabled)', + '

Easily deploy and embed AI across your business.

' + ); + const cta = text('CTA Label', 'Try Today'); + const ctaType = select('CTA Type', ctaTypes, ctaTypes[0]); + const tocLayout = boolean('Render in TOC', false); + const hasImage = boolean('Has Image', true); + return { + heading, + body, + cta, + ctaType, + tocLayout, + hasImage, + }; + }, + }, + }, + + decorators: [ + (story, { args }) => { + return html` +
+ ${args?.PromoBanner?.tocLayout + ? html` + +
+
+
+ ${story()} +
+
+
+
+ ` + : html` +
+
+
${story()}
+
+
+ `} +
+ `; + }, + ], +}; + +export const Default = (args) => { + const { heading, body, hasImage, cta, ctaType } = args?.PromoBanner ?? {}; + return html` + + ${hasImage !== false // Need explicit check to test image container queries. + ? html` + + + + + ` + : ''} + ${unsafeHTML(heading)} ${unsafeHTML(body)} + ${cta + ? html` + ${cta} + ` + : ''} + + `; +}; diff --git a/packages/web-components/src/components/promo-banner/index.ts b/packages/web-components/src/components/promo-banner/index.ts new file mode 100644 index 00000000000..6eb8aac4010 --- /dev/null +++ b/packages/web-components/src/components/promo-banner/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * + * Copyright IBM Corp. 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import './promo-banner'; diff --git a/packages/web-components/src/components/promo-banner/promo-banner.scss b/packages/web-components/src/components/promo-banner/promo-banner.scss new file mode 100644 index 00000000000..46cf6e039fc --- /dev/null +++ b/packages/web-components/src/components/promo-banner/promo-banner.scss @@ -0,0 +1,163 @@ +/** + * @license + * + * Copyright IBM Corp. 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +@use '@carbon/ibmdotcom-styles/scss/globals/vars' as *; +@use '@carbon/styles/scss/utilities'; +@use '@carbon/styles/scss/spacing' as *; +@use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/type' as *; +@use '@carbon/grid' as *; +@use '@carbon/ibmdotcom-styles/scss/globals/utils/flex-grid' as *; + +$css--plex: true !default; +$half-gutter: $grid-gutter / 2; + +$md-breakpoint-width: map-get(map-get($grid-breakpoints, md), width); +$lg-breakpoint-width: map-get(map-get($grid-breakpoints, lg), width); +$xlg-breakpoint-width: map-get(map-get($grid-breakpoints, xlg), width); +$max-breakpoint-width: map-get(map-get($grid-breakpoints, max), width); + +// TODO: Move these calculations to a single importable file. +// +// We need lower and upper bounds for a container occupying 12 / 16 columns at +// the lg and xlg breakpoints. For the lower bound we take the breakpoint +// size, less the 2rem of outer padding, multiplied by 12 / 16, less 2rem of +// column padding. For the upper bound we take the next breakpoint size and +// do a similar calculation with a slight tweak of subtracting 0.02 from the +// breakpoint size as a starting point, similar to how the +// breakpoint-between mixin works. Note that this approach my not work +// cleanly for the narrow or condensed grid due to the necessary assumptions +// made for padding widths. +$lg-12-column-lower-bound: ($lg-breakpoint-width * (12 / 16)) - 2rem; +$lg-12-column-upper-bound: ($xlg-breakpoint-width - 0.02) * (12 / 16); +$xlg-12-column-lower-bound: $xlg-breakpoint-width * (12 / 16); +$xlg-12-column-upper-bound: ($max-breakpoint-width - 0.02) * (12 / 16); + +:host(#{$c4d-prefix}-promo-banner) { + display: block; + /* stylelint-disable-next-line property-no-unknown */ + container: promo-banner / inline-size; + + [hidden] { + /* stylelint-disable-next-line declaration-no-important */ + display: none !important; + } + + .banner-wrapper { + position: relative; + display: flex; + flex-wrap: nowrap; + align-items: start; + background-color: $layer; + color: $text-primary; + + @include breakpoint-down(lg) { + &:not(.no-cta) { + /* stylelint-disable-next-line c4d/require-color-with-bg */ + &:hover { + background-color: $layer-hover; + } + + &:focus-within { + outline: $spacing-01 solid $focus; + outline-offset: calc(-1 * #{$spacing-01}); + } + } + } + } + + .banner-image { + position: relative; + align-self: stretch; + + @include make-col(4, 16); + @include breakpoint(lg) { + /* stylelint-disable-next-line scss/at-rule-no-unknown */ + @container promo-banner (inline-size <= #{$lg-12-column-upper-bound}) { + display: none; + } + } + + @include breakpoint(xlg) { + /* stylelint-disable-next-line scss/at-rule-no-unknown */ + @container promo-banner (inline-size <= #{$xlg-12-column-upper-bound}) { + display: none; + } + } + } + + ::slotted([slot='image']) { + /* stylelint-disable-next-line property-no-unknown */ + position: absolute; + aspect-ratio: auto; + block-size: 100%; + inline-size: 100%; + padding-block: 0; + } + + .banner-content { + flex-grow: 1; + padding-block: $spacing-05; + padding-inline: $half-gutter; + + @include breakpoint(md) { + padding-block: $spacing-07; + } + } + + .banner-cta { + padding-block: $spacing-05; + padding-inline: $half-gutter; + text-align: end; + + @include make-col(1, 4); + + @include breakpoint-down(lg) { + &:focus { + outline: none; + } + + &::after { + position: absolute; + display: block; + content: ''; + inset: 0; + } + } + + @include breakpoint(md) { + @include make-col(1, 8); + + padding-block: $spacing-07; + } + + @include breakpoint(lg) { + text-align: start; + + @include make-col(4, 16); + @include make-col-offset(2, 16); + + /* stylelint-disable-next-line scss/at-rule-no-unknown */ + @container promo-banner (inline-size <= #{$lg-12-column-upper-bound}) { + @include make-col(4, 12); + } + } + + @include breakpoint(xlg) { + /* stylelint-disable-next-line scss/at-rule-no-unknown */ + @container promo-banner (inline-size <= #{$xlg-12-column-upper-bound}) { + @include make-col(4, 12); + } + } + } + + ::slotted([slot='cta']) { + inline-size: 100%; + } +} diff --git a/packages/web-components/src/components/promo-banner/promo-banner.ts b/packages/web-components/src/components/promo-banner/promo-banner.ts new file mode 100644 index 00000000000..09297c9da02 --- /dev/null +++ b/packages/web-components/src/components/promo-banner/promo-banner.ts @@ -0,0 +1,167 @@ +/** + * @license + * + * Copyright IBM Corp. 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LitElement, html } from 'lit'; +import { state, queryAssignedNodes } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { carbonElement as customElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js'; +import { baseFontSize, breakpoints } from '@carbon/layout'; +import styles from './promo-banner.scss'; +import settings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js'; +import CTAMixin, { + icons as ctaIcons, + CTAMixinImpl, +} from '../../component-mixins/cta/cta'; +import ifNonEmpty from '@carbon/web-components/es/globals/directives/if-non-empty'; +import { CTA_TYPE } from '../cta/defs'; + +const { stablePrefix: c4dPrefix, prefix } = settings; + +const breakpoint = parseFloat(breakpoints.lg.width) * baseFontSize; +const layoutBreakpoint = window.matchMedia(`(min-width: ${breakpoint}px)`); + +/** + * The Promo Banner component. + * @element c4d-promo-banner + */ +@customElement(`${c4dPrefix}-promo-banner`) +class C4DPromoBanner extends CTAMixin(LitElement) { + @state() + isDesktopVersion = layoutBreakpoint.matches; + + @queryAssignedNodes('image', false) + slottedImage; + + @queryAssignedNodes('cta', false) + slottedCta; + + handleSlotChange(e) { + this.requestUpdate(); + + const slotname = e?.target?.name ?? ''; + + if (slotname === 'cta') { + this.harvestCtaProps(); + } + } + + firstUpdated() { + layoutBreakpoint.addEventListener('change', (e) => { + this.isDesktopVersion = e.matches; + }); + this.harvestCtaProps(); + } + + harvestCtaProps() { + const slotted_cta = this.querySelector('[slot="cta"]'); + + if (slotted_cta) { + const { _linkNode, ctaType, disabled, download, href, target } = + slotted_cta as CTAMixinImpl; + + this._linkNode = _linkNode; + this.ctaType = ctaType; + this.disabled = disabled; + this.download = download; + this.href = href; + this.target = target; + this.requestUpdate(); + return; + } + + this._linkNode = undefined; + this.ctaType = '' as CTA_TYPE; + this.disabled = false; + this.download = ''; + this.href = ''; + this.target = ''; + this.requestUpdate(); + } + + /** + * @inheritdoc + */ + _renderIcon() { + const { ctaType } = this; + return html` + + ${ctaIcons[ctaType]?.({ + class: `${prefix}--card__cta ${c4dPrefix}-ce--cta__icon`, + })} + + `; + } + + _renderMobileLayout() { + const { href, disabled, target, download, innerText } = this; + + const classes = { + 'banner-wrapper': true, + 'no-cta': !href, + }; + + return html` +
+ + ${href + ? html` + + ` + : ''} +
+ `; + } + + _renderDesktopLayout() { + const { slottedImage, slottedCta, handleSlotChange } = this; + return html` + + `; + } + + render() { + const { isDesktopVersion } = this; + + return isDesktopVersion + ? this._renderDesktopLayout() + : this._renderMobileLayout(); + } + + static get styles() { + return styles; + } +} + +export default C4DPromoBanner;