From ffd126ff24307a79719d569d0d9fa3a2c9b54c07 Mon Sep 17 00:00:00 2001
From: Vahid Nesro <63849626+Vahid1919@users.noreply.github.com>
Date: Mon, 1 Jul 2024 10:02:33 +0200
Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20sd-flipcard=20(#1121)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/components/divider/divider.ts | 2 +-
.../components/flipcard/flipcard.stories.ts | 330 +++++++++++++++++
.../src/components/flipcard/flipcard.test.ts | 61 ++++
.../src/components/flipcard/flipcard.ts | 345 ++++++++++++++++++
.../src/docs/Migration/ui-flashcard.mdx | 107 ++++++
templates/migration-guide-template.mdx | 12 +
6 files changed, 856 insertions(+), 1 deletion(-)
create mode 100644 packages/components/src/components/flipcard/flipcard.stories.ts
create mode 100644 packages/components/src/components/flipcard/flipcard.test.ts
create mode 100644 packages/components/src/components/flipcard/flipcard.ts
create mode 100644 packages/components/src/docs/Migration/ui-flashcard.mdx
diff --git a/packages/components/src/components/divider/divider.ts b/packages/components/src/components/divider/divider.ts
index d7d59df36..5aa5ed577 100644
--- a/packages/components/src/components/divider/divider.ts
+++ b/packages/components/src/components/divider/divider.ts
@@ -11,7 +11,7 @@ import SolidElement from '../../internal/solid-element';
* @status stable
* @since 1.0
*
- * @cssparts base - The component's base wrapper.
+ * @csspart base - The component's base wrapper.
*/
@customElement('sd-divider')
export default class SdDivider extends SolidElement {
diff --git a/packages/components/src/components/flipcard/flipcard.stories.ts b/packages/components/src/components/flipcard/flipcard.stories.ts
new file mode 100644
index 000000000..a8bbf9db6
--- /dev/null
+++ b/packages/components/src/components/flipcard/flipcard.stories.ts
@@ -0,0 +1,330 @@
+import '../../solid-components';
+import { html } from 'lit';
+import { storybookDefaults, storybookHelpers, storybookTemplate } from '../../../scripts/storybook/helper';
+import { waitUntil } from '@open-wc/testing-helpers';
+import { withActions } from '@storybook/addon-actions/decorator';
+
+const { argTypes, parameters } = storybookDefaults('sd-flipcard');
+const { generateTemplate } = storybookTemplate('sd-flipcard');
+const { overrideArgs } = storybookHelpers('sd-flipcard');
+
+export default {
+ title: 'Components/sd-flipcard',
+ component: 'sd-flipcard',
+ args: overrideArgs([
+ {
+ type: 'slot',
+ name: 'front',
+ value: `
Front slot
`
+ },
+ {
+ type: 'slot',
+ name: 'back',
+ value: `Back slot
`
+ },
+ {
+ type: 'slot',
+ name: 'media-front',
+ value: ``
+ },
+ {
+ type: 'slot',
+ name: 'media-back',
+ value: ``
+ }
+ ]),
+
+ argTypes,
+
+ parameters: { ...parameters },
+ decorators: [withActions] as any
+};
+/**
+ * This shows sd-flipcard in its default state.
+ */
+
+export const Default = {
+ render: (args: any) => {
+ return generateTemplate({ args });
+ }
+};
+
+/**
+ * The sd-flipcard can be displayed in several ways using the `front-variant` and `back-variant` attributes. This example shows the usage `front-variant` attribute.
+ */
+
+export const Variants = {
+ parameters: { controls: { exclude: ['front-variant'] } },
+ render: (args: any) =>
+ generateTemplate({
+ axis: {
+ y: {
+ type: 'attribute',
+ name: 'front-variant'
+ }
+ },
+ args,
+ constants: [
+ {
+ type: 'template',
+ name: 'style',
+ value: '%TEMPLATE%
'
+ }
+ ]
+ })
+};
+
+/**
+ * Use the `activation` attribute to determine the activation type of the flipcard. There are two options: `click-only` and `hover-and-click`.
+ */
+
+export const Activation = {
+ parameters: { controls: { exclude: ['activation'] } },
+ render: (args: any) =>
+ generateTemplate({
+ axis: {
+ x: {
+ type: 'attribute',
+ name: 'activation'
+ }
+ },
+ args,
+ constants: [
+ {
+ type: 'template',
+ name: 'style',
+ value: '%TEMPLATE%
'
+ }
+ ]
+ })
+};
+
+/**
+ * Use the `flip-direction` attribute to determine the direction of the flipcard. There are two options: `horizontal` and `vertical`.
+ */
+
+export const flipDirection = {
+ parameters: { controls: { exclude: ['flip-direction'] } },
+ render: (args: any) =>
+ generateTemplate({
+ axis: {
+ x: {
+ type: 'attribute',
+ name: 'flip-direction'
+ }
+ },
+ args,
+ constants: [
+ {
+ type: 'template',
+ name: 'style',
+ value: '%TEMPLATE%
'
+ }
+ ]
+ })
+};
+
+/**
+ * Use the `front`, `back`, `front-media` and `back-media` slots to add content to the flipcard.
+ */
+export const Slots = {
+ parameters: {
+ controls: { exclude: ['front', 'back', 'front-media', 'back-media'] }
+ },
+ render: (args: any) => {
+ return html`
+ ${['front', 'back', 'front-media', 'back-media'].map(slot => {
+ return generateTemplate({
+ axis: {
+ x: {
+ type: 'slot',
+ name: slot,
+ title: 'slot=..',
+ values: [
+ {
+ value: ``,
+ title: slot
+ }
+ ]
+ }
+ },
+ args,
+ constants: [
+ {
+ type: 'template',
+ name: 'style',
+ value: '%TEMPLATE%
'
+ },
+ {
+ type: 'attribute',
+ name: 'front-variant',
+ value: 'gradient-dark-top'
+ },
+ {
+ type: 'attribute',
+ name: 'back-variant',
+ value: 'gradient-dark-bottom'
+ }
+ ]
+ });
+ })}
+ `;
+ }
+};
+
+/**
+ * Use the `base`, `front`, `back`, `front-slot-container`, `back-slot-container`, `front-media`, `back-media`, `front-secondary-gradient` and `back-secondary-gradient` parts to style the flipcard.
+ */
+export const Parts = {
+ parameters: {
+ controls: {
+ exclude: [
+ 'base',
+ 'front',
+ 'back',
+ 'front-slot-container',
+ 'back-slot-container',
+ 'front-media',
+ 'back-media',
+ 'front-secondary-gradient',
+ 'back-secondary-gradient'
+ ]
+ }
+ },
+ render: (args: any) => {
+ return generateTemplate({
+ axis: {
+ y: {
+ type: 'template',
+ name: 'sd-flipcard::part(...){outline: solid 2px red}',
+ values: [
+ 'base',
+ 'front',
+ 'back',
+ 'front-slot-container',
+ 'back-slot-container',
+ 'front-media',
+ 'back-media',
+ 'front-secondary-gradient',
+ 'back-secondary-gradient'
+ ].map(part => {
+ return {
+ title: part,
+ value: `%TEMPLATE%
`
+ };
+ })
+ }
+ },
+ args,
+ constants: [
+ {
+ type: 'template',
+ name: 'style',
+ value: '%TEMPLATE%
'
+ },
+ {
+ type: 'attribute',
+ name: 'front-variant',
+ value: 'gradient-dark-top'
+ },
+ {
+ type: 'attribute',
+ name: 'back-variant',
+ value: 'gradient-dark-bottom'
+ }
+ ]
+ });
+ }
+};
+
+/**
+ * `sd-flipcard` is fully accessibile via keyboard.
+ */
+
+export const Mouseless = {
+ render: (args: any) => {
+ return html`${generateTemplate({ args })}
`;
+ },
+
+ play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => {
+ const el = canvasElement.querySelector('.mouseless sd-flipcard');
+
+ await waitUntil(() => el?.shadowRoot?.querySelector('.flip-card__side--front'));
+
+ el?.shadowRoot?.querySelector('.flip-card__side--front')!.focus();
+ }
+};
+
+/**
+ * Here is a sample of the `sd-flipcard` with custom content in the `front` and `back` slots. The activation is set to `click-only` in order allow the user to click on links/buttons inside the flipcard.
+ */
+
+export const Sample = {
+ name: 'Sample: Custom Content',
+ render: () => {
+ return html`
+
+
+
+
+ Nisi eu excepteur anim esse
+
+
+
+ Lorem ipsum dolor sit amet per niente da faremmasds nonnummy dolore lorem ipsum dolor sit amet consectuer
+
+
+
+
+
+
+ Nisi eu excepteur anim esse
+
+
+
+ Lorem ipsum dolor sit amet per niente da faremmasds nonnummy dolore lorem ipsum dolor sit amet consectuer
+
+
+
Link
+
+
+ `;
+ }
+};
+
+/**
+ * You can set a custom aspect ratio (eg: 16:9) for the `sd-flipcard` using plain CSS.
+ */
+
+export const AspectRatio = {
+ name: 'Sample: Aspect Ratio',
+ render: () => {
+ return html`
+
+
+
+
+ Nisi eu excepteur anim esse
+
+
+
+ Lorem ipsum dolor sit amet per niente da faremmasds nonnummy dolore lorem ipsum dolor sit amet consectuer
+
+
+
+
+
+
+ Nisi eu excepteur anim esse
+
+
+
+ Lorem ipsum dolor sit amet per niente da faremmasds nonnummy dolore lorem ipsum dolor sit amet consectuer
+
+
+
Link
+
+
+ `;
+ }
+};
diff --git a/packages/components/src/components/flipcard/flipcard.test.ts b/packages/components/src/components/flipcard/flipcard.test.ts
new file mode 100644
index 000000000..7f32a122f
--- /dev/null
+++ b/packages/components/src/components/flipcard/flipcard.test.ts
@@ -0,0 +1,61 @@
+import { expect, fixture, html, waitUntil } from '@open-wc/testing';
+import { userEvent } from '@storybook/test';
+import sinon from 'sinon';
+import type SdFlipcard from './flipcard';
+
+describe('', () => {
+ it('should pass accessibility tests', async () => {
+ const el = await fixture(html``);
+ await expect(el).to.be.accessible();
+ });
+
+ it('should generate proper defaults', async () => {
+ const el = await fixture(html``);
+
+ expect(el.activation).to.equal('click hover');
+ expect(el.frontVariant).to.equal('empty');
+ expect(el.backVariant).to.equal('empty');
+ });
+
+ it('should allow custom activation', async () => {
+ const el = await fixture(html``);
+
+ expect(el.activation).to.equal('click');
+ });
+
+ it('should flip on hover', async () => {
+ const el = await fixture(html``);
+
+ expect(el.shadowRoot!.querySelector('.flip-card__side--front')).to.have.class('hover');
+ });
+
+ it('should not flip on hover', async () => {
+ const el = await fixture(html``);
+
+ expect(el.shadowRoot!.querySelector('.flip-card__side--front')).to.not.have.class('hover');
+ });
+
+ describe('when a flip is triggered', () => {
+ it('should emit sd-flip-front and sd-flip-back', async () => {
+ const el = await fixture(html``);
+ const flipFrontHandler = sinon.spy();
+ const flipBackHandler = sinon.spy();
+
+ el.addEventListener('sd-flip-front', flipFrontHandler);
+ el.addEventListener('sd-flip-back', flipBackHandler);
+
+ await userEvent.type(el.shadowRoot!.querySelector('.flip-card__side--front')!, '{return}', {
+ pointerEventsCheck: 0
+ });
+ await waitUntil(() => flipFrontHandler.calledOnce);
+
+ await userEvent.type(el.shadowRoot!.querySelector('.flip-card__side--back')!, '{return}', {
+ pointerEventsCheck: 0
+ });
+ await waitUntil(() => flipBackHandler.calledOnce);
+
+ expect(flipFrontHandler).to.have.been.calledOnce;
+ expect(flipBackHandler).to.have.been.calledOnce;
+ });
+ });
+});
diff --git a/packages/components/src/components/flipcard/flipcard.ts b/packages/components/src/components/flipcard/flipcard.ts
new file mode 100644
index 000000000..af1862d4f
--- /dev/null
+++ b/packages/components/src/components/flipcard/flipcard.ts
@@ -0,0 +1,345 @@
+import { css, html } from 'lit';
+import { customElement } from '../../internal/register-custom-element';
+import { property, query } from 'lit/decorators.js';
+import componentStyles from '../../styles/component.styles';
+import cx from 'classix';
+import SolidElement from '../../internal/solid-element';
+
+/**
+ * @summary Flipcard allows for the addition of content/information on both "sides" of the card, through means of a flip animation. Used to add dynamism and interactivity to a page.
+ * @documentation https://solid.union-investment.com/[storybook-link]/flipcard
+ * @status stable
+ * @since 3.8.0
+ *
+ * @event sd-flip-front - Emmited when the front face of the flipcard is clicked.
+ * @event sd-flip-back - Emmited when the back face of the flipcard is clicked.
+ *
+ * @slot front - The front face of the flipcard.
+ * @slot back - The back face of the flipcard.
+ * @slot media-front - An optional media slot which can be as a background. Dependent from gradient variant.
+ * @slot media-back - An optional media slot which can be as a background. Dependent from gradient variant.
+ *
+ * @csspart base - The component's base wrapper.
+ * @csspart front - The container that wraps the front-side of the flipcard.
+ * @csspart back - The container that wraps the back-side of the flipcard.
+ * @csspart front-slot-container - The container that wraps the front slot.
+ * @csspart back-slot-container - The container that wraps the back slot.
+ * @csspart media-front - The container that wraps the media-front slot.
+ * @csspart media-back - The container that wraps the media-back slot.
+ * @csspart front-secondary-gradient - The container that wraps the secondary gradient of the front side.
+ * @csspart back-secondary-gradient - The container that wraps the secondary gradient of the back side.
+ *
+ * @cssproperty --name - Description of the flipcard.
+ * @cssproperty --height - Use this property to set the height of the flipcard.
+ */
+
+@customElement('sd-flipcard')
+export default class SdFlipcard extends SolidElement {
+ @query('[part="front"]') front: HTMLElement;
+ @query('[part="back"]') back: HTMLElement;
+
+ /**
+ * Determines the activation type of the flipcard.
+ */
+ @property({ reflect: true }) activation: 'click' | 'click hover' = 'click hover';
+
+ /**
+ * Allows the flipcard to flip vertically or horizontally.
+ */
+ @property({ reflect: true, attribute: 'flip-direction' }) flipDirection: 'horizontal' | 'vertical' = 'horizontal';
+
+ /** Determines the variant of the front face of the flipcard. */
+ @property({ type: String, reflect: true, attribute: 'front-variant' })
+ frontVariant:
+ | 'empty'
+ | 'primary'
+ | 'primary-100'
+ | 'gradient-light-top'
+ | 'gradient-light-bottom'
+ | 'gradient-dark-top'
+ | 'gradient-dark-bottom' = 'empty';
+
+ /** Determines the variant of the back face of the flipcard. */
+ @property({ type: String, reflect: true, attribute: 'back-variant' }) backVariant:
+ | 'empty'
+ | 'primary'
+ | 'primary-100'
+ | 'gradient-light-top'
+ | 'gradient-light-bottom'
+ | 'gradient-dark-top'
+ | 'gradient-dark-bottom' = 'empty';
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ private flipFront() {
+ this.front.classList.add('clicked--front');
+ this.back.classList.add('clicked--back');
+ this.emit('sd-flip-front');
+ this.back.focus();
+ }
+
+ private flipBack() {
+ this.front.classList.remove('clicked--front');
+ this.back.classList.remove('clicked--back');
+ this.emit('sd-flip-back');
+ this.front.focus();
+ }
+
+ private handleFrontClick(event: PointerEvent) {
+ const eventNode = event.target as HTMLElement;
+
+ // Prevent flipping when clicking on interactive elements
+ if (eventNode.getAttribute('onclick') === null && eventNode.getAttribute('href') === null) {
+ this.flipFront();
+ }
+ }
+
+ private handleBackClick(event: PointerEvent) {
+ const eventNode = event.target as HTMLElement;
+
+ // Prevent flipping when clicking on interactive elements
+ if (eventNode.getAttribute('onclick') === null && eventNode.getAttribute('href') === null) {
+ this.flipBack();
+ }
+ }
+
+ private handleFrontKeydown(event: KeyboardEvent) {
+ if (event.code === 'Enter' && this.front === event.target) {
+ this.flipFront();
+ }
+ }
+
+ private handleBackKeydown(event: KeyboardEvent) {
+ if (event.code === 'Enter' && this.back === event.target) {
+ this.flipBack();
+ }
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+
+ /**
+ * Inherits Tailwindclasses and includes additional styling.
+ */
+ static styles = [
+ componentStyles,
+ SolidElement.styles,
+ css`
+ :host {
+ @apply block aspect-3/4;
+ --name: '';
+ --height: 480px;
+ height: var(--height);
+ }
+
+ .flip-card {
+ perspective: 100rem;
+ }
+
+ .flip-card__side {
+ backface-visibility: hidden;
+ }
+
+ .flip-card__side--back {
+ transform: rotateY(180deg);
+ }
+
+ .clicked--front {
+ transform: rotateY(-180deg);
+ }
+
+ .clicked--back {
+ transform: rotateY(0);
+ }
+
+ .flip-card__side--back.vertical {
+ transform: rotateX(180deg);
+ }
+
+ .clicked--front.vertical {
+ transform: rotateX(-180deg);
+ }
+
+ .clicked--back.vertical {
+ transform: rotateX(0);
+ }
+
+ .flip-card__gradient {
+ flex: 0.4 1 0;
+ }
+
+ @media (hover: hover) and (pointer: fine) {
+ .flip-card:hover .flip-card__side--front.hover {
+ transform: rotateY(-180deg);
+ }
+
+ .flip-card:hover .flip-card__side--back.hover {
+ transform: rotateY(0);
+ }
+
+ .flip-card:hover .flip-card__side--front.hover.vertical {
+ transform: rotateX(-180deg);
+ }
+
+ .flip-card:hover .flip-card__side--back.hover.vertical {
+ transform: rotateX(0);
+ }
+ }
+ `
+ ];
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'sd-flipcard': SdFlipcard;
+ }
+}
diff --git a/packages/components/src/docs/Migration/ui-flashcard.mdx b/packages/components/src/docs/Migration/ui-flashcard.mdx
new file mode 100644
index 000000000..d80522759
--- /dev/null
+++ b/packages/components/src/docs/Migration/ui-flashcard.mdx
@@ -0,0 +1,107 @@
+# Migration Guide: From `[ui-flashcard]` to `[sd-flipcard]`
+
+The new `[sd-flipcard]` is designed to replace the `[ui-flashcard]`. Instead of mainly providing content via attributes, the `[sd-flipcard]` component uses slots to allow for more flexibility and customization.
+
+## 💾 Slots
+
+### ✨ New Slots
+
+#### [front]
+
+The front of the card.
+
+#### [back]
+
+The back of the card.
+
+#### [front-media]
+
+An optional media slot which can be as a background. Dependent from gradient variant.
+
+#### [back-media]
+
+An optional media slot which can be as a background. Dependent from gradient variant.
+
+
+
+## ⚙️ Attributes
+
+### ✨ New Attributes
+
+#### [activation]
+
+Determines the activation type of the flipcard. Options include `click-only` and `click-and-hover`. Default is `click-and-hover`. 'click-only' is generally used for flipcards that include a button or other interactive element.
+
+#### [flipDirection]
+
+Determines the direction of the flip animation. Options include `horizontal` and `vertical`. Default is `horizontal`.
+
+#### [frontVariant]
+
+Determines the style variant of the front face of the flipcard.
+
+#### [backVariant]
+
+Determines the style variant of the back face of the flipcard.
+
+### ❌ Removed Attributes
+
+The following attributes have been removed from the new [sd-flipcard] component:
+
+1. [back]
+ - The back interface has been removed in favor of the `back` slot.
+2. [front]
+ - The front interface has been removed in favor of the `front` slot.
+3. [content-alignment]
+ - The content-alignment attribute has been removed. This functionality can be replicated with the use the `front` and `back` CSS parts to align content.
+4. [front-color]
+ - The front-color attribute has been removed. Use the `frontVariant` attribute to set the style variant of the front face of the flipcard.
+5. [gradient]
+ - The gradient attribute has been removed. Use the `frontVariant` and `backVariant` attributes to set the style variant of the front and back faces of the flipcard.
+6. [ratio]
+ - The ratio attribute has been removed. Use the `base` CSS part to set custom aspect ratio of the flipcard. See this [sample](https://solid-design-system.fe.union-investment.de/x.x.x/storybook/?path=/docs/components-sd-flipcard--docs#sample%3A%20aspect%20ratio) for more information.
+7. [transition-delay]
+ - The transition-delay attribute has been removed in favor of a standard 1000ms delay. Use the `front` and `back` CSS parts to set custom transition duration of the flipcard.
+8. [image-focal-point-x]
+9. [image-focal-point-y]
+10. [replace]
+11. [sizes]
+12. [utility-front]
+13. [utility-back]
+
+
+
+## ✍️ CSS Variables
+
+### ✨ New CSS Variables
+
+#### [name]
+
+Description of the flipcard.
+
+#### [height]
+
+Use this property to set the height of the flipcard.
+
+
+
+## 🥳 Events
+
+### ✨ New Events
+
+#### [sd-flip-front]
+
+This event is emitted when the front of the flipcard is clicked.
+
+#### [sd-flip-back]
+
+This event is emitted when the back of the flipcard is clicked.
+
+### ❌ Removed Events:
+
+The following events have been removed from the new [sd-flipcard] component:
+
+1. [flashcardFlip]
+ - The flashcardFlip event has been removed. Use the `sd-flip-front` and `sd-flip-back` events instead.
+
+
diff --git a/templates/migration-guide-template.mdx b/templates/migration-guide-template.mdx
index 803990506..37f55b819 100644
--- a/templates/migration-guide-template.mdx
+++ b/templates/migration-guide-template.mdx
@@ -29,7 +29,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following slots have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_slot]
+ - [brief reasoning for removal]
2. [2nd_removed_slot]
+ - [brief reasoning for removal]
@@ -60,7 +62,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following attributes have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_attribute]
+ - [brief reasoning for removal]
2. [2nd_removed_attribute]
+ - [brief reasoning for removal]
@@ -91,7 +95,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following CSS variables have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_CSS_variable]
+ - [brief reasoning for removal]
2. [2nd_removed_CSS_variable]
+ - [brief reasoning for removal]
@@ -122,7 +128,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following events have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_event]
+ - [brief reasoning for removal]
2. [2nd_removed_event]
+ - [brief reasoning for removal]
@@ -153,7 +161,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following event listeners have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_event_listeners]
+ - [brief reasoning for removal]
2. [2nd_removed_event_listeners]
+ - [brief reasoning for removal]
@@ -184,7 +194,9 @@ Lorem_ipsum_dolor_sit_amet_consectetur_adipisicing_elit
The following methods have been removed from the new [SD_COMPONENT] component:
1. [1st_removed_method]
+ - [brief reasoning for removal]
2. [2nd_removed_method]
+ - [brief reasoning for removal]