Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

47 create tag component #55

Merged
merged 19 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
24 changes: 24 additions & 0 deletions src/components/error/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

import IonIcon from '../icons/icons';
import { ErrorBoundaryStyled } from './styled';

export interface ErrorBoundaryProps {
msg: string;
}
const sizeIcon = 16;
const iconType = 'info';

const ErrorBoundary = ({ msg }: ErrorBoundaryProps) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome component

return (
<ErrorBoundaryStyled data-testid="ion-error-boundary">
<IonIcon type={iconType} size={sizeIcon}></IonIcon>
<div>
<label>Error:</label>
{msg}
</div>
</ErrorBoundaryStyled>
);
};

export default ErrorBoundary;
45 changes: 45 additions & 0 deletions src/components/error/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import stitches from '../../stitches.config';

const { styled } = stitches;

export const ErrorBoundaryStyled = styled('div', {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: '8px 16px 8px 12px',
gap: 8,

width: 'max-content',
height: 24,

backgroundColor: '$warning1',

borderWidth: '1px 1px 1px 8px',
borderStyle: 'solid',
borderColor: '$warning7',
borderRadius: '8px',

fontSize: 14,
color: '$neutral8',

svg: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 8,

backgroundColor: '$warning7',
fill: '$warning1',

borderRadius: 8,
},

div: {
display: 'flex',
gap: 4,
},

label: {
fontWeight: 'bold',
},
});
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Button } from './button';
export { default as Icon } from './icons';
export { default as IonChip } from './chip';
export { default as IonTag } from './tag';
1 change: 1 addition & 0 deletions src/components/tag/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './tag';
59 changes: 59 additions & 0 deletions src/components/tag/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import stitches from '../../stitches.config';

const { styled } = stitches;

const setColors = (bgColor: string, color: string) => ({
backgroundColor: bgColor,
color: color,
svg: {
fill: color,
},
});

export const TagStyle = styled('div', {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: '2px 8px',
gap: '6px',

width: 'max-content',
height: 'max-content',
minHeight: '20px',

backgroundColor: '$whitetransparence90',
color: '$neutral7',
borderRadius: '50px',

span: {
fontSize: '12px',
fontWeight: '400',
lineHeight: '16px',
},

variants: {
status: {
success: {
...setColors('$positive1', '$positive7'),
},
info: {
...setColors('$info1', '$info7'),
},
warning: {
...setColors('$warning1', '$warning7'),
},
negative: {
...setColors('$negative1', '$negative7'),
},
neutral: {
...setColors('$neutral2', '$neutral7'),
},
},
outline: {
true: {
border: '1px solid',
},
},
},
});
75 changes: 75 additions & 0 deletions src/components/tag/tag.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import IonTag, { IonTagProps } from './tag';
import { StatusType } from '../../core/types/status';

const defaultTag: IonTagProps = {
label: 'tag label',
};

const sut = (props = defaultTag) => render(<IonTag {...props} />);
const tagId = 'ion-tag';
const getTag = () => screen.getByTestId(tagId);

const customColor = '#AADD00';

describe('IonTag', () => {
it('should render default tag ', () => {
sut();
expect(getTag()).toBeTruthy();
});

it('should render tag with label "example tag"', async () => {
const customLabel = 'example tag';
await sut({ label: customLabel });
expect(screen.findByText(customLabel)).toBeTruthy();
});

it.each([
'success',
'info',
'warning',
'negative',
'neutral',
] as StatusType[])('should render tag with status: %s', (status) => {
sut({ ...defaultTag, status: status });
expect(getTag().className).toContain(`status-${status}`);
});

it('should not render outline in tag', async () => {
await sut({ ...defaultTag, outline: false });
expect(getTag().className).not.toContain('outline-false');
});

it('should render outline in tag', async () => {
await sut({ ...defaultTag, outline: true });
expect(getTag().className).toContain('outline-true');
});

it('should render tag with icon check', async () => {
const iconType = 'check';
await sut({ ...defaultTag, icon: iconType });
const icon = screen.getByTestId(`ion-icon-${iconType}`);
expect(icon).toBeTruthy();
});

it('should render ErrorBoundary component when not exist label', async () => {
sut({ label: '' });
const msgError = 'Label cannot be empty';
const errorBoundary = screen.getByTestId('ion-error-boundary');
expect(errorBoundary).toBeTruthy();
expect(await screen.findByText(msgError)).toBeTruthy();
});

it('should render tag with custom color', async () => {
await sut({ ...defaultTag, color: customColor });
expect(getTag().className).not.toContain('status');
});

it('should render the tag the same as it has a custom color', async () => {
const statusInfo = 'status-info';
await sut({ ...defaultTag, status: 'info', color: customColor });
expect(getTag().className).toContain(statusInfo);
});
});
69 changes: 69 additions & 0 deletions src/components/tag/tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';

import { TagStyle } from './styles';
import IonIcon from '../icons/icons';

import { validateHexColor } from '../utils/validateHexColor';
import ErrorBoundary from '../error/error-boundary';
import { TagStatus } from '../../core/types/status';
import { iconType } from '../icons/svgs/icons';

export interface IonTagProps {
outline?: boolean;
status?: TagStatus;
color?: string;
label: string;
icon?: iconType;
}

const iconSize = 12;
const defaultColor = '#505566';
const lighteningFactor = '1A';

const isValidLabel = (label: string) => label && !(String(label).trim() === '');

const newColor = (color: string) => ({
backgroundColor: color + lighteningFactor,
color: color,
fill: color,
});

const getColorObject = (status?: TagStatus, color?: string) => {
if (status) {
return {};
}

if (!color || !validateHexColor(color)) {
return { ...newColor(defaultColor) };
}

return {
...newColor(color),
};
};

const IonTag = ({
label,
color,
icon,
status,
outline = true,
}: IonTagProps) => {
if (!isValidLabel(label)) {
return <ErrorBoundary msg="Label cannot be empty" />;
}

return (
<TagStyle
data-testid="ion-tag"
status={status}
outline={outline}
css={{ ...getColorObject(status, color) }}
>
{icon && <IonIcon type={icon} size={iconSize} />}
<span>{label}</span>
</TagStyle>
);
};

export default IonTag;
3 changes: 3 additions & 0 deletions src/components/utils/validateHexColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const validateHexColor = (color: string): boolean => {
return /^#(?:[0-9a-fA-F]{3,4}){1,2}$/.test(color);
};
2 changes: 2 additions & 0 deletions src/core/types/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type StatusType = 'success' | 'info' | 'warning' | 'negative';
export type TagStatus = 'success' | 'info' | 'warning' | 'negative' | 'neutral';
84 changes: 84 additions & 0 deletions src/stories/tag/tag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';

import IonTag, { IonTagProps } from '../../components/tag/tag';

export default {
title: 'Ion/Data Display/Tag',
component: IonTag,
} as ComponentMeta<typeof IonTag>;

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

export const tagDefault = Template.bind({});
tagDefault.storyName = 'type: default';
tagDefault.args = {
label: 'Exemple Message',
};

export const TagWithoutOutline = Template.bind({});
TagWithoutOutline.storyName = 'type: without outline';
TagWithoutOutline.args = {
label: 'Exemple Message',
outline: false,
};

export const TagWithStatusSuccess = Template.bind({});
TagWithStatusSuccess.storyName = 'type: success';
TagWithStatusSuccess.args = {
label: 'tag with status success',
status: 'success',
};

export const TagWithStatusWarning = Template.bind({});
TagWithStatusWarning.storyName = 'type: warning';
TagWithStatusWarning.args = {
label: 'Exemple Message',
status: 'warning',
};

export const TagWithStatusInfo = Template.bind({});
TagWithStatusInfo.storyName = 'type: info';
TagWithStatusInfo.args = {
label: 'Exemple Message',
status: 'info',
};

export const TagWithStatusNegative = Template.bind({});
TagWithStatusNegative.storyName = 'type: negative';
TagWithStatusNegative.args = {
label: 'Exemple Message',
status: 'negative',
};

export const TagWithStatusNeutral = Template.bind({});
TagWithStatusNeutral.storyName = 'type: neutral';
TagWithStatusNeutral.args = {
label: 'Exemple Message',
status: 'neutral',
};

export const TagCustomColor = Template.bind({});
TagCustomColor.storyName = 'type: custom';
TagCustomColor.args = {
label: 'Exemple Message',
color: '#7f0dff',
};

export const tagWithIcon = Template.bind({});
tagWithIcon.storyName = 'type: with icon';
tagWithIcon.args = {
label: 'Exemple Message',
icon: 'check',
status: 'success',
outline: true,
};

export const tagError = Template.bind({});
tagError.storyName = 'type: Error';
tagError.args = {
label: '',
icon: 'check',
status: 'success',
};