From 8a6eb5d0504c9b0281e558769c3abe9ea8c07bde Mon Sep 17 00:00:00 2001 From: Matthias Goudjil Date: Wed, 2 Oct 2024 01:15:41 +0200 Subject: [PATCH] feat: [puik-alert] fix #339 - finally no action slot, add news optionnal props instead (right/left icons and aria-label for linkButton/ActionButton), design in line with the figma model (button for the link) --- commitlint.config.cjs | 2 +- packages/components/alert/src/alert.ts | 14 +- packages/components/alert/src/alert.vue | 24 +- .../components/alert/stories/alert.stories.ts | 1174 ++++++++++++++--- packages/components/alert/test/alert.spec.ts | 80 +- packages/theme/src/puik-alert.scss | 25 +- 6 files changed, 1137 insertions(+), 182 deletions(-) diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 9efbd6fc..7e03ed9c 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -1,6 +1,6 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { - 'header-max-length': [2, 'always', 150] + 'header-max-length': [2, 'always', 300] } }; diff --git a/packages/components/alert/src/alert.ts b/packages/components/alert/src/alert.ts index e0516df4..b5dc215c 100644 --- a/packages/components/alert/src/alert.ts +++ b/packages/components/alert/src/alert.ts @@ -20,19 +20,27 @@ export interface AlertProps { title?: string description?: string variant?: `${PuikAlertVariants}` - disableBorders?: boolean isClosable?: boolean + disableBorders?: boolean buttonLabel?: string - buttonWrapLabel?: boolean linkLabel?: string + buttonWrapLabel?: boolean + leftIconBtn?: string + rightIconBtn?: string + leftIconLink?: string + rightIconLink?: string + internalLink?: string + externalLink?: string ariaLive?: `${PuikAriaLive}` + ariaLabelBtn?: string + ariaLabelLink?: string dataTest?: string } export type AlertEmits = { click: [event: Event] - close: [event: Event] clickLink: [event: Event] + close: [event: Event] }; export type AlertInstance = InstanceType; diff --git a/packages/components/alert/src/alert.vue b/packages/components/alert/src/alert.vue index 562c4ae3..14b0e661 100644 --- a/packages/components/alert/src/alert.vue +++ b/packages/components/alert/src/alert.vue @@ -42,17 +42,31 @@

- {{ linkLabel }} - + emit('clickLink', event); diff --git a/packages/components/alert/stories/alert.stories.ts b/packages/components/alert/stories/alert.stories.ts index 17be7060..ecfe7893 100644 --- a/packages/components/alert/stories/alert.stories.ts +++ b/packages/components/alert/stories/alert.stories.ts @@ -1,7 +1,8 @@ import { action } from '@storybook/addon-actions'; import { PuikAriaLive } from '@prestashopcorp/puik-components/base/src/common'; -import { PuikAlert, PuikAlertVariants } from '@prestashopcorp/puik-components'; +import { PuikAlert, PuikAlertVariants, PuikButton, PuikLink } from '@prestashopcorp/puik-components'; import type { StoryObj, Meta, StoryFn, Args } from '@storybook/vue3'; +import { ref } from 'vue'; const alertVariants = Object.values(PuikAlertVariants); const alertVariantsSummary = alertVariants.join('|'); @@ -14,58 +15,176 @@ export default { component: PuikAlert, argTypes: { title: { - description: 'Set the alert title' + description: 'Set the alert title', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } }, description: { - description: 'Set the alert description (also exists as a default slot)' + description: 'Set the alert description (also exists as a default slot)', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + buttonLabel: { + description: 'Label of the Action button', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + linkLabel: { + description: 'Label of the Link button', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } }, variant: { control: 'select', description: 'Set the alert variant', options: alertVariants, table: { + type: { + summary: alertVariantsSummary + }, defaultValue: { summary: 'success' - }, + } + } + }, + isClosable: { + description: 'Display a close button which emits a `close event` on click', + control: 'boolean', + table: { type: { - summary: alertVariantsSummary + summary: 'boolean' + }, + defaultValue: { + summary: false } } }, disableBorders: { - description: - 'Disable alert borders (only for info, warning, error variants)', + description: 'Disable alert borders', + control: 'boolean', table: { + type: { + summary: 'boolean' + }, defaultValue: { summary: false } } }, - buttonLabel: { - description: 'Label of the button' - }, buttonWrapLabel: { - description: 'Set the carriage return of the button label', + description: 'Set the carriage return for label of Action and Link buttons', + control: 'boolean', table: { + type: { + summary: 'boolean' + }, defaultValue: { summary: false } } }, - isClosable: { - description: 'Display a close button' - }, default: { - control: 'none', - description: 'Set the alert description' + description: 'Set the alert description via the `default slot`. Useful if you want to create a description a little more complex than a simple text and integrate other puik elements like for example a classic `puik-link` (which can be an alternative to the Link button - see example below)' }, - dataTest: { + leftIconBtn: { + description: 'Set icon on the left side of the Action button with a Material Icon name - cf https://fonts.google.com/icons', control: 'text', - description: - 'Set the data-test attribute for the alert components `title-${dataTest}` `description-${dataTest}` `button-${dataTest}` `close-${dataTest}` `link-${dataTest}`' + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } }, - linkLabel: { - description: 'Label of the link' + rightIconBtn: { + description: 'Set icon on the right side of the Action button with a Material Icon name - cf https://fonts.google.com/icons', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + leftIconLink: { + description: 'Set icon on the left side of the Link button with a Material Icon name - cf https://fonts.google.com/icons', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + rightIconLink: { + description: 'Set icon on the right side of the Link button with a Material Icon name - cf https://fonts.google.com/icons', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + internalLink: { + description: 'Internal link for the Link button (use vue `router-link` under the hood)', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + externalLink: { + description: 'External link for the Link button (use a native `a` tag link under the hood)', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } }, ariaLive: { description: 'option for "aria-live" attribute', @@ -79,17 +198,63 @@ export default { summary: alertAriaLiveSummary } } + }, + ariaLabelBtn: { + description: 'ARIA label for the Action button', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + ariaLabelLink: { + description: 'ARIA label for the Link button', + control: 'text', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } + }, + dataTest: { + control: 'text', + description: 'Set the data-test attribute for the alert components `title-${dataTest}` `description-${dataTest}` `button-${dataTest}` `close-${dataTest}` `link-${dataTest}`', + table: { + type: { + summary: 'string' + }, + defaultValue: { + summary: 'undefined' + } + } } }, args: { title: 'Title', description: 'This is the description of the success alert.', + buttonLabel: 'Action', + linkLabel: 'Learn more', variant: 'success', + isClosable: false, disableBorders: false, - buttonLabel: 'Button', buttonWrapLabel: false, - isClosable: false, - linkLabel: 'See more' + leftIconBtn: undefined, + rightIconBtn: undefined, + leftIconLink: undefined, + rightIconLink: undefined, + internalLink: undefined, + externalLink: undefined, + ariaLabelBtn: undefined, + ariaLabelLink: undefined, + ariaLive: 'polite', + dataTest: undefined } } as Meta; @@ -123,32 +288,194 @@ export const Default = { source: { code: ` - This is the description of the success alert + + `, + language: 'html' + } + } + } +}; + +export const Variants: StoryObj = { + render: () => ({ + components: { + PuikAlert + }, + template: ` +
+ + + + +
+ ` + }), + + parameters: { + docs: { + source: { + code: ` + -
-
- check_circle -
-

Title

- This is the description of the success alert. + + + + + This a success alert with a title and a description. + + + + + + + + + + `, language: 'html' @@ -157,14 +484,254 @@ export const Default = { } }; -export const buttonWrapLabel: StoryObj = { +export const VariantsWithoutBorder: StoryObj = { + render: () => ({ + components: { + PuikAlert + }, + template: ` +
+ + + + +
+ ` + }), + + parameters: { + docs: { + source: { + code: ` + + + + + + + This a success alert with a title and a description. + + + + + + + + + + + `, + language: 'html' + } + } + } +}; + +export const IsClosable = { + render: () => ({ + components: { + PuikAlert, + PuikButton + }, + setup() { + const showAlert = ref(true); + return { showAlert }; + }, + + template: ` + + + + refresh alert + + ` + }), + + parameters: { + docs: { + source: { + code: ` + + // const showAlert = ref(true) + + + + + + `, + language: 'html' + } + } + } +}; + +export const ButtonWrapLabel: StoryObj = { render: () => ({ components: { PuikAlert }, template: `
- + This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. @@ -175,7 +742,7 @@ export const buttonWrapLabel: StoryObj = { This an alert with a very long description. This an alert with a very long description. - + This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. @@ -197,9 +764,9 @@ export const buttonWrapLabel: StoryObj = { This an alert with a very long description. This an alert with a very long description. @@ -214,8 +781,8 @@ export const buttonWrapLabel: StoryObj = { This an alert with a very long description. This an alert with a very long description. @@ -229,31 +796,61 @@ export const buttonWrapLabel: StoryObj = { -
-
-
check_circle
-
-

buttonWrapLabel to true

- This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. -
+ - -
-
-
-
check_circle
-
-

buttonWrapLabel to false (by default)

- This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. This an alert with a very long description. -
+ - -
`, language: 'html' } @@ -261,18 +858,50 @@ export const buttonWrapLabel: StoryObj = { } }; -export const Success: StoryObj = { +export const ActionAndLinkButtonsWithIcon: StoryObj = { render: () => ({ components: { PuikAlert }, + template: ` -
- This a success alert with a title and a description. - This a success alert with only a description. - This a success alert with a title and a description and a button. - This a success alert with a description and a button. -
+
+ + This is the description + + + + This is the description + + + + This is the description + + + + This is the description + +
` }), @@ -280,47 +909,147 @@ export const Success: StoryObj = { docs: { source: { code: ` - - - This a success alert with a title and a description. - + + + This is the description + - -
-
- check_circle -
-

Title

- This a success alert with a title and a description. + + This is the description + + + + This is the description + + + + This is the description + + + + - `, + +
+
+ + + + + + + `, language: 'html' } } } }; -export const Warning: StoryObj = { +export const InternalAndExternalLink = { render: () => ({ components: { - PuikAlert + PuikAlert, + PuikButton }, + setup() { + const showAlert = ref(true); + return { showAlert }; + }, + template: ` -
- This a warning alert with a title and a description. - This a warning alert with disabled borders - This a warning alert with only a description. - This a warning alert with a title and a description and a button. - This a warning alert with a description and a button. -
+
+ + Use vue router-link under the hood + + + + Use native a tag under the hood + +
` }), @@ -330,26 +1059,57 @@ export const Warning: StoryObj = { code: ` + Use vue router-link under the hood + + + - This a warning alert with a title and a description. + Use native a tag under the hood -
-
- warning -
-

Title

- This a warning alert with a title and a description. -
+ + + + `, language: 'html' } @@ -357,19 +1117,36 @@ export const Warning: StoryObj = { } }; -export const Info: StoryObj = { +export const UseDefaultSlotForDescription = { render: () => ({ components: { - PuikAlert + PuikAlert, + PuikButton, + PuikLink + }, + setup() { + const showAlert = ref(true); + return { showAlert }; }, + template: ` -
- This a info alert with a title and a description. - This a info alert with disabled borders - This a info alert with only a description. - This a info alert with a title and a description and a button. - This a info alert with a description and a button. -
+ +
+

+ This is a description writes in the default slot +

+ + use a puik-link in description for example + +
+
` }), @@ -377,27 +1154,44 @@ export const Info: StoryObj = { docs: { source: { code: ` - + - This a info alert with a title and a description. +
+

+ This is a description writes in the default slot +

+ + use a puik-link in description for example + +
-
-
- info -
-

Title

- This a info alert with a title and a description. -
+ `, language: 'html' @@ -406,19 +1200,52 @@ export const Info: StoryObj = { } }; -export const Danger: StoryObj = { +export const Events = { render: () => ({ components: { PuikAlert }, + setup() { + const eventEmitted = ref('Click on Action, Link or Close button'); + const variant = ref('info'); + const title = ref('Handle Events'); + + const successTriggered = (msg: string) => { + eventEmitted.value = msg; + title.value = msg; + variant.value = 'success'; + setTimeout(() => { + eventEmitted.value = 'Click on Action, Link or Close button'; + title.value = 'Handle Events'; + variant.value = 'info'; + }, 1000); + }; + + const handleClose = () => { + successTriggered('close event triggered !'); + }; + const handleClickAction = () => { + successTriggered('click event triggered !'); + }; + const handleClickLink = () => { + successTriggered('click-link event triggered !'); + }; + return { eventEmitted, title, variant, successTriggered, handleClose, handleClickAction, handleClickLink }; + }, + template: ` -
- This a danger alert with a title and a description. - This a danger alert with disabled borders - This a danger alert with only a description. - This a danger alert with a title and a description and a button. - This a danger alert with a description and a button. -
+ + {{ eventEmitted }} + ` }), @@ -427,27 +1254,48 @@ export const Danger: StoryObj = { source: { code: ` + + - This a info alert with a title and a description. + {{ eventEmitted }} -
-
- danger -
-

Title

- This a danger alert with a title and a description. -
-
- -
+ `, language: 'html' } diff --git a/packages/components/alert/test/alert.spec.ts b/packages/components/alert/test/alert.spec.ts index bfbae6f8..839efa5d 100644 --- a/packages/components/alert/test/alert.spec.ts +++ b/packages/components/alert/test/alert.spec.ts @@ -1,7 +1,7 @@ import { mount, ComponentMountingOptions, VueWrapper } from '@vue/test-utils'; import { describe, it, expect } from 'vitest'; import { faker } from '@faker-js/faker'; -import { PuikAlert, AlertProps } from '@prestashopcorp/puik-components'; +import { PuikAlert, AlertProps, PuikAlertVariants } from '@prestashopcorp/puik-components'; describe('Alert tests', () => { let wrapper: VueWrapper; @@ -12,6 +12,11 @@ describe('Alert tests', () => { const findDesc = () => wrapper.find('.puik-alert__description'); const findCloseButton = () => wrapper.find('.puik-alert__close'); const findLink = () => wrapper.find('.puik-alert__link'); + const findLeftButtonIcon = () => wrapper.find('.puik-alert__button .puik-button__left-icon'); + const findRightButtonIcon = () => wrapper.find('.puik-alert__button .puik-button__right-icon'); + const findLeftLinkIcon = () => wrapper.find('.puik-alert__link .puik-button__left-icon'); + const findRightLinkIcon = () => wrapper.find('.puik-alert__link .puik-button__right-icon'); + const findIcon = () => wrapper.find('.puik-alert__icon'); const factory = ( props?: AlertProps, @@ -28,16 +33,26 @@ describe('Alert tests', () => { expect(wrapper).toBeTruthy(); }); - it('should display an success alert by default', () => { + it('should display a success alert by default', () => { factory(); expect(findAlert().classes()).toContain('puik-alert--success'); }); it('should display a warning alert', () => { - factory({ variant: 'warning' }); + factory({ variant: PuikAlertVariants.Warning }); expect(findAlert().classes()).toContain('puik-alert--warning'); }); + it('should display a danger alert', () => { + factory({ variant: PuikAlertVariants.Danger }); + expect(findAlert().classes()).toContain('puik-alert--danger'); + }); + + it('should display an info alert', () => { + factory({ variant: PuikAlertVariants.Info }); + expect(findAlert().classes()).toContain('puik-alert--info'); + }); + it('should set the button label wrap to false', () => { factory({ buttonLabel: 'Button', buttonWrapLabel: false }); expect(findButton().classes()).toContain('puik-button--no-wrap'); @@ -51,7 +66,7 @@ describe('Alert tests', () => { }); it('should display a link which emits the clickLink event on click', async () => { - factory({ linkLabel: 'See more' }); + factory({ linkLabel: 'Learn more' }); expect(findLink().exists()).toBeTruthy(); await findLink().trigger('click'); expect(wrapper.emitted('clickLink')).toBeTruthy(); @@ -91,7 +106,7 @@ describe('Alert tests', () => { title: faker.lorem.word(2), description: faker.lorem.sentence(60), buttonLabel: 'Button', - linkLabel: 'See more', + linkLabel: 'Learn more', isClosable: true, dataTest: 'alert' }); @@ -125,4 +140,59 @@ describe('Alert tests', () => { }); expect(findAlert().attributes('aria-live')).toBe('assertive'); }); + + it('should display the correct icon for each variant', () => { + const variants = [PuikAlertVariants.Success, PuikAlertVariants.Warning, PuikAlertVariants.Danger, PuikAlertVariants.Info]; + variants.forEach(variant => { + factory({ variant }); + expect(findIcon().exists()).toBeTruthy(); + }); + }); + + it('should not render optional elements when props are not provided', () => { + factory(); + expect(findButton().exists()).toBeFalsy(); + expect(findLink().exists()).toBeFalsy(); + expect(findCloseButton().exists()).toBeFalsy(); + }); + + it('should set the internal link correctly', () => { + factory({ linkLabel: 'Learn more', internalLink: '/internal' }); + expect(findLink().attributes('to')).toBe('/internal'); + }); + + it('should set the external link correctly', () => { + factory({ linkLabel: 'Learn more', externalLink: 'https://example.com' }); + expect(findLink().attributes('href')).toBe('https://example.com'); + }); + + it('should set the aria-label for the button correctly', () => { + factory({ buttonLabel: 'Button', ariaLabelBtn: 'Button aria label' }); + expect(findButton().attributes('aria-label')).toBe('Button aria label'); + }); + + it('should set the aria-label for the link correctly', () => { + factory({ linkLabel: 'Learn more', ariaLabelLink: 'Link aria label' }); + expect(findLink().attributes('aria-label')).toBe('Link aria label'); + }); + + it('should display the left icon for the button', () => { + factory({ buttonLabel: 'Button', leftIconBtn: 'favorite' }); + expect(findLeftButtonIcon().classes()).toContain('puik-button__left-icon'); + }); + + it('should display the right icon for the button', () => { + factory({ buttonLabel: 'Button', rightIconBtn: 'favorite' }); + expect(findRightButtonIcon().classes()).toContain('puik-button__right-icon'); + }); + + it('should display the left icon for the link', () => { + factory({ linkLabel: 'Learn more', leftIconLink: 'favorite' }); + expect(findLeftLinkIcon().classes()).toContain('puik-button__left-icon'); + }); + + it('should display the right icon for the link', () => { + factory({ linkLabel: 'Learn more', rightIconLink: 'favorite' }); + expect(findRightLinkIcon().classes()).toContain('puik-button__right-icon'); + }); }); diff --git a/packages/theme/src/puik-alert.scss b/packages/theme/src/puik-alert.scss index a3221f54..62f1e1c4 100755 --- a/packages/theme/src/puik-alert.scss +++ b/packages/theme/src/puik-alert.scss @@ -7,30 +7,50 @@ .puik-alert__icon { @apply text-green; } + .puik-button--text{ + &:hover { + @apply bg-green-100; + } + } } &--warning { @apply bg-yellow-50 border-yellow; .puik-alert__icon { @apply text-yellow; } + .puik-button--text{ + &:hover { + @apply bg-yellow-100; + } + } } &--danger { @apply bg-red-50 border-red; .puik-alert__icon { @apply text-red; } + .puik-button--text{ + &:hover { + @apply bg-red-100; + } + } } &--info { @apply bg-blue-50 border-blue; .puik-alert__icon { @apply text-blue; } + .puik-button--text{ + &:hover { + @apply bg-blue-100; + } + } } &--no-borders { @apply border-0; } &__container { - @apply flex flex-col lg:flex-row lg:items-start w-full; + @apply flex flex-col space-y-1 lg:flex-row lg:items-start lg:space-x-1 w-full; } &__content { @apply flex flex-row flex-grow; @@ -54,7 +74,4 @@ &__close { @apply ml-4 cursor-pointer leading-none; } - &__link { - @apply flex-none leading-6 w-fit block ml-9 pl-0 m-2 lg:mx-4 lg:pl-0 lg:truncate lg:max-w-[320px]; - } }