Skip to content

Commit da6ecd3

Browse files
committed
feat(react-headless-components-preview): split Tag-family stories into v9-style example pages
1 parent 2d28da0 commit da6ecd3

26 files changed

Lines changed: 1061 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,11 @@
11
import * as React from 'react';
22
import { InteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag';
33
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';
74

85
import styles from './interactionTag.module.css';
96

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-
};
7+
export const Default = (): React.ReactNode => (
8+
<InteractionTag className={styles.interactionTag}>
9+
<InteractionTagPrimary className={styles.primary}>Primary text</InteractionTagPrimary>
10+
</InteractionTag>
11+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { CalendarMonthRegular, DismissRegular } from '@fluentui/react-icons';
6+
7+
import styles from './interactionTag.module.css';
8+
9+
export const Disabled = (): React.ReactNode => (
10+
<div className={styles.demo}>
11+
<InteractionTag disabled className={styles.interactionTag}>
12+
<InteractionTagPrimary
13+
className={styles.primary}
14+
icon={{ className: styles.icon, children: <CalendarMonthRegular aria-hidden /> }}
15+
hasSecondaryAction
16+
>
17+
Disabled
18+
</InteractionTagPrimary>
19+
<InteractionTagSecondary aria-label="remove" className={styles.secondary}>
20+
<DismissRegular aria-hidden />
21+
</InteractionTagSecondary>
22+
</InteractionTag>
23+
</div>
24+
);
25+
26+
Disabled.parameters = {
27+
docs: {
28+
description: {
29+
story:
30+
'A disabled InteractionTag forwards `data-disabled` to its primary + secondary actions and blocks click/keyboard handlers.',
31+
},
32+
},
33+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 type { TagGroupProps } from '@fluentui/react-headless-components-preview/tag-group';
7+
import { DismissRegular } from '@fluentui/react-icons';
8+
9+
import styles from './interactionTag.module.css';
10+
11+
const initialTags = [
12+
{ value: '1', children: 'Tag 1' },
13+
{ value: '2', children: 'Tag 2' },
14+
{ value: '3', children: 'Tag 3' },
15+
];
16+
17+
const useResetExample = (visibleTagsLength: number) => {
18+
const resetButtonRef = React.useRef<HTMLButtonElement>(null);
19+
const firstTagRef = React.useRef<HTMLButtonElement>(null);
20+
const prevLength = React.useRef(visibleTagsLength);
21+
22+
React.useEffect(() => {
23+
if (visibleTagsLength === 0) {
24+
resetButtonRef.current?.focus();
25+
} else if (prevLength.current === 0) {
26+
firstTagRef.current?.focus();
27+
}
28+
prevLength.current = visibleTagsLength;
29+
}, [visibleTagsLength]);
30+
31+
return { firstTagRef, resetButtonRef };
32+
};
33+
34+
export const Dismiss = (): React.ReactNode => {
35+
const [visibleTags, setVisibleTags] = React.useState(initialTags);
36+
const onDismiss: TagGroupProps['onDismiss'] = (_e, { value }) =>
37+
setVisibleTags(prev => prev.filter(t => t.value !== value));
38+
const { firstTagRef, resetButtonRef } = useResetExample(visibleTags.length);
39+
40+
return (
41+
<div className={styles.demoCol}>
42+
{visibleTags.length > 0 && (
43+
<TagGroup aria-label="Dismiss example" onDismiss={onDismiss}>
44+
<div className={styles.demo}>
45+
{visibleTags.map((tag, i) => (
46+
<InteractionTag key={tag.value} value={tag.value} className={styles.interactionTag}>
47+
<InteractionTagPrimary
48+
className={styles.primary}
49+
ref={i === 0 ? (firstTagRef as React.Ref<HTMLButtonElement>) : null}
50+
hasSecondaryAction
51+
>
52+
{tag.children}
53+
</InteractionTagPrimary>
54+
<InteractionTagSecondary aria-label="remove" className={styles.secondary}>
55+
<DismissRegular aria-hidden />
56+
</InteractionTagSecondary>
57+
</InteractionTag>
58+
))}
59+
</div>
60+
</TagGroup>
61+
)}
62+
<button
63+
type="button"
64+
ref={resetButtonRef}
65+
className={styles.resetButton}
66+
disabled={visibleTags.length === initialTags.length}
67+
onClick={() => setVisibleTags(initialTags)}
68+
>
69+
Reset the example
70+
</button>
71+
</div>
72+
);
73+
};
74+
75+
Dismiss.parameters = {
76+
docs: {
77+
description: {
78+
story:
79+
'An InteractionTag pairs a focusable primary action with an optional dismiss `InteractionTagSecondary`. The headless TagGroup does NOT restore focus after a dismiss - the consumer wires that up via refs.',
80+
},
81+
},
82+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview/popover';
6+
import { DismissRegular } from '@fluentui/react-icons';
7+
8+
import styles from './interactionTag.module.css';
9+
10+
export const HasPrimaryAction = (): React.ReactNode => (
11+
<InteractionTag className={styles.interactionTag}>
12+
<Popover>
13+
<PopoverTrigger>
14+
<InteractionTagPrimary className={styles.primary} hasSecondaryAction>
15+
Golden retriever
16+
</InteractionTagPrimary>
17+
</PopoverTrigger>
18+
<PopoverSurface className={styles.popover}>
19+
<a href="https://en.wikipedia.org/wiki/Golden_Retriever">Find out more on wiki</a>
20+
<ul>
21+
<li>Size: Medium to large-sized dog breed.</li>
22+
<li>Coat: Luxurious double coat with a dense, water-repellent outer layer and a soft, dense undercoat.</li>
23+
<li>Color: Typically a luscious golden or cream color, with variations in shade.</li>
24+
<li>Build: Sturdy and well-proportioned body with a friendly and intelligent expression.</li>
25+
</ul>
26+
</PopoverSurface>
27+
</Popover>
28+
<InteractionTagSecondary aria-label="dismiss" className={styles.secondary}>
29+
<DismissRegular aria-hidden />
30+
</InteractionTagSecondary>
31+
</InteractionTag>
32+
);
33+
34+
HasPrimaryAction.parameters = {
35+
docs: {
36+
description: {
37+
story:
38+
'An InteractionTag can host a primary action - here, the primary opens a headless Popover. The secondary remains the dismiss affordance.',
39+
},
40+
},
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 { CalendarMonthRegular } from '@fluentui/react-icons';
5+
6+
import styles from './interactionTag.module.css';
7+
8+
export const Icon = (): React.ReactNode => (
9+
<InteractionTag className={styles.interactionTag}>
10+
<InteractionTagPrimary
11+
className={styles.primary}
12+
icon={{ className: styles.icon, children: <CalendarMonthRegular aria-hidden /> }}
13+
>
14+
Primary text
15+
</InteractionTagPrimary>
16+
</InteractionTag>
17+
);
18+
19+
Icon.parameters = {
20+
docs: { description: { story: 'An InteractionTag can render a custom icon if provided.' } },
21+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
5+
import styles from './interactionTag.module.css';
6+
7+
export const Media = (): React.ReactNode => (
8+
<InteractionTag className={styles.interactionTag}>
9+
<InteractionTagPrimary
10+
className={styles.primary}
11+
media={{ className: styles.media, 'aria-hidden': 'true', children: 'KA' }}
12+
>
13+
Primary text
14+
</InteractionTagPrimary>
15+
</InteractionTag>
16+
);
17+
18+
Media.parameters = {
19+
docs: {
20+
description: {
21+
story:
22+
'An InteractionTag can render arbitrary media in its `media` slot. The headless package does not ship an Avatar primitive - consumers project whatever element fits their design.',
23+
},
24+
},
25+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
5+
import styles from './interactionTag.module.css';
6+
7+
export const SecondaryText = (): React.ReactNode => (
8+
<InteractionTag className={styles.interactionTag}>
9+
<InteractionTagPrimary
10+
className={styles.primary}
11+
secondaryText={{ className: styles.secondaryText, children: 'Secondary text' }}
12+
>
13+
Primary text
14+
</InteractionTagPrimary>
15+
</InteractionTag>
16+
);
17+
18+
SecondaryText.parameters = {
19+
docs: { description: { story: 'An InteractionTag can have a secondary text.' } },
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 { CalendarMonthRegular, DismissRegular } from '@fluentui/react-icons';
6+
7+
import styles from './interactionTag.module.css';
8+
9+
export const Selected = (): React.ReactNode => (
10+
<div className={styles.demo}>
11+
<InteractionTag selected className={styles.interactionTag}>
12+
<InteractionTagPrimary
13+
className={styles.primary}
14+
icon={{ className: styles.icon, children: <CalendarMonthRegular aria-hidden /> }}
15+
hasSecondaryAction
16+
>
17+
Selected
18+
</InteractionTagPrimary>
19+
<InteractionTagSecondary aria-label="remove" className={styles.secondary}>
20+
<DismissRegular aria-hidden />
21+
</InteractionTagSecondary>
22+
</InteractionTag>
23+
<InteractionTag className={styles.interactionTag}>
24+
<InteractionTagPrimary
25+
className={styles.primary}
26+
icon={{ className: styles.icon, children: <CalendarMonthRegular aria-hidden /> }}
27+
hasSecondaryAction
28+
>
29+
Not selected
30+
</InteractionTagPrimary>
31+
<InteractionTagSecondary aria-label="remove" className={styles.secondary}>
32+
<DismissRegular aria-hidden />
33+
</InteractionTagSecondary>
34+
</InteractionTag>
35+
</div>
36+
);
37+
38+
Selected.parameters = {
39+
docs: {
40+
description: {
41+
story:
42+
'A selected InteractionTag exposes `data-selected` on its root for styling. Selection is driven by the `selected` prop or by a TagGroup with `onTagSelect`.',
43+
},
44+
},
45+
};

packages/react-components/react-headless-components-preview/stories/src/Tags/InteractionTag/index.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { InteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag';
2+
import { InteractionTagPrimary } from '@fluentui/react-headless-components-preview/interaction-tag-primary';
3+
import { InteractionTagSecondary } from '@fluentui/react-headless-components-preview/interaction-tag-secondary';
24

35
import descriptionMd from './InteractionTagDescription.md';
46
export { Default } from './InteractionTagDefault.stories';
7+
export { Icon } from './InteractionTagIcon.stories';
8+
export { Media } from './InteractionTagMedia.stories';
9+
export { SecondaryText } from './InteractionTagSecondaryText.stories';
10+
export { Dismiss } from './InteractionTagDismiss.stories';
11+
export { Disabled } from './InteractionTagDisabled.stories';
12+
export { HasPrimaryAction } from './InteractionTagHasPrimaryAction.stories';
13+
export { Selected } from './InteractionTagSelected.stories';
514

615
export default {
716
title: 'Components/Tags/InteractionTag',
817
component: InteractionTag,
18+
subcomponents: { InteractionTagPrimary, InteractionTagSecondary },
919
parameters: {
1020
docs: {
1121
description: {

0 commit comments

Comments
 (0)