From b08661298d6f6bf42e98182d760007e97ab716d1 Mon Sep 17 00:00:00 2001 From: Mario Castigliano Date: Fri, 19 Jul 2024 18:00:20 +0200 Subject: [PATCH] feat(sbb-flip-card): first implementation (#2946) --- src/elements/core/i18n/i18n.ts | 14 ++ src/elements/flip-card.ts | 3 + src/elements/flip-card/flip-card-details.ts | 1 + .../flip-card-details.snapshot.spec.snap.js | 66 ++++++++ .../flip-card-details/flip-card-details.scss | 25 +++ .../flip-card-details.snapshot.spec.ts | 25 +++ .../flip-card-details.spec.ts | 18 +++ .../flip-card-details.ssr.spec.ts | 20 +++ .../flip-card-details.stories.ts | 34 ++++ .../flip-card-details/flip-card-details.ts | 68 ++++++++ .../flip-card-details.visual.spec.ts | 29 ++++ .../flip-card/flip-card-details/index.ts | 1 + .../flip-card/flip-card-details/readme.md | 22 +++ src/elements/flip-card/flip-card-summary.ts | 1 + .../flip-card-summary.snapshot.spec.snap.js | 89 +++++++++++ .../flip-card-summary/flip-card-summary.scss | 81 ++++++++++ .../flip-card-summary.snapshot.spec.ts | 39 +++++ .../flip-card-summary.spec.ts | 18 +++ .../flip-card-summary.ssr.spec.ts | 23 +++ .../flip-card-summary.stories.ts | 88 ++++++++++ .../flip-card-summary/flip-card-summary.ts | 53 ++++++ .../flip-card-summary.visual.spec.ts | 44 +++++ .../flip-card/flip-card-summary/index.ts | 1 + .../flip-card/flip-card-summary/readme.md | 30 ++++ src/elements/flip-card/flip-card.ts | 1 + .../flip-card.snapshot.spec.snap.js | 151 ++++++++++++++++++ .../flip-card/flip-card/flip-card.scss | 92 +++++++++++ .../flip-card/flip-card.snapshot.spec.ts | 50 ++++++ .../flip-card/flip-card/flip-card.spec.ts | 64 ++++++++ .../flip-card/flip-card/flip-card.ssr.spec.ts | 20 +++ .../flip-card/flip-card/flip-card.stories.ts | 141 ++++++++++++++++ src/elements/flip-card/flip-card/flip-card.ts | 77 +++++++++ .../flip-card/flip-card.visual.spec.ts | 96 +++++++++++ src/elements/flip-card/flip-card/index.ts | 1 + src/elements/flip-card/flip-card/readme.md | 42 +++++ 35 files changed, 1528 insertions(+) create mode 100644 src/elements/flip-card.ts create mode 100644 src/elements/flip-card/flip-card-details.ts create mode 100644 src/elements/flip-card/flip-card-details/__snapshots__/flip-card-details.snapshot.spec.snap.js create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.scss create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.snapshot.spec.ts create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.spec.ts create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.ssr.spec.ts create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.stories.ts create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.ts create mode 100644 src/elements/flip-card/flip-card-details/flip-card-details.visual.spec.ts create mode 100644 src/elements/flip-card/flip-card-details/index.ts create mode 100644 src/elements/flip-card/flip-card-details/readme.md create mode 100644 src/elements/flip-card/flip-card-summary.ts create mode 100644 src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.scss create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.snapshot.spec.ts create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.spec.ts create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.ssr.spec.ts create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.stories.ts create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.ts create mode 100644 src/elements/flip-card/flip-card-summary/flip-card-summary.visual.spec.ts create mode 100644 src/elements/flip-card/flip-card-summary/index.ts create mode 100644 src/elements/flip-card/flip-card-summary/readme.md create mode 100644 src/elements/flip-card/flip-card.ts create mode 100644 src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js create mode 100644 src/elements/flip-card/flip-card/flip-card.scss create mode 100644 src/elements/flip-card/flip-card/flip-card.snapshot.spec.ts create mode 100644 src/elements/flip-card/flip-card/flip-card.spec.ts create mode 100644 src/elements/flip-card/flip-card/flip-card.ssr.spec.ts create mode 100644 src/elements/flip-card/flip-card/flip-card.stories.ts create mode 100644 src/elements/flip-card/flip-card/flip-card.ts create mode 100644 src/elements/flip-card/flip-card/flip-card.visual.spec.ts create mode 100644 src/elements/flip-card/flip-card/index.ts create mode 100644 src/elements/flip-card/flip-card/readme.md diff --git a/src/elements/core/i18n/i18n.ts b/src/elements/core/i18n/i18n.ts index ffea1857f8..a25727e02c 100644 --- a/src/elements/core/i18n/i18n.ts +++ b/src/elements/core/i18n/i18n.ts @@ -629,6 +629,20 @@ export const i18nClearInput: Record = { it: 'Cancella il valore dell’input', }; +export const i18nFlipCard: Record = { + de: 'Klicken Sie auf diese Karte für Details', + en: 'Click on this card for details', + fr: 'Cliquez sur cette carte pour plus de détails', + it: 'Clicca su questa scheda per i dettagli', +}; + +export const i18nReverseCard: Record = { + de: 'Klicken Sie auf diese Karte, um zurück zur Zusammenfassung zu gelangen', + en: 'Click on this card to go back to summary', + fr: 'Cliquez sur cette carte pour revenir au résumé', + it: 'Clicca su questa scheda per tornare al sommario', +}; + export const i18nFileSelectorButtonLabel: Record = { de: 'Datei auswählen', en: 'Choose a file', diff --git a/src/elements/flip-card.ts b/src/elements/flip-card.ts new file mode 100644 index 0000000000..c272505108 --- /dev/null +++ b/src/elements/flip-card.ts @@ -0,0 +1,3 @@ +export * from './flip-card/flip-card.js'; +export * from './flip-card/flip-card-summary.js'; +export * from './flip-card/flip-card-details.js'; diff --git a/src/elements/flip-card/flip-card-details.ts b/src/elements/flip-card/flip-card-details.ts new file mode 100644 index 0000000000..a308d44b0f --- /dev/null +++ b/src/elements/flip-card/flip-card-details.ts @@ -0,0 +1 @@ +export * from './flip-card-details/flip-card-details.js'; diff --git a/src/elements/flip-card/flip-card-details/__snapshots__/flip-card-details.snapshot.spec.snap.js b/src/elements/flip-card/flip-card-details/__snapshots__/flip-card-details.snapshot.spec.snap.js new file mode 100644 index 0000000000..68caec6273 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/__snapshots__/flip-card-details.snapshot.spec.snap.js @@ -0,0 +1,66 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "text", + "name": "Example text" + } + ] +} +

+`; +/* end snapshot A11y tree Chrome */ + +snapshots["sbb-flip-card-details DOM"] = +` + Example text + +`; +/* end snapshot sbb-flip-card-details DOM */ + +snapshots["sbb-flip-card-details Shadow DOM"] = +`
+ + +
+`; +/* end snapshot sbb-flip-card-details Shadow DOM */ + +snapshots["sbb-flip-card-details A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "text", + "name": "Example text" + } + ] +} +

+`; +/* end snapshot sbb-flip-card-details A11y tree Chrome */ + +snapshots["sbb-flip-card-details A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "text leaf", + "name": "Example text" + } + ] +} +

+`; +/* end snapshot sbb-flip-card-details A11y tree Firefox */ + 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 new file mode 100644 index 0000000000..4101932e3a --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.scss @@ -0,0 +1,25 @@ +@use '../../core/styles' as sbb; + +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; + +:host { + --sbb-flip-card-details-opacity: 0; + --sbb-flip-card-details-translate-y: var(--sbb-spacing-fixed-2x); + + display: contents; +} + +.sbb-flip-card-details { + pointer-events: none; + color: var(--sbb-color-milk); + opacity: var(--sbb-flip-card-details-opacity); + padding: var(--sbb-spacing-responsive-s); + padding-block-end: calc( + var(--sbb-spacing-responsive-s) + var(--sbb-spacing-responsive-xs) + var(--sbb-size-element-m) + ); + transform: translateY(var(--sbb-flip-card-details-translate-y)); + transition: var(--sbb-flip-card-details-transition-duration) ease-out; + transition-delay: var(--sbb-flip-card-details-transition-delay); + max-height: var(--sbb-flip-card-details-min-height); +} diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.snapshot.spec.ts b/src/elements/flip-card/flip-card-details/flip-card-details.snapshot.spec.ts new file mode 100644 index 0000000000..2a649cb31d --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.snapshot.spec.ts @@ -0,0 +1,25 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; + +import type { SbbFlipCardDetailsElement } from './flip-card-details.js'; +import './flip-card-details.js'; + +describe(`sbb-flip-card-details`, () => { + let element: SbbFlipCardDetailsElement; + + beforeEach(async () => { + element = await fixture(html`Example text`); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); +}); diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts b/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts new file mode 100644 index 0000000000..57269d2f58 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts @@ -0,0 +1,18 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../../core/testing/private.js'; + +import { SbbFlipCardDetailsElement } from './flip-card-details.js'; + +describe('sbb-flip-card-details', () => { + let element: SbbFlipCardDetailsElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbFlipCardDetailsElement); + }); +}); diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.ssr.spec.ts b/src/elements/flip-card/flip-card-details/flip-card-details.ssr.spec.ts new file mode 100644 index 0000000000..3ecd16f30c --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.ssr.spec.ts @@ -0,0 +1,20 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { ssrHydratedFixture } from '../../core/testing/private.js'; + +import { SbbFlipCardDetailsElement } from './flip-card-details.js'; + +describe(`sbb-flip-card-details ssr`, () => { + let root: SbbFlipCardDetailsElement; + + beforeEach(async () => { + root = await ssrHydratedFixture(html``, { + modules: ['./flip-card-details.js'], + }); + }); + + it('renders', () => { + assert.instanceOf(root, SbbFlipCardDetailsElement); + }); +}); diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.stories.ts b/src/elements/flip-card/flip-card-details/flip-card-details.stories.ts new file mode 100644 index 0000000000..8f1cb601cd --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.stories.ts @@ -0,0 +1,34 @@ +import { withActions } from '@storybook/addon-actions/decorator'; +import type { Decorator, Meta, StoryObj } from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html } from 'lit'; + +import readme from './readme.md?raw'; + +import '../../link.js'; +import './flip-card-details.js'; + +const Template = (): TemplateResult => + 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 + `; + +export const Default: StoryObj = { + render: Template, +}; + +const meta: Meta = { + decorators: [withActions as Decorator], + parameters: { + backgroundColor: () => 'var(--sbb-color-midnight)', + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-flip-card/sbb-flip-card-details', +}; + +export default meta; diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.ts b/src/elements/flip-card/flip-card-details/flip-card-details.ts new file mode 100644 index 0000000000..b5ee5ee118 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.ts @@ -0,0 +1,68 @@ +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { IS_FOCUSABLE_QUERY } from '../../core/a11y.js'; +import { hostAttributes } from '../../core/decorators.js'; +import { AgnosticMutationObserver } from '../../core/observers.js'; + +import style from './flip-card-details.scss?lit&inline'; + +/** + * Combined with a `sbb-flip-card`, it displays its content when the card is flipped. + * + * @slot - Use the unnamed slot to provide any kind of content. + */ +@customElement('sbb-flip-card-details') +@hostAttributes({ + slot: 'details', +}) +export class SbbFlipCardDetailsElement extends LitElement { + public static override styles: CSSResultGroup = style; + + private _flipCardMutationObserver = new AgnosticMutationObserver(() => + this._checkForSlottedActions(), + ); + + private _checkForSlottedActions(): void { + const cardFocusableAttributeName = 'data-card-focusable'; + + Array.from(this.querySelectorAll?.(IS_FOCUSABLE_QUERY) ?? []) + .filter((el) => !el.hasAttribute(cardFocusableAttributeName)) + .forEach((el: Element) => el.setAttribute(cardFocusableAttributeName, '')); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._checkForSlottedActions(); + this._flipCardMutationObserver.observe(this, { + childList: true, + subtree: true, + }); + } + + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._checkForSlottedActions(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._flipCardMutationObserver.disconnect(); + } + + protected override render(): TemplateResult { + return html` +
+ +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-flip-card-details': SbbFlipCardDetailsElement; + } +} 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 new file mode 100644 index 0000000000..c916225c13 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/flip-card-details.visual.spec.ts @@ -0,0 +1,29 @@ +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-details/index.ts b/src/elements/flip-card/flip-card-details/index.ts new file mode 100644 index 0000000000..dd5902e4b4 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/index.ts @@ -0,0 +1 @@ +export * from './flip-card-details.js'; diff --git a/src/elements/flip-card/flip-card-details/readme.md b/src/elements/flip-card/flip-card-details/readme.md new file mode 100644 index 0000000000..6029c00371 --- /dev/null +++ b/src/elements/flip-card/flip-card-details/readme.md @@ -0,0 +1,22 @@ +The `sbb-flip-card-details`, when used inside a [sbb-flip-card](/docs/elements-sbb-flip-card-sbb-flip-card--docs), will show its contents when the card is flipped. +The component's slot is implicitly set to `"details"`. + +```html + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer placerat ipsum rhoncus viverra + dapibus. Aenean id nibh ac tortor elementum vestibulum eu vitae dui. Integer tellus ex, bibendum + eget purus id, pellentesque interdum tortor. Sed bibendum neque nisi, ac egestas magna consequat + eu. + Link + + +``` + + + +## Slots + +| Name | Description | +| ---- | ---------------------------------------------------- | +| | Use the unnamed slot to provide any kind of content. | diff --git a/src/elements/flip-card/flip-card-summary.ts b/src/elements/flip-card/flip-card-summary.ts new file mode 100644 index 0000000000..7d6a5c6ad1 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary.ts @@ -0,0 +1 @@ +export * from './flip-card-summary/flip-card-summary.js'; 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 new file mode 100644 index 0000000000..22189347f7 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/__snapshots__/flip-card-summary.snapshot.spec.snap.js @@ -0,0 +1,89 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + } + ] +} +

+`; +/* end snapshot A11y tree Chrome */ + +snapshots["sbb-flip-card-summary DOM"] = +` + + Summary + + + + +`; +/* end snapshot sbb-flip-card-summary DOM */ + +snapshots["sbb-flip-card-summary Shadow DOM"] = +`
+ + +
+ + +
+
+`; +/* end snapshot sbb-flip-card-summary Shadow DOM */ + +snapshots["sbb-flip-card-summary A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + } + ] +} +

+`; +/* end snapshot sbb-flip-card-summary A11y tree Chrome */ + +snapshots["sbb-flip-card-summary A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + } + ] +} +

+`; +/* end snapshot sbb-flip-card-summary A11y tree Firefox */ + 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 new file mode 100644 index 0000000000..6aaaa56024 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.scss @@ -0,0 +1,81 @@ +@use '../../core/styles' as sbb; + +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; + +:host { + --sbb-flip-card-summary-opacity: 1; + --sbb-flip-card-summary-pointer-events: all; + + display: contents; +} + +.sbb-flip-card-summary { + display: grid; + position: absolute; + 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; + width: 100%; + height: 100%; + transition: all var(--sbb-flip-card-summary-transition-duration) ease-out; + transition-delay: var(--sbb-flip-card-summary-transition-delay); + + :host([image-alignment='after']) & { + @include sbb.mq($from: small) { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 1fr; + } + + @include sbb.mq($from: medium) { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: 1fr; + } + } +} + +::slotted(sbb-title) { + padding-inline: var(--sbb-spacing-responsive-s); + padding-block: var(--sbb-spacing-responsive-s) var(--sbb-spacing-responsive-xs); + margin: 0; + grid-area: 1 / 1 / 2 / 2; + + :host([image-alignment='after']) & { + @include sbb.mq($from: small) { + grid-area: 1 / 1 / 2 / 3; + } + + @include sbb.mq($from: medium) { + grid-area: 1 / 1 / 2 / 2; + } + } +} + +.sbb-flip-card-summary--image-wrapper { + max-height: var(--sbb-flip-card-min-height); + grid-area: 2 / 1 / 3 / 2; + + :host([image-alignment='after']) & { + @include sbb.mq($from: small) { + grid-area: 1 / 3 / 2 / 4; + } + + @include sbb.mq($from: medium) { + grid-area: 1 / 2 / 2 / 3; + } + } +} + +::slotted(img) { + object-fit: cover; + width: 100%; + height: 100%; +} + +::slotted(sbb-image) { + width: 100%; + height: 100%; +} diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.snapshot.spec.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.snapshot.spec.ts new file mode 100644 index 0000000000..03eaf3a04b --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.snapshot.spec.ts @@ -0,0 +1,39 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import sampleImages from '../../core/images.js'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; + +import type { SbbFlipCardSummaryElement } from './flip-card-summary.js'; + +import './flip-card-summary.js'; +import '../../title.js'; +import '../../image.js'; + +describe(`sbb-flip-card-summary`, () => { + let element: SbbFlipCardSummaryElement; + + beforeEach(async () => { + element = await fixture( + html` + Summary + + `, + ); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); +}); diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.spec.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.spec.ts new file mode 100644 index 0000000000..fe30570a1e --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.spec.ts @@ -0,0 +1,18 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../../core/testing/private.js'; + +import { SbbFlipCardSummaryElement } from './flip-card-summary.js'; + +describe('sbb-flip-card-summary', () => { + let element: SbbFlipCardSummaryElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbFlipCardSummaryElement); + }); +}); diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.ssr.spec.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.ssr.spec.ts new file mode 100644 index 0000000000..9ff6924b5d --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.ssr.spec.ts @@ -0,0 +1,23 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { ssrHydratedFixture } from '../../core/testing/private.js'; + +import { SbbFlipCardSummaryElement } from './flip-card-summary.js'; + +describe(`sbb-flip-card-summary ssr`, () => { + let root: SbbFlipCardSummaryElement; + + beforeEach(async () => { + root = await ssrHydratedFixture( + html``, + { + modules: ['./flip-card-summary.js'], + }, + ); + }); + + it('renders', () => { + assert.instanceOf(root, SbbFlipCardSummaryElement); + }); +}); diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.stories.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.stories.ts new file mode 100644 index 0000000000..0cc2544136 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.stories.ts @@ -0,0 +1,88 @@ +import { withActions } from '@storybook/addon-actions/decorator'; +import type { InputType } from '@storybook/types'; +import type { + Args, + ArgTypes, + Decorator, + Meta, + StoryContext, + StoryObj, +} from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; + +import sampleImages from '../../core/images.js'; + +import readme from './readme.md?raw'; + +import './flip-card-summary.js'; +import '../../title.js'; +import '../../image.js'; + +const imageAlignment: InputType = { + control: { + type: 'select', + }, + options: ['after', 'below'], + table: { + category: 'Summary', + }, +}; + +const defaultArgTypes: ArgTypes = { + imageAlignment, +}; + +const defaultArgs: Args = { + imageAlignment: imageAlignment.options![0], +}; + +const Template = (args: Args): TemplateResult => html` +
+ + Summary + + +
+`; + +export const Default: StoryObj = { + render: Template, + args: defaultArgs, + argTypes: defaultArgTypes, +}; + +export const ImageBelow: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs, imageAlignment: imageAlignment.options![1] }, +}; + +const meta: Meta = { + decorators: [withActions as Decorator], + parameters: { + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-flip-card/sbb-flip-card-summary', +}; + +export default meta; diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.ts new file mode 100644 index 0000000000..56b0253931 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.ts @@ -0,0 +1,53 @@ +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { hostAttributes } from '../../core/decorators.js'; + +import style from './flip-card-summary.scss?lit&inline'; + +export type SbbFlipCardImageAlignment = 'after' | 'below'; + +/** + * Combined with a `sbb-flip-card`, it displays its content when the card is not flipped. + * + * @slot - Use the unnamed slot to provide a title for the `sbb-flip-card-summary`. + * @slot image - Use this slot to provide an image for the `sbb-flip-card-summary`. + */ +@customElement('sbb-flip-card-summary') +@hostAttributes({ + slot: 'summary', +}) +export class SbbFlipCardSummaryElement extends LitElement { + public static override styles: CSSResultGroup = style; + + /** The position where to render the image. */ + @property({ attribute: 'image-alignment', reflect: true }) + public imageAlignment: SbbFlipCardImageAlignment = 'after'; + + protected override willUpdate(_changedProperties: PropertyValues): void { + super.willUpdate(_changedProperties); + + if (_changedProperties.has('imageAlignment')) { + this.closest?.('sbb-flip-card')?.setAttribute('data-image-alignment', this.imageAlignment); + } + } + + protected override render(): TemplateResult { + return html` +
+ +
+ +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-flip-card-summary': SbbFlipCardSummaryElement; + } +} diff --git a/src/elements/flip-card/flip-card-summary/flip-card-summary.visual.spec.ts b/src/elements/flip-card/flip-card-summary/flip-card-summary.visual.spec.ts new file mode 100644 index 0000000000..feb5c50253 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/flip-card-summary.visual.spec.ts @@ -0,0 +1,44 @@ +import { html } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; + +import sampleImages from '../../core/images.js'; +import { describeViewports, visualDiffDefault } from '../../core/testing/private.js'; + +import './flip-card-summary.js'; +import '../../flip-card.js'; +import '../../title.js'; +import '../../image.js'; + +describe(`sbb-flip-card-summary`, () => { + describeViewports({ viewports: ['zero', 'medium'] }, () => { + for (const imageAlignment of ['after', 'below']) { + it( + `image-alignment=${imageAlignment}`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` +
+ + Summary + + +
+ `); + }), + ); + } + }); +}); diff --git a/src/elements/flip-card/flip-card-summary/index.ts b/src/elements/flip-card/flip-card-summary/index.ts new file mode 100644 index 0000000000..65c7611691 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/index.ts @@ -0,0 +1 @@ +export * from './flip-card-summary.js'; diff --git a/src/elements/flip-card/flip-card-summary/readme.md b/src/elements/flip-card/flip-card-summary/readme.md new file mode 100644 index 0000000000..86fac4b926 --- /dev/null +++ b/src/elements/flip-card/flip-card-summary/readme.md @@ -0,0 +1,30 @@ +The `sbb-flip-card-summary`, when used inside a [sbb-flip-card](/docs/elements-sbb-flip-card-sbb-flip-card--docs), shows its contents when the card is not flipped. +The component's slot is implicitly set to `"summary"`. + +```html + + + Card Title + + + +``` + +## 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`). + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------------- | ----------------- | ------- | --------------------------- | --------- | --------------------------------------- | +| `imageAlignment` | `image-alignment` | public | `SbbFlipCardImageAlignment` | `'after'` | The position where to render the image. | + +## Slots + +| Name | Description | +| ------- | ------------------------------------------------------------------------ | +| | Use the unnamed slot to provide a title for the `sbb-flip-card-summary`. | +| `image` | Use this slot to provide an image for the `sbb-flip-card-summary`. | diff --git a/src/elements/flip-card/flip-card.ts b/src/elements/flip-card/flip-card.ts new file mode 100644 index 0000000000..75fe2749c4 --- /dev/null +++ b/src/elements/flip-card/flip-card.ts @@ -0,0 +1 @@ +export * from './flip-card/flip-card.js'; 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 new file mode 100644 index 0000000000..2551b0c702 --- /dev/null +++ b/src/elements/flip-card/flip-card/__snapshots__/flip-card.snapshot.spec.snap.js @@ -0,0 +1,151 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + }, + { + "role": "button", + "name": "Click on this card to show more details" + }, + { + "role": "button", + "name": "" + } + ] +} +

+`; +/* end snapshot A11y tree Chrome */ + +snapshots["sbb-flip-card DOM"] = +` + + + Summary + + + + + + 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 + + + +`; +/* end snapshot sbb-flip-card DOM */ + +snapshots["sbb-flip-card Shadow DOM"] = +`
+ + + + + + + +
+`; +/* end snapshot sbb-flip-card Shadow DOM */ + +snapshots["sbb-flip-card A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + }, + { + "role": "button", + "name": "Click on this card for details" + }, + { + "role": "button", + "name": "" + } + ] +} +

+`; +/* end snapshot sbb-flip-card A11y tree Chrome */ + +snapshots["sbb-flip-card A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "heading", + "name": "Summary", + "level": 4 + }, + { + "role": "button", + "name": "Click on this card for details" + }, + { + "role": "button", + "name": "" + } + ] +} +

+`; +/* end snapshot sbb-flip-card A11y tree Firefox */ + diff --git a/src/elements/flip-card/flip-card/flip-card.scss b/src/elements/flip-card/flip-card/flip-card.scss new file mode 100644 index 0000000000..7e99f47c96 --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.scss @@ -0,0 +1,92 @@ +@use '../../core/styles' as sbb; + +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; + +// TODO: improve component's animation +:host { + --sbb-flip-card-background-color: var(--sbb-color-cloud); + --sbb-flip-card-border-radius: var(--sbb-border-radius-4x); + --sbb-flip-card-min-height: #{sbb.px-to-rem-build(280)}; + --sbb-flip-card-details-min-height: var(--sbb-flip-card-min-height); + --sbb-flip-card-summary-transition-duration: var(--sbb-animation-duration-5x); + --sbb-flip-card-summary-transition-delay: var(--sbb-animation-duration-2x); + --sbb-flip-card-details-transition-duration: var(--sbb-animation-duration-4x); + --sbb-flip-card-details-transition-delay: var(--sbb-disable-animation-zero-time); + + position: relative; + display: block; + + @include sbb.mq($from: medium) { + --sbb-flip-card-min-height: #{sbb.px-to-rem-build(320)}; + } +} + +:host(:hover) { + --sbb-flip-card-background-color: var(--sbb-color-cloud-alpha-80); +} + +:host([data-flipped]) { + --sbb-flip-card-background-color: var(--sbb-color-midnight); + --sbb-flip-card-details-transition-duration: var(--sbb-animation-duration-2x); + --sbb-flip-card-details-transition-delay: var(--sbb-animation-duration-5x); + --sbb-flip-card-summary-transition-delay: var(--sbb-disable-animation-zero-time); + --sbb-flip-card-details-min-height: fit-content; + + ::slotted(sbb-flip-card-summary) { + --sbb-flip-card-summary-opacity: 0; + --sbb-flip-card-summary-pointer-events: none; + } + + ::slotted(sbb-flip-card-details) { + --sbb-flip-card-details-opacity: 1; + --sbb-flip-card-details-translate-y: 0; + } +} + +:host([data-image-alignment='after']) { + @include sbb.mq($from: small, $to: medium) { + --sbb-flip-card-min-height: #{sbb.px-to-rem-build(240)}; + } + + @include sbb.mq($from: wide) { + --sbb-flip-card-min-height: #{sbb.px-to-rem-build(400)}; + } +} + +.sbb-flip-card { + position: relative; + display: flex; + height: 100%; + flex-flow: wrap; + flex-direction: column; + gap: var(--sbb-spacing-responsive-xs); + 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-delay: var(--sbb-flip-card-summary-transition-delay); +} + +.sbb-flip-card--toggle-button { + position: absolute; + inset-inline-start: var(--sbb-spacing-responsive-s); + inset-block-end: var(--sbb-spacing-responsive-s); + + :host([data-flipped]) & { + --sbb-focus-outline-color: var(--sbb-focus-outline-color-dark); + } +} + +button { + @include sbb.button-reset; + + cursor: pointer; + position: absolute; + inset: 0; + border-radius: var(--sbb-flip-card-border-radius); + + &:not([data-focus-origin='mouse'], [data-focus-origin='touch']):focus-visible { + @include sbb.focus-outline; + } +} diff --git a/src/elements/flip-card/flip-card/flip-card.snapshot.spec.ts b/src/elements/flip-card/flip-card/flip-card.snapshot.spec.ts new file mode 100644 index 0000000000..715d7bd916 --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.snapshot.spec.ts @@ -0,0 +1,50 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import sampleImages from '../../core/images.js'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; + +import type { SbbFlipCardElement } from './flip-card.js'; + +import './flip-card.js'; +import '../flip-card-details.js'; +import '../flip-card-summary.js'; +import '../../title.js'; +import '../../image.js'; +import '../../link.js'; + +describe(`sbb-flip-card`, () => { + let element: SbbFlipCardElement; + + beforeEach(async () => { + element = await fixture(html` + + + Summary + + + 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 + + + `); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); +}); diff --git a/src/elements/flip-card/flip-card/flip-card.spec.ts b/src/elements/flip-card/flip-card/flip-card.spec.ts new file mode 100644 index 0000000000..909c7bd091 --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.spec.ts @@ -0,0 +1,64 @@ +import { assert, expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../../core/testing/private.js'; +import { waitForLitRender } from '../../core/testing.js'; +import type { SbbFlipCardDetailsElement } from '../flip-card-details.js'; +import type { SbbFlipCardSummaryElement } from '../flip-card-summary.js'; + +import { SbbFlipCardElement } from './flip-card.js'; + +import '../flip-card-summary.js'; +import '../flip-card-details.js'; +import '../../title.js'; + +describe('sbb-flip-card', () => { + let element: SbbFlipCardElement; + + beforeEach(async () => { + element = await fixture( + html` + + Card Title + + Some additional text. + `, + ); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbFlipCardElement); + }); + + it('it toggles on click', async () => { + const summary: SbbFlipCardSummaryElement = element.summary; + const details: SbbFlipCardDetailsElement = element.details; + + expect(summary.inert).to.be.equal(false); + expect(details.inert).to.be.equal(true); + + element.shadowRoot?.querySelector('button')!.click(); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-flipped'); + + expect(summary.inert).to.be.equal(true); + expect(details.inert).to.be.equal(false); + }); + + it('it toggles programmatically', async () => { + const summary: SbbFlipCardSummaryElement = element.summary; + const details: SbbFlipCardDetailsElement = element.details; + + expect(summary.inert).to.be.equal(false); + expect(details.inert).to.be.equal(true); + + element.toggle(); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-flipped'); + + expect(summary.inert).to.be.equal(true); + expect(details.inert).to.be.equal(false); + }); +}); diff --git a/src/elements/flip-card/flip-card/flip-card.ssr.spec.ts b/src/elements/flip-card/flip-card/flip-card.ssr.spec.ts new file mode 100644 index 0000000000..4400574e7e --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.ssr.spec.ts @@ -0,0 +1,20 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { ssrHydratedFixture } from '../../core/testing/private.js'; + +import { SbbFlipCardElement } from './flip-card.js'; + +describe(`sbb-flip-card ssr`, () => { + let root: SbbFlipCardElement; + + beforeEach(async () => { + root = await ssrHydratedFixture(html``, { + modules: ['./flip-card.js'], + }); + }); + + it('renders', () => { + assert.instanceOf(root, SbbFlipCardElement); + }); +}); diff --git a/src/elements/flip-card/flip-card/flip-card.stories.ts b/src/elements/flip-card/flip-card/flip-card.stories.ts new file mode 100644 index 0000000000..f89e01b22e --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.stories.ts @@ -0,0 +1,141 @@ +import type { InputType } from '@storybook/types'; +import type { Args, ArgTypes, Meta, StoryObj } from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html, nothing } from 'lit'; + +import sampleImages from '../../core/images.js'; + +import readme from './readme.md?raw'; + +import '../../image/image.js'; +import '../../link/link/link.js'; +import '../../title/title.js'; +import '../flip-card-details.js'; +import '../flip-card-summary.js'; +import './flip-card.js'; + +const imageAlignment: InputType = { + control: { + type: 'select', + }, + options: ['after', 'below'], + table: { + category: 'Summary', + }, +}; + +const defaultArgTypes: ArgTypes = { + imageAlignment, +}; + +const defaultArgs: Args = { + imageAlignment: imageAlignment.options![0], +}; + +const cardSummary = (imageAlignment: any, showImage: boolean): TemplateResult => html` + + Summary + ${showImage + ? html`` + : nothing} + +`; + +const cardDetails = (): TemplateResult => 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 +`; + +const DefaultTemplate = (args: Args): TemplateResult => + html` ${cardSummary(args.imageAlignment, true)} ${cardDetails()} `; + +const NoImageTemplate = (args: Args): TemplateResult => + html` + ${cardSummary(args.imageAlignment, false)} ${cardDetails()} + `; + +const LongContentTemplate = (args: Args): TemplateResult => + html` + ${cardSummary(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. 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 + `; + +const LongTitleTemplate = (args: Args): TemplateResult => + html` + + This is a very long title that should break into multiple lines + + + ${cardDetails()} + `; + +export const ImageAfter: StoryObj = { + render: DefaultTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +export const ImageBelow: StoryObj = { + render: DefaultTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs, imageAlignment: imageAlignment.options![1] }, +}; + +export const NoImage: StoryObj = { + render: NoImageTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +export const LongContent: StoryObj = { + render: LongContentTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +export const LongTitle: StoryObj = { + render: LongTitleTemplate, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +const meta: Meta = { + decorators: [(story) => html`
${story()}
`], + parameters: { + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-flip-card/sbb-flip-card', +}; + +export default meta; diff --git a/src/elements/flip-card/flip-card/flip-card.ts b/src/elements/flip-card/flip-card/flip-card.ts new file mode 100644 index 0000000000..042890ab68 --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.ts @@ -0,0 +1,77 @@ +import type { CSSResultGroup, TemplateResult } from 'lit'; +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +import { SbbLanguageController } from '../../core/controllers.js'; +import { i18nFlipCard, i18nReverseCard } from '../../core/i18n.js'; +import { SbbHydrationMixin } from '../../core/mixins.js'; +import type { SbbFlipCardDetailsElement } from '../flip-card-details.js'; +import type { SbbFlipCardSummaryElement } from '../flip-card-summary.js'; + +import style from './flip-card.scss?lit&inline'; + +import '../../button/secondary-button.js'; + +/** + * Displays an informative card that reveals more informations upon being clicked. + * + * @slot summary - Use this slot to provide a sbb-flip-card-summary component. + * @slot details - Use this slot to provide a sbb-flip-card-details component. + * + */ +@customElement('sbb-flip-card') +export class SbbFlipCardElement extends SbbHydrationMixin(LitElement) { + public static override styles: CSSResultGroup = style; + + /** Returns the slotted sbb-flip-card-summary. */ + public get summary(): SbbFlipCardSummaryElement { + return this.querySelector('sbb-flip-card-summary')!; + } + + /** Returns the slotted sbb-flip-card-details. */ + public get details(): SbbFlipCardDetailsElement { + return this.querySelector('sbb-flip-card-details')!; + } + + /** Whether the card is flipped or not. */ + @state() private _flipped = false; + + private _language = new SbbLanguageController(this); + + /** Toggles the state of the sbb-flip-card. */ + public toggle(): void { + this._flipped = !this._flipped; + this.toggleAttribute('data-flipped', this._flipped); + this.summary.inert = this._flipped; + this.details.inert = !this._flipped; + } + + protected override render(): TemplateResult { + return html` +
+ (this.summary.inert = this._flipped)}> + + (this.details.inert = !this._flipped)}> + this.toggle()} + size="s" + > +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-flip-card': SbbFlipCardElement; + } +} 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 new file mode 100644 index 0000000000..0c95fdc319 --- /dev/null +++ b/src/elements/flip-card/flip-card/flip-card.visual.spec.ts @@ -0,0 +1,96 @@ +import { html, nothing, type TemplateResult } from 'lit'; + +import sampleImages from '../../core/images.js'; +import { + describeViewports, + visualDiffDefault, + visualDiffFocus, +} from '../../core/testing/private.js'; +import './flip-card.js'; +import '../flip-card-summary.js'; +import '../flip-card-details.js'; +import '../../title.js'; +import '../../link.js'; +import '../../image.js'; +import type { SbbFlipCardImageAlignment } from '../flip-card-summary.js'; + +const content = ( + title: string = 'Summary', + imageAlignment: SbbFlipCardImageAlignment = 'after', + longConent: boolean = 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. + ${longConent + ? `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. 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.` + : nothing} + Link`; + +describe(`sbb-flip-card`, () => { + describeViewports({ viewports: ['zero', 'medium'] }, () => { + for (const imageAlignment of ['after', 'below']) { + for (const state of [visualDiffDefault, visualDiffFocus]) { + it( + `image-alignment=${imageAlignment} ${state.name}`, + state.with(async (setup) => { + await setup.withFixture(html` + + ${content('Summary', imageAlignment as SbbFlipCardImageAlignment)} + + `); + }), + ); + } + } + + it( + 'flipped', + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` ${content()}`); + }), + ); + + it( + 'multiline', + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + ${content( + 'This is a very long title that should break into multiple lines', + )}`, + ); + }), + ); + + 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)} + `, + ); + }), + ); + } + }); +}); diff --git a/src/elements/flip-card/flip-card/index.ts b/src/elements/flip-card/flip-card/index.ts new file mode 100644 index 0000000000..50384bb471 --- /dev/null +++ b/src/elements/flip-card/flip-card/index.ts @@ -0,0 +1 @@ +export * from './flip-card.js'; diff --git a/src/elements/flip-card/flip-card/readme.md b/src/elements/flip-card/flip-card/readme.md new file mode 100644 index 0000000000..4512dad1c8 --- /dev/null +++ b/src/elements/flip-card/flip-card/readme.md @@ -0,0 +1,42 @@ +The `sbb-flip-card` component displays an informative card that reveals more informations when clicked or toggled programmatically. +It's meant to be used together with [sbb-flip-card-summary](/docs/elements-sbb-flip-card-sbb-flip-card-summary--docs) and [sbb-flip-card-details](/docs/elements-sbb-flip-card-sbb-flip-card-details--docs). + +```html + + + 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. + +## States + +The `sbb-flip-card` will switch to the flipped state after the user clicks on it or after the `toggle` method is called. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| --------- | --------- | ------- | --------------------------- | ------- | ------------------------------------------ | +| `details` | - | public | `SbbFlipCardDetailsElement` | | Returns the slotted sbb-flip-card-details. | +| `summary` | - | public | `SbbFlipCardSummaryElement` | | Returns the slotted sbb-flip-card-summary. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| -------- | ------- | --------------------------------------- | ---------- | ------ | -------------- | +| `toggle` | public | Toggles the state of the sbb-flip-card. | | `void` | | + +## Slots + +| Name | Description | +| --------- | ----------------------------------------------------------- | +| `details` | Use this slot to provide a sbb-flip-card-details component. | +| `summary` | Use this slot to provide a sbb-flip-card-summary component. |