From 62227c7f34d50da014816a01690e6a8c356c994d Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Tue, 26 Nov 2024 21:08:37 +0200 Subject: [PATCH 1/8] Created Alert component --- src/design-system/components/alert/Alert.scss | 130 ++++++++ src/design-system/components/alert/Alert.tsx | 88 +++++ src/design-system/stories/Alert.stories.tsx | 307 ++++++++++++++++++ 3 files changed, 525 insertions(+) create mode 100644 src/design-system/components/alert/Alert.scss create mode 100644 src/design-system/components/alert/Alert.tsx create mode 100644 src/design-system/stories/Alert.stories.tsx diff --git a/src/design-system/components/alert/Alert.scss b/src/design-system/components/alert/Alert.scss new file mode 100644 index 000000000..e80782d6c --- /dev/null +++ b/src/design-system/components/alert/Alert.scss @@ -0,0 +1,130 @@ +@use '~scss/utilities' as *; + +.#{$prefix}alert { + --#{$prefix}alert-padding-x: #{get-var('space-2')}; + --#{$prefix}alert-padding-y: #{get-var('space-1-5')}; + --#{$prefix}alert-inner-gap: #{get-var('space-0')}; + --#{$prefix}alert-font-size: #{get-var('font-size-sm')}; + --#{$prefix}alert-font-weight: #{get-var('font-weight-regular')}; + --#{$prefix}alert-line-height: #{get-var('line-height-md')}; + --#{$prefix}alert-border-width: #{get-var('border-width-xs')}; + --#{$prefix}alert-border-radius: #{get-var('border-radius-xs')}; + --#{$prefix}alert-shadow: #{get-var('box-shadow-xs')}; + --#{$prefix}alert-color: #{get-var('blue-gray-800')}; + + position: relative; + padding: get-var('alert-padding-y') get-var('alert-padding-x'); + font-size: get-var('alert-font-size'); + line-height: get-var('alert-line-height'); + font-weight: get-var('alert-font-weight'); + border-width: get-var('alert-border-width'); + border-radius: get-var('alert-border-radius'); + box-shadow: get-var('alert-shadow'); + gap: get-var('alert-inner-gap'); + color: get-var('alert-color'); + + & .MuiAlertTitle-root { + font-weight: $font-weight-medium; + font-size: $font-size-md; + line-height: $line-height-lg; + } + + &.MuiAlert-filled { + color: $neutral-0; + + .s2s-alert-close-button { + color: $blue-gray-50; + } + } + + &.MuiAlert-outlined { + color: $neutral-0; + + .s2s-alert-close-button { + color: $blue-gray-800; + } + } + + &.MuiAlert-colorError { + border-color: $red-500; + } + + &.MuiAlert-colorError.MuiAlert-filled { + color: $red-50; + background-color: $red-500; + } + + &.MuiAlert-colorError.MuiAlert-outlined { + color: $red-800; + background-color: $red-50; + } + + &.MuiAlert-colorWarning { + border-color: $yellow-500; + } + + &.MuiAlert-colorWarning.MuiAlert-filled { + background-color: $yellow-500; + } + + &.MuiAlert-colorWarning.MuiAlert-outlined { + color: $yellow-800; + background-color: $yellow-100; + } + + &.MuiAlert-colorInfo { + border-color: $blue-500; + } + + &.MuiAlert-colorInfo.MuiAlert-filled { + background-color: $blue-500; + } + + &.MuiAlert-colorInfo.MuiAlert-outlined { + color: $blue-800; + background-color: $blue-100; + } + + &.MuiAlert-colorSuccess { + border-color: $green-600; + } + + &.MuiAlert-colorSuccess.MuiAlert-filled { + background-color: $green-600; + } + + &.MuiAlert-colorSuccess.MuiAlert-outlined { + color: $green-800; + background-color: $green-100; + } + + & .s2s-alert-label { + position: absolute; + top: 1.5rem; + right: 4rem; + font-weight: $font-weight-medium; + font-size: $font-size-sm; + line-height: $line-height-sm; + } + + & .s2s-alert-close-button { + background: transparent; + border: none; + border-radius: $border-radius-md; + font-weight: $font-weight-medium; + font-size: $font-size-sm; + line-height: $line-height-sm; + cursor: pointer; + display: flex; + align-items: center; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } + } + + & .s2s-alert-close-button-label { + margin: 0 0.5rem; + height: auto; + } +} diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx new file mode 100644 index 000000000..e7eb74a4c --- /dev/null +++ b/src/design-system/components/alert/Alert.tsx @@ -0,0 +1,88 @@ +import { Ref, SyntheticEvent, forwardRef } from 'react' +import { + Alert as MuiAlert, + AlertProps as MuiAlertProps, + AlertTitle as MuiAlertTitle, + AlertTitleProps +} from '@mui/material' +import { + ErrorOutline, + WarningAmberOutlined, + InfoOutlined, + CheckCircleOutline, + CloseRounded +} from '@mui/icons-material' + +import { cn } from '~/utils/cn' + +import '~scss-components/alert/Alert.scss' + +export const AlertTitle = ({ children, ...props }: AlertTitleProps) => { + return {children} +} + +AlertTitle.displayName = 'AlertTitle' + +interface AlertProps extends MuiAlertProps { + title?: string + description?: string + label?: string +} + +type AlertRef = Ref + +const Alert = forwardRef( + ( + { + title, + label, + description, + children, + icon, + onClose, + className, + ...props + }: AlertProps, + forwardedRef: AlertRef + ) => { + const handleClose = (event: SyntheticEvent) => { + if (onClose) { + onClose(event) + } + } + + const CloseButton = ( + + ) + + return ( + , + warning: , + info: , + success: + }} + onClose={handleClose} + ref={forwardedRef} + slots={{ + closeButton: () => CloseButton + }} + {...props} + > + {title && {title}} + {description &&

{description}

} + {children} +
+ ) + } +) + +Alert.displayName = 'Alert' + +export default Alert diff --git a/src/design-system/stories/Alert.stories.tsx b/src/design-system/stories/Alert.stories.tsx new file mode 100644 index 000000000..b53a58557 --- /dev/null +++ b/src/design-system/stories/Alert.stories.tsx @@ -0,0 +1,307 @@ +import { Meta, StoryObj } from '@storybook/react' + +import Alert from '~scss-components/alert/Alert' + +const meta: Meta = { + title: 'Components/Alert', + component: Alert, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + "The `Alert` displays a short, important message in a way that attracts the user's attention without interrupting the user's task." + } + } + }, + args: { + variant: 'standard', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'success' + }, + argTypes: { + variant: { + description: 'The variant to use.', + control: 'radio', + options: ['filled', 'outlined', 'standard'] + }, + label: { + description: 'The label to display in the alert.', + control: 'text' + }, + title: { + description: 'The title to display in the alert.', + control: 'text' + }, + description: { + description: 'The description to display in the alert.', + control: 'text' + }, + severity: { + description: + 'The severity of the alert. This defines the color and icon used.', + control: 'radio', + options: ['error', 'info', 'success', 'warning'] + }, + icon: { + description: + 'Override the icon displayed before the children. Unless provided, the icon is mapped to the value of the severity prop.' + }, + children: { + description: 'The content of the alert.' + } + } +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + label: 'Label', + title: 'Title', + description: 'Description' + }, + parameters: { + docs: { + description: { + story: + 'This story displays a default alert with the `standard` variant and `success` severity.' + } + } + } +} + +export const All: Story = { + render: (args) => ( +
+ + + + + + + + +
+ ), + parameters: { + docs: { + description: { + story: + 'This story showcases all alert variants in two columns for easy comparison.' + } + } + } +} + +export const FilledError: Story = { + args: { + variant: 'filled', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'error' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `filled` variant and `error` severity. Use this style for critical error messages.' + } + } + } +} + +export const FilledWarning: Story = { + args: { + variant: 'filled', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'warning' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `filled` variant and `warning` severity. Ideal for cautionary messages.' + } + } + } +} + +export const FilledInfo: Story = { + args: { + variant: 'filled', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'info' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `filled` variant and `info` severity. Use this for informational messages.' + } + } + } +} + +export const FilledSuccess: Story = { + args: { + variant: 'filled', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'success' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `filled` variant and `success` severity. Use this for success confirmation messages.' + } + } + } +} + +export const OutlinedError: Story = { + args: { + variant: 'outlined', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'error' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `outlined` variant and `error` severity. Use this style for subtle but critical error messages.' + } + } + } +} + +export const OutlinedWarning: Story = { + args: { + variant: 'outlined', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'warning' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `outlined` variant and `warning` severity. Ideal for understated cautionary messages.' + } + } + } +} + +export const OutlinedInfo: Story = { + args: { + variant: 'outlined', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'info' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `outlined` variant and `info` severity. Use this for understated informational messages.' + } + } + } +} + +export const OutlinedSuccess: Story = { + args: { + variant: 'outlined', + label: 'Label', + title: 'Title', + description: 'Description', + severity: 'success' + }, + parameters: { + docs: { + description: { + story: + 'This story displays an alert with the `outlined` variant and `success` severity. Use this for subtle success confirmation messages.' + } + } + } +} From ddb48874ff2a584493da87b693b26ed83a269233 Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Tue, 26 Nov 2024 21:40:54 +0200 Subject: [PATCH 2/8] Added unit tests --- .../components/Alert/Alert.spec.jsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/unit/design-system/components/Alert/Alert.spec.jsx diff --git a/tests/unit/design-system/components/Alert/Alert.spec.jsx b/tests/unit/design-system/components/Alert/Alert.spec.jsx new file mode 100644 index 000000000..2037d7ed9 --- /dev/null +++ b/tests/unit/design-system/components/Alert/Alert.spec.jsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react' +import { InfoOutlined } from '@mui/icons-material' + +import Alert from '~scss-components/alert/Alert' + +describe('Alert Component', () => { + test('renders the Alert component with the correct title and description', () => { + render() + + expect(screen.getByText('Test Title')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + }) + + test('renders children content', () => { + render( + + Child Content + + ) + + expect(screen.getByText('Child Content')).toBeInTheDocument() + }) + + test('renders the icon provided via the icon prop', () => { + render(} />) + + expect(screen.getByTestId('custom-icon')).toBeInTheDocument() + }) + + test('renders the close button when the onClose prop is provided', () => { + const handleClose = vi.fn() + render() + + const closeButton = screen.getByRole('button', { name: /.*close.*/i }) + expect(closeButton).toBeInTheDocument() + }) + + test('renders the close button with a label when provided', () => { + render( {}} />) + + expect(screen.getByText('Close')).toBeInTheDocument() + }) + + test('applies custom className to the Alert component', () => { + render() + + const alertElement = screen.getByRole('alert') + expect(alertElement).toHaveClass('custom-class') + }) + + test('renders description as a paragraph', () => { + render() + + expect(screen.getByText('Test Description').tagName).toBe('P') + }) +}) From 0d98243695439e54e5976bdaa365bfa93a0e85af Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Tue, 26 Nov 2024 21:46:11 +0200 Subject: [PATCH 3/8] Fixed unit tests --- tests/unit/design-system/components/Alert/Alert.spec.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/design-system/components/Alert/Alert.spec.jsx b/tests/unit/design-system/components/Alert/Alert.spec.jsx index 2037d7ed9..7efa9e8b3 100644 --- a/tests/unit/design-system/components/Alert/Alert.spec.jsx +++ b/tests/unit/design-system/components/Alert/Alert.spec.jsx @@ -31,7 +31,7 @@ describe('Alert Component', () => { const handleClose = vi.fn() render() - const closeButton = screen.getByRole('button', { name: /.*close.*/i }) + const closeButton = screen.getByRole('button', { name: 'Close alert' }) expect(closeButton).toBeInTheDocument() }) From d35a787df4bf32349ae94e281b19a68defc66c6f Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Tue, 26 Nov 2024 21:48:17 +0200 Subject: [PATCH 4/8] Fixed tests --- src/design-system/components/alert/Alert.scss | 9 --------- src/design-system/components/alert/Alert.tsx | 2 +- tests/unit/design-system/components/Alert/Alert.spec.jsx | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/design-system/components/alert/Alert.scss b/src/design-system/components/alert/Alert.scss index e80782d6c..55c4bb01b 100644 --- a/src/design-system/components/alert/Alert.scss +++ b/src/design-system/components/alert/Alert.scss @@ -98,15 +98,6 @@ background-color: $green-100; } - & .s2s-alert-label { - position: absolute; - top: 1.5rem; - right: 4rem; - font-weight: $font-weight-medium; - font-size: $font-size-sm; - line-height: $line-height-sm; - } - & .s2s-alert-close-button { background: transparent; border: none; diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx index e7eb74a4c..6afbe2e4f 100644 --- a/src/design-system/components/alert/Alert.tsx +++ b/src/design-system/components/alert/Alert.tsx @@ -52,7 +52,7 @@ const Alert = forwardRef( } const CloseButton = ( - diff --git a/tests/unit/design-system/components/Alert/Alert.spec.jsx b/tests/unit/design-system/components/Alert/Alert.spec.jsx index 7efa9e8b3..2037d7ed9 100644 --- a/tests/unit/design-system/components/Alert/Alert.spec.jsx +++ b/tests/unit/design-system/components/Alert/Alert.spec.jsx @@ -31,7 +31,7 @@ describe('Alert Component', () => { const handleClose = vi.fn() render() - const closeButton = screen.getByRole('button', { name: 'Close alert' }) + const closeButton = screen.getByRole('button', { name: /.*close.*/i }) expect(closeButton).toBeInTheDocument() }) From 765a6abaef1438749be57f6ded25dc915937d332 Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Tue, 26 Nov 2024 21:56:12 +0200 Subject: [PATCH 5/8] Extract CloseButton into a separate component --- src/design-system/components/alert/Alert.tsx | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx index 6afbe2e4f..8f046dc06 100644 --- a/src/design-system/components/alert/Alert.tsx +++ b/src/design-system/components/alert/Alert.tsx @@ -1,4 +1,4 @@ -import { Ref, SyntheticEvent, forwardRef } from 'react' +import { Ref, SyntheticEvent, forwardRef, ButtonHTMLAttributes } from 'react' import { Alert as MuiAlert, AlertProps as MuiAlertProps, @@ -23,6 +23,19 @@ export const AlertTitle = ({ children, ...props }: AlertTitleProps) => { AlertTitle.displayName = 'AlertTitle' +interface CloseButtonProps extends ButtonHTMLAttributes { + label?: string +} + +const CloseButton = ({ label }: CloseButtonProps) => { + return ( + + ) +} + interface AlertProps extends MuiAlertProps { title?: string description?: string @@ -51,13 +64,6 @@ const Alert = forwardRef( } } - const CloseButton = ( - - ) - return ( CloseButton + closeButton: () => }} {...props} > From f905a54d8264675d45491e5d2b1940f7a5435eea Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Wed, 27 Nov 2024 12:18:24 +0200 Subject: [PATCH 6/8] Extract icon mapping into a variable outside the component --- src/design-system/components/alert/Alert.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx index 8f046dc06..03ccd53b5 100644 --- a/src/design-system/components/alert/Alert.tsx +++ b/src/design-system/components/alert/Alert.tsx @@ -36,6 +36,15 @@ const CloseButton = ({ label }: CloseButtonProps) => { ) } +CloseButton.displayName = 'CloseButton' + +const iconMapping = { + error: , + warning: , + info: , + success: +} + interface AlertProps extends MuiAlertProps { title?: string description?: string @@ -68,12 +77,7 @@ const Alert = forwardRef( , - warning: , - info: , - success: - }} + iconMapping={iconMapping} onClose={handleClose} ref={forwardedRef} slots={{ From e815237fa992b30580b58faf077037e691f89b57 Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Thu, 28 Nov 2024 11:16:30 +0200 Subject: [PATCH 7/8] Used generic in forwardRef function --- src/design-system/components/alert/Alert.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx index 03ccd53b5..391cce917 100644 --- a/src/design-system/components/alert/Alert.tsx +++ b/src/design-system/components/alert/Alert.tsx @@ -1,4 +1,4 @@ -import { Ref, SyntheticEvent, forwardRef, ButtonHTMLAttributes } from 'react' +import { SyntheticEvent, forwardRef, ButtonHTMLAttributes } from 'react' import { Alert as MuiAlert, AlertProps as MuiAlertProps, @@ -51,9 +51,7 @@ interface AlertProps extends MuiAlertProps { label?: string } -type AlertRef = Ref - -const Alert = forwardRef( +const Alert = forwardRef( ( { title, @@ -65,7 +63,7 @@ const Alert = forwardRef( className, ...props }: AlertProps, - forwardedRef: AlertRef + ref ) => { const handleClose = (event: SyntheticEvent) => { if (onClose) { @@ -79,7 +77,7 @@ const Alert = forwardRef( icon={icon} iconMapping={iconMapping} onClose={handleClose} - ref={forwardedRef} + ref={ref} slots={{ closeButton: () => }} From 8bab3dc50b968ff3043ea319859a1b8e6d69b8ce Mon Sep 17 00:00:00 2001 From: Vadym Pavlyk Date: Thu, 28 Nov 2024 11:25:48 +0200 Subject: [PATCH 8/8] Remove unnecessary type --- src/design-system/components/alert/Alert.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/design-system/components/alert/Alert.tsx b/src/design-system/components/alert/Alert.tsx index 391cce917..87ad71ff3 100644 --- a/src/design-system/components/alert/Alert.tsx +++ b/src/design-system/components/alert/Alert.tsx @@ -53,16 +53,7 @@ interface AlertProps extends MuiAlertProps { const Alert = forwardRef( ( - { - title, - label, - description, - children, - icon, - onClose, - className, - ...props - }: AlertProps, + { title, label, description, children, icon, onClose, className, ...props }, ref ) => { const handleClose = (event: SyntheticEvent) => {