Skip to content

Commit 207fe43

Browse files
feat(Tag): add TagsGroup (#367)
* fix(Tag): fully convert Tag as parahraph element by default * feat(Tag): add TagsGroup * chore(release): v1.16.10-rc.0 * feat(TagsGroup): use group sm and add story * fix: doc default and bw comp
1 parent 68e8ed3 commit 207fe43

File tree

3 files changed

+235
-93
lines changed

3 files changed

+235
-93
lines changed

src/Tag.tsx

Lines changed: 107 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type DataAttribute = Record<`data-${string}`, string | boolean | null | undefine
2222

2323
export type TagProps = TagProps.Common &
2424
(TagProps.WithIcon | TagProps.WithoutIcon) &
25-
(TagProps.AsAnchor | TagProps.AsButton | TagProps.AsSpan);
25+
(TagProps.AsAnchor | TagProps.AsButton | TagProps.AsParagraph);
2626
export namespace TagProps {
2727
export type Common = {
2828
id?: string;
@@ -47,134 +47,148 @@ export namespace TagProps {
4747
linkProps: RegisteredLinkProps;
4848
onClick?: never;
4949
nativeButtonProps?: never;
50+
/** @deprecated Tag is now <p> by default. Use `nativeParagraphProps` instead. */
5051
nativeSpanProps?: never;
52+
nativeParagraphProps?: never;
5153
dismissible?: never;
5254
pressed?: never;
5355
};
5456
export type AsButton = {
5557
linkProps?: never;
58+
/** @deprecated Tag is now <p> by default. Use `nativeParagraphProps` instead. */
5659
nativeSpanProps?: never;
60+
nativeParagraphProps?: never;
5761
/** Default: false */
5862
dismissible?: boolean;
5963
pressed?: boolean;
6064
onClick?: React.MouseEventHandler<HTMLButtonElement>;
6165
nativeButtonProps?: ComponentProps<"button"> & DataAttribute;
6266
};
63-
export type AsSpan = {
67+
export type AsParagraph = {
6468
linkProps?: never;
6569
onClick?: never;
6670
dismissible?: never;
6771
pressed?: never;
6872
nativeButtonProps?: never;
73+
/** @deprecated Tag is now <p> by default. Use `nativeParagraphProps` instead. */
6974
nativeSpanProps?: ComponentProps<"span"> & DataAttribute;
75+
nativeParagraphProps?: ComponentProps<"p"> & DataAttribute;
7076
};
77+
78+
/** @deprecated Tag is now <p> by default. Use `AsParagraph` instead. */
79+
export type AsSpan = AsParagraph;
7180
}
7281

7382
/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-tag> */
7483
export const Tag = memo(
75-
forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement, TagProps>((props, ref) => {
76-
const {
77-
id: id_props,
78-
className: prop_className,
79-
children,
80-
title,
81-
iconId,
82-
small = false,
83-
pressed,
84-
dismissible = false,
85-
linkProps,
86-
nativeButtonProps,
87-
nativeSpanProps,
88-
style,
89-
onClick,
90-
...rest
91-
} = props;
84+
forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLParagraphElement, TagProps>(
85+
(props, ref) => {
86+
const {
87+
id: id_props,
88+
className: prop_className,
89+
children,
90+
title,
91+
iconId,
92+
small = false,
93+
pressed,
94+
dismissible = false,
95+
linkProps,
96+
nativeButtonProps,
97+
nativeParagraphProps,
98+
nativeSpanProps,
99+
style,
100+
onClick,
101+
...rest
102+
} = props;
92103

93-
assert<Equals<keyof typeof rest, never>>();
104+
assert<Equals<keyof typeof rest, never>>();
94105

95-
const id = useAnalyticsId({
96-
"defaultIdPrefix": "fr-tag",
97-
"explicitlyProvidedId": id_props
98-
});
106+
const id = useAnalyticsId({
107+
"defaultIdPrefix": "fr-tag",
108+
"explicitlyProvidedId": id_props
109+
});
99110

100-
const { Link } = getLink();
111+
const { Link } = getLink();
101112

102-
const className = cx(
103-
fr.cx(
104-
"fr-tag",
105-
small && `fr-tag--sm`,
106-
iconId,
107-
iconId && "fr-tag--icon-left", // actually, it's always left but we need it in order to have the icon rendering
108-
dismissible && "fr-tag--dismiss"
109-
),
110-
linkProps !== undefined && linkProps.className,
111-
prop_className
112-
);
113+
const className = cx(
114+
fr.cx(
115+
"fr-tag",
116+
small && `fr-tag--sm`,
117+
iconId,
118+
iconId && "fr-tag--icon-left", // actually, it's always left but we need it in order to have the icon rendering
119+
dismissible && "fr-tag--dismiss"
120+
),
121+
linkProps !== undefined && linkProps.className,
122+
prop_className
123+
);
124+
125+
const nativeParagraphOrSpanProps = nativeParagraphProps ?? nativeSpanProps;
113126

114-
return (
115-
<>
116-
{linkProps !== undefined && (
117-
<Link
118-
{...linkProps}
119-
id={id_props ?? linkProps.id ?? id}
120-
title={title ?? linkProps.title}
121-
className={cx(linkProps?.className, className)}
122-
style={{
123-
...linkProps?.style,
124-
...style
125-
}}
126-
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
127-
{...rest}
128-
>
129-
{children}
130-
</Link>
131-
)}
132-
{nativeButtonProps !== undefined && (
133-
<button
134-
{...nativeButtonProps}
135-
id={id_props ?? nativeButtonProps.id ?? id}
136-
className={cx(nativeButtonProps?.className, className)}
137-
style={{
138-
...nativeButtonProps?.style,
139-
...style
140-
}}
141-
title={title ?? nativeButtonProps?.title}
142-
onClick={onClick ?? nativeButtonProps?.onClick}
143-
disabled={nativeButtonProps?.disabled}
144-
ref={ref as React.ForwardedRef<HTMLButtonElement>}
145-
aria-pressed={pressed}
146-
{...rest}
147-
>
148-
{children}
149-
</button>
150-
)}
151-
{linkProps === undefined && nativeButtonProps === undefined && (
152-
<p
153-
{...nativeSpanProps}
154-
id={id_props ?? nativeSpanProps?.id ?? id}
155-
className={cx(nativeSpanProps?.className, className)}
156-
style={{
157-
...nativeSpanProps?.style,
158-
...style
159-
}}
160-
title={title ?? nativeSpanProps?.title}
161-
ref={ref as React.ForwardedRef<HTMLParagraphElement>}
162-
{...rest}
163-
>
164-
{children}
165-
</p>
166-
)}
167-
</>
168-
);
169-
})
127+
return (
128+
<>
129+
{linkProps !== undefined && (
130+
<Link
131+
{...linkProps}
132+
id={id_props ?? linkProps.id ?? id}
133+
title={title ?? linkProps.title}
134+
className={cx(linkProps?.className, className)}
135+
style={{
136+
...linkProps?.style,
137+
...style
138+
}}
139+
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
140+
{...rest}
141+
>
142+
{children}
143+
</Link>
144+
)}
145+
{nativeButtonProps !== undefined && (
146+
<button
147+
{...nativeButtonProps}
148+
id={id_props ?? nativeButtonProps.id ?? id}
149+
className={cx(nativeButtonProps?.className, className)}
150+
style={{
151+
...nativeButtonProps?.style,
152+
...style
153+
}}
154+
title={title ?? nativeButtonProps?.title}
155+
onClick={onClick ?? nativeButtonProps?.onClick}
156+
disabled={nativeButtonProps?.disabled}
157+
ref={ref as React.ForwardedRef<HTMLButtonElement>}
158+
aria-pressed={pressed}
159+
{...rest}
160+
>
161+
{children}
162+
</button>
163+
)}
164+
{linkProps === undefined && nativeButtonProps === undefined && (
165+
<p
166+
{...nativeParagraphOrSpanProps}
167+
id={id_props ?? nativeParagraphOrSpanProps?.id ?? id}
168+
className={cx(nativeParagraphOrSpanProps?.className, className)}
169+
style={{
170+
...nativeParagraphOrSpanProps?.style,
171+
...style
172+
}}
173+
title={title ?? nativeParagraphOrSpanProps?.title}
174+
ref={ref as React.ForwardedRef<HTMLParagraphElement>}
175+
{...rest}
176+
>
177+
{children}
178+
</p>
179+
)}
180+
</>
181+
);
182+
}
183+
)
170184
) as MemoExoticComponent<
171185
ForwardRefExoticComponent<
172186
TagProps.Common &
173187
(TagProps.WithIcon | TagProps.WithoutIcon) &
174188
(
175189
| (TagProps.AsAnchor & RefAttributes<HTMLAnchorElement>)
176190
| (TagProps.AsButton & RefAttributes<HTMLButtonElement>)
177-
| (TagProps.AsSpan & RefAttributes<HTMLSpanElement>)
191+
| (TagProps.AsParagraph & RefAttributes<HTMLParagraphElement>)
178192
)
179193
>
180194
>;

src/TagsGroup.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { CSSProperties, forwardRef, memo } from "react";
2+
import { assert, Equals } from "tsafe";
3+
import { symToStr } from "tsafe/symToStr";
4+
import Tag, { TagProps } from "./Tag";
5+
import { useAnalyticsId } from "./tools/useAnalyticsId";
6+
import { cx } from "./tools/cx";
7+
import { fr } from "./fr";
8+
9+
export type TagsGroupProps = TagsGroupProps.Common;
10+
11+
//https://main--ds-gouv.netlify.app/example/component/tag/#:~:text=Groupe%20de%20tags
12+
export namespace TagsGroupProps {
13+
export type Common = {
14+
id?: string;
15+
className?: string;
16+
style?: CSSProperties;
17+
/** @default false */
18+
smallTags?: TagProps["small"];
19+
/** 6 tags should be the maximum. */
20+
tags: [TagProps, ...TagProps[]];
21+
};
22+
}
23+
24+
export const TagsGroup = memo(
25+
forwardRef<HTMLUListElement, TagsGroupProps>((props, ref) => {
26+
const { id: props_id, className, tags, smallTags = false, style, ...rest } = props;
27+
28+
assert<Equals<keyof typeof rest, never>>();
29+
30+
const id = useAnalyticsId({
31+
"defaultIdPrefix": "fr-tags-group",
32+
"explicitlyProvidedId": props_id
33+
});
34+
35+
const tagsGroupClassName = cx(
36+
fr.cx("fr-tags-group", smallTags && "fr-tags-group--sm"),
37+
className
38+
);
39+
40+
return (
41+
<ul className={tagsGroupClassName} style={style} id={id} ref={ref}>
42+
{tags.map((tagProps, i) => (
43+
<li key={i}>
44+
<Tag {...tagProps} />
45+
</li>
46+
))}
47+
</ul>
48+
);
49+
})
50+
);
51+
52+
TagsGroup.displayName = symToStr({ TagsGroup });
53+
54+
export default TagsGroup;

stories/TagsGroup.stories.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { TagsGroup, TagsGroupProps } from "../dist/TagsGroup";
2+
import { sectionName } from "./sectionName";
3+
import { getStoryFactory } from "./getStory";
4+
import { TagProps } from "../dist/Tag";
5+
6+
const { meta, getStory } = getStoryFactory({
7+
sectionName,
8+
"wrappedComponent": { TagsGroup },
9+
"description": `
10+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/composants-et-modeles/composants/tag)
11+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/TagsGroup.tsx)`,
12+
"argTypes": {
13+
"smallTags": {
14+
"description": `
15+
Default: false, if true, the tags will be smaller.
16+
`,
17+
"control": { "type": "boolean" }
18+
},
19+
"tags": {
20+
"description": `An array of TagProps (at least 1, max recommended: 6).`,
21+
"control": { "type": null }
22+
}
23+
},
24+
"disabledProps": ["lang"],
25+
"defaultContainerWidth": 800
26+
});
27+
28+
export default meta;
29+
30+
const tagsWithProps = (props?: Omit<TagProps, "children">) =>
31+
Array.from(
32+
{ "length": 6 },
33+
(_, i) =>
34+
({
35+
...props,
36+
"children": `Libellé tag ${i + 1}`
37+
} as TagProps)
38+
) as TagsGroupProps["tags"];
39+
40+
export const Default = getStory({
41+
"tags": tagsWithProps()
42+
});
43+
44+
export const SmallTags = getStory({
45+
"tags": tagsWithProps(),
46+
"smallTags": true
47+
});
48+
49+
export const TagsAsAnchor = getStory({
50+
"tags": tagsWithProps({ "linkProps": { "href": "#" } })
51+
});
52+
53+
export const SmallTagsAsAnchor = getStory({
54+
"tags": tagsWithProps({ "linkProps": { "href": "#" } }),
55+
"smallTags": true
56+
});
57+
58+
export const TagsPressed = getStory({
59+
"tags": tagsWithProps({ "pressed": true })
60+
});
61+
62+
export const SmallTagsPressed = getStory({
63+
"tags": tagsWithProps({ "pressed": true }),
64+
"smallTags": true
65+
});
66+
67+
export const TagsDismissable = getStory({
68+
"tags": tagsWithProps({ "dismissible": true })
69+
});
70+
71+
export const SmallTagsDismissable = getStory({
72+
"tags": tagsWithProps({ "dismissible": true }),
73+
"smallTags": true
74+
});

0 commit comments

Comments
 (0)