From 0bd51cd1783454f2cccc6ce1f6a5be5886bdf94c Mon Sep 17 00:00:00 2001 From: danilo-moreira-brisa Date: Fri, 24 May 2024 10:52:52 -0300 Subject: [PATCH 1/2] feat: add notification component --- src/components/notification/index.ts | 1 + .../notification/notification.test.tsx | 71 +++++++++++++++++++ src/components/notification/notification.tsx | 55 ++++++++++++++ src/components/notification/styles.ts | 42 +++++++++++ .../notification/notification.stories.tsx | 29 ++++++++ 5 files changed, 198 insertions(+) create mode 100644 src/components/notification/index.ts create mode 100644 src/components/notification/notification.test.tsx create mode 100644 src/components/notification/notification.tsx create mode 100644 src/components/notification/styles.ts create mode 100644 src/stories/notification/notification.stories.tsx diff --git a/src/components/notification/index.ts b/src/components/notification/index.ts new file mode 100644 index 0000000..d9b217c --- /dev/null +++ b/src/components/notification/index.ts @@ -0,0 +1 @@ +export * from './notification'; diff --git a/src/components/notification/notification.test.tsx b/src/components/notification/notification.test.tsx new file mode 100644 index 0000000..60cd6c1 --- /dev/null +++ b/src/components/notification/notification.test.tsx @@ -0,0 +1,71 @@ +import { IconType } from '../icons/svgs/icons'; +import { ICON_BY_TYPE, MessageTypes } from '../message'; +import { renderWithTheme } from '../utils/test-utils'; +import { IonNotification, IonNotificationProps } from './notification'; + +const title = 'Notification title'; +const description = 'Notification description'; + +const sut = (props: IonNotificationProps) => { + return renderWithTheme(); +}; + +describe('IonNotification', () => { + describe('Default', () => { + it('should render the notification with the default icon', () => { + const { getByText, getByTestId } = sut({ title, description }); + + expect(getByText(title)).toBeInTheDocument(); + expect(getByText(description)).toBeInTheDocument(); + expect(getByTestId('ion-icon-info-solid')).toBeInTheDocument(); + }); + it('should render a close button', () => { + const { getByTestId } = sut({ title, description }); + + expect(getByTestId('ion-button')).toBeInTheDocument(); + expect(getByTestId('ion-icon-close')).toBeInTheDocument(); + }); + it('should call onClose when the close button is clicked', () => { + const onClose = jest.fn(); + const { getByTestId } = sut({ title, description, onClose }); + + getByTestId('ion-button').click(); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + describe.each(Object.keys(ICON_BY_TYPE) as MessageTypes[])( + 'Type %s', + (type) => { + it(`should render the notification with the ${type} icon`, () => { + const { getByText, getByTestId } = sut({ title, description, type }); + + expect(getByText(title)).toBeInTheDocument(); + expect(getByText(description)).toBeInTheDocument(); + expect( + getByTestId(`ion-icon-${ICON_BY_TYPE[type].type}`) + ).toBeInTheDocument(); + }); + } + ); + describe('Custom Icon', () => { + it('should render the notification with the custom icon', () => { + const customIcon = { + type: 'box' as IconType, + color: '#070ac9', + }; + const { getByText, getByTestId } = sut({ + title, + description, + customIcon, + }); + + expect(getByText(title)).toBeInTheDocument(); + expect(getByText(description)).toBeInTheDocument(); + expect(getByTestId(`ion-icon-${customIcon.type}`)).toBeInTheDocument(); + expect(getByTestId(`ion-icon-${customIcon.type}`)).toHaveStyle( + `fill: ${customIcon.color}` + ); + }); + }); +}); diff --git a/src/components/notification/notification.tsx b/src/components/notification/notification.tsx new file mode 100644 index 0000000..6355b48 --- /dev/null +++ b/src/components/notification/notification.tsx @@ -0,0 +1,55 @@ +import { IonButton } from '../button'; +import { IonIcon, IonIconProps } from '../icons'; +import { ICON_BY_TYPE, MessageTypes } from '../message'; +import { + Container, + Description, + Notification, + TextContainer, + Title, +} from './styles'; + +export interface IonNotificationProps { + title: string; + description: string; + customIcon?: { + type: IonIconProps['type']; + color?: IonIconProps['color']; + }; + type?: MessageTypes; + onClose?: () => void; +} + +export const IonNotification = ({ + title, + description, + type = 'info', + customIcon, + onClose, +}: IonNotificationProps) => { + const { type: icon, color } = ICON_BY_TYPE[type]; + + return ( + + + + + + {title} + {description} + + + + + ); +}; diff --git a/src/components/notification/styles.ts b/src/components/notification/styles.ts new file mode 100644 index 0000000..a4455f6 --- /dev/null +++ b/src/components/notification/styles.ts @@ -0,0 +1,42 @@ +import { css, styled } from 'styled-components'; + +export const Notification = styled.div` + ${({ theme }) => css` + width: 360px; + padding: 12px 16px; + border-radius: 8px; + background: ${theme.colors.transparency.white[90]}; + ${theme.utils.shadow.doubleShadow}; + ${theme.utils.flex.spaceBetween(8)} + align-items: flex-start; + `} +`; + +export const Container = styled.div` + ${({ theme }) => css` + ${theme.utils.flex.start(8)} + align-items: flex-start; + `} +`; + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const Title = styled.span` + ${({ theme }) => css` + ${theme.font.size[16]} + font-weight: 600; + color: ${theme.colors.neutral[8]}; + `} +`; + +export const Description = styled.p` + ${({ theme }) => css` + ${theme.font.size[14]} + font-weight: 400; + color: ${theme.colors.neutral[7]}; + `} +`; diff --git a/src/stories/notification/notification.stories.tsx b/src/stories/notification/notification.stories.tsx new file mode 100644 index 0000000..78e7aa2 --- /dev/null +++ b/src/stories/notification/notification.stories.tsx @@ -0,0 +1,29 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { + IonNotification, + IonNotificationProps, +} from '../../components/notification'; + +export default { + title: 'Ion/Feedback/Notification', + component: IonNotification, +} as ComponentMeta; + +const Template: ComponentStory = ( + args: IonNotificationProps +) => ; + +export const Default = Template.bind({}); +Default.args = { + title: 'Notification Title', + description: 'Notification Description', +}; + +export const CustomIcon = Template.bind({}); +CustomIcon.args = { + title: 'Notification Title', + description: 'Notification Description', + customIcon: { + type: 'box', + }, +}; From 6c7d2807bf0721af43f0f8958e9eb09ad46a2c86 Mon Sep 17 00:00:00 2001 From: danilo-moreira-brisa Date: Fri, 24 May 2024 11:59:45 -0300 Subject: [PATCH 2/2] feat: add a feedback icon to reuse on message and notification --- src/components/icons/icons.tsx | 4 +- src/components/message/message.test.tsx | 11 ++-- src/components/message/message.tsx | 30 +++-------- .../notification/notification.test.tsx | 7 ++- src/components/notification/notification.tsx | 23 +++------ .../shared/FeedbackIcon/feedbackIcon.test.tsx | 50 +++++++++++++++++++ .../shared/FeedbackIcon/feedbackIcon.tsx | 33 ++++++++++++ 7 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 src/components/shared/FeedbackIcon/feedbackIcon.test.tsx create mode 100644 src/components/shared/FeedbackIcon/feedbackIcon.tsx diff --git a/src/components/icons/icons.tsx b/src/components/icons/icons.tsx index 26a9f0e..e876f49 100644 --- a/src/components/icons/icons.tsx +++ b/src/components/icons/icons.tsx @@ -1,7 +1,7 @@ import DOMPurify from 'dompurify'; -import { iconsPaths, IconType } from './svgs/icons'; import { Icon, IconHighlight } from './styles'; +import { iconsPaths, IconType } from './svgs/icons'; export type Highlight = 'none' | 'simple' | 'double'; export interface IonIconProps { @@ -88,7 +88,7 @@ export const IonIcon = ({ const iconContent = ( { ).toBeInTheDocument(); }); }); - describe.each(Object.keys(ICON_BY_TYPE) as MessageTypes[])( + describe.each(Object.keys(ICON_BY_TYPE) as FeedbackTypes[])( 'Type %s', (type) => { it(`should render the message with the ${type} icon`, () => { diff --git a/src/components/message/message.tsx b/src/components/message/message.tsx index 0d79c39..b26a534 100644 --- a/src/components/message/message.tsx +++ b/src/components/message/message.tsx @@ -1,40 +1,24 @@ -import theme from '../../styles/theme'; -import { IonIcon, IonIconProps } from '../icons'; +import { IonIconProps } from '../icons'; +import { + FeedbackIcon, + FeedbackTypes, +} from '../shared/FeedbackIcon/feedbackIcon'; import { Message } from './styles'; -export type MessageTypes = 'success' | 'alert' | 'error' | 'warning' | 'info'; - export interface IonMessageProps { message: string; - type?: MessageTypes; + type?: FeedbackTypes; customIcon?: Pick; } -export const ICON_BY_TYPE: Record< - MessageTypes, - Pick -> = { - success: { type: 'check-solid', color: theme.colors.positive[6] }, - alert: { type: 'attention-solid', color: theme.colors.negative[6] }, - error: { type: 'close-solid', color: theme.colors.negative[6] }, - warning: { type: 'attention-solid', color: theme.colors.warning[6] }, - info: { type: 'info-solid', color: theme.colors.info[6] }, -}; - export const IonMessage = ({ type = 'info', message, customIcon, }: IonMessageProps) => { - const { type: icon, color } = ICON_BY_TYPE[type]; - return ( - + {message} ); diff --git a/src/components/notification/notification.test.tsx b/src/components/notification/notification.test.tsx index 60cd6c1..7258e32 100644 --- a/src/components/notification/notification.test.tsx +++ b/src/components/notification/notification.test.tsx @@ -1,5 +1,8 @@ import { IconType } from '../icons/svgs/icons'; -import { ICON_BY_TYPE, MessageTypes } from '../message'; +import { + FeedbackTypes, + ICON_BY_TYPE, +} from '../shared/FeedbackIcon/feedbackIcon'; import { renderWithTheme } from '../utils/test-utils'; import { IonNotification, IonNotificationProps } from './notification'; @@ -34,7 +37,7 @@ describe('IonNotification', () => { expect(onClose).toHaveBeenCalledTimes(1); }); }); - describe.each(Object.keys(ICON_BY_TYPE) as MessageTypes[])( + describe.each(Object.keys(ICON_BY_TYPE) as FeedbackTypes[])( 'Type %s', (type) => { it(`should render the notification with the ${type} icon`, () => { diff --git a/src/components/notification/notification.tsx b/src/components/notification/notification.tsx index 6355b48..7aaf2dc 100644 --- a/src/components/notification/notification.tsx +++ b/src/components/notification/notification.tsx @@ -1,6 +1,9 @@ import { IonButton } from '../button'; -import { IonIcon, IonIconProps } from '../icons'; -import { ICON_BY_TYPE, MessageTypes } from '../message'; +import { IonIconProps } from '../icons'; +import { + FeedbackIcon, + FeedbackTypes, +} from '../shared/FeedbackIcon/feedbackIcon'; import { Container, Description, @@ -12,11 +15,8 @@ import { export interface IonNotificationProps { title: string; description: string; - customIcon?: { - type: IonIconProps['type']; - color?: IonIconProps['color']; - }; - type?: MessageTypes; + customIcon?: Pick; + type?: FeedbackTypes; onClose?: () => void; } @@ -27,17 +27,10 @@ export const IonNotification = ({ customIcon, onClose, }: IonNotificationProps) => { - const { type: icon, color } = ICON_BY_TYPE[type]; - return ( - - + {title} {description} diff --git a/src/components/shared/FeedbackIcon/feedbackIcon.test.tsx b/src/components/shared/FeedbackIcon/feedbackIcon.test.tsx new file mode 100644 index 0000000..d351144 --- /dev/null +++ b/src/components/shared/FeedbackIcon/feedbackIcon.test.tsx @@ -0,0 +1,50 @@ +import { IconType } from '@ion/components/icons/svgs/icons'; +import { renderWithTheme } from '@ion/components/utils/test-utils'; +import { + FeedbackIcon, + FeedbackIconProps, + FeedbackTypes, + ICON_BY_TYPE, +} from './feedbackIcon'; + +const sut = (props: FeedbackIconProps) => { + return renderWithTheme(); +}; + +describe('FeedbackIcon', () => { + describe.each(Object.keys(ICON_BY_TYPE) as FeedbackTypes[])( + 'Type %s', + (type) => { + it(`should render the feedback icon with the ${type} icon`, () => { + const { getByTestId } = sut({ type }); + + expect( + getByTestId(`ion-icon-${ICON_BY_TYPE[type].type}`) + ).toBeInTheDocument(); + }); + } + ); + it('should render the feedback icon with the custom icon', () => { + const customIcon = { + type: 'box' as IconType, + color: '#33b7ce', + }; + const { getByTestId } = sut({ type: 'info', customIcon }); + + expect(getByTestId(`ion-icon-${customIcon.type}`)).toBeInTheDocument(); + expect(getByTestId(`ion-icon-${customIcon.type}`)).toHaveStyle( + `fill: ${customIcon.color}` + ); + }); + it('should render default color if custom color is not provided', () => { + const customIcon = { + type: 'box' as IconType, + }; + const { getByTestId } = sut({ type: 'info', customIcon }); + + expect(getByTestId(`ion-icon-${customIcon.type}`)).toBeInTheDocument(); + expect(getByTestId(`ion-icon-${customIcon.type}`)).toHaveStyle( + 'fill: #282B33' + ); + }); +}); diff --git a/src/components/shared/FeedbackIcon/feedbackIcon.tsx b/src/components/shared/FeedbackIcon/feedbackIcon.tsx new file mode 100644 index 0000000..a16f2f3 --- /dev/null +++ b/src/components/shared/FeedbackIcon/feedbackIcon.tsx @@ -0,0 +1,33 @@ +import theme from '../../../styles/theme'; + +import { IonIcon, IonIconProps } from '../../icons'; + +export type FeedbackTypes = 'success' | 'alert' | 'error' | 'warning' | 'info'; + +export interface FeedbackIconProps { + type: FeedbackTypes; + customIcon?: Pick; +} + +export const ICON_BY_TYPE: Record< + FeedbackTypes, + Pick +> = { + success: { type: 'check-solid', color: theme.colors.positive[6] }, + alert: { type: 'attention-solid', color: theme.colors.negative[6] }, + error: { type: 'close-solid', color: theme.colors.negative[6] }, + warning: { type: 'attention-solid', color: theme.colors.warning[6] }, + info: { type: 'info-solid', color: theme.colors.info[6] }, +}; + +export const FeedbackIcon = ({ type, customIcon }: FeedbackIconProps) => { + const { type: icon, color } = ICON_BY_TYPE[type]; + + return ( + + ); +};