Skip to content

Commit b110247

Browse files
authored
feat: Tag colours, variant, icon only (#1370)
* feat: Icon only Tag * feat: Added variant to Tag * ci: Fix tests not running because of playwright/ubuntu support * fix: Tag coverage failing because of FC * fix: Fix text colors for some of the tag pastel colors * docs: Remove iconOnly control in Tag story
1 parent 323df01 commit b110247

4 files changed

Lines changed: 331 additions & 32 deletions

File tree

‎.github/workflows/tests.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ on:
44
workflow_call:
55
jobs:
66
tests:
7-
runs-on: ubuntu-latest
7+
# Playwright does not support chromium on ubuntu-latest (26.04) yet
8+
runs-on: ubuntu-24.04
89
outputs:
910
tests_failed: ${{ steps.set_failed.outputs.tests_failed }}
1011
coverage_failed: ${{ steps.set_coverage.outputs.coverage_failed }}

‎src/molecules/Tag/Tag.stories.tsx‎

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
check,
33
code,
44
color_palette,
5+
computer,
56
edit,
67
users_circle,
78
visibility,
89
} from '@equinor/eds-icons';
910
import { Meta, StoryObj } from '@storybook/react-vite';
1011

11-
import { Tag, TagProps } from './Tag';
12+
import { DefaultTagProps, IconTagProps, Tag, TagProps } from './Tag';
1213
import { capitalize } from 'src/atoms/utils';
1314
import { VariantShowcase } from 'src/storybook/VariantShowcase';
1415

@@ -36,6 +37,10 @@ const meta: Meta<typeof Tag> = {
3637
'orange',
3738
'yellow',
3839
'grey',
40+
'white',
41+
'success',
42+
'warning',
43+
'danger',
3944
],
4045
description: 'Default is blue',
4146
},
@@ -57,6 +62,11 @@ const meta: Meta<typeof Tag> = {
5762
edit,
5863
},
5964
},
65+
variant: {
66+
control: 'select',
67+
options: ['pastel', 'vibrant', 'outlined'],
68+
description: 'Default is pastel',
69+
},
6070
iconColor: {
6171
control: 'color',
6272
type: 'string',
@@ -74,7 +84,8 @@ const meta: Meta<typeof Tag> = {
7484
};
7585

7686
export default meta;
77-
type Story = StoryObj<typeof Tag>;
87+
type Story = StoryObj<DefaultTagProps>;
88+
type IconStory = StoryObj<IconTagProps>;
7889

7990
export const Default: Story = {
8091
args: {},
@@ -92,6 +103,10 @@ const AVAILABLE_COLORS: NonNullable<TagProps['color']>[] = [
92103
'orange',
93104
'yellow',
94105
'grey',
106+
'white',
107+
'success',
108+
'warning',
109+
'danger',
95110
];
96111

97112
export const Colors: Story = {
@@ -112,6 +127,73 @@ export const Colors: Story = {
112127
),
113128
};
114129

130+
const AVAILABLE_VARIANTS: NonNullable<TagProps['variant']>[] = [
131+
'pastel',
132+
'vibrant',
133+
'outlined',
134+
];
135+
136+
export const Variants: Story = {
137+
args: {
138+
leadingIcon: computer,
139+
trailingIcon: undefined,
140+
children: 'Developer',
141+
},
142+
render: (args) => (
143+
<div style={{ display: 'flex', gap: 48 }}>
144+
<VariantShowcase
145+
GenericComponent={Tag}
146+
otherProps={{ ...args }}
147+
columns={[
148+
{
149+
label: 'Pastel',
150+
value: { variant: 'pastel' },
151+
},
152+
{
153+
label: 'Vibrant',
154+
value: { variant: 'vibrant' },
155+
},
156+
{
157+
label: 'Outlined',
158+
value: { variant: 'outlined' },
159+
},
160+
]}
161+
rows={AVAILABLE_COLORS.map((color) => ({
162+
label: color,
163+
value: { color },
164+
}))}
165+
/>
166+
</div>
167+
),
168+
};
169+
170+
export const VariantStyles: Story = {
171+
tags: ['!dev', '!autodocs'],
172+
args: {
173+
leadingIcon: undefined,
174+
},
175+
render: (args) => (
176+
<>
177+
{AVAILABLE_VARIANTS.map((variant) => (
178+
<Tag key={variant} {...args} variant={variant}>
179+
{variant}
180+
</Tag>
181+
))}
182+
</>
183+
),
184+
play: async ({ canvas }) => {
185+
const getTag = (name: string) =>
186+
getComputedStyle(canvas.getByText(name).parentElement!);
187+
188+
await expect(getTag('pastel').outlineStyle).toBe('none');
189+
await expect(getTag('outlined').outlineStyle).toBe('solid');
190+
await expect(getTag('vibrant').outlineStyle).toBe('none');
191+
await expect(getTag('vibrant').backgroundColor).not.toBe(
192+
getTag('pastel').backgroundColor
193+
);
194+
},
195+
};
196+
115197
export const CustomTextAndIconColors: Story = {
116198
args: {
117199
leadingIcon: code,
@@ -152,6 +234,41 @@ export const TrailingIcon: Story = {
152234
},
153235
};
154236

237+
export const IconOnly: IconStory = {
238+
args: {
239+
iconOnly: true,
240+
icon: check,
241+
label: 'Approved',
242+
},
243+
render: (args) => (
244+
<VariantShowcase
245+
GenericComponent={Tag}
246+
otherProps={args}
247+
rows={AVAILABLE_COLORS.map((color) => ({
248+
label: capitalize(color),
249+
value: { color },
250+
}))}
251+
/>
252+
),
253+
};
254+
255+
export const IconOnlyAccessibleLabel: IconStory = {
256+
tags: ['!dev', '!autodocs'],
257+
args: {
258+
iconOnly: true,
259+
icon: code,
260+
label: 'Developer',
261+
},
262+
play: async ({ canvas }) => {
263+
await expect(canvas.getByRole('img', { name: 'Developer' })).toBeVisible();
264+
await expect(canvas.queryByText('Developer')).not.toBeInTheDocument();
265+
await expect(canvas.getByTestId('eds-icon-path')).toHaveAttribute(
266+
'd',
267+
code.svgPathData
268+
);
269+
},
270+
};
271+
155272
export const ReactNodeContent: Story = {
156273
name: 'React node content',
157274
args: {

‎src/molecules/Tag/Tag.tsx‎

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import { FC, ReactNode } from 'react';
1+
import { ReactNode } from 'react';
22

33
import { Icon, Typography } from '@equinor/eds-core-react';
44
import { IconData } from '@equinor/eds-icons';
55

66
import { shape, spacings } from 'src/atoms/style';
77
import { TAG_COLORS } from 'src/molecules/Tag/Tag.utils';
88

9-
import styled from 'styled-components';
9+
import styled, { css } from 'styled-components';
1010

1111
interface ContainerProps {
12-
$color: NonNullable<TagProps['color']>;
12+
$color: NonNullable<BaseTagProps['color']>;
13+
$variant: NonNullable<BaseTagProps['variant']>;
1314
$iconColor?: string;
1415
$textColor?: string;
16+
$iconOnly?: boolean;
1517
}
1618

1719
const Container = styled.div<ContainerProps>`
@@ -20,22 +22,38 @@ const Container = styled.div<ContainerProps>`
2022
border-radius: ${shape.corners.borderRadius};
2123
padding: ${spacings.xx_small} ${spacings.x_small};
2224
gap: ${spacings.x_small};
23-
background: ${({ $color }) => TAG_COLORS[$color].background};
25+
background: ${({ $color, $variant }) =>
26+
TAG_COLORS[$variant][$color].background};
27+
${({ $color, $variant }) => {
28+
const { border } = TAG_COLORS[$variant][$color];
29+
// Outline does not take up space, so the tag size is the same for all variants
30+
return border && `outline: 1px solid ${border};`;
31+
}}
2432
min-height: 24px;
2533
min-width: fit-content;
2634
width: fit-content;
2735
36+
${({ $iconOnly }) =>
37+
$iconOnly &&
38+
css`
39+
justify-content: center;
40+
padding: 4px;
41+
min-width: 24px;
42+
`}
43+
2844
> span {
29-
color: ${({ $color, $textColor }) => $textColor ?? TAG_COLORS[$color].text};
45+
color: ${({ $color, $variant, $textColor }) =>
46+
$textColor ?? TAG_COLORS[$variant][$color].text};
3047
line-height: normal;
3148
}
3249
> svg {
3350
flex-shrink: 0;
34-
fill: ${({ $color, $iconColor }) => $iconColor ?? TAG_COLORS[$color].text};
51+
fill: ${({ $color, $variant, $iconColor }) =>
52+
$iconColor ?? TAG_COLORS[$variant][$color].text};
3553
}
3654
`;
3755

38-
export interface TagProps {
56+
export interface BaseTagProps {
3957
color?:
4058
| 'blue'
4159
| 'green'
@@ -44,29 +62,67 @@ export interface TagProps {
4462
| 'yellow'
4563
| 'dark pink'
4664
| 'pink'
47-
| 'grey';
65+
| 'grey'
66+
| 'white'
67+
| 'success'
68+
| 'warning'
69+
| 'danger';
70+
/** @default 'pastel' */
71+
variant?: 'pastel' | 'vibrant' | 'outlined';
72+
iconColor?: string;
73+
className?: string;
74+
style?: React.CSSProperties | undefined;
75+
}
76+
77+
export interface DefaultTagProps extends BaseTagProps {
78+
iconOnly?: false;
4879
leadingIcon?: IconData;
4980
trailingIcon?: IconData;
50-
iconColor?: string;
5181
textColor?: string;
5282
children: ReactNode;
53-
className?: string;
54-
style?: React.CSSProperties | undefined;
5583
}
5684

57-
export const Tag: FC<TagProps> = ({
58-
children,
59-
color = 'blue',
60-
leadingIcon,
61-
trailingIcon,
62-
textColor,
63-
iconColor,
64-
className,
65-
style,
66-
}) => {
85+
export interface IconTagProps extends BaseTagProps {
86+
iconOnly: true;
87+
icon: IconData;
88+
/** Accessible label, since the tag has no visible text */
89+
label: string;
90+
}
91+
92+
export type TagProps = DefaultTagProps | IconTagProps;
93+
94+
export function Tag(props: TagProps) {
95+
const {
96+
color = 'blue',
97+
variant = 'pastel',
98+
iconColor,
99+
className,
100+
style,
101+
} = props;
102+
103+
if (props.iconOnly) {
104+
return (
105+
<Container
106+
$color={color}
107+
$variant={variant}
108+
$iconColor={iconColor}
109+
$iconOnly
110+
role="img"
111+
aria-label={props.label}
112+
className={className}
113+
style={style}
114+
>
115+
<Icon data={props.icon} size={16} />
116+
</Container>
117+
);
118+
}
119+
120+
const { children, leadingIcon, trailingIcon, textColor } = props;
121+
67122
return (
68123
<Container
69124
$color={color}
125+
$variant={variant}
70126
$iconColor={iconColor}
71127
$textColor={textColor}
72128
className={className}
@@ -79,4 +135,4 @@ export const Tag: FC<TagProps> = ({
79135
{trailingIcon && <Icon data={trailingIcon} size={16} />}
80136
</Container>
81137
);
82-
};
138+
}

0 commit comments

Comments
 (0)