Skip to content

Commit e021a51

Browse files
mainframevclaude
andcommitted
feat(react-headless-components-preview): add Tag, InteractionTag, TagGroup stories
Each story imports from the package's kebab-case subpath (./tag, ./tag-group, ./interaction-tag, ./interaction-tag-primary, ./interaction-tag-secondary) and demonstrates styling via the data-* attributes exposed by the headless hooks: data-disabled, data-dismissible, data-selected, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 38531b7 commit e021a51

12 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { InteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag';
3+
import { InteractionTagPrimary } from '@fluentui/react-headless-components-preview/interaction-tag-primary';
4+
import { InteractionTagSecondary } from '@fluentui/react-headless-components-preview/interaction-tag-secondary';
5+
import { TagGroup } from '@fluentui/react-headless-components-preview/tag-group';
6+
import { DismissRegular } from '@fluentui/react-icons';
7+
8+
import styles from './interactionTag.module.css';
9+
10+
const initial = [
11+
{ value: '1', label: 'Project Alpha' },
12+
{ value: '2', label: 'Project Beta' },
13+
];
14+
15+
export const Default = (): React.ReactNode => {
16+
const [tags, setTags] = React.useState(initial);
17+
18+
return (
19+
<TagGroup
20+
aria-label="Headless interaction tags"
21+
onDismiss={(_, data) => setTags(prev => prev.filter(t => t.value !== data.value))}
22+
>
23+
<div className={styles.demo}>
24+
{tags.map(t => (
25+
<InteractionTag key={t.value} value={t.value} className={styles.interactionTag}>
26+
<InteractionTagPrimary className={styles.primary}>{t.label}</InteractionTagPrimary>
27+
<InteractionTagSecondary aria-label={`Remove ${t.label}`} className={styles.secondary}>
28+
<DismissRegular aria-hidden />
29+
</InteractionTagSecondary>
30+
</InteractionTag>
31+
))}
32+
</div>
33+
</TagGroup>
34+
);
35+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
An InteractionTag pairs a focusable primary action with an optional secondary (dismiss) action.
2+
3+
The headless `InteractionTagSecondary` does NOT inject a default icon - pass `children` to render whichever icon fits the design.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { InteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag';
2+
3+
import descriptionMd from './InteractionTagDescription.md';
4+
export { Default } from './InteractionTagDefault.stories';
5+
6+
export default {
7+
title: 'Components/InteractionTag',
8+
component: InteractionTag,
9+
parameters: {
10+
docs: {
11+
description: {
12+
component: descriptionMd,
13+
},
14+
},
15+
},
16+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
.interactionTag {
2+
display: inline-flex;
3+
align-items: stretch;
4+
border-radius: var(--radius-pill);
5+
overflow: hidden;
6+
border: 1px solid var(--border-strong);
7+
background: var(--surface);
8+
font-size: 12px;
9+
height: 28px;
10+
}
11+
12+
.interactionTag[data-selected] {
13+
background: var(--accent);
14+
border-color: var(--accent);
15+
color: var(--accent-contrast);
16+
}
17+
18+
.primary {
19+
appearance: none;
20+
border: none;
21+
background: transparent;
22+
color: inherit;
23+
font: inherit;
24+
padding: 0 12px;
25+
cursor: pointer;
26+
display: inline-flex;
27+
align-items: center;
28+
gap: 6px;
29+
}
30+
31+
.primary:hover {
32+
background: var(--surface-muted);
33+
}
34+
35+
.primary:focus-visible,
36+
.secondary:focus-visible {
37+
outline: none;
38+
box-shadow: 0 0 0 2px var(--bg-elev) inset, 0 0 0 4px var(--accent) inset;
39+
}
40+
41+
.primary[data-disabled] {
42+
cursor: not-allowed;
43+
opacity: 0.5;
44+
}
45+
46+
.secondary {
47+
appearance: none;
48+
border: none;
49+
background: transparent;
50+
color: inherit;
51+
padding: 0 10px;
52+
cursor: pointer;
53+
display: inline-flex;
54+
align-items: center;
55+
justify-content: center;
56+
border-left: 1px solid var(--border);
57+
}
58+
59+
.secondary:hover {
60+
background: var(--surface-muted);
61+
}
62+
63+
.demo {
64+
display: flex;
65+
gap: 8px;
66+
align-items: center;
67+
flex-wrap: wrap;
68+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as React from 'react';
2+
import { Tag } from '@fluentui/react-headless-components-preview/tag';
3+
import { DismissRegular } from '@fluentui/react-icons';
4+
5+
import styles from './tag.module.css';
6+
7+
export const Default = (): React.ReactNode => (
8+
<div className={styles.demo}>
9+
<div className={styles.demoRow}>
10+
<Tag className={styles.tag}>Default</Tag>
11+
<Tag className={styles.tag} selected>
12+
Selected
13+
</Tag>
14+
<Tag className={styles.tag} disabled>
15+
Disabled
16+
</Tag>
17+
</div>
18+
19+
<div className={styles.demoRow}>
20+
<Tag
21+
className={styles.tag}
22+
dismissible
23+
dismissIcon={{
24+
className: styles.dismissIcon,
25+
children: <DismissRegular aria-hidden />,
26+
}}
27+
>
28+
Dismissible
29+
</Tag>
30+
<Tag
31+
className={styles.tag}
32+
dismissible
33+
disabled
34+
dismissIcon={{
35+
className: styles.dismissIcon,
36+
children: <DismissRegular aria-hidden />,
37+
}}
38+
>
39+
Disabled & dismissible
40+
</Tag>
41+
</div>
42+
</div>
43+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A Tag is a compact visual representation of an attribute, label, or filter that can optionally be dismissed.
2+
3+
The headless `Tag` exposes `data-disabled`, `data-dismissible`, and `data-selected` attributes on its root so consumers can style each state without overriding any built-in styles.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Tag } from '@fluentui/react-headless-components-preview/tag';
2+
3+
import descriptionMd from './TagDescription.md';
4+
export { Default } from './TagDefault.stories';
5+
6+
export default {
7+
title: 'Components/Tag',
8+
component: Tag,
9+
parameters: {
10+
docs: {
11+
description: {
12+
component: descriptionMd,
13+
},
14+
},
15+
},
16+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* tag — outlined pill with selectable + dismissible states */
2+
.tag {
3+
display: inline-flex;
4+
align-items: center;
5+
gap: 6px;
6+
height: 24px;
7+
padding: 0 10px;
8+
border-radius: var(--radius-pill);
9+
border: 1px solid var(--border-strong);
10+
background: var(--surface);
11+
color: var(--text);
12+
font-size: 12px;
13+
font-weight: 500;
14+
letter-spacing: 0;
15+
cursor: default;
16+
user-select: none;
17+
transition: background-color var(--duration-fast) var(--ease-standard),
18+
color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
19+
}
20+
21+
.tag[data-dismissible] {
22+
cursor: pointer;
23+
border: none;
24+
padding: 0 0 0 10px;
25+
background: var(--surface-muted);
26+
}
27+
28+
.tag[data-dismissible]:hover {
29+
background: var(--surface-sunken);
30+
}
31+
32+
.tag[data-dismissible]:focus-visible {
33+
outline: none;
34+
box-shadow: 0 0 0 2px var(--bg-elev), 0 0 0 4px var(--accent);
35+
}
36+
37+
.tag[data-selected] {
38+
background: var(--accent);
39+
color: var(--accent-contrast);
40+
border-color: var(--accent);
41+
}
42+
43+
.tag[data-disabled] {
44+
opacity: 0.4;
45+
cursor: not-allowed;
46+
}
47+
48+
.tag[data-disabled][data-dismissible]:hover {
49+
background: var(--surface-muted);
50+
}
51+
52+
.dismissIcon {
53+
display: inline-flex;
54+
align-items: center;
55+
justify-content: center;
56+
width: 22px;
57+
height: 22px;
58+
margin-left: 2px;
59+
border-radius: 50%;
60+
}
61+
62+
.tag[data-dismissible]:hover .dismissIcon {
63+
background: var(--surface);
64+
}
65+
66+
.demo {
67+
display: flex;
68+
flex-direction: column;
69+
gap: 16px;
70+
}
71+
72+
.demoRow {
73+
display: flex;
74+
gap: 8px;
75+
align-items: center;
76+
flex-wrap: wrap;
77+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from 'react';
2+
import { TagGroup } from '@fluentui/react-headless-components-preview/tag-group';
3+
import { Tag } from '@fluentui/react-headless-components-preview/tag';
4+
import { DismissRegular } from '@fluentui/react-icons';
5+
6+
import styles from './tagGroup.module.css';
7+
8+
const initialTags = [
9+
{ value: '1', label: 'Tag one' },
10+
{ value: '2', label: 'Tag two' },
11+
{ value: '3', label: 'Tag three' },
12+
];
13+
14+
export const Default = (): React.ReactNode => {
15+
const [tags, setTags] = React.useState(initialTags);
16+
17+
return (
18+
<div className={styles.demo}>
19+
<TagGroup
20+
aria-label="Headless tag group"
21+
className={styles.group}
22+
onDismiss={(_, data) => setTags(prev => prev.filter(t => t.value !== data.value))}
23+
>
24+
{tags.map(t => (
25+
<Tag
26+
key={t.value}
27+
value={t.value}
28+
dismissible
29+
className={styles.tag}
30+
dismissIcon={{ className: styles.dismissIcon, children: <DismissRegular aria-hidden /> }}
31+
>
32+
{t.label}
33+
</Tag>
34+
))}
35+
</TagGroup>
36+
37+
<TagGroup aria-label="Disabled tag group" className={styles.group} disabled>
38+
<Tag value="a" className={styles.tag} dismissible dismissIcon={{ children: <DismissRegular aria-hidden /> }}>
39+
Locked
40+
</Tag>
41+
<Tag value="b" className={styles.tag} dismissible dismissIcon={{ children: <DismissRegular aria-hidden /> }}>
42+
Read-only
43+
</Tag>
44+
</TagGroup>
45+
</div>
46+
);
47+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A TagGroup is a container that coordinates dismissal and selection across one or more `Tag` children.
2+
3+
The headless `TagGroup` ships without keyboard navigation - it does not include Tabster's arrow-key group or post-dismiss focus restoration. Consumers wire those behaviours up themselves with a focus-management strategy that fits their application (Tabster, a virtual focus manager, etc.).

0 commit comments

Comments
 (0)