Skip to content

Commit

Permalink
feat(tag): add new Tag component
Browse files Browse the repository at this point in the history
  • Loading branch information
donaldjbrady committed Aug 18, 2020
1 parent 7b7af19 commit 2fa81a4
Show file tree
Hide file tree
Showing 5 changed files with 840 additions and 0 deletions.
43 changes: 43 additions & 0 deletions packages/hs-react-ui/src/components/Tag/Tag.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { select, text, boolean, number, color } from '@storybook/addon-knobs';
import { mdiMessage, mdiSend } from '@mdi/js';
import { storiesOf } from '@storybook/react';

import Tag from './Tag';
import colors from '../../enums/colors';
import variants from '../../enums/variants';

const options = {
none: '',
mdiMessage,
mdiSend,
};

const design = {
type: 'figma',
url: 'https://www.figma.com/file/3r2G00brulOwr9j7F6JF59/Generic-UI-Style?node-id=0%3A1',
};

const testId = `tag-${Math.floor(Math.random() * 100000)}`;
const containerProps = { 'data-test-id': testId };

storiesOf('Tag', module).add(
'Basic Tag',
() => {
return (
<Tag
variant={select('variant', variants, variants.fill)}
color={color('color', colors.primaryDark)}
isLoading={boolean('isLoading', false)}
elevation={number('elevation', 1)}
isProcessing={boolean('isProcessing', false)}
iconPrefix={select('iconPrefix', options, options.none)}
iconSuffix={select('iconSuffix', options, options.none)}
containerProps={containerProps}
>
{text('children', 'Default text')}
</Tag>
);
},
{ design },
);
171 changes: 171 additions & 0 deletions packages/hs-react-ui/src/components/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { ReactNode } from 'react';
import UnstyledIcon from '@mdi/react';
import { mdiLoading } from '@mdi/js';
import styled, { StyledComponentBase } from 'styled-components';
import timings from '../../enums/timings';
import { useTheme } from '../../context';
import variants from '../../enums/variants';
import Progress from '../Progress/Progress';
import { Div } from '../../htmlElements';
import { getFontColorFromVariant, getBackgroundColorFromVariant } from '../../utils/color';
import { SubcomponentPropsType } from '../commonTypes';
import { getShadowStyle } from '../../utils/styles';

export type TagContainerProps = {
elevation: number;
color: string;
variant: variants;
type: string;
disabled: boolean;
};

export type IconContainerProps = {
hasContent: boolean;
position: 'right' | 'left';
};

export type TagProps = {
StyledContainer?: string & StyledComponentBase<any, {}>;
containerProps?: SubcomponentPropsType;
iconPrefix?: string | JSX.Element;
iconSuffix?: string | JSX.Element;
isLoading?: boolean;
isProcessing?: boolean;
children?: ReactNode;
elevation?: number;
variant?: variants;
color?: string;
StyledLoadingBar?: string & StyledComponentBase<any, {}>;
loadingBarProps?: SubcomponentPropsType;
StyledIconContainer?: string & StyledComponentBase<any, {}>;
iconPrefixContainerProps?: SubcomponentPropsType;
iconSuffixContainerProps?: SubcomponentPropsType;
id?: string;
};

export const Container: string & StyledComponentBase<any, {}, TagContainerProps> = styled(Div)`
${({ elevation = 0, color, variant }: TagContainerProps) => {
const { colors } = useTheme();
const backgroundColor = getBackgroundColorFromVariant(variant, color, colors.transparent);
const fontColor = getFontColorFromVariant(variant, color, colors.background, colors.grayDark);
return `
display: inline-flex;
font-size: 1em;
padding: .75em 1em;
border-radius: 0.25em;
transition:
background-color ${timings.fast},
color ${timings.slow},
outline ${timings.slow},
filter ${timings.slow},
box-shadow ${timings.slow};
${getShadowStyle(elevation, colors.shadow)}
outline: 0 none;
border: ${variant === variants.outline ? `1px solid ${color || colors.grayDark}` : '0 none;'};
cursor: pointer;
background-color: ${backgroundColor};
color: ${fontColor};
align-items: center;
`;
}}
`;

const StyledProgress = styled(Progress)`
width: 5rem;
height: 10px;
margin-top: -5px;
margin-bottom: -5px;
`;

const IconContainer = styled(Div)`
${({ position, hasContent }: IconContainerProps) => {
return `
height: 1rem;
${hasContent ? `margin-${position === 'right' ? 'left' : 'right'}: 1em;` : ''}
`;
}}
`;

const Tag = ({
StyledContainer = Container,
containerProps = {},
StyledIconContainer = IconContainer,
iconPrefixContainerProps = {},
iconSuffixContainerProps = {},
StyledLoadingBar = StyledProgress,
loadingBarProps = {},
iconPrefix,
iconSuffix,
isLoading,
isProcessing,
children,
elevation = 0,
variant = variants.fill,
color,
id,
}: TagProps): JSX.Element => {
const hasContent = Boolean(children);
const { colors } = useTheme();
const containerColor = color || colors.grayLight;
// get everything we expose + anything consumer wants to send to container
const mergedContainerProps = {
id,
elevation,
color: containerColor,
variant,
...containerProps,
};

return isLoading ? (
<StyledContainer {...mergedContainerProps}>
<StyledLoadingBar {...loadingBarProps} />
</StyledContainer>
) : (
<StyledContainer {...mergedContainerProps}>
{!isProcessing &&
iconPrefix &&
(typeof iconPrefix === 'string' && iconPrefix !== '' ? (
<StyledIconContainer
hasContent={hasContent}
position="left"
{...iconPrefixContainerProps}
>
<UnstyledIcon path={iconPrefix} size="1rem" />
</StyledIconContainer>
) : (
<StyledIconContainer>{iconPrefix}</StyledIconContainer>
))}
{isProcessing && (
<StyledIconContainer hasContent={hasContent} position="left" {...iconPrefixContainerProps}>
<UnstyledIcon path={mdiLoading} size="1rem" spin={1} />
</StyledIconContainer>
)}
{children}

{iconSuffix &&
(typeof iconSuffix === 'string' ? (
<StyledIconContainer
hasContent={hasContent}
position="right"
{...iconSuffixContainerProps}
>
<UnstyledIcon path={iconSuffix} size="1rem" />
</StyledIconContainer>
) : (
<StyledIconContainer
hasContent={hasContent}
position="right"
{...iconSuffixContainerProps}
>
{iconSuffix}
</StyledIconContainer>
))}
</StyledContainer>
);
};

Tag.Container = Container;
Tag.LoadingBar = StyledProgress;
Tag.IconContainer = IconContainer;
export default Tag;
146 changes: 146 additions & 0 deletions packages/hs-react-ui/src/components/Tag/__tests__/Tag.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React from 'react';
import { render, waitFor, configure } from '@testing-library/react';
import Icon from '@mdi/react';
import colors from '../../../enums/colors';
import variants from '../../../enums/variants';
import Tag from '../Tag';
import { mdiComment } from '@mdi/js';

configure({ testIdAttribute: 'data-test-id' });

describe('Tag', () => {
it('shows loading text when provided', async () => {
const testId = 'tag-1';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag isLoading={true} containerProps={containerProps}>
Shows Loading
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Tag with non-default props variant and color', async () => {
const testId = 'tag-2';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag
color={colors.black}
elevation={2}
variant={variants.outline}
containerProps={containerProps}
>
Variant and color
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Tag with text variant', async () => {
const testId = 'tag-3';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag variant={variants.text} containerProps={containerProps}>
text variant
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Tag with fill variant', async () => {
const testId = 'tag-4';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag variant={variants.fill} containerProps={containerProps}>
fill variant
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows Tag with outline variant', async () => {
const testId = 'tag-5';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag variant={variants.outline} containerProps={containerProps}>
outline variant
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows LeftIconContainer when isProcessing', async () => {
const testId = 'tag-6';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag isProcessing containerProps={containerProps}>
processing
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows icons with type string', async () => {
const testId = 'tag-7';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag iconSuffix={mdiComment} iconPrefix={mdiComment} containerProps={containerProps}>
string icon props
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('shows icons', async () => {
const testId = 'tag-8';
const containerProps = { 'data-test-id': testId };
const { container, getByTestId } = render(
<Tag
iconSuffix={<Icon path={mdiComment} />}
iconPrefix={<Icon path={mdiComment} />}
containerProps={containerProps}
>
shows icons
</Tag>,
);

await waitFor(() => getByTestId(testId));
expect(container).toMatchSnapshot();
});

it('keeps the container the same when switching between isLoading and not isLoading', async () => {
const testId = 'tag-9';
const containerProps = { 'data-test-id': testId };
const { getByTestId, rerender, asFragment } = render(
<Tag isLoading={true} containerProps={containerProps}>
Hello World!
</Tag>,
);

await waitFor(() => getByTestId(testId));
const loadingFragment = asFragment();

rerender(<Tag containerProps={containerProps}>Hello World!</Tag>);
await waitFor(() => getByTestId(testId));
const loadedFragment = asFragment();

// TODO: Use toMatchDiffSnapshot() between the fragments once we can figure out
// how to make it use the jest-styled-components plugin
expect(loadingFragment.firstChild).toMatchSnapshot();
expect(loadedFragment.firstChild).toMatchSnapshot();
});
});
Loading

0 comments on commit 2fa81a4

Please sign in to comment.