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: `Generic` + }, + { + type: 'slot', + name: 'media-back', + value: `Generic` + } + ]), + + 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]