diff --git a/src/elements/container/container/container.scss b/src/elements/container/container/container.scss index 27b441d9eb..0950dc6905 100644 --- a/src/elements/container/container/container.scss +++ b/src/elements/container/container/container.scss @@ -4,6 +4,8 @@ @include sbb.box-sizing; :host { + --sbb-container-background-border-radius: 0; + display: block; } @@ -28,6 +30,12 @@ position: relative; } +:host(:not([expanded], [background-expanded])) { + @include sbb.mq($from: ultra) { + --sbb-container-background-border-radius: var(--sbb-border-radius-4x); + } +} + .sbb-container { background-color: var(--sbb-container-background-color); padding: var(--sbb-container-padding); @@ -62,22 +70,8 @@ } ::slotted([slot='image']) { - --sbb-image-border-radius: 0; - position: absolute; inset: 0; - - :host(:not([expanded], [background-expanded])) & { - @include sbb.mq($from: ultra) { - --sbb-image-border-radius: var(--sbb-border-radius-4x); - - border-radius: var(--sbb-border-radius-4x); - } - } -} - -::slotted(img[slot='image']) { - object-fit: cover; height: 100%; width: 100%; } diff --git a/src/elements/container/container/container.visual.spec.ts b/src/elements/container/container/container.visual.spec.ts index d95e6344d7..da75e666c8 100644 --- a/src/elements/container/container/container.visual.spec.ts +++ b/src/elements/container/container/container.visual.spec.ts @@ -11,6 +11,7 @@ import { waitForImageReady } from '../../core/testing.js'; import '../../button.js'; import '../../card.js'; +import '../../chip-label.js'; import '../../image.js'; import '../../title.js'; import './container.js'; @@ -25,10 +26,20 @@ describe(`sbb-container`, () => { const images = [ { + name: 'sbb-image', selector: 'sbb-image', image: html``, }, { + name: 'figure-sbb-image', + selector: 'sbb-image', + image: html`
+ + AI generated +
`, + }, + { + name: 'img', selector: 'img', image: html` { alt='' >`, }, + { + name: 'figure-img', + selector: 'img', + image: html`
+ + AI generated +
`, + }, ]; const containerContent = (): TemplateResult => html` @@ -88,7 +107,7 @@ describe(`sbb-container`, () => { describe(`expanded=${expanded}`, () => { for (const image of images) { it( - `slotted=${image.selector}`, + `slotted=${image.name}`, visualDiffDefault.with(async (setup) => { await setup.withFixture( html` @@ -146,23 +165,24 @@ describe(`sbb-container`, () => { }), ); - it( - `background-image`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html` - - ${backgroundImageContent} - - - `, - wrapperStyles, - ); + for (const image of images) { + it( + `background-image slotted=${image.name}`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + + ${backgroundImageContent} ${image.image} + + `, + wrapperStyles, + ); - await setViewport(viewport); - await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); - }), - ); + await setViewport(viewport); + await waitForImageReady(setup.snapshotElement.querySelector(image.selector)!); + }), + ); + } }); } }); diff --git a/src/elements/container/container/readme.md b/src/elements/container/container/readme.md index e5a7cc9f83..3bb1543a81 100644 --- a/src/elements/container/container/readme.md +++ b/src/elements/container/container/readme.md @@ -19,6 +19,21 @@ use CSS object-position for slotted `img`, or `--sbb-image-object-position` vari If an image is present, the container receives a pre-defined padding. It's possible to override the padding by using the CSS variable `--sbb-container-padding`. +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + +```html + +
+ + ... +
+ ... +
+``` + ## Style By default `sbb-container` uses the `page spacing` defined in the [layout documentation](/docs/styles-layout--docs). Optionally the user can use the `expanded` property (default: `false`) to switch to the `page spacing expanded` layout. diff --git a/src/elements/core/styles/_index.scss b/src/elements/core/styles/_index.scss index db8fac890e..568231e357 100644 --- a/src/elements/core/styles/_index.scss +++ b/src/elements/core/styles/_index.scss @@ -9,6 +9,7 @@ @forward './mixins/chip'; @forward './mixins/font-face'; @forward './mixins/helpers'; +@forward './mixins/image'; @forward './mixins/inputs'; @forward './mixins/layout'; @forward './mixins/link'; diff --git a/src/elements/core/styles/core.scss b/src/elements/core/styles/core.scss index 89284eb0ec..220a7f68e3 100644 --- a/src/elements/core/styles/core.scss +++ b/src/elements/core/styles/core.scss @@ -154,6 +154,121 @@ input[data-sbb-time-input] { } } +img { + aspect-ratio: var(--sbb-image-aspect-ratio); + object-fit: var(--sbb-image-object-fit); + object-position: var(--sbb-image-object-position); +} + +// TODO: Move back to the sbb-container components when the global css refactoring happens +sbb-container { + [slot='image']:is(sbb-image, img), + [slot='image'] :is(sbb-image, img) { + --sbb-image-object-fit: cover; + + border-radius: var(--sbb-container-background-border-radius); + height: 100%; + position: absolute; + } +} + +// TODO: Move back to the sbb-flip-card-summary components when the global css refactoring happens +sbb-flip-card-summary { + [slot='image']:is(sbb-image, img), + [slot='image'] :is(sbb-image, img) { + --sbb-image-aspect-ratio: auto; + --sbb-image-object-fit: cover; + + border-radius: 0; + display: block; + height: 100%; + } +} + +// TODO: Move back to the sbb-lead-container components when the global css refactoring happens +sbb-lead-container { + [slot='image']:is(sbb-image, img, picture), + [slot='image'] :is(sbb-image, img, picture) { + --sbb-image-aspect-ratio: var(--sbb-lead-container-image-ratio); + --sbb-image-object-fit: cover; + + border-radius: var(--sbb-lead-container-image-border-radius); + } +} + +// Target the slotted `sbb-image` which are generally wrapped by a
(therefore are not reachable with the :slotted) +// Apply the brightness effect on mouse hover +// TODO: Move back to the teaser components when the global css refactoring happens +:is(sbb-teaser, sbb-teaser-hero, sbb-teaser-product) { + --sbb-teaser-image-brightness-hover: var(--sbb-hover-image-brightness); + --sbb-teaser-image-animation-duration: var( + --sbb-disable-animation-duration, + var(--sbb-animation-duration-4x) + ); + --sbb-teaser-image-animation-easing: var(--sbb-animation-easing); + + &:hover { + @include mediaqueries.hover-mq($hover: true) { + --sbb-teaser-image-brightness: var(--sbb-teaser-image-brightness-hover); + } + } + + [slot='image']:is(sbb-image, img), + [slot='image'] :is(sbb-image, img) { + will-change: filter; + filter: brightness(var(--sbb-teaser-image-brightness, 1)); + transition: filter var(--sbb-teaser-image-animation-duration) + var(--sbb-teaser-image-animation-easing); + } +} + +// TODO: Move back to the teaser components when the global css refactoring happens +:is(sbb-teaser-product, sbb-teaser-product-static) { + :is(sbb-image, img) { + border-radius: 0; // Reset sbb-image border radius in order to control it from teaser product. + + --sbb-image-object-fit: cover; + --sbb-image-aspect-ratio: 16 / 9; + } + + img { + place-self: stretch; + } +} + +// TODO: Move back to the teaser components when the global css refactoring happens +:is(sbb-teaser) { + [slot='image']:is(sbb-image, img), + [slot='image'] :is(sbb-image, img) { + transition-property: filter, scale; + will-change: filter, scale; + scale: var(--sbb-teaser-scale, 1); + } + + :is(sbb-image, img) { + --sbb-image-object-fit: cover; + --sbb-image-aspect-ratio: 4 / 3; + } +} + +// TODO: Move back to the teaser-hero components when the global css refactoring happens +:is(sbb-teaser-hero) { + :is(sbb-image, img) { + --sbb-image-aspect-ratio: 1 / 1; + + border-radius: 0; + + @include mediaqueries.mq($from: small) { + --sbb-image-aspect-ratio: 16 / 9; + } + } + + img { + width: 100%; + display: block; + } +} + // TODO: move to train formation after css refactoring sbb-train-formation:has(sbb-train[direction-label]) { --sbb-train-formation-reserve-spacing-display: block; diff --git a/src/elements/core/styles/image.scss b/src/elements/core/styles/image.scss new file mode 100644 index 0000000000..2d4f29b98b --- /dev/null +++ b/src/elements/core/styles/image.scss @@ -0,0 +1,66 @@ +@use './mixins/typo'; +@use './mixins/image'; + +.sbb-figure { + @include image.figure; + + :is(img, sbb-image, .sbb-image) { + @include image.figure-image; + } + + :is(figcaption, .sbb-caption) { + @include image.figure-caption; + } + + // Utility classes for placing elements over an image (eg. 'sbb-figure-overlap-start-start') + :is( + .sbb-figure-overlap-start-start, + .sbb-figure-overlap-start-end, + .sbb-figure-overlap-end-start, + .sbb-figure-overlap-end-end + ) { + @include image.figure-overlap-base; + } + + $alignments: start, end; + @each $row-alignment in $alignments { + @each $column-alignment in $alignments { + .sbb-figure-overlap-#{$row-alignment}-#{$column-alignment} { + @include image.figure-overlap($row-alignment, $column-alignment); + } + } + } +} + +// Utility classes for the aspect ratio (eg. 'sbb-image-16-9') +$aspects-ratio: ( + 'free': 'auto', + '1-1': '1 / 1', + '1-2': '1 / 2', + '2-1': '2 / 1', + '2-3': '2 / 3', + '3-2': '3 / 2', + '3-4': '3 / 4', + '4-3': '4 / 3', + '4-5': '4 / 5', + '5-4': '5 / 4', + '9-16': '9 / 16', + '16-9': '16 / 9', +); +@each $name, $ratio in $aspects-ratio { + :is(sbb-image, img).sbb-image-#{$name} { + --sbb-image-aspect-ratio: #{$ratio}; + } +} + +// Utility classes for the border radius (eg. 'sbb-image-border-radius-none') +$border-radius: ( + 'default': 'var(--sbb-border-radius-4x)', + 'none': '0', + 'round': 'var(--sbb-border-radius-infinity)', +); +@each $name, $radius in $border-radius { + :is(img, sbb-image).sbb-image-border-radius-#{$name} { + border-radius: #{$radius}; + } +} diff --git a/src/elements/core/styles/mixins/image.scss b/src/elements/core/styles/mixins/image.scss new file mode 100644 index 0000000000..8124911314 --- /dev/null +++ b/src/elements/core/styles/mixins/image.scss @@ -0,0 +1,34 @@ +@use './typo'; + +@mixin figure { + display: grid; + grid-template-rows: auto; + grid-template-columns: 100%; + grid-auto-rows: auto; + margin: 0; +} + +@mixin figure-image { + grid-row: 1; + grid-column: 1; + width: 100%; +} + +@mixin figure-caption { + grid-row: 2; + grid-column: 1; + padding-block-start: var(--sbb-spacing-fixed-4x); + @include typo.text-xs--regular; +} + +@mixin figure-overlap-base { + position: relative; + order: 1; // Alternative to z-index + grid-row: 1; + grid-column: 1; + margin: var(--sbb-spacing-responsive-xxxs); +} + +@mixin figure-overlap($row-alignment, $column-alignment) { + place-self: #{$row-alignment} #{$column-alignment}; +} diff --git a/src/elements/core/styles/standard-theme.scss b/src/elements/core/styles/standard-theme.scss index 984efe558b..3540ec02f0 100644 --- a/src/elements/core/styles/standard-theme.scss +++ b/src/elements/core/styles/standard-theme.scss @@ -5,6 +5,7 @@ @use './typography'; @use './a11y'; @use './animation'; +@use './image'; @use './layout'; @use './lists'; @use './table'; diff --git a/src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js b/src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js index ff68aacd62..f1ab816c5f 100644 --- a/src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js +++ b/src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js @@ -30,11 +30,7 @@ snapshots["sbb-flip-card-summary DOM"] = > Summary - + `; diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.scss b/src/elements/flip-card/flip-card-summary/flip-card-summary.scss index 9d4e48486d..78346862ca 100644 --- a/src/elements/flip-card/flip-card-summary/flip-card-summary.scss +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.scss @@ -74,16 +74,7 @@ } } -::slotted(img) { - object-fit: cover; - width: 100%; - height: 100%; -} - -::slotted(sbb-image) { - --sbb-image-border-radius: 0; - --sbb-image-aspect-ratio: auto; - +::slotted([slot='image']) { width: 100%; height: 100%; } diff --git a/src/elements/flip-card/flip-card-summary/readme.md b/src/elements/flip-card/flip-card-summary/readme.md index 9063e233e4..c2cb95dd87 100644 --- a/src/elements/flip-card/flip-card-summary/readme.md +++ b/src/elements/flip-card/flip-card-summary/readme.md @@ -12,7 +12,24 @@ The component's slot is implicitly set to `"summary"`. ## Slots -Use the unnamed slot of `sbb-flip-card-summary` to provide a title and, optionally, the `image` slot to provide an image (via either `sbb-image` or `img`). +Use the unnamed slot to provide a title and the `image` slot to provide an image (via either `sbb-image` or `img`). + +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + +```html + + + ... +
+ + ... +
+
+
+``` diff --git a/src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js b/src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js index c2b823d882..1b3a1a62f6 100644 --- a/src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js +++ b/src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js @@ -39,11 +39,7 @@ snapshots["sbb-flip-card DOM"] = > Summary - + html` + +`; + +const imgWithChipTemplate = (): TemplateResult => html` +
+ + AI generated +
+`; + const cardSummary = ( label: string, imageAlignment: any, showImage: boolean, + showChip?: boolean, ): TemplateResult => html` ${label} - ${showImage - ? html`` - : nothing} + ${showImage ? (showChip ? imgWithChipTemplate() : imgTemplate()) : nothing} `; @@ -84,6 +95,11 @@ const DefaultTemplate = (args: Args): TemplateResult => ${cardSummary(args.label, args.imageAlignment, true)} ${cardDetails()} `; +const WithChipTemplate = (args: Args): TemplateResult => + html` + ${cardSummary(args.label, args.imageAlignment, true, true)} ${cardDetails()} + `; + const NoImageTemplate = (args: Args): TemplateResult => html` ${cardSummary(args.label, args.imageAlignment, false)} ${cardDetails()} @@ -153,6 +169,12 @@ export const ImageBelow: StoryObj = { args: { ...defaultArgs, imageAlignment: imageAlignment.options![1] }, }; +export const WithChipOnImage: StoryObj = { + render: WithChipTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + export const NoImage: StoryObj = { render: NoImageTemplate, argTypes: defaultArgTypes, diff --git a/src/elements/flip-card/flip-card/flip-card.visual.spec.ts b/src/elements/flip-card/flip-card/flip-card.visual.spec.ts index 7ed3c32148..f7a619267b 100644 --- a/src/elements/flip-card/flip-card/flip-card.visual.spec.ts +++ b/src/elements/flip-card/flip-card/flip-card.visual.spec.ts @@ -15,9 +15,10 @@ import type { SbbFlipCardElement } from './flip-card.js'; import './flip-card.js'; import '../flip-card-summary.js'; import '../flip-card-details.js'; -import '../../title.js'; -import '../../link.js'; +import '../../chip-label.js'; import '../../image.js'; +import '../../link.js'; +import '../../title.js'; const imageUrl = import.meta.resolve('../../core/testing/assets/placeholder-image.png'); @@ -25,10 +26,13 @@ const content = ( title: string = 'Summary', imageAlignment: SbbFlipCardImageAlignment = 'after', longContent: boolean = false, + imgTemplate?: () => TemplateResult, ): TemplateResult => html` ${title} - + ${imgTemplate + ? imgTemplate() + : html``} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam luctus ornare condimentum. @@ -45,6 +49,37 @@ const content = ( Link `; +const imgTestCases = [ + { + title: 'with sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => html``, + }, + { + title: 'with img tag', + imgSelector: 'img', + imgTemplate: () => html``, + }, + { + title: 'with figure_sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => + html`
+ + AI generated +
`, + }, + { + title: 'with figure_img', + imgSelector: 'img', + imgTemplate: () => + html`
+ + AI generated +
`, + }, +]; + describe(`sbb-flip-card`, () => { describeViewports({ viewports: ['zero', 'medium'] }, () => { for (const imageAlignment of ['after', 'below']) { @@ -95,20 +130,6 @@ describe(`sbb-flip-card`, () => { }), ); - for (const imageAlignment of ['after', 'below']) { - it( - `long content image-alignment=${imageAlignment}`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html` - ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} - `, - ); - await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); - }), - ); - } - for (const imageAlignment of ['after', 'below']) { describe(`imageAlignment=${imageAlignment}`, () => { it( @@ -146,6 +167,37 @@ describe(`sbb-flip-card`, () => { await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); + + it( + `long content`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} + `, + ); + await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); + }), + ); + + for (const testCase of imgTestCases) { + it( + testCase.title, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + ${content( + 'Summary', + imageAlignment as SbbFlipCardImageAlignment, + false, + testCase.imgTemplate, + )} + `, + ); + await waitForImageReady(setup.snapshotElement.querySelector(testCase.imgSelector)!); + }), + ); + } }); } diff --git a/src/elements/image/__snapshots__/image.snapshot.spec.snap.js b/src/elements/image/__snapshots__/image.snapshot.spec.snap.js index b4bb7b14df..a2ed420dcf 100644 --- a/src/elements/image/__snapshots__/image.snapshot.spec.snap.js +++ b/src/elements/image/__snapshots__/image.snapshot.spec.snap.js @@ -2,50 +2,44 @@ export const snapshots = {}; snapshots["sbb-image should render DOM"] = -` +` `; /* end snapshot sbb-image should render DOM */ snapshots["sbb-image should render Shadow DOM"] = -`
-
+`
+ + + + + - - - - - - -
-
+ + `; /* end snapshot sbb-image should render Shadow DOM */ diff --git a/src/elements/image/image.scss b/src/elements/image/image.scss index 30b5a40530..0cafbe72d4 100644 --- a/src/elements/image/image.scss +++ b/src/elements/image/image.scss @@ -4,8 +4,7 @@ @include sbb.box-sizing; :host { - --sbb-image-border-radius: var(--sbb-border-radius-4x); - --sbb-image-aspect-ratio: auto; + --sbb-image-aspect-ratio: 16 / 9; --sbb-image-animation-duration: var( --sbb-disable-animation-duration, var(--sbb-animation-duration-4x) @@ -13,75 +12,8 @@ --sbb-image-object-fit: cover; display: block; -} - -:host([aspect-ratio='1-1']) { - --sbb-image-aspect-ratio: 1 / 1; -} - -:host([aspect-ratio='1-2']) { - --sbb-image-aspect-ratio: 1 / 2; -} - -:host([aspect-ratio='2-1']) { - --sbb-image-aspect-ratio: 2 / 1; -} - -:host([aspect-ratio='2-3']) { - --sbb-image-aspect-ratio: 2 / 3; -} - -:host([aspect-ratio='3-2']) { - --sbb-image-aspect-ratio: 3 / 2; -} - -:host([aspect-ratio='3-4']) { - --sbb-image-aspect-ratio: 3 / 4; -} - -:host([aspect-ratio='4-3']) { - --sbb-image-aspect-ratio: 4 / 3; -} - -:host([aspect-ratio='4-5']) { - --sbb-image-aspect-ratio: 4 / 5; -} - -:host([aspect-ratio='5-4']) { - --sbb-image-aspect-ratio: 5 / 4; -} - -:host([aspect-ratio='16-9']) { - --sbb-image-aspect-ratio: 16 / 9; -} - -:host([aspect-ratio='9-16']) { - --sbb-image-aspect-ratio: 9 / 16; -} - -// Variant: Hero Teaser and Paid Teaser -:host([data-teaser]) { - --sbb-image-aspect-ratio: 1 / 1; - - @include sbb.mq($from: small) { - --sbb-image-aspect-ratio: 16 / 9; - } -} - -:host(:is([border-radius='none'], [data-teaser])) { - --sbb-image-border-radius: 0; -} - -:host([border-radius='round']:not([data-teaser])) { - --sbb-image-border-radius: var(--sbb-border-radius-infinity); -} - -.sbb-image__figure { - display: flex; - flex-direction: column; - gap: var(--sbb-spacing-fixed-4x); - margin: 0; - height: 100%; + border-radius: var(--sbb-border-radius-4x); + overflow: hidden; } .sbb-image__img { @@ -122,13 +54,7 @@ picture { .sbb-image__wrapper { display: flex; position: relative; - overflow: hidden; width: 100%; height: 100%; aspect-ratio: var(--sbb-image-aspect-ratio); - border-radius: var(--sbb-image-border-radius); -} - -.sbb-image__caption { - @include sbb.text-xs--regular; } diff --git a/src/elements/image/image.stories.ts b/src/elements/image/image.stories.ts index 55618550bc..27dd0a745d 100644 --- a/src/elements/image/image.stories.ts +++ b/src/elements/image/image.stories.ts @@ -3,14 +3,47 @@ import type { InputType } from '@storybook/types'; import type { Args, ArgTypes, Decorator, Meta, StoryObj } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html } from 'lit'; +import { classMap } from 'lit/directives/class-map.js'; import { sbbSpread } from '../../storybook/helpers/spread.js'; import images from '../core/images.js'; import { SbbImageElement } from './image.js'; import readme from './readme.md?raw'; - -const Template = (args: Args): TemplateResult => html``; +import '../chip-label.js'; + +const ImageTemplate = ({ aspectRatio, borderRadius, ...args }: Args): TemplateResult => html` + + +`; + +const WithCaptionTemplate = (args: Args): TemplateResult => html` +
+ ${ImageTemplate(args)} +
+ With the + Half Fare Travelcard + , you can travel for half price on all SBB routes and most other railways as well as on boats + and Postbuses. You also benefit from discounts on urban transport as well as other additional + attractive services and discounts. +
+
+`; + +const WithChipTemplate = ({ chipPosition, ...args }: Args): TemplateResult => html` +
+ ${ImageTemplate(args)} + AI generated +
+`; const imageSrc: InputType = { control: { @@ -24,41 +57,30 @@ const borderRadius: InputType = { type: 'select', }, options: ['default', 'none', 'round'], + table: { + category: 'Utility classes', + }, }; const aspectRatio: InputType = { control: { type: 'select' }, options: ['free', '1-1', '1-2', '2-1', '2-3', '3-2', '3-4', '4-3', '4-5', '5-4', '9-16', '16-9'], -}; - -const alt: InputType = { - control: { - type: 'text', - }, -}; - -const caption: InputType = { - control: { - type: 'text', + table: { + category: 'Utility classes', }, }; -const copyright: InputType = { - control: { - type: 'text', - }, +const chipPosition: InputType = { + control: { type: 'select' }, + options: ['start-start', 'start-end', 'end-start', 'end-end'], table: { - category: 'Legal', + category: 'Utility classes', }, }; -const copyrightHolder: InputType = { +const alt: InputType = { control: { - type: 'inline-radio', - }, - options: ['Organization', 'Person'], - table: { - category: 'Legal', + type: 'text', }, }; @@ -128,11 +150,8 @@ const performanceMark: InputType = { const defaultArgTypes: ArgTypes = { alt, - caption, - 'border-radius': borderRadius, - 'aspect-ratio': aspectRatio, - copyright, - 'copyright-holder': copyrightHolder, + borderRadius, + aspectRatio, 'custom-focal-point': customFocalPoint, 'focal-point-debug': focalPointDebug, 'focal-point-x': focalPointX, @@ -145,12 +164,8 @@ const defaultArgTypes: ArgTypes = { const defaultArgs: Args = { alt: '', - caption: undefined, - // we need a string and not boolean, otherwise storybook add/remove the attribute but don't write the value - 'border-radius': 'default', - 'aspect-ratio': aspectRatio.options![0], - copyright: '', - 'copyright-holder': copyrightHolder.options![0], + borderRadius: borderRadius.options![0], + aspectRatio: aspectRatio.options![0], 'custom-focal-point': false, 'focal-point-debug': false, 'focal-point-x': '', @@ -162,17 +177,13 @@ const defaultArgs: Args = { }; export const Default: StoryObj = { - render: Template, + render: WithCaptionTemplate, argTypes: defaultArgTypes, - args: { - ...defaultArgs, - caption: - 'Mit Ihrem Halbtax profitieren Sie zudem von attraktiven Zusatzleistungen und Rabatten. Wenn Sie unter 25 Jahre jung sind, können Sie zu Ihrem Halbtax das beliebte Gleis 7 dazu kaufen.', - }, + args: defaultArgs, }; export const TransparentImage: StoryObj = { - render: Template, + render: ImageTemplate, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -181,26 +192,26 @@ export const TransparentImage: StoryObj = { }; export const NoCaptionNoRadius: StoryObj = { - render: Template, + render: ImageTemplate, argTypes: defaultArgTypes, args: { ...defaultArgs, - 'border-radius': 'none', + borderRadius: 'none', }, }; export const RoundBorderRadius: StoryObj = { - render: Template, + render: ImageTemplate, argTypes: defaultArgTypes, args: { ...defaultArgs, - 'border-radius': 'round', - 'aspect-ratio': '1-1', + borderRadius: 'round', + aspectRatio: '1-1', }, }; export const SkipLqip: StoryObj = { - render: Template, + render: ImageTemplate, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -208,6 +219,12 @@ export const SkipLqip: StoryObj = { }, }; +export const WithChip: StoryObj = { + render: WithChipTemplate, + argTypes: { ...defaultArgTypes, chipPosition }, + args: { ...defaultArgs, chipPosition: chipPosition.options![0] }, +}; + const meta: Meta = { decorators: [ (story) => html`
${story()}
`, diff --git a/src/elements/image/image.ts b/src/elements/image/image.ts index 1f561dd67c..6f764f9b61 100644 --- a/src/elements/image/image.ts +++ b/src/elements/image/image.ts @@ -24,10 +24,8 @@ import { type TemplateResult, } from 'lit'; import { customElement, eventOptions, property } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref.js'; import { forceType } from '../core/decorators.js'; -import { hostContext } from '../core/dom.js'; import style from './image.scss?lit&inline'; @@ -154,8 +152,6 @@ const breakpointMap: Record = { * @cssprop [--sbb-image-aspect-ratio=auto] - Can be used to override `aspectRatio` property. * This way we can have, for example, an image component with an aspect * ratio of 4/3 in smaller viewports and 16/9 in larger viewports. - * @cssprop [--sbb-image-border-radius=var(--sbb-border-radius-4x)] - Can be used to override the - * `borderRadius` property in case of different values for different viewports. * @cssprop [--sbb-image-object-position] - Can be used to set the object-position css property of the image itself if the image itself is cropped. * @cssprop [--sbb-image-object-fit=cover] - Can be used to set the object-fit css property of the image itself if the image itself is cropped. */ @@ -197,31 +193,6 @@ class SbbImageElement extends LitElement { @property({ attribute: 'skip-lqip', type: Boolean, reflect: true }) public accessor skipLqip: boolean = false; - /** - * A caption can provide additional context to the image (e.g. - * descriptions and the like). - * Links will automatically receive tabindex=-1 if hideFromScreenreader - * is set to true. That way they will no longer become focusable. - */ - @forceType() - @property() - public accessor caption: string = ''; - - /** - * If a copyright text is provided, we will add it to the caption - * and create a structured data json-ld block with the copyright - * information. - */ - @forceType() - @property() - public accessor copyright: string = ''; - - /** - * Copyright holder can either be an Organization or a Person - */ - @property({ attribute: 'copyright-holder' }) - public accessor copyrightHolder: 'Organization' | 'Person' = 'Organization'; - /** * Set this to true, if you want to pass a custom focal point * for the image. See full documentation here: @@ -388,48 +359,11 @@ class SbbImageElement extends LitElement { @property({ attribute: 'picture-sizes-config' }) public accessor pictureSizesConfig: string = ''; - /** - * Border radius of the image. Choose between a default radius, no radius and a completely round image. - */ - @property({ attribute: 'border-radius', reflect: true }) public accessor borderRadius: - | 'default' - | 'none' - | 'round' = 'default'; - - /** - * Set an aspect ratio - * default is '16-9' (16/9) - * other values: 'free', '1-1', '1-2', '2-1', '2-3', '3-2', '3-4', '4-3', '4-5', '5-4', '9-16' - */ - @property({ attribute: 'aspect-ratio', reflect: true }) - public accessor aspectRatio: - | 'free' - | '1-1' - | '1-2' - | '2-1' - | '2-3' - | '3-2' - | '3-4' - | '4-3' - | '4-5' - | '5-4' - | '9-16' - | '16-9' = '16-9'; - /** Whether the image is finished loading or failed to load. */ public get complete(): boolean { return this.shadowRoot?.querySelector?.('.sbb-image__img')?.complete ?? false; } - public override connectedCallback(): void { - super.connectedCallback(); - // Check if the current element is nested in an `` element on in an `` element. - this.toggleAttribute( - 'data-teaser', - !!hostContext('sbb-teaser-hero', this) || !!this.closest('sbb-teaser-paid'), - ); - } - protected override updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -611,9 +545,6 @@ class SbbImageElement extends LitElement { } protected override render(): TemplateResult { - let { caption } = this; - let schemaData = ''; - const imageUrlLQIP = this._prepareImageUrl(this.imageSrc, true); const imageUrlWithParams = this._prepareImageUrl(this.imageSrc, false); @@ -622,19 +553,6 @@ class SbbImageElement extends LitElement { this.importance = 'low'; } - if (this.copyright) { - caption = `${this.caption} ©${this.copyright}`; - schemaData = `{ - "@context": "https://schema.org", - "@type": "Photograph", - "image": "${this.imageSrc}", - "copyrightHolder": { - "@type": "${this.copyrightHolder}", - "name": "${this.copyright}" - } - }`; - } - const pictureSizeConfigs = this._preparePictureSizeConfigs(); /** @@ -644,66 +562,51 @@ class SbbImageElement extends LitElement { * they might try to interpret the img element. */ return html` -
-
- ${!this.skipLqip - ? html`` - : nothing} - - - - ${pictureSizeConfigs.map((config) => { - const imageHeight = config.image.height; - const imageWidth = config.image.width; - const mediaQuery = this._createMediaQueryString(config.mediaQueries); - return [ - html` `, - ]; - })} - ${this.alt this.dispatchEvent(new Event('error'))} - class="sbb-image__img" - src=${this.imageSrc!} +
+ ${!this.skipLqip + ? html` - -
- ${caption - ? html`
{ - this._captionElement = el as HTMLElement; - })} - >
` - : nothing} - ${schemaData - ? html`` + />` : nothing} -
+ + + + ${pictureSizeConfigs.map((config) => { + const imageHeight = config.image.height; + const imageWidth = config.image.width; + const mediaQuery = this._createMediaQueryString(config.mediaQueries); + return html` + `; + })} + ${this.alt this.dispatchEvent(new Event('error'))} + class="sbb-image__img" + src=${this.imageSrc!} + width="1000" + height="562" + loading=${this.loading ?? nothing} + decoding=${this.decoding ?? nothing} + .fetchPriority=${this.importance ?? nothing} + /> + + `; } } diff --git a/src/elements/image/image.visual.spec.ts b/src/elements/image/image.visual.spec.ts index b69f110c20..99b4d208bf 100644 --- a/src/elements/image/image.visual.spec.ts +++ b/src/elements/image/image.visual.spec.ts @@ -29,7 +29,7 @@ describe(`sbb-image`, () => { `aspect-ratio=${aspectRatio}`, visualDiffDefault.with(async (setup) => { await setup.withFixture( - html``, + html``, ); await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); @@ -43,7 +43,7 @@ describe(`sbb-image`, () => { await setup.withFixture( html``, ); @@ -52,15 +52,14 @@ describe(`sbb-image`, () => { }), ); - for (const borderRadius of ['none', 'round']) { + for (const borderRadius of ['none', 'round', 'default']) { it( `border-radius=${borderRadius}`, visualDiffDefault.with(async (setup) => { await setup.withFixture( html``, ); @@ -73,11 +72,16 @@ describe(`sbb-image`, () => { 'with caption', visualDiffDefault.with(async (setup) => { await setup.withFixture( - html`Gleis 7. After the link there is more text.`} - >`, + html`
+ +
+ A long text which takes several lines and contains a link + Gleis 7. After the link there is more text. +
+
`, ); await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); @@ -108,11 +112,10 @@ describe(`sbb-image`, () => { 'cropped with caption', visualDiffDefault.with(async (setup) => { await setup.withFixture( - html``, + html`
+ +
I am a caption below
+
`, ); await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); @@ -151,7 +154,7 @@ describe(`sbb-image`, () => { 'skipLqip=true', visualDiffDefault.with(async (setup) => { await setup.withFixture( - html``, + html``, ); await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); diff --git a/src/elements/image/readme.md b/src/elements/image/readme.md index 2d53ca42f9..264f2a7d4f 100644 --- a/src/elements/image/readme.md +++ b/src/elements/image/readme.md @@ -3,30 +3,96 @@ The `sbb-image` component is used to render an image. Mainly from cdn.img.sbb.ch (with `imageSrc`), but we can set an external image too. The size can be set with `pictureSizesConfig`. +```html + +``` + +## Usage + +For image related elements, it is strongly recommended to wrap an `sbb-image` and all its related elements in a `figure` tag. +E.g. `
` or ``. + +```html +
+ or +
Caption / Copyright
+
+``` + +You can place overlapping content by using the `sbb-figure-overlap-${horizontal-alignment}-${vertical-alignment}` utility classes. + +| Position | CSS class | +| -------------- | -------------------------------- | +| `top-left` | `sbb-figure-overlap-start-start` | +| `top-right` | `sbb-figure-overlap-start-end` | +| `bottom-left` | `sbb-figure-overlap-end-start` | +| `bottom-right` | `sbb-figure-overlap-end-end` | + +```html +
+ + +
+``` + +### Utility classes + +Use the `sbb-image-border-radius-${value}` utility classes to set the image border radius. + +| Border Radius | CSS class | +| ------------- | ------------------- | +| `default` | `sbb-image-default` | +| `none` | `sbb-image-none` | +| `round` | `sbb-image-round` | + +```html + + + +``` + +Use the `sbb-image-${ratio}` utility classes to set the image aspect ratio. + +| Aspect Ratio | CSS class | +| ------------ | ---------------- | +| `free` | `sbb-image-free` | +| `1-1` | `sbb-image-1-1` | +| `1-2` | `sbb-image-1-2` | +| `2-1` | `sbb-image-2-1` | +| `2-3` | `sbb-image-2-3` | +| `3-2` | `sbb-image-3-2` | +| `3-4` | `sbb-image-3-4` | +| `4-3` | `sbb-image-4-3` | +| `4-5` | `sbb-image-4-5` | +| `5-4` | `sbb-image-5-4` | +| `9-16` | `sbb-image-9-16` | +| `16-9` | `sbb-image-16-9` | + +```html + + + +``` + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `alt` | `alt` | public | `string` | `''` | An alt text is not always necessary (e.g. in teaser cards when additional link text is provided). In this case we can leave the value of the alt attribute blank, but the attribute itself still needs to be present. That way we can signal assistive technology, that they can skip the image. | -| `aspectRatio` | `aspect-ratio` | public | `'free' \| '1-1' \| '1-2' \| '2-1' \| '2-3' \| '3-2' \| '3-4' \| '4-3' \| '4-5' \| '5-4' \| '9-16' \| '16-9'` | `'16-9'` | Set an aspect ratio default is '16-9' (16/9) other values: 'free', '1-1', '1-2', '2-1', '2-3', '3-2', '3-4', '4-3', '4-5', '5-4', '9-16' | -| `borderRadius` | `border-radius` | public | `'default' \| 'none' \| 'round'` | `'default'` | Border radius of the image. Choose between a default radius, no radius and a completely round image. | -| `caption` | `caption` | public | `string` | `''` | A caption can provide additional context to the image (e.g. descriptions and the like). Links will automatically receive tabindex=-1 if hideFromScreenreader is set to true. That way they will no longer become focusable. | -| `complete` | - | public | `boolean` | | Whether the image is finished loading or failed to load. | -| `copyright` | `copyright` | public | `string` | `''` | If a copyright text is provided, we will add it to the caption and create a structured data json-ld block with the copyright information. | -| `copyrightHolder` | `copyright-holder` | public | `'Organization' \| 'Person'` | `'Organization'` | Copyright holder can either be an Organization or a Person | -| `customFocalPoint` | `custom-focal-point` | public | `boolean` | `false` | Set this to true, if you want to pass a custom focal point for the image. See full documentation here: https://docs.imgix.com/apis/rendering/focalpoint-crop | -| `decoding` | `decoding` | public | `'sync' \| 'async' \| 'auto'` | `'auto'` | If the lazy property is set to true, the module will automatically change the decoding to async, otherwise the decoding is set to auto which leaves the handling up to the browser. Read more about the decoding attribute here: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding | -| `focalPointDebug` | `focal-point-debug` | public | `boolean` | `false` | Set this to true, to receive visual guidance where the custom focal point is currently set. | -| `focalPointX` | `focal-point-x` | public | `number` | `1` | Pass in a floating number between 0 (left) and 1 (right). | -| `focalPointY` | `focal-point-y` | public | `number` | `1` | Pass in a floating number between 0 (top) and 1 (bottom). | -| `imageSrc` | `image-src` | public | `string` | `''` | Right now the module is heavily coupled with the image delivery service imgix and depends on the original files being stored inside AEM. You can pass in any https://cdn.img.sbb.ch img src address you find on sbb.ch to play around with it. Just strip the url parameters and paste in the plain file address. If you want to know how to best work with this module with images coming from a different source, please contact the LYNE Core Team. | -| `importance` | `importance` | public | `'auto' \| 'high' \| 'low'` | `'high'` | The importance attribute is fairly new attribute which should help the browser decide which resources it should prioritise during page load. We will set the attribute value based on the value, we receive in the loading attribute. 'eager', which we use for the largest image within the initial viewport, will set the attribute value to 'high'. 'lazy', which we use for images below the fold, will set the attribute value to 'low'. | -| `loading` | `loading` | public | `'eager' \| 'lazy'` | `'eager'` | With the support of native image lazy loading, we can now decide whether we want to load the image immediately or only once it is close to the visible viewport. The value eager is best used for images within the initial viewport. We want to load these images as fast as possible to improve the Core Web Vitals values. lazy on the other hand works best for images which are further down the page or invisible during the loading of the initial viewport. | -| `performanceMark` | `performance-mark` | public | `string` | `''` | With performance.mark you can log a timestamp associated with the name you define in performanceMark when a certain event is happening. In our case we will log the performance.mark into the PerformanceEntry API once the image is fully loaded. Performance monitoring tools like SpeedCurve or Lighthouse are then able to grab these entries from the PerformanceEntry API and give us additional information and insights about our page loading behaviour. We are then also able to monitor these values over a long period to see if our performance increases or decreases over time. Best to use lowercase strings here, separate words with underscores or dashes. | -| `pictureSizesConfig` | `picture-sizes-config` | public | `string` | `''` | With the pictureSizesConfig object, you can pass in information into image about what kind of source elements should get rendered. mediaQueries accepts multiple Media Query entries which can get combined by defining a conditionOperator. Type is: stringified InterfaceImageAttributesSizesConfig-Object An example could look like this: { "breakpoints": \[ { "image": { "height": "675", "width": "1200" }, "mediaQueries": \[ { "conditionFeature": "min-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-large-min" }, "conditionOperator": false } ] }, { "image": { "height": "549", "width": "976" }, "mediaQueries": \[ { "conditionFeature": "min-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-small-min" }, "conditionOperator": false } ] }, { "image": { "height": "180", "width": "320" }, "mediaQueries": \[ { "conditionFeature": "max-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-micro-max" }, "conditionOperator": "and" }, { "conditionFeature": "orientation", "conditionFeatureValue": { "lyneDesignToken": false, "value": "landscape" }, "conditionOperator": false } ] } ] } | -| `skipLqip` | `skip-lqip` | public | `boolean` | `false` | If set to false, we show a blurred version of the image as placeholder before the actual image shows up. This will help to improve the perceived loading performance. Read more about the idea of lqip here: https://medium.com/@imgix/lqip-your-images-for-fast-loading-2523d9ee4a62 | +| Name | Attribute | Privacy | Type | Default | Description | +| -------------------- | ---------------------- | ------- | ----------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `alt` | `alt` | public | `string` | `''` | An alt text is not always necessary (e.g. in teaser cards when additional link text is provided). In this case we can leave the value of the alt attribute blank, but the attribute itself still needs to be present. That way we can signal assistive technology, that they can skip the image. | +| `complete` | - | public | `boolean` | | Whether the image is finished loading or failed to load. | +| `customFocalPoint` | `custom-focal-point` | public | `boolean` | `false` | Set this to true, if you want to pass a custom focal point for the image. See full documentation here: https://docs.imgix.com/apis/rendering/focalpoint-crop | +| `decoding` | `decoding` | public | `'sync' \| 'async' \| 'auto'` | `'auto'` | If the lazy property is set to true, the module will automatically change the decoding to async, otherwise the decoding is set to auto which leaves the handling up to the browser. Read more about the decoding attribute here: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding | +| `focalPointDebug` | `focal-point-debug` | public | `boolean` | `false` | Set this to true, to receive visual guidance where the custom focal point is currently set. | +| `focalPointX` | `focal-point-x` | public | `number` | `1` | Pass in a floating number between 0 (left) and 1 (right). | +| `focalPointY` | `focal-point-y` | public | `number` | `1` | Pass in a floating number between 0 (top) and 1 (bottom). | +| `imageSrc` | `image-src` | public | `string` | `''` | Right now the module is heavily coupled with the image delivery service imgix and depends on the original files being stored inside AEM. You can pass in any https://cdn.img.sbb.ch img src address you find on sbb.ch to play around with it. Just strip the url parameters and paste in the plain file address. If you want to know how to best work with this module with images coming from a different source, please contact the LYNE Core Team. | +| `importance` | `importance` | public | `'auto' \| 'high' \| 'low'` | `'high'` | The importance attribute is fairly new attribute which should help the browser decide which resources it should prioritise during page load. We will set the attribute value based on the value, we receive in the loading attribute. 'eager', which we use for the largest image within the initial viewport, will set the attribute value to 'high'. 'lazy', which we use for images below the fold, will set the attribute value to 'low'. | +| `loading` | `loading` | public | `'eager' \| 'lazy'` | `'eager'` | With the support of native image lazy loading, we can now decide whether we want to load the image immediately or only once it is close to the visible viewport. The value eager is best used for images within the initial viewport. We want to load these images as fast as possible to improve the Core Web Vitals values. lazy on the other hand works best for images which are further down the page or invisible during the loading of the initial viewport. | +| `performanceMark` | `performance-mark` | public | `string` | `''` | With performance.mark you can log a timestamp associated with the name you define in performanceMark when a certain event is happening. In our case we will log the performance.mark into the PerformanceEntry API once the image is fully loaded. Performance monitoring tools like SpeedCurve or Lighthouse are then able to grab these entries from the PerformanceEntry API and give us additional information and insights about our page loading behaviour. We are then also able to monitor these values over a long period to see if our performance increases or decreases over time. Best to use lowercase strings here, separate words with underscores or dashes. | +| `pictureSizesConfig` | `picture-sizes-config` | public | `string` | `''` | With the pictureSizesConfig object, you can pass in information into image about what kind of source elements should get rendered. mediaQueries accepts multiple Media Query entries which can get combined by defining a conditionOperator. Type is: stringified InterfaceImageAttributesSizesConfig-Object An example could look like this: { "breakpoints": \[ { "image": { "height": "675", "width": "1200" }, "mediaQueries": \[ { "conditionFeature": "min-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-large-min" }, "conditionOperator": false } ] }, { "image": { "height": "549", "width": "976" }, "mediaQueries": \[ { "conditionFeature": "min-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-small-min" }, "conditionOperator": false } ] }, { "image": { "height": "180", "width": "320" }, "mediaQueries": \[ { "conditionFeature": "max-width", "conditionFeatureValue": { "lyneDesignToken": true, "value": "sbb-breakpoint-micro-max" }, "conditionOperator": "and" }, { "conditionFeature": "orientation", "conditionFeatureValue": { "lyneDesignToken": false, "value": "landscape" }, "conditionOperator": false } ] } ] } | +| `skipLqip` | `skip-lqip` | public | `boolean` | `false` | If set to false, we show a blurred version of the image as placeholder before the actual image shows up. This will help to improve the perceived loading performance. Read more about the idea of lqip here: https://medium.com/@imgix/lqip-your-images-for-fast-loading-2523d9ee4a62 | ## Events @@ -37,9 +103,8 @@ The size can be set with `pictureSizesConfig`. ## CSS Properties -| Name | Default | Description | -| ----------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `--sbb-image-aspect-ratio` | `auto` | Can be used to override `aspectRatio` property. This way we can have, for example, an image component with an aspect ratio of 4/3 in smaller viewports and 16/9 in larger viewports. | -| `--sbb-image-border-radius` | `var(--sbb-border-radius-4x)` | Can be used to override the `borderRadius` property in case of different values for different viewports. | -| `--sbb-image-object-fit` | `cover` | Can be used to set the object-fit css property of the image itself if the image itself is cropped. | -| `--sbb-image-object-position` | | Can be used to set the object-position css property of the image itself if the image itself is cropped. | +| Name | Default | Description | +| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `--sbb-image-aspect-ratio` | `auto` | Can be used to override `aspectRatio` property. This way we can have, for example, an image component with an aspect ratio of 4/3 in smaller viewports and 16/9 in larger viewports. | +| `--sbb-image-object-fit` | `cover` | Can be used to set the object-fit css property of the image itself if the image itself is cropped. | +| `--sbb-image-object-position` | | Can be used to set the object-position css property of the image itself if the image itself is cropped. | diff --git a/src/elements/lead-container/__snapshots__/lead-container.snapshot.spec.snap.js b/src/elements/lead-container/__snapshots__/lead-container.snapshot.spec.snap.js index 3a88024149..737465bddf 100644 --- a/src/elements/lead-container/__snapshots__/lead-container.snapshot.spec.snap.js +++ b/src/elements/lead-container/__snapshots__/lead-container.snapshot.spec.snap.js @@ -4,8 +4,6 @@ export const snapshots = {}; snapshots["sbb-lead-container DOM"] = ` diff --git a/src/elements/lead-container/lead-container.scss b/src/elements/lead-container/lead-container.scss index b08d33c3e0..69f997a787 100644 --- a/src/elements/lead-container/lead-container.scss +++ b/src/elements/lead-container/lead-container.scss @@ -27,17 +27,9 @@ padding-block-end: var(--sbb-spacing-responsive-l); } -::slotted(sbb-image[slot='image']) { - --sbb-image-aspect-ratio: var(--sbb-lead-container-image-ratio); - --sbb-image-border-radius: var(--sbb-lead-container-image-border-radius); -} - -::slotted(:is(img[slot='image'], picture[slot='image'])) { +::slotted([slot='image']) { display: block; width: 100%; - object-fit: cover; - aspect-ratio: var(--sbb-lead-container-image-ratio); - border-radius: var(--sbb-lead-container-image-border-radius); } ::slotted(:is(sbb-breadcrumb-group, sbb-block-link).sbb-lead-container-spacing) { diff --git a/src/elements/lead-container/lead-container.stories.ts b/src/elements/lead-container/lead-container.stories.ts index b16082d69f..10da1282f1 100644 --- a/src/elements/lead-container/lead-container.stories.ts +++ b/src/elements/lead-container/lead-container.stories.ts @@ -3,6 +3,7 @@ import { html, type TemplateResult } from 'lit'; import '../alert.js'; import '../breadcrumb.js'; +import '../chip-label.js'; import '../image.js'; import '../link/block-link.js'; import '../link/link.js'; @@ -14,57 +15,74 @@ import images from '../core/images.js'; import readme from './readme.md?raw'; +const content = (): TemplateResult => html` + + + + The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. + Find out more + + + + + Level 1 + Level 2 + Level 3 + Level 4 + + + Link + + Title +

+ Lead text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer enim elit, ultricies + in tincidunt quis, mattis eu quam. Nulla sit amet lorem fermentum, molestie nunc ut, hendrerit + risus. +

+ + Vestibulum rutrum elit et lacus sollicitudin, quis malesuada lorem vehicula. Suspendisse at + augue quis tellus vulputate tempor. Vivamus urna velit, varius nec est ac, mollis efficitur + lorem. Quisque non nisl eget massa interdum tempus. Praesent vel feugiat metus. + +

+ Other content. Vestibulum rutrum elit et lacus sollicitudin, quis malesuada lorem vehicula. + Suspendisse at augue quis tellus vulputate tempor. Vivamus urna velit, varius nec est ac, mollis + efficitur lorem. Quisque non nisl eget massa interdum tempus. Praesent vel feugiat metus. +

+`; + const DefaultTemplate = (): TemplateResult => html` - + ${content()}; - - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - Find out more - - - - - Level 1 - Level 2 - Level 3 - Level 4 - - - Link - - Title -

- Lead text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer enim elit, - ultricies in tincidunt quis, mattis eu quam. Nulla sit amet lorem fermentum, molestie nunc ut, - hendrerit risus. -

- - Vestibulum rutrum elit et lacus sollicitudin, quis malesuada lorem vehicula. Suspendisse at - augue quis tellus vulputate tempor. Vivamus urna velit, varius nec est ac, mollis efficitur - lorem. Quisque non nisl eget massa interdum tempus. Praesent vel feugiat metus. - -

- Other content. Vestibulum rutrum elit et lacus sollicitudin, quis malesuada lorem vehicula. - Suspendisse at augue quis tellus vulputate tempor. Vivamus urna velit, varius nec est ac, - mollis efficitur lorem. Quisque non nisl eget massa interdum tempus. Praesent vel feugiat - metus. -

+
+`; + +const WithChipTemplate = (): TemplateResult => html` + + ${content()}; + +
+ + + AI generated +
`; @@ -72,6 +90,10 @@ export const Default: StoryObj = { render: DefaultTemplate, }; +export const WithChip: StoryObj = { + render: WithChipTemplate, +}; + const meta: Meta = { parameters: { docs: { diff --git a/src/elements/lead-container/lead-container.visual.spec.ts b/src/elements/lead-container/lead-container.visual.spec.ts index 6d0eff1ea8..436bf0db95 100644 --- a/src/elements/lead-container/lead-container.visual.spec.ts +++ b/src/elements/lead-container/lead-container.visual.spec.ts @@ -10,6 +10,7 @@ import { waitForImageReady } from '../core/testing.js'; import '../alert.js'; import '../breadcrumb.js'; +import '../chip-label.js'; import '../image.js'; import '../link/block-link.js'; import '../link/link.js'; @@ -23,14 +24,63 @@ const leadImageBase64 = await loadAssetAsBase64(leadImageUrl); describe(`sbb-lead-container`, () => { const wrapperStyles = { backgroundColor: `var(--sbb-color-milk)`, padding: '0' }; - const leadContainerTemplate = (image: TemplateResult): TemplateResult => html` + const testCases = [ + { + title: 'with sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => html``, + }, + { + title: 'with img tag', + imgSelector: 'img', + imgTemplate: () => html``, + }, + { + title: 'with figure_sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => + html`
+ + AI generated +
`, + }, + { + title: 'with figure_img', + imgSelector: 'img', + imgTemplate: () => + html`
+ + AI generated +
`, + }, + { + title: 'with picture_sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => + html` + + AI generated + `, + }, + { + title: 'with picture_img', + imgSelector: 'img', + imgTemplate: () => + html` + + AI generated + `, + }, + ]; + + const leadContainerTemplate = (image: () => TemplateResult): TemplateResult => html` - ${image} + ${image()} The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. @@ -71,36 +121,15 @@ describe(`sbb-lead-container`, () => { `; describeViewports(() => { - it( - 'with sbb-image', - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - leadContainerTemplate( - html``, - ), - wrapperStyles, - ); - - await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); - }), - ); - - it( - 'with img tag', - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - leadContainerTemplate( - html`Station of Lucerne from outside`, - ), - wrapperStyles, - ); + for (const testCase of testCases) { + it( + testCase.title, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(leadContainerTemplate(testCase.imgTemplate), wrapperStyles); - await waitForImageReady(setup.snapshotElement.querySelector('img')!); - }), - ); + await waitForImageReady(setup.snapshotElement.querySelector(testCase.imgSelector)!); + }), + ); + } }); }); diff --git a/src/elements/lead-container/readme.md b/src/elements/lead-container/readme.md index 22291b3951..c675c422fc 100644 --- a/src/elements/lead-container/readme.md +++ b/src/elements/lead-container/readme.md @@ -67,6 +67,21 @@ Full example with applied spacings (CSS classes) in content: ``` +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + +```html + +
+ + ... +
+ ... +
+``` + ## Accessibility Please either define the `alt` attribute of your image or set `aria-hidden="true"` to the image diff --git a/src/elements/message/__snapshots__/message.snapshot.spec.snap.js b/src/elements/message/__snapshots__/message.snapshot.spec.snap.js index bd5bb84d96..bcf94c327d 100644 --- a/src/elements/message/__snapshots__/message.snapshot.spec.snap.js +++ b/src/elements/message/__snapshots__/message.snapshot.spec.snap.js @@ -4,8 +4,6 @@ export const snapshots = {}; snapshots["sbb-message renders DOM"] = ` diff --git a/src/elements/teaser-hero/__snapshots__/teaser-hero.snapshot.spec.snap.js b/src/elements/teaser-hero/__snapshots__/teaser-hero.snapshot.spec.snap.js index 89f896b51e..368976b45f 100644 --- a/src/elements/teaser-hero/__snapshots__/teaser-hero.snapshot.spec.snap.js +++ b/src/elements/teaser-hero/__snapshots__/teaser-hero.snapshot.spec.snap.js @@ -6,21 +6,27 @@ snapshots["sbb-teaser-hero renders DOM"] = accessibility-label="label" data-action="" data-link="" - data-slot-names="chip unnamed" + data-slot-names="image link-content unnamed" href="https://www.sbb.ch" - image-alt="SBB CFF FFS Employee" - link-content="Find out more" - rel="external" - target="_blank" > Break out and explore castles and palaces. - + Find out more + +
- Label - + + + + Label + +
`; /* end snapshot sbb-teaser-hero renders DOM */ @@ -30,11 +36,7 @@ snapshots["sbb-teaser-hero renders Shadow DOM"] = aria-label="label" class="sbb-action-base sbb-teaser-hero" href="https://www.sbb.ch" - rel="external" - target="_blank" > - -

@@ -44,74 +46,92 @@ snapshots["sbb-teaser-hero renders Shadow DOM"] = class="sbb-teaser-hero__panel-link" data-action="" data-sbb-link="" - data-slot-names="unnamed" + data-slot-names="link-content unnamed" icon-name="chevron-small-right-small" icon-placement="end" negative="" size="m" > - Find out more - - - - . Link target opens in a new window. - `; /* end snapshot sbb-teaser-hero renders Shadow DOM */ -snapshots["sbb-teaser-hero renders with slots DOM"] = +snapshots["sbb-teaser-hero renders A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "link", + "name": "label", + "value": "https://www.sbb.ch/" + } + ] +} +

+`; +/* end snapshot sbb-teaser-hero renders A11y tree Firefox */ + +snapshots["sbb-teaser-hero renders A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "link", + "name": "label" + } + ] +} +

+`; +/* end snapshot sbb-teaser-hero renders A11y tree Chrome */ + +snapshots["sbb-teaser-hero renders with img DOM"] = ` Break out and explore castles and palaces. - - Find out more - - - - - Label - + alt + + Label + +
`; -/* end snapshot sbb-teaser-hero renders with slots DOM */ +/* end snapshot sbb-teaser-hero renders with img DOM */ -snapshots["sbb-teaser-hero renders with slots Shadow DOM"] = +snapshots["sbb-teaser-hero renders with img Shadow DOM"] = ` - -

@@ -121,52 +141,23 @@ snapshots["sbb-teaser-hero renders with slots Shadow DOM"] = class="sbb-teaser-hero__panel-link" data-action="" data-sbb-link="" - data-slot-names="link-content unnamed" + data-slot-names="unnamed" icon-name="chevron-small-right-small" icon-placement="end" negative="" size="m" > + Find out more + + . Link target opens in a new window. + `; -/* end snapshot sbb-teaser-hero renders with slots Shadow DOM */ - -snapshots["sbb-teaser-hero renders A11y tree Chrome"] = -`

- { - "role": "WebArea", - "name": "", - "children": [ - { - "role": "link", - "name": "label" - } - ] -} -

-`; -/* end snapshot sbb-teaser-hero renders A11y tree Chrome */ - -snapshots["sbb-teaser-hero renders A11y tree Firefox"] = -`

- { - "role": "document", - "name": "", - "children": [ - { - "role": "link", - "name": "label", - "value": "https://www.sbb.ch/" - } - ] -} -

-`; -/* end snapshot sbb-teaser-hero renders A11y tree Firefox */ +/* end snapshot sbb-teaser-hero renders with img Shadow DOM */ diff --git a/src/elements/teaser-hero/readme.md b/src/elements/teaser-hero/readme.md index 5d77d2d30c..278fd46827 100644 --- a/src/elements/teaser-hero/readme.md +++ b/src/elements/teaser-hero/readme.md @@ -5,23 +5,31 @@ it should be an eye-catcher and should have an emotional effect on the user with It is possible to provide the panel label via an unnamed slot, while the link text can be provided using the `link-content` slot or the `linkContent` property; -similarly, the background image can be provided using the `image` slot or the `imageSrc` property. -Optionally a `sbb-chip` can be slotted in the `chip` slot, either together with the other slottable elements or alone. + +Use the `image` slot to pass an `sbb-image` or an `img` that will be used as background. ```html - + Break out and explore castles and palaces. + Find out more + +``` + +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). +If the `sbb-chip-label` should appear on top of the red panel (e.g. on small screens), you need to set a `z-index` on the `sbb-chip-label`. +Otherwise, it stays behind the red panel. +```html Break out and explore castles and palaces. - Find out more +
+ + + Chip label + +
``` @@ -40,8 +48,6 @@ Avoid slotting block elements (e.g. `div`) as this violates semantic rules and c | `accessibilityLabel` | `accessibility-label` | public | `string` | `''` | This will be forwarded as aria-label to the inner anchor element. | | `download` | `download` | public | `boolean` | `false` | Whether the browser will show the download dialog on click. | | `href` | `href` | public | `string` | `''` | The href value you want to link to. | -| `imageAlt` | `image-alt` | public | `string` | `''` | Image alt text will be passed to `sbb-image`. | -| `imageSrc` | `image-src` | public | `string` | `''` | Image src will be passed to `sbb-image`. | | `linkContent` | `link-content` | public | `string` | `''` | Panel link text. | | `rel` | `rel` | public | `string` | `''` | The relationship of the linked URL as space-separated link types. | | `target` | `target` | public | `LinkTargetType \| string` | `''` | Where to display the linked URL. | diff --git a/src/elements/teaser-hero/teaser-hero.scss b/src/elements/teaser-hero/teaser-hero.scss index 929777a159..8f7c96b17f 100644 --- a/src/elements/teaser-hero/teaser-hero.scss +++ b/src/elements/teaser-hero/teaser-hero.scss @@ -10,17 +10,12 @@ // which appears in normalize css of several frameworks. outline: none !important; - --sbb-teaser-hero-brightness-hover: var(--sbb-hover-image-brightness); - --sbb-teaser-hero-chip-label-inset: var(--sbb-spacing-responsive-xxxs) auto auto - var(--sbb-spacing-responsive-xxxs); - @include sbb.panel-variables; } @include sbb.hover-mq($hover: true) { :host(:hover) { --sbb-panel-background-color: var(--sbb-panel-background-color-hover); - --sbb-teaser-hero-brightness: var(--sbb-teaser-hero-brightness-hover); } } @@ -28,6 +23,7 @@ position: relative; display: block; min-height: var(--sbb-panel-height); + text-decoration: none; // Hide focus outline when focus origin is mouse or touch. This is being used as a workaround in various components. :host(:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) &:focus-visible { @@ -60,16 +56,3 @@ margin: 0; padding: 0; } - -::slotted([slot='image']), -sbb-image { - will-change: filter; - filter: brightness(var(--sbb-teaser-hero-brightness, 1)); - transition: filter var(--sbb-panel-animation-duration) var(--sbb-panel-animation-easing); -} - -::slotted([slot='chip']) { - position: absolute; - inset: var(--sbb-teaser-hero-chip-label-inset); - z-index: 2; -} diff --git a/src/elements/teaser-hero/teaser-hero.snapshot.spec.ts b/src/elements/teaser-hero/teaser-hero.snapshot.spec.ts index 867c2e5dcf..7c584d236e 100644 --- a/src/elements/teaser-hero/teaser-hero.snapshot.spec.ts +++ b/src/elements/teaser-hero/teaser-hero.snapshot.spec.ts @@ -14,23 +14,20 @@ const imageUrl = import.meta.resolve('../core/testing/assets/lucerne.png'); describe(`sbb-teaser-hero`, () => { let element: SbbTeaserHeroElement; - describe('renders', () => { + describe('renders', async () => { beforeEach(async () => { element = await fixture( - html` + html` Break out and explore castles and palaces. - Label + Find out more + +
+ + Label +
`, ); - await waitForImageReady(element.shadowRoot!.querySelector('sbb-image')!); + await waitForImageReady(element.querySelector('sbb-image')!); }); it('DOM', async () => { @@ -38,28 +35,35 @@ describe(`sbb-teaser-hero`, () => { }); it('Shadow DOM', async () => { - await expect(element).shadowDom.to.be.equalSnapshot({ ignoreAttributes: ['image-src'] }); + await expect(element).shadowDom.to.be.equalSnapshot(); }); testA11yTreeSnapshot(); }); - describe('renders with slots', async () => { + describe('renders with img', () => { beforeEach(async () => { element = await fixture( - html` + html` Break out and explore castles and palaces. - Find out more - - - Label + +
+ alt + Label +
`, ); - await waitForImageReady(element.querySelector('sbb-image')!); + await waitForImageReady(element.querySelector('img')!); }); it('DOM', async () => { - await expect(element).dom.to.be.equalSnapshot({ ignoreAttributes: ['image-src'] }); + await expect(element).dom.to.be.equalSnapshot({ ignoreAttributes: ['src'] }); }); it('Shadow DOM', async () => { diff --git a/src/elements/teaser-hero/teaser-hero.spec.ts b/src/elements/teaser-hero/teaser-hero.spec.ts index 7b975100e7..ce02d210af 100644 --- a/src/elements/teaser-hero/teaser-hero.spec.ts +++ b/src/elements/teaser-hero/teaser-hero.spec.ts @@ -4,10 +4,10 @@ import { html } from 'lit/static-html.js'; import type { SbbChipLabelElement } from '../chip-label.js'; import { fixture } from '../core/testing/private.js'; import { waitForLitRender } from '../core/testing.js'; -import type { SbbImageElement } from '../image.js'; import { SbbTeaserHeroElement } from './teaser-hero.js'; import '../chip-label.js'; +import '../image.js'; const imageUrl = import.meta.resolve('../core/testing/assets/lucerne.png'); @@ -15,9 +15,7 @@ describe(`sbb-teaser-hero`, () => { let element: SbbTeaserHeroElement; it('renders', async () => { - element = await fixture( - html``, - ); + element = await fixture(html``); assert.instanceOf(element, SbbTeaserHeroElement); }); @@ -35,15 +33,16 @@ describe(`sbb-teaser-hero`, () => { it('styles slotted components', async () => { element = await fixture( - html` - Label + html` +
+ + Label +
`, ); const chip = element.querySelector('sbb-chip-label')!; - const image = element.shadowRoot!.querySelector('sbb-image')!; expect(chip).to.have.attribute('color', 'charcoal'); - expect(image).to.have.attribute('data-teaser'); }); }); diff --git a/src/elements/teaser-hero/teaser-hero.ssr.spec.ts b/src/elements/teaser-hero/teaser-hero.ssr.spec.ts index 2b1c9c1106..acdbd68664 100644 --- a/src/elements/teaser-hero/teaser-hero.ssr.spec.ts +++ b/src/elements/teaser-hero/teaser-hero.ssr.spec.ts @@ -5,14 +5,21 @@ import images from '../core/images.js'; import { ssrHydratedFixture } from '../core/testing/private.js'; import { SbbTeaserHeroElement } from './teaser-hero.js'; +import '../chip-label.js'; +import '../image.js'; describe(`sbb-teaser-hero ssr`, () => { let root: SbbTeaserHeroElement; beforeEach(async () => { root = await ssrHydratedFixture( - html``, - { modules: ['./teaser-hero.js'] }, + html` +
+ + Label +
+
`, + { modules: ['./teaser-hero.js', '../image.js', '../chip-label.js'] }, ); }); diff --git a/src/elements/teaser-hero/teaser-hero.stories.ts b/src/elements/teaser-hero/teaser-hero.stories.ts index 8a1389fda5..3e7c23dc37 100644 --- a/src/elements/teaser-hero/teaser-hero.stories.ts +++ b/src/elements/teaser-hero/teaser-hero.stories.ts @@ -10,6 +10,7 @@ import sampleImages from '../core/images.js'; import readme from './readme.md?raw'; import './teaser-hero.js'; import '../chip-label.js'; +import '../image.js'; const accessibilityLabel: InputType = { control: { @@ -75,12 +76,6 @@ const imageSrc: InputType = { }, }; -const imageAlt: InputType = { - control: { - type: 'text', - }, -}; - const chipLabel: InputType = { control: { type: 'text', @@ -95,7 +90,6 @@ const defaultArgTypes: ArgTypes = { content, 'link-content': linkContent, 'image-src': imageSrc, - 'image-alt': imageAlt, 'chip-label': chipLabel, }; @@ -107,40 +101,35 @@ const defaultArgs: Args = { content: 'Break out and explore castles and palaces.', 'link-content': 'Find out more', 'image-src': sampleImages[1], - 'image-alt': 'SBB CFF FFS Employee', 'chip-label': undefined, }; -const chipLabelTemplate = (content: string): TemplateResult => html` - ${content} -`; - -const TemplateSbbTeaserHeroDefault = ({ +const Template = ({ content, 'chip-label': chipLabel, - ...args -}: Args): TemplateResult => html` - ${content} ${chipLabel ? chipLabelTemplate(chipLabel) : nothing} - -`; - -const TemplateSbbTeaserWithSlots = ({ - content, 'link-content': linkContent, 'image-src': imageSrc, 'image-alt': imageAlt, ...args }: Args): TemplateResult => html` - ${content} - ${linkContent} - + ${content ?? nothing} + ${linkContent ? html`${linkContent}` : nothing} + ${!chipLabel + ? html`` + : html` +
+ + + ${chipLabel} + +
+ `}
`; export const defaultTeaser: StoryObj = { - render: TemplateSbbTeaserHeroDefault, + render: Template, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -148,7 +137,7 @@ export const defaultTeaser: StoryObj = { }; export const openInNewWindow: StoryObj = { - render: TemplateSbbTeaserHeroDefault, + render: Template, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -157,7 +146,7 @@ export const openInNewWindow: StoryObj = { }; export const withChip: StoryObj = { - render: TemplateSbbTeaserHeroDefault, + render: Template, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -166,7 +155,7 @@ export const withChip: StoryObj = { }; export const chipOnly: StoryObj = { - render: TemplateSbbTeaserHeroDefault, + render: Template, argTypes: defaultArgTypes, args: { ...defaultArgs, @@ -177,14 +166,6 @@ export const chipOnly: StoryObj = { }, }; -export const withSlots: StoryObj = { - render: TemplateSbbTeaserWithSlots, - argTypes: defaultArgTypes, - args: { - ...defaultArgs, - }, -}; - const meta: Meta = { decorators: [withActions as Decorator], parameters: { diff --git a/src/elements/teaser-hero/teaser-hero.ts b/src/elements/teaser-hero/teaser-hero.ts index 33440f6383..d18313046a 100644 --- a/src/elements/teaser-hero/teaser-hero.ts +++ b/src/elements/teaser-hero/teaser-hero.ts @@ -7,7 +7,6 @@ import { forceType, omitEmptyConverter, slotState } from '../core/decorators.js' import style from './teaser-hero.scss?lit&inline'; -import '../image.js'; import '../link/block-link-static.js'; /** @@ -29,23 +28,12 @@ class SbbTeaserHeroElement extends SbbLinkBaseElement { @property({ attribute: 'link-content', reflect: true, converter: omitEmptyConverter }) public accessor linkContent: string = ''; - /** Image src will be passed to `sbb-image`. */ - @forceType() - @property({ attribute: 'image-src' }) - public accessor imageSrc: string = ''; - - /** Image alt text will be passed to `sbb-image`. */ - @forceType() - @property({ attribute: 'image-alt' }) - public accessor imageAlt: string = ''; - - private _chipSlotChanged(): void { - this.querySelector('sbb-chip-label')?.setAttribute('color', 'charcoal'); + private _imageSlotChanged(): void { + Array.from(this.querySelectorAll('sbb-chip-label')).forEach((c) => (c.color = 'charcoal')); } protected override renderTemplate(): TemplateResult { return html` - this._chipSlotChanged()}>

@@ -62,11 +50,7 @@ class SbbTeaserHeroElement extends SbbLinkBaseElement { ` : nothing} - - ${this.imageSrc - ? html`` - : nothing} - + `; } } diff --git a/src/elements/teaser-hero/teaser-hero.visual.spec.ts b/src/elements/teaser-hero/teaser-hero.visual.spec.ts index 8e44debe36..5423b4eeee 100644 --- a/src/elements/teaser-hero/teaser-hero.visual.spec.ts +++ b/src/elements/teaser-hero/teaser-hero.visual.spec.ts @@ -2,6 +2,7 @@ import { html } from 'lit'; import { describeViewports, + loadAssetAsBase64, visualDiffDefault, visualDiffFocus, visualDiffHover, @@ -12,6 +13,7 @@ import '../image.js'; import '../chip-label.js'; const imageUrl = import.meta.resolve('../core/testing/assets/placeholder-image.png'); +const imageBase64 = await loadAssetAsBase64(imageUrl); describe(`sbb-teaser-hero`, () => { describeViewports({ viewports: ['zero', 'micro', 'small', 'medium', 'wide'] }, () => { @@ -20,17 +22,19 @@ describe(`sbb-teaser-hero`, () => { state.name, state.with(async (setup) => { await setup.withFixture(html` - + Break out and explore castles and palaces. - Label + +

+ + + Label + +
`); - await waitForImageReady( - setup.snapshotElement - .querySelector('sbb-teaser-hero')! - .shadowRoot!.querySelector('sbb-image')!, - ); + await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); @@ -42,7 +46,6 @@ describe(`sbb-teaser-hero`, () => { Break out and explore castles and palaces. Find out more - Label
`); @@ -50,20 +53,31 @@ describe(`sbb-teaser-hero`, () => { }), ); + it( + `slotted-image ${state.name}`, + state.with(async (setup) => { + await setup.withFixture(html` + + Break out and explore castles and palaces. + Find out more + + + `); + + await waitForImageReady(setup.snapshotElement.querySelector('img')!); + }), + ); + it( `without content ${state.name}`, state.with(async (setup) => { await setup.withFixture(html` - - Label + + `); - await waitForImageReady( - setup.snapshotElement - .querySelector('sbb-teaser-hero')! - .shadowRoot!.querySelector('sbb-image')!, - ); + await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); } diff --git a/src/elements/teaser-product/common/teaser-product-common.scss b/src/elements/teaser-product/common/teaser-product-common.scss index 3168cdd3f2..cb9c0caec8 100644 --- a/src/elements/teaser-product/common/teaser-product-common.scss +++ b/src/elements/teaser-product/common/teaser-product-common.scss @@ -51,21 +51,6 @@ } } -::slotted(img) { - display: flex; - width: 100%; - height: 100%; - object-fit: cover; - aspect-ratio: 16 / 9; -} - -// Reset sbb-image border radius in order to control it from teaser product. -::slotted(sbb-image) { - --sbb-image-border-radius: 0; - - height: 100%; -} - ::slotted(p.sbb-teaser-product--spacing) { margin: 0; } @@ -78,6 +63,12 @@ margin-block-start: var(--sbb-spacing-responsive-xxs); } +::slotted([slot='image']) { + display: flex; + width: 100%; + height: 100%; +} + .sbb-teaser-product__root { @include sbb.if-forced-colors { // Apply a visual border for forced color mode diff --git a/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js b/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js index eae0b1d4d2..e82c65f49c 100644 --- a/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js +++ b/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js @@ -7,12 +7,13 @@ snapshots["sbb-teaser-product-static renders DOM"] = data-slot-names="footnote image unnamed" image-alignment="after" > - - + + +

Content

diff --git a/src/elements/teaser-product/teaser-product-static/readme.md b/src/elements/teaser-product/teaser-product-static/readme.md index 554f896ea4..ff73fa8ea5 100644 --- a/src/elements/teaser-product/teaser-product-static/readme.md +++ b/src/elements/teaser-product/teaser-product-static/readme.md @@ -15,15 +15,21 @@ otherwise, see [sbb-teaser-product](/docs/elements-sbb-teaser-sbb-teaser-product ## Slots -Use the `image` slot to pass a `sbb-image` or an `img` that will be used as a background, -and use the optional `footnote` slot to add a text anchored to the bottom-end of the component. +Use the `image` slot to pass an `sbb-image` or an `img` that will be used as background. +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + +Use the optional `footnote` slot to add a text anchored to the bottom-end of the component. The default slot is reserved for the main content: it could be a simple text or a text combined with more elements, like a `sbb-title` or some interactive elements, like buttons or links within the `sbb-action-group` component. ```html - +
+ + Chip label +
+

Content ...

``` diff --git a/src/elements/teaser-product/teaser-product-static/teaser-product-static.snapshot.spec.ts b/src/elements/teaser-product/teaser-product-static/teaser-product-static.snapshot.spec.ts index 546242e430..92b50b86ea 100644 --- a/src/elements/teaser-product/teaser-product-static/teaser-product-static.snapshot.spec.ts +++ b/src/elements/teaser-product/teaser-product-static/teaser-product-static.snapshot.spec.ts @@ -16,7 +16,9 @@ describe(`sbb-teaser-product-static`, () => { beforeEach(async () => { element = await fixture(html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product-static/teaser-product-static.spec.ts b/src/elements/teaser-product/teaser-product-static/teaser-product-static.spec.ts index 95fc01af24..5438de529f 100644 --- a/src/elements/teaser-product/teaser-product-static/teaser-product-static.spec.ts +++ b/src/elements/teaser-product/teaser-product-static/teaser-product-static.spec.ts @@ -15,7 +15,9 @@ describe('sbb-teaser-product-static', () => { beforeEach(async () => { element = await fixture(html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product-static/teaser-product-static.ssr.spec.ts b/src/elements/teaser-product/teaser-product-static/teaser-product-static.ssr.spec.ts index 9e943fdd5b..2299d4cf19 100644 --- a/src/elements/teaser-product/teaser-product-static/teaser-product-static.ssr.spec.ts +++ b/src/elements/teaser-product/teaser-product-static/teaser-product-static.ssr.spec.ts @@ -16,7 +16,9 @@ describe(`sbb-teaser-product-static ssr`, () => { root = await ssrHydratedFixture( html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product-static/teaser-product-static.stories.ts b/src/elements/teaser-product/teaser-product-static/teaser-product-static.stories.ts index d16eb4b64f..8deccd21b1 100644 --- a/src/elements/teaser-product/teaser-product-static/teaser-product-static.stories.ts +++ b/src/elements/teaser-product/teaser-product-static/teaser-product-static.stories.ts @@ -19,6 +19,7 @@ import './teaser-product-static.js'; import '../../action-group.js'; import '../../button/button.js'; import '../../button/secondary-button.js'; +import '../../chip-label.js'; import '../../image.js'; import '../../title.js'; @@ -94,6 +95,25 @@ const Template = ({ withFooter, slottedImg, ...args }: Args): TemplateResult => `; +const WithChipTemplate = ({ withFooter, slottedImg, ...args }: Args): TemplateResult => html` + +
+ ${slottedImg + ? html`` + : html``} + + + AI generated + +
+ ${content()} ${withFooter ? footer() : nothing} +
+`; + export const Default: StoryObj = { render: Template, argTypes: defaultArgTypes, @@ -124,6 +144,12 @@ export const SlottedImg: StoryObj = { args: { ...defaultArgs, slottedImg: true }, }; +export const WithChip: StoryObj = { + render: WithChipTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + const meta: Meta = { decorators: [withActions as Decorator], parameters: { diff --git a/src/elements/teaser-product/teaser-product-static/teaser-product-static.visual.spec.ts b/src/elements/teaser-product/teaser-product-static/teaser-product-static.visual.spec.ts index 3172c79db7..dd5c87255c 100644 --- a/src/elements/teaser-product/teaser-product-static/teaser-product-static.visual.spec.ts +++ b/src/elements/teaser-product/teaser-product-static/teaser-product-static.visual.spec.ts @@ -12,6 +12,7 @@ import './teaser-product-static.js'; import '../../action-group.js'; import '../../button/button.js'; import '../../button/secondary-button.js'; +import '../../chip-label.js'; import '../../image.js'; import '../../title.js'; @@ -60,7 +61,36 @@ const template = ({ ${slottedImg ? html`` - : html``} + : html``} + ${content(longContent)} ${showFooter ? footer() : nothing} + +`; + +const withChipTemplate = ({ + negative, + imageAlignment, + showFooter, + slottedImg, + longContent, +}: { + negative?: boolean; + imageAlignment?: string; + showFooter?: boolean; + slottedImg?: boolean; + longContent?: boolean; +} = {}): TemplateResult => html` + +
+ ${slottedImg + ? html`` + : html``} + Label +
${content(longContent)} ${showFooter ? footer() : nothing}
`; @@ -83,6 +113,21 @@ describe('sbb-teaser-product-static', () => { ); }), ); + + it( + `withChip_${visualState.name}`, + visualState.with(async (setup) => { + await setup.withFixture( + withChipTemplate({ negative, showFooter: true, slottedImg }), + { + backgroundColor: negative ? 'var(--sbb-color-black)' : undefined, + }, + ); + await waitForImageReady( + setup.snapshotElement.querySelector(slottedImg ? 'img' : 'sbb-image')!, + ); + }), + ); } }); } diff --git a/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js b/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js index 09cd442e4c..b6e4cb2d4b 100644 --- a/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js +++ b/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js @@ -9,12 +9,13 @@ snapshots["sbb-teaser-product renders DOM"] = href="https://www.sbb.ch" image-alignment="after" > - - + + +

Content

diff --git a/src/elements/teaser-product/teaser-product/readme.md b/src/elements/teaser-product/teaser-product/readme.md index 6166250124..6d170b7953 100644 --- a/src/elements/teaser-product/teaser-product/readme.md +++ b/src/elements/teaser-product/teaser-product/readme.md @@ -16,15 +16,21 @@ If it has to include more than one interactive element, use the [sbb-teaser-prod ## Slots -Use the `image` slot to pass a `sbb-image` or an `img` that will be used as a background, -and use the optional `footnote` slot to add a text anchored to the bottom-end of the component. +Use the `image` slot to pass an `sbb-image` or an `img` that will be used as background. +Optionally, you can add an overlapping `sbb-chip-label` by wrapping the `sbb-image` in a `figure` tag (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + +Use the optional `footnote` slot to add a text anchored to the bottom-end of the component. The default slot is reserved for the main content: it could be a simple text or a text combined with more elements, like the `sbb-title` or an interactive element, like a button or a link (needs to be in static variant!). ```html - +
+ + Chip label +
+

Content ...

``` diff --git a/src/elements/teaser-product/teaser-product/teaser-product.scss b/src/elements/teaser-product/teaser-product/teaser-product.scss index 5cc06d1cd4..3f48795931 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.scss +++ b/src/elements/teaser-product/teaser-product/teaser-product.scss @@ -1,12 +1,6 @@ @use '../../core/styles' as sbb; :host { - --sbb-teaser-product-brightness-hover: var(--sbb-hover-image-brightness); - --sbb-teaser-product-animation-duration: var( - --sbb-disable-animation-duration, - var(--sbb-animation-duration-4x) - ); - --sbb-teaser-product-animation-easing: var(--sbb-animation-easing); --sbb-teaser-product-border-radius: var(--sbb-border-radius-4x); // Simulate link color optically @@ -17,20 +11,6 @@ } } -:host(:hover) { - @include sbb.hover-mq($hover: true) { - --sbb-teaser-product-brightness: var(--sbb-teaser-product-brightness-hover); - } -} - -::slotted(:is(img, sbb-image)) { - will-change: filter; - transition-property: filter; - transition-duration: var(--sbb-teaser-product-animation-duration); - transition-timing-function: var(--sbb-animation-easing); - filter: brightness(var(--sbb-teaser-product-brightness, 1)); -} - .sbb-teaser-product__wrapper { position: relative; } diff --git a/src/elements/teaser-product/teaser-product/teaser-product.snapshot.spec.ts b/src/elements/teaser-product/teaser-product/teaser-product.snapshot.spec.ts index 4e75eccf7c..5eea0dc29a 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.snapshot.spec.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.snapshot.spec.ts @@ -16,7 +16,9 @@ describe(`sbb-teaser-product`, () => { beforeEach(async () => { element = await fixture(html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product/teaser-product.spec.ts b/src/elements/teaser-product/teaser-product/teaser-product.spec.ts index d2504d5d82..9dbba53e38 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.spec.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.spec.ts @@ -14,7 +14,9 @@ describe('sbb-teaser-product', () => { beforeEach(async () => { element = await fixture(html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product/teaser-product.ssr.spec.ts b/src/elements/teaser-product/teaser-product/teaser-product.ssr.spec.ts index ff856d0b06..ac2db8687b 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.ssr.spec.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.ssr.spec.ts @@ -16,7 +16,9 @@ describe(`sbb-teaser-product ssr`, () => { root = await ssrHydratedFixture( html` - +
+ +

Content

Footnote

diff --git a/src/elements/teaser-product/teaser-product/teaser-product.stories.ts b/src/elements/teaser-product/teaser-product/teaser-product.stories.ts index 2f02fd0eb6..ac5f7c8317 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.stories.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.stories.ts @@ -17,6 +17,7 @@ import readme from './readme.md?raw'; import './teaser-product.js'; import '../../button/button-static.js'; +import '../../chip-label.js'; import '../../image.js'; import '../../title.js'; @@ -111,6 +112,24 @@ const Template = ({ withFooter, slottedImg, ...args }: Args): TemplateResult => `; +const WithChipTemplate = ({ withFooter, slottedImg, ...args }: Args): TemplateResult => html` + +
+ ${slottedImg + ? html`` + : html``} + + AI generated + +
+ ${content()} ${withFooter ? footer() : nothing} +
+`; + export const Default: StoryObj = { render: Template, argTypes: defaultArgTypes, @@ -141,6 +160,12 @@ export const SlottedImg: StoryObj = { args: { ...defaultArgs, slottedImg: true }, }; +export const WithChip: StoryObj = { + render: WithChipTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + const meta: Meta = { decorators: [withActions as Decorator], parameters: { diff --git a/src/elements/teaser-product/teaser-product/teaser-product.visual.spec.ts b/src/elements/teaser-product/teaser-product/teaser-product.visual.spec.ts index 5484b3501e..cedb15c592 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.visual.spec.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.visual.spec.ts @@ -11,6 +11,7 @@ import { waitForImageReady } from '../../core/testing/wait-for-image-ready.js'; import './teaser-product.js'; import '../../button/button-static.js'; +import '../../chip-label.js'; import '../../image.js'; import '../../title.js'; @@ -61,6 +62,35 @@ const template = ({ `; +const withChipTemplate = ({ + negative, + imageAlignment, + showFooter, + slottedImg, + longContent, +}: { + negative?: boolean; + imageAlignment?: string; + showFooter?: boolean; + slottedImg?: boolean; + longContent?: boolean; +} = {}): TemplateResult => html` + +
+ ${slottedImg + ? html`` + : html``} + Label +
+ ${content(longContent)} ${showFooter ? footer() : nothing} +
+`; + describe('sbb-teaser-product', () => { describeViewports({ viewports: ['zero', 'medium', 'large'] }, () => { for (const slottedImg of [false, true]) { @@ -79,6 +109,21 @@ describe('sbb-teaser-product', () => { ); }), ); + + it( + `withChip_${visualState.name}`, + visualState.with(async (setup) => { + await setup.withFixture( + withChipTemplate({ negative, showFooter: true, slottedImg }), + { + backgroundColor: negative ? 'var(--sbb-color-black)' : undefined, + }, + ); + await waitForImageReady( + setup.snapshotElement.querySelector(slottedImg ? 'img' : 'sbb-image')!, + ); + }), + ); } }); } diff --git a/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js b/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js index e952365c64..4678d9f94d 100644 --- a/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js +++ b/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js @@ -125,11 +125,15 @@ snapshots["sbb-teaser renders below with projected content DOM"] = data-slot-names="chip image title unnamed" href="https://github.com/sbb-design-systems/lyne-components" > - 400x300 + 400x300 + Chip diff --git a/src/elements/teaser/readme.md b/src/elements/teaser/readme.md index 006ec28d63..4804cea8f7 100644 --- a/src/elements/teaser/readme.md +++ b/src/elements/teaser/readme.md @@ -9,7 +9,9 @@ Simple teaser example: title-content="Title" chip-content="Chip label" > - 400x300 +
+ 400x300 +
A brief description. ``` @@ -19,9 +21,15 @@ Simple teaser example: The default slot is reserved for the description. The component displays the `image` and the `title` with the self-named slots. It's also possible to display a [sbb-chip-label](/docs/elements-sbb-chip-label--docs) using the `chip` slot. +Use the `image` slot to pass a `figure` containing an `sbb-image` or an `img` that will be used as background. +Optionally, you can add an overlapping `sbb-chip-label` to the slotted `figure` (see [sbb-image doc](/docs/elements-sbb-image--docs#utility%classes)). + ```html - 400x300 +
+ 400x300 + AI Generated +
Chip label Title A brief description. diff --git a/src/elements/teaser/teaser.scss b/src/elements/teaser/teaser.scss index 8afb537787..8cc5dceaaf 100644 --- a/src/elements/teaser/teaser.scss +++ b/src/elements/teaser/teaser.scss @@ -17,11 +17,6 @@ --sbb-teaser-gap: var(--sbb-spacing-fixed-4x); --sbb-teaser-width: fit-content; --sbb-teaser-border-radius: var(--sbb-border-radius-4x); - --sbb-teaser-brightness-hover: var(--sbb-hover-image-brightness); - --sbb-teaser-animation-duration: var( - --sbb-disable-animation-duration, - var(--sbb-animation-duration-4x) - ); @include sbb.if-forced-colors { --sbb-teaser-description-color: LinkText; @@ -40,6 +35,12 @@ --sbb-teaser-width: 100%; } +@include sbb.hover-mq($hover: true) { + :host(:hover) { + --sbb-teaser-scale: var(--sbb-teaser-scale-hover); + } +} + .sbb-teaser__wrapper { display: flex; position: relative; @@ -88,27 +89,6 @@ ::slotted([slot='image']) { width: #{sbb.px-to-rem-build(300)}; - will-change: transform; - display: block; - filter: brightness(var(--sbb-teaser-brightness, 1)); - transition: var(--sbb-teaser-animation-duration) var(--sbb-animation-easing); - - @include sbb.hover-mq($hover: true) { - .sbb-teaser__wrapper:hover & { - transform: scale(var(--sbb-teaser-scale-hover)); - - --sbb-teaser-brightness: var(--sbb-teaser-brightness-hover); - } - } -} - -::slotted(sbb-image[slot='image']) { - --sbb-image-aspect-ratio: 4 / 3; -} - -::slotted(img[slot='image']) { - aspect-ratio: 4/3; - object-fit: cover; } .sbb-teaser__image-wrapper { diff --git a/src/elements/teaser/teaser.snapshot.spec.ts b/src/elements/teaser/teaser.snapshot.spec.ts index 9ed4f1c472..696efb458b 100644 --- a/src/elements/teaser/teaser.snapshot.spec.ts +++ b/src/elements/teaser/teaser.snapshot.spec.ts @@ -62,7 +62,9 @@ describe(`sbb-teaser`, () => { accessibility-label="SBB teaser" alignment="below" > - 400x300 +
+ 400x300 +
Chip TITLE description diff --git a/src/elements/teaser/teaser.stories.ts b/src/elements/teaser/teaser.stories.ts index fec1a00ad2..e8a9895468 100644 --- a/src/elements/teaser/teaser.stories.ts +++ b/src/elements/teaser/teaser.stories.ts @@ -10,6 +10,7 @@ import images from '../core/images.js'; import placeholderImage from './assets/placeholder.png'; import readme from './readme.md?raw'; +import '../chip-label.js'; import '../image.js'; import './teaser.js'; @@ -89,7 +90,10 @@ const defaultArgs: Args = { const TemplateDefault = ({ description, ...remainingArgs }: Args): TemplateResult => { return html` - 400x300 +
+ 400x300 + AI Generated +
${description}
`; @@ -98,7 +102,9 @@ const TemplateDefault = ({ description, ...remainingArgs }: Args): TemplateResul const TemplateDefaultFixedWidth = ({ description, ...remainingArgs }: Args): TemplateResult => { return html` - 400x300 +
+ 400x300 +
${description}
`; @@ -107,12 +113,9 @@ const TemplateDefaultFixedWidth = ({ description, ...remainingArgs }: Args): Tem const TemplateCustom = ({ description, ...remainingArgs }: Args): TemplateResult => { return html` - 200x100 +
+ 200x100 +
${description}
`; @@ -126,7 +129,9 @@ const TemplateSlots = ({ }: Args): TemplateResult => { return html` - 400x300 +
+ 400x300 +
${chipContent} ${titleContent} ${description} @@ -149,12 +154,9 @@ const TemplateGrid = ({ description, ...remainingArgs }: Args): TemplateResult = new Array(4), () => html` - +
+ +
${description}
`, diff --git a/src/elements/teaser/teaser.visual.spec.ts b/src/elements/teaser/teaser.visual.spec.ts index 842128c86a..1c79c87128 100644 --- a/src/elements/teaser/teaser.visual.spec.ts +++ b/src/elements/teaser/teaser.visual.spec.ts @@ -52,7 +52,9 @@ describe(`sbb-teaser`, () => { await setup.withFixture( html` - +
+ +
This is a paragraph
`, @@ -75,7 +77,14 @@ describe(`sbb-teaser`, () => { alignment=${alignment} chip-content=${hasChip ? 'This is a chip.' : nothing} > - +
+ + ${hasChip + ? html`AI chip` + : nothing} +
${withLongContent ? loremIpsum : 'This is a paragraph'}
`, @@ -98,7 +107,9 @@ describe(`sbb-teaser`, () => { alignment=${alignment} chip-content=${longChip} > - +
+ +
This is a paragraph
`, @@ -123,7 +134,9 @@ describe(`sbb-teaser`, () => { href="#" alignment=${alignment} > - +
+ +
This is the paragraph n.${i + 1} @@ -155,7 +168,12 @@ describe(`sbb-teaser`, () => { alignment="below" style="--sbb-teaser-align-items: stretch;" > - +
+ + AI chip +
This is a paragraph `, diff --git a/src/storybook/pages/home/home.common.ts b/src/storybook/pages/home/home.common.ts index 5a72f68418..fcd93e260b 100644 --- a/src/storybook/pages/home/home.common.ts +++ b/src/storybook/pages/home/home.common.ts @@ -8,6 +8,7 @@ import '../../../elements/clock.js'; import '../../../elements/divider.js'; import '../../../elements/footer.js'; import '../../../elements/icon.js'; +import '../../../elements/image.js'; import '../../../elements/header.js'; import '../../../elements/logo.js'; import '../../../elements/link.js'; @@ -206,13 +207,12 @@ export const liberoProduct = (): TemplateResult => html` export const teaserHero = (): TemplateResult => html`
- + Considerate with SBB Green Class. +
`;