Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/icons/icons.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -88,7 +88,7 @@ export const IonIcon = ({

const iconContent = (
<Icon
$color={color}
$color={color || defaultColor}
$size={size}
data-testid={`ion-icon-${type}`}
viewBox='0 0 24 24'
Expand Down
11 changes: 5 additions & 6 deletions src/components/message/message.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { IonIconProps } from '../icons';
import { renderWithTheme } from '../utils/test-utils';
import {
FeedbackTypes,
ICON_BY_TYPE,
IonMessage,
IonMessageProps,
MessageTypes,
} from './message';
} from '../shared/FeedbackIcon/feedbackIcon';
import { renderWithTheme } from '../utils/test-utils';
import { IonMessage, IonMessageProps } from './message';

const message = 'This is a message';

Expand All @@ -24,7 +23,7 @@ describe('IonMessage', () => {
).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`, () => {
Expand Down
30 changes: 7 additions & 23 deletions src/components/message/message.tsx
Original file line number Diff line number Diff line change
@@ -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<IonIconProps, 'type' | 'color'>;
}

export const ICON_BY_TYPE: Record<
MessageTypes,
Pick<IonIconProps, 'type' | 'color'>
> = {
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>
<IonIcon
size={24}
type={customIcon?.type || icon}
color={customIcon ? customIcon.color || '' : color}
/>
<FeedbackIcon type={type} customIcon={customIcon} />
<span>{message}</span>
</Message>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './notification';
74 changes: 74 additions & 0 deletions src/components/notification/notification.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { IconType } from '../icons/svgs/icons';
import {
FeedbackTypes,
ICON_BY_TYPE,
} from '../shared/FeedbackIcon/feedbackIcon';
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(<IonNotification {...props} />);
};

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 FeedbackTypes[])(
'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}`
);
});
});
});
48 changes: 48 additions & 0 deletions src/components/notification/notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IonButton } from '../button';
import { IonIconProps } from '../icons';
import {
FeedbackIcon,
FeedbackTypes,
} from '../shared/FeedbackIcon/feedbackIcon';
import {
Container,
Description,
Notification,
TextContainer,
Title,
} from './styles';

export interface IonNotificationProps {
title: string;
description: string;
customIcon?: Pick<IonIconProps, 'type' | 'color'>;
type?: FeedbackTypes;
onClose?: () => void;
}

export const IonNotification = ({
title,
description,
type = 'info',
customIcon,
onClose,
}: IonNotificationProps) => {
return (
<Notification>
<Container>
<FeedbackIcon type={type} customIcon={customIcon} />
<TextContainer>
<Title>{title}</Title>
<Description>{description}</Description>
</TextContainer>
</Container>
<IonButton
icon='close'
variant='ghost'
circular
size='sm'
onClick={onClose}
/>
</Notification>
);
};
42 changes: 42 additions & 0 deletions src/components/notification/styles.ts
Original file line number Diff line number Diff line change
@@ -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]};
`}
`;
50 changes: 50 additions & 0 deletions src/components/shared/FeedbackIcon/feedbackIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<FeedbackIcon {...props} />);
};

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'
);
});
});
33 changes: 33 additions & 0 deletions src/components/shared/FeedbackIcon/feedbackIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<IonIconProps, 'type' | 'color'>;
}

export const ICON_BY_TYPE: Record<
FeedbackTypes,
Pick<IonIconProps, 'type' | 'color'>
> = {
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 (
<IonIcon
size={24}
type={customIcon?.type || icon}
color={customIcon ? customIcon.color || '' : color}
/>
);
};
29 changes: 29 additions & 0 deletions src/stories/notification/notification.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof IonNotification>;

const Template: ComponentStory<typeof IonNotification> = (
args: IonNotificationProps
) => <IonNotification {...args} />;

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',
},
};