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

[NDS-804] feat: Tag component #686

Merged
merged 15 commits into from
Oct 30, 2023
4 changes: 2 additions & 2 deletions src/components/Avatar/Avatar.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export type AvatarColors = 'blue' | 'teal' | 'purple' | 'red' | 'orange';
export type AvatarSizes = 1 | 2 | 3 | 4 | 5 | 6;

export type AvatarProps = {
/** The size of the avatar
/** The size of the Avatar
* @default 1
* */
size?: AvatarSizes;
/** the color of the button based on our colors eg. red-500
/** The color of the Avatar
* @default 'blue'
* */
color?: AvatarColors;
Expand Down
223 changes: 223 additions & 0 deletions src/components/Tag/Tag.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { Meta, Preview, Props, Story } from '@storybook/addon-docs';
import { boolean, select, text, withKnobs } from '@storybook/addon-knobs';
import Tag from './Tag';
import Typography from '../Typography';
import Stack from '../storyUtils/Stack';
import { FIGMA_URL, Function } from '../../utils/common';
import SectionHeader from '../../storybook/SectionHeader';
import TagShowcase from '../../storybook/Showcases/TagShowcase';
import { getIconSelectorKnob } from '../../utils/stories';
import { useState } from 'react';

<Meta
title="Design System/Tag"
component={Tag}
parameters={{
design: [
{
type: 'figma',
name: 'Tag',
url: `${FIGMA_URL}?node-id=3325%3A58246`,
},
],
chromatic: { diffThreshold: 0.3 },
}}
/>

<SectionHeader title={'Tag'} />

- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Variants](#variants)

## Overview

A universal Tag that is used to highlight system-generated metadata.

## Props

<Props of={Tag} />

## Usage

<UsageGuidelines
guidelines={[
'Use tags to show metadata that needs to be highlighted somehow',
'Use tags to relate secondary information about a page element (px a “Writer” tag next to a name)s',
'Use selectable tags to quickly filter existing content',
'Do not use tags as standalone elements in a page; they should always relate to something elseh',
panvourtsis marked this conversation as resolved.
Show resolved Hide resolved
'Do not use tags for complex filtering. If you want a tag that shows a select menu upon being clicked, used filters instead',
'Try not to use selectable tags within tables, as they might confuse the user. Opt for read-only tag variants when possible',
]}
/>

## Variants

### Read-only Tag

Read-only Tag comes in 5 colors: `neutral`, `blue`, `red`, `purple` and `teal`.

<Preview>
<Story name="Read-only Tag">
<Stack>
<Tag>Label</Tag>
<Tag color="blue">Label</Tag>
<Tag color="red">Label</Tag>
<Tag color="purple">Label</Tag>
<Tag color="teal">Label</Tag>
</Stack>
</Story>
</Preview>

### Read-only Tag with icon

An leading icon can be added to the read-only Tag.

<Preview>
<Story name="Read-only Tag with icon">
<Stack>
<Tag iconName={'dashboard'}>Label</Tag>
<Tag iconName={'dashboard'} color="blue">
Label
</Tag>
<Tag iconName={'dashboard'} color="red">
Label
</Tag>
<Tag iconName={'dashboard'} color="purple">
Label
</Tag>
<Tag iconName={'dashboard'} color="teal">
Label
</Tag>
</Stack>
</Story>
</Preview>

### Selectable Tag

Tag can also be selectable. To enable this functionality, provide the `onSelect` prop to the component.<br/>
The `isSelected` prop indicates whether the Tag is selected or not.

<Preview>
<Story name="Selectable Tag">
<Stack>
<Function>
{() => {
const [isSelected, setIsSelected] = useState(false);
return (
<Tag isSelected={isSelected} onSelect={() => setIsSelected(true)}>
panvourtsis marked this conversation as resolved.
Show resolved Hide resolved
Label
</Tag>
);
}}
</Function>
</Stack>
</Story>
</Preview>

### Clearable Tag

Tag can also be clearable. To enable this functionality, provide the `onClear` prop to the component

<Tip>
Tags cannot be selectable and clearable at the same time. If the onClear prop is provided, it
overrides the selectable functionality
</Tip>

<Preview>
<Story name="Clearable Tag">
<Stack>
<Tag onClear={() => console.log('clear')}>Label</Tag>
</Stack>
</Story>
</Preview>

### Tag Sizes

There are two Tag sizes: `normal` and `small`.

<Preview>
<Story name="Tag Sizes">
<Stack>
<Typography>Read-only Tag</Typography>
</Stack>
<Stack>
<Tag>Normal</Tag>
<Tag color="blue">Normal</Tag>
<Tag color="red">Normal</Tag>
<Tag color="purple">Normal</Tag>
<Tag color="teal">Normal</Tag>
</Stack>
<Stack>
<Tag size="small">Small</Tag>
<Tag size="small" color="blue">
Small
</Tag>
<Tag size="small" color="red">
Small
</Tag>
<Tag size="small" color="purple">
Small
</Tag>
<Tag size="small" color="teal">
Small
</Tag>
</Stack>
<Stack>
<Typography>Selectable Tag</Typography>
</Stack>
<Stack>
<Function>
{() => {
const [isSelected, setIsSelected] = useState(false);
return (
<Tag isSelected={isSelected} onSelect={() => setIsSelected(true)}>
Normal
</Tag>
);
}}
</Function>
</Stack>
<Stack>
<Function>
{() => {
const [isSelected, setIsSelected] = useState(false);
return (
<Tag size="small" isSelected={isSelected} onSelect={() => setIsSelected(true)}>
Small
</Tag>
);
}}
</Function>
</Stack>
<Stack>
<Typography>Clearable Tag</Typography>
</Stack>
<Stack>
<Tag onClear={() => console.log('clear')}>Normal</Tag>
</Stack>
<Stack>
<Tag size="small" onClear={() => console.log('clear')}>
Small
</Tag>
</Stack>
</Story>
</Preview>

### Playground

<Preview>
<Story name="Playground" parameters={{ decorators: [withKnobs] }}>
<Stack>
<TagShowcase
text={text('text', 'Label')}
type={select('type', ['read-only', 'selectable', 'clearable'], 'read-only')}
size={select('size', ['normal', 'small'], 'normal')}
color={select('color', ['neutral', 'blue', 'red', 'purple', 'teal'], 'neutral')}
hasIcon={boolean('hasIcon', false)}
iconName={getIconSelectorKnob('iconLeftName')}
/>
</Stack>
</Story>
</Preview>
91 changes: 91 additions & 0 deletions src/components/Tag/Tag.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { css, SerializedStyles } from '@emotion/react';
import { rem } from 'theme/utils';

import { getTagTokens } from './Tag.tokens';
import { TagProps } from './Tag.types';
import { Theme } from '../../theme';
import { generateStylesFromTokens } from 'components/Typography/utils';

export const tagContainerStyles =
({
size = 'normal',
color = 'blue',
isSelectable,
isClearable,
isSelected,
}: Pick<TagProps, 'size' | 'color' | 'isSelected'> & {
isSelectable?: boolean;
isClearable?: boolean;
}) =>
(theme: Theme): SerializedStyles => {
const tokens = getTagTokens(theme);

const isInteractive = isSelectable || isClearable;

const getBackgroundColor = () => {
if (isInteractive) {
if (isSelected) return tokens('backgroundColor.interactive.focused');

return tokens('backgroundColor.interactive.default');
}

return tokens(`backgroundColor.readOnly.${color}` as const);
};

return css`
display: flex;
justify-content: center;
align-items: center;

height: ${tokens(`${size}.height` as const)};
width: fit-content;
box-sizing: border-box;
gap: ${tokens('paddingContent')};

padding: ${`${tokens(`${size}.paddingVertical` as const)} ${tokens(
`${size}.paddingHorizontal` as const
)}`};

cursor: ${isSelectable ? 'pointer' : 'auto'};
background: ${getBackgroundColor()};
color: ${isInteractive ? tokens('textColor.blue') : tokens(`textColor.${color}` as const)};
border: ${tokens('borderWidth')} solid;

border-color: ${isInteractive
? tokens('borderColor.interactive.default')
: tokens(`borderColor.readOnly.${color}` as const)};

border-radius: ${tokens(`borderRadius.${size}` as const)};

&:hover,
&:focus-visible {
background: ${isInteractive ? tokens('backgroundColor.interactive.focused') : null};
}

&:focus-visible {
border-color: ${isInteractive ? tokens('borderColor.interactive.focused') : null};
}

${generateStylesFromTokens(tokens(`label.${size}` as const))}
`;
};

export const iconStyles =
() =>
(theme: Theme): SerializedStyles => {
return css`
/** @TODO: revisit all these styles when Interactive Icon is implemented */
width: ${rem(16)};
height: ${rem(16)};
display: flex;
align-items: center;
justify-content: center;

cursor: pointer;

&:focus-visible {
background: ${theme.tokens.state.get('backgroundColor.hover')};
border-radius: ${theme.globals.borderRadius.get('7')};
}
`;
};
54 changes: 54 additions & 0 deletions src/components/Tag/Tag.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { fireEvent, render } from '../../test';

import Tag from './Tag';

const handleSelect = jest.fn();
const handleClear = jest.fn();

describe('Tag', () => {
test('that on selectable Tag the onSelect handler is being called', () => {
const { getByTestId } = render(
<Tag onSelect={handleSelect} dataTestPrefixId="test">
Tag
</Tag>
);

const tag = getByTestId('test_tag_container');
expect(tag).toBeInTheDocument();

fireEvent.click(tag);

expect(handleSelect).toHaveBeenCalledTimes(1);
});

test('that selectable Tag is rendered correctly when selected', () => {
const { getByTestId } = render(
<Tag onSelect={handleSelect} isSelected dataTestPrefixId="test">
Tag
</Tag>
);
const tag = getByTestId('test_tag_container');
expect(tag).toBeInTheDocument();

const tagCheck = getByTestId('test_tag_prefix');
expect(tagCheck).toBeInTheDocument();
});

test('that on clearable Tag the onClear handler is being called', () => {
const { getByTestId } = render(
<Tag onClear={handleClear} dataTestPrefixId="test">
Tag
</Tag>
);
const tag = getByTestId('test_tag_container');
expect(tag).toBeInTheDocument();

const tagClear = getByTestId('test_tag_suffix');
expect(tagClear).toBeInTheDocument();

fireEvent.click(tagClear);

expect(handleClear).toHaveBeenCalledTimes(1);
});
});
10 changes: 10 additions & 0 deletions src/components/Tag/Tag.tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import tag from 'theme/tokens/components/variables/tag';
import { getComponentTokens, DotKeys } from 'theme/tokens/utils';

import { Theme } from '../../theme';

export type TagTokens = DotKeys<typeof tag>;

export const getTagTokens = (theme: Theme) => {
return getComponentTokens<TagTokens>(tag, theme);
};
Loading
Loading