diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.scss b/src/elements/flip-card/flip-card-details/flip-card-details.scss index 6b7b02b25f..2d8343758e 100644 --- a/src/elements/flip-card/flip-card-details/flip-card-details.scss +++ b/src/elements/flip-card/flip-card-details/flip-card-details.scss @@ -19,18 +19,16 @@ } .sbb-flip-card-details--wrapper { - display: grid; - grid-template-rows: 0fr; + position: absolute; pointer-events: none; color: var(--sbb-color-milk); opacity: var(--sbb-flip-card-details-opacity); transform: translateY(var(--sbb-flip-card-details-translate-y)); - transition-property: grid-template-rows, transform, opacity; + transition-property: transform, opacity; transition-duration: var(--sbb-flip-card-details-transition-duration); :host([data-flipped]) & { - grid-template-rows: 1fr; - transition-delay: 0s, var(--sbb-flip-card-details-transition-delay), + transition-delay: var(--sbb-flip-card-details-transition-delay), var(--sbb-flip-card-details-transition-delay); } } diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.visual.spec.ts b/src/elements/flip-card/flip-card-details/flip-card-details.visual.spec.ts deleted file mode 100644 index c916225c13..0000000000 --- a/src/elements/flip-card/flip-card-details/flip-card-details.visual.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { html } from 'lit'; - -import { describeViewports, visualDiffDefault } from '../../core/testing/private.js'; - -import './flip-card-details.js'; -import '../../link.js'; - -describe(`sbb-flip-card-details`, () => { - describeViewports({ viewports: ['medium'] }, () => { - it( - visualDiffDefault.name, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html` - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam luctus ornare - condimentum. Vivamus turpis elit, dapibus eget fringilla pellentesque, lobortis in - nibh. Duis dapibus vitae tortor ullamcorper maximus. In convallis consectetur felis. - Link - - `, - { - backgroundColor: 'var(--sbb-color-midnight)', - }, - ); - }), - ); - }); -}); 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 a5d9df6b27..64699826c4 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 @@ -12,13 +12,13 @@ .sbb-flip-card-summary { display: grid; - position: absolute; + min-height: var(--sbb-flip-card-min-height); pointer-events: var(--sbb-flip-card-summary-pointer-events); opacity: var(--sbb-flip-card-summary-opacity); border-radius: var(--sbb-flip-card-border-radius); overflow: hidden; - grid-template-columns: 1fr; - grid-template-rows: auto 1fr; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); width: 100%; height: 100%; transition: all var(--sbb-flip-card-summary-transition-duration) ease-out; @@ -26,12 +26,12 @@ :host([image-alignment='after']) & { @include sbb.mq($from: small) { - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: 1fr; } @include sbb.mq($from: medium) { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: 1fr; } } @@ -61,7 +61,6 @@ } .sbb-flip-card-summary--image-wrapper { - max-height: var(--sbb-flip-card-min-height); grid-area: 2 / 1 / 3 / 2; :host([image-alignment='after']) & { diff --git a/src/elements/flip-card/flip-card/flip-card.scss b/src/elements/flip-card/flip-card/flip-card.scss index 7b92c5b2cf..745afa79fc 100644 --- a/src/elements/flip-card/flip-card/flip-card.scss +++ b/src/elements/flip-card/flip-card/flip-card.scss @@ -75,10 +75,16 @@ min-height: var(--sbb-flip-card-min-height); background-color: var(--sbb-flip-card-background-color); border-radius: var(--sbb-flip-card-border-radius); - transition: var(--sbb-flip-card-summary-transition-duration) ease-out; + transition: + all var(--sbb-flip-card-summary-transition-duration) ease-out, + min-height var(--sbb-flip-card-details-transition-duration); transition-delay: var(--sbb-flip-card-summary-transition-delay); cursor: pointer; + :host([data-flipped]) & { + min-height: max(var(--sbb-flip-card-min-height), var(--sbb-flip-card-details-height)); + } + @include sbb.if-forced-colors { &::after { content: ''; diff --git a/src/elements/flip-card/flip-card/flip-card.stories.ts b/src/elements/flip-card/flip-card/flip-card.stories.ts index 3062040609..101635a45f 100644 --- a/src/elements/flip-card/flip-card/flip-card.stories.ts +++ b/src/elements/flip-card/flip-card/flip-card.stories.ts @@ -3,6 +3,7 @@ import type { InputType } from '@storybook/types'; import type { Args, ArgTypes, Meta, StoryObj, Decorator } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html, nothing } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; import sampleImages from '../../core/images.js'; @@ -107,6 +108,39 @@ const LongContentTemplate = (args: Args): TemplateResult => `; +const GridTemplate = (args: Args): TemplateResult => + html`
+ + ${cardSummary(args.label, args.imageAlignment, true)} ${cardDetails()} + + + ${cardSummary(args.label, args.imageAlignment, true)} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam luctus ornare condimentum. + Vivamus turpis elit, dapibus eget fringilla pellentesque, lobortis in nibh. Duis dapibus + vitae tortor ullamcorper maximus. In convallis consectetur felis. Lorem ipsum dolor sit + amet, consectetur adipiscing elit. Nam luctus ornare condimentum. Vivamus turpis elit, + dapibus eget fringilla pellentesque, lobortis in nibh. Duis dapibus vitae tortor ullamcorper + maximus. In convallis consectetur felis. + Link + + + + ${cardSummary(args.label, args.imageAlignment, true)} ${cardDetails()} + + + ${cardSummary(args.label, args.imageAlignment, true)} ${cardDetails()} + +
`; + export const ImageAfter: StoryObj = { render: DefaultTemplate, argTypes: defaultArgTypes, @@ -140,6 +174,12 @@ export const LongTitle: StoryObj = { }, }; +export const Grid: StoryObj = { + render: GridTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs, imageAlignment: imageAlignment.options![1] }, +}; + const meta: Meta = { decorators: [ (story) => html`
${story()}
`, diff --git a/src/elements/flip-card/flip-card/flip-card.ts b/src/elements/flip-card/flip-card/flip-card.ts index e7badb1317..c225fa1f46 100644 --- a/src/elements/flip-card/flip-card/flip-card.ts +++ b/src/elements/flip-card/flip-card/flip-card.ts @@ -1,3 +1,4 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, isServer, LitElement, type TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { until } from 'lit/directives/until.js'; @@ -52,6 +53,11 @@ class SbbFlipCardElement extends SbbHydrationMixin(LitElement) { return this.querySelector?.('sbb-flip-card-details'); } + /** Returns the card details content element wrapper. */ + private get _detailsContentElement(): HTMLElement | null { + return this.details!.shadowRoot!.firstElementChild as HTMLElement; + } + /** Whether the flip card is flipped. */ public get isFlipped(): boolean { return this._flipped; @@ -62,6 +68,11 @@ class SbbFlipCardElement extends SbbHydrationMixin(LitElement) { private _abort = new SbbConnectedAbortController(this); private _language = new SbbLanguageController(this); + private _cardDetailsResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this._setCardDetailsHeight(), + }); public override connectedCallback(): void { super.connectedCallback(); @@ -82,6 +93,12 @@ class SbbFlipCardElement extends SbbHydrationMixin(LitElement) { /** Toggles the state of the sbb-flip-card. */ public toggle(): void { this._flipped = !this._flipped; + if (this._flipped) { + this._setCardDetailsHeight(); + this._cardDetailsResizeObserver.observe(this._detailsContentElement!); + } else { + this._cardDetailsResizeObserver.unobserve(this._detailsContentElement!); + } this.toggleAttribute('data-flipped', this._flipped); this.details!.toggleAttribute('data-flipped', this._flipped); this.summary!.inert = this._flipped; @@ -89,6 +106,11 @@ class SbbFlipCardElement extends SbbHydrationMixin(LitElement) { this.flip.emit(); } + private _setCardDetailsHeight(): any { + const contentHeight = Math.floor(this._detailsContentElement!.offsetHeight); + this.style?.setProperty('--sbb-flip-card-details-height', `${contentHeight}px`); + } + private async _accessibilityLabel(): Promise { if (isServer) { return ''; 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 2fe8894c54..4b4cc92a4c 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 @@ -1,4 +1,5 @@ import { html, nothing, type TemplateResult } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; import { describeViewports, @@ -9,6 +10,8 @@ import { import { waitForImageReady } from '../../core/testing/wait-for-image-ready.js'; import type { SbbFlipCardImageAlignment } from '../flip-card-summary.js'; +import type { SbbFlipCardElement } from './flip-card.js'; + import './flip-card.js'; import '../flip-card-summary.js'; import '../flip-card-details.js'; @@ -22,13 +25,12 @@ const content = ( title: string = 'Summary', imageAlignment: SbbFlipCardImageAlignment = 'after', longContent: boolean = false, - flipped = false, ): TemplateResult => html` ${title} - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam luctus ornare condimentum. Vivamus turpis elit, dapibus eget fringilla pellentesque, lobortis in nibh. ${longContent @@ -68,10 +70,13 @@ describe(`sbb-flip-card`, () => { 'flipped', visualDiffDefault.with(async (setup) => { await setup.withFixture( - html` - ${content('Summary', 'after', false, true)}`, + html` ${content('Summary', 'after', false)}`, ); + setup.withPostSetupAction(() => { + const flipCard = + setup.snapshotElement.querySelector('sbb-flip-card')!; + flipCard.click(); + }); await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); @@ -104,6 +109,44 @@ describe(`sbb-flip-card`, () => { ); } + for (const imageAlignment of ['after', 'below']) { + it( + `grid`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html`
+ + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} + + + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} + + + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} + + + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment, true)} + +
`, + ); + setup.withPostSetupAction(() => { + const flipCard = + setup.snapshotElement.querySelector('sbb-flip-card')!; + flipCard.click(); + }); + await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); + }), + ); + } + describe('forcedColors=true', () => { for (const state of [visualDiffDefault, visualDiffHover, visualDiffFocus]) { it( diff --git a/src/elements/flip-card/flip-card/readme.md b/src/elements/flip-card/flip-card/readme.md index 44483bc260..5591b7fd17 100644 --- a/src/elements/flip-card/flip-card/readme.md +++ b/src/elements/flip-card/flip-card/readme.md @@ -11,6 +11,32 @@ It's meant to be used together with [sbb-flip-card-summary](/docs/elements-sbb-f ``` +## Style + +The `sbb-flip-card` component has a predefined minimum height that can be customized by specifying the `min-height` property directly in the style of the host element with a custom height. Alternatively, when used within a CSS grid layout alongside other cards, the height can be adjusted using the `grid-template-rows` property. For consistent behavior and flexibility, it is recommended to use the `minmax()` function, for example: `grid-template-rows: minmax(320px, 1fr)`. + +```html +
+ + + Card Title + + + Some additional text. + + + + + Card Title + + + Some additional text. + +
+``` + ## Slots The component will display the content slotted in the `summary` slot in the main view, and the content slotted inside the `details` slot after the card has been flipped.