Skip to content

Commit 5f5cf44

Browse files
committed
feat: Implement clickable tag
1 parent 9927511 commit 5f5cf44

23 files changed

+225
-9
lines changed

src/@next/Tag/Tag.stories.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
33

44
import { BaseContainer } from '../../Layout/GlintsContainer/GlintsContainer';
55
import { Tag, TagProps } from './Tag';
6+
import { Blue } from '../utilities/colors';
67

78
(Tag as React.FunctionComponent<TagProps>).displayName = 'Tag';
89

@@ -25,10 +26,22 @@ export default {
2526
type: null,
2627
},
2728
},
29+
onClick: {
30+
description:
31+
'A function callback to pass in to when the tag is clicked, if this is supplied, the tag will be clickable',
32+
control: {
33+
type: null,
34+
},
35+
},
2836
textColor: {
2937
description: 'Text color of the tag',
3038
control: 'color',
3139
},
40+
disabled: {
41+
description:
42+
'A prop to disable the tag, only works when onClick is supplied',
43+
control: null,
44+
},
3245
},
3346

3447
parameters: {
@@ -74,10 +87,47 @@ const RemoveableTemplate: Story<TagProps> = () => {
7487
return <>{tagMarkup}</>;
7588
};
7689

90+
const ClickableTemplate: Story<TagProps> = args => {
91+
return (
92+
<Tag textColor={args.textColor} onClick={() => window.alert('Clicked')}>
93+
Clickable Tag
94+
</Tag>
95+
);
96+
};
97+
98+
const ClickableDisabledTemplate: Story<TagProps> = args => {
99+
return (
100+
<Tag
101+
textColor={args.textColor}
102+
onClick={() => window.alert('Clicked')}
103+
disabled
104+
>
105+
Clickable Tag - Disabled
106+
</Tag>
107+
);
108+
};
109+
77110
export const Default = DefaultTemplate.bind({});
78111

79-
Default.args = {};
112+
Default.args = {
113+
onRemove: undefined,
114+
onClick: undefined,
115+
};
80116

81117
export const Removeable = RemoveableTemplate.bind({});
82118

83119
Removeable.args = {};
120+
121+
export const Clickable = ClickableTemplate.bind({});
122+
123+
Clickable.args = {
124+
onRemove: null,
125+
textColor: Blue.S99,
126+
};
127+
128+
export const ClickableDisabled = ClickableDisabledTemplate.bind({});
129+
ClickableDisabled.args = {
130+
onRemove: null,
131+
textColor: Blue.S99,
132+
disabled: true,
133+
};

src/@next/Tag/Tag.tsx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,40 @@ export interface TagProps extends React.HTMLAttributes<HTMLDivElement> {
1414
value?: string;
1515
onRemove?: (() => void) | null;
1616
textColor?: string;
17+
disabled?: boolean;
1718
}
1819

1920
export type TagRemoveContainerProps = React.HTMLAttributes<HTMLDivElement>;
2021

2122
export type TagContentProps = React.HTMLAttributes<HTMLSpanElement> & TagProps;
2223

2324
export const Tag = React.forwardRef<HTMLDivElement, TagProps>(function Tag(
24-
{ children, onRemove, value, textColor, ...props }: TagProps,
25+
{
26+
children,
27+
onRemove,
28+
value,
29+
textColor,
30+
onClick,
31+
disabled,
32+
...props
33+
}: TagProps,
2534
ref
2635
) {
36+
const handleTextColor = () => {
37+
if (disabled) {
38+
return Neutral.B85;
39+
}
40+
41+
if (textColor) {
42+
return textColor;
43+
}
44+
45+
return Neutral.B18;
46+
};
47+
2748
const content =
2849
typeof children === 'string' || typeof children === 'number' ? (
29-
<Typography
30-
variant="caption"
31-
color={textColor ?? Neutral.B18}
32-
as={'span'}
33-
>
50+
<Typography variant="caption" color={handleTextColor()} as={'span'}>
3451
{children}
3552
</Typography>
3653
) : (
@@ -51,7 +68,16 @@ export const Tag = React.forwardRef<HTMLDivElement, TagProps>(function Tag(
5168
);
5269

5370
return (
54-
<TagStyle ref={ref} {...props} value={value}>
71+
<TagStyle
72+
ref={ref}
73+
{...props}
74+
value={value}
75+
onClick={!disabled && onClick}
76+
data-clickable={!!onClick}
77+
role={!!onClick ? 'button' : undefined}
78+
data-disabled={disabled}
79+
as={!!onClick ? 'button' : 'div'}
80+
>
5581
<TagContentStyle data-removeable={!!onRemove}>{content}</TagContentStyle>
5682
{removeButton}
5783
</TagStyle>

src/@next/Tag/TagStyle.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { TagContentProps, TagProps, TagRemoveContainerProps } from './Tag';
88
export const TagContentStyle = styled.span<TagContentProps>`
99
padding: ${space4} ${space8};
1010
11+
transform: translateY(1px);
12+
1113
&[data-removeable='true'] {
1214
padding-right: 0;
1315
}
@@ -35,9 +37,37 @@ export const TagStyle = styled.div<TagProps>`
3537
display: inline-flex;
3638
align-items: center;
3739
background-color: ${Blue.S08};
38-
40+
border: 0;
3941
border-radius: ${borderRadius4};
4042
width: fit-content;
43+
padding: 0;
44+
45+
&[data-clickable='true'] {
46+
cursor: pointer;
47+
48+
&:hover {
49+
outline: 1px solid ${Blue.S100}E6;
50+
}
51+
52+
&:active {
53+
outline: 1px solid ${Blue.S100};
54+
}
55+
56+
&:focus-visible {
57+
outline: 1px solid ${Blue.S100};
58+
box-shadow: 0px 0px 0px 2px ${Neutral.B100}, 0px 0px 0px 4px ${Blue.S54};
59+
}
60+
61+
&[data-disabled='true'] {
62+
cursor: not-allowed;
63+
outline: none;
64+
background-color: ${Neutral.B95};
65+
&:focus-visible {
66+
outline: none;
67+
box-shadow: none;
68+
}
69+
}
70+
}
4171
4272
@media (max-width: ${Breakpoints.large}) {
4373
font-size: 12px;

test/e2e/tag/tag.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ const getPage = (page: Page) =>
77
const getRemoveableTagPage = (page: Page) =>
88
new StoryBookPage(page, '?path=/story/next-tag--removeable');
99

10+
const getClickableTagPage = (page: Page) =>
11+
new StoryBookPage(page, '?path=/story/next-tag--clickable');
12+
13+
const getClickableDisabledTagPage = (page: Page) =>
14+
new StoryBookPage(page, '?path=/story/next-tag--clickable-disabled');
15+
1016
test('Tag - standard size', async ({ page }) => {
1117
const tagPage = getPage(page);
1218
await tagPage.goto();
@@ -133,3 +139,107 @@ test('removeable tag pressed state - small size', async ({ page }) => {
133139
'tag-removeable-pressed-small-size.png'
134140
);
135141
});
142+
143+
test('clickable tag hover state', async ({ page }) => {
144+
const tagPage = getClickableTagPage(page);
145+
await tagPage.goto();
146+
await tagPage.page
147+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
148+
.getByText('Clickable Tag')
149+
.first()
150+
.hover({ force: true });
151+
await expect(tagPage.container).toHaveScreenshot('tag-clickable-hover.png');
152+
});
153+
154+
test('clickable tag hover state - small size', async ({ page }) => {
155+
page.setViewportSize({ width: 768, height: 600 });
156+
157+
const tagPage = getClickableTagPage(page);
158+
await tagPage.goto();
159+
await tagPage.page
160+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
161+
.getByText('Clickable Tag')
162+
.first()
163+
.hover({ force: true });
164+
await expect(tagPage.container).toHaveScreenshot(
165+
'tag-clickable-hover-small-size.png'
166+
);
167+
});
168+
169+
test('clickable tag active state', async ({ page }) => {
170+
const tagPage = getClickableTagPage(page);
171+
await tagPage.goto();
172+
await tagPage.page
173+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
174+
.getByText('Clickable Tag')
175+
.first()
176+
.click();
177+
178+
await tagPage.page.mouse.down();
179+
await expect(tagPage.container).toHaveScreenshot('tag-clickable-active.png');
180+
});
181+
182+
test('clickable tag active state - small size', async ({ page }) => {
183+
page.setViewportSize({ width: 768, height: 600 });
184+
185+
const tagPage = getClickableTagPage(page);
186+
await tagPage.goto();
187+
await tagPage.page
188+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
189+
.getByText('Clickable Tag')
190+
.first()
191+
.click();
192+
193+
await tagPage.page.mouse.down();
194+
await expect(tagPage.container).toHaveScreenshot(
195+
'tag-clickable-active-small-size.png'
196+
);
197+
});
198+
199+
test('clickable tag focus state', async ({ page }) => {
200+
const tagPage = getClickableTagPage(page);
201+
await tagPage.goto();
202+
await tagPage.page
203+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
204+
.getByText('Clickable Tag')
205+
.first()
206+
.focus();
207+
208+
await expect(tagPage.container).toHaveScreenshot('tag-clickable-focus.png');
209+
});
210+
211+
test('clickable tag focus state - small size', async ({ page }) => {
212+
page.setViewportSize({ width: 768, height: 600 });
213+
214+
const tagPage = getClickableTagPage(page);
215+
await tagPage.goto();
216+
await tagPage.page
217+
.frameLocator('internal:attr=[title="storybook-preview-iframe"i]')
218+
.getByText('Clickable Tag')
219+
.first()
220+
.focus();
221+
222+
await expect(tagPage.container).toHaveScreenshot(
223+
'tag-clickable-focus-small-size.png'
224+
);
225+
});
226+
227+
test('clickable tag disabled state', async ({ page }) => {
228+
const tagPage = getClickableDisabledTagPage(page);
229+
await tagPage.goto();
230+
231+
await expect(tagPage.container).toHaveScreenshot(
232+
'tag-clickable-disabled.png'
233+
);
234+
});
235+
236+
test('clickable tag disabled state - small size', async ({ page }) => {
237+
page.setViewportSize({ width: 768, height: 600 });
238+
239+
const tagPage = getClickableDisabledTagPage(page);
240+
await tagPage.goto();
241+
242+
await expect(tagPage.container).toHaveScreenshot(
243+
'tag-clickable-disabled-small-size.png'
244+
);
245+
});
2.71 KB
Loading
2.05 KB
Loading
2.56 KB
Loading
1.91 KB
Loading
2.5 KB
Loading
1.84 KB
Loading

0 commit comments

Comments
 (0)