Skip to content

Commit 0567e10

Browse files
authored
Merge pull request #220 from codegouvfr/feat/follow-component
feat: Follow component
2 parents 9c745b1 + c61dec1 commit 0567e10

File tree

8 files changed

+740
-9
lines changed

8 files changed

+740
-9
lines changed

src/Follow.tsx

Lines changed: 474 additions & 0 deletions
Large diffs are not rendered by default.

src/Input.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export namespace InputProps {
3535
state?: "success" | "error" | "default";
3636
/** The message won't be displayed if state is "default" */
3737
stateRelatedMessage?: ReactNode;
38+
addon?: ReactNode;
3839
};
3940

4041
export type RegularInput = Common & {
@@ -82,6 +83,7 @@ export const Input = memo(
8283
textArea = false,
8384
nativeTextAreaProps,
8485
nativeInputProps,
86+
addon,
8587
...rest
8688
} = props;
8789

@@ -115,7 +117,6 @@ export const Input = memo(
115117
case "default":
116118
return undefined;
117119
}
118-
assert<Equals<typeof state, never>>(false);
119120
})()
120121
),
121122
classes.root,
@@ -149,7 +150,6 @@ export const Input = memo(
149150
case "default":
150151
return undefined;
151152
}
152-
assert<Equals<typeof state, never>>(false);
153153
})()
154154
),
155155
classes.nativeInputOrTextArea
@@ -161,12 +161,21 @@ export const Input = memo(
161161
/>
162162
);
163163

164-
return iconId === undefined ? (
165-
nativeInputOrTextArea
166-
) : (
167-
<div className={fr.cx("fr-input-wrap", iconId)}>
164+
const hasIcon = iconId !== undefined;
165+
const hasAddon = addon !== undefined;
166+
return hasIcon || hasAddon ? (
167+
<div
168+
className={fr.cx(
169+
"fr-input-wrap",
170+
hasIcon && iconId,
171+
hasAddon && "fr-input-wrap--addon"
172+
)}
173+
>
168174
{nativeInputOrTextArea}
175+
{hasAddon && addon}
169176
</div>
177+
) : (
178+
nativeInputOrTextArea
170179
);
171180
})()}
172181
{state !== "default" && (
@@ -181,7 +190,6 @@ export const Input = memo(
181190
case "success":
182191
return "fr-valid-text";
183192
}
184-
assert<Equals<typeof state, never>>(false);
185193
})()
186194
),
187195
classes.message

src/blocks/PasswordInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useAnalyticsId } from "../tools/useAnalyticsId";
1717

1818
export type PasswordInputProps = Omit<
1919
InputProps.Common,
20-
"state" | "stateRelatedMessage" | "iconId" | "classes"
20+
"state" | "stateRelatedMessage" | "iconId" | "classes" | "addon"
2121
> & {
2222
classes?: Partial<Record<"root" | "input" | "label" | "checkbox", string>>;
2323
/** Default "Your password must contain:", if empty string the hint wont be displayed */

stories/ButtonsGroup.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const { meta, getStory } = getStoryFactory({
8888
"control": { "type": "select" }
8989
},
9090
"buttons": {
91-
"description": `An array of ButtonProps (at least 2, RGAA)`,
91+
"description": `An array of ButtonProps (at least 1)`,
9292
"control": { "type": null }
9393
}
9494
},

stories/Follow.stories.tsx

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { Follow, type FollowProps } from "../dist/Follow";
2+
import { sectionName } from "./sectionName";
3+
import { getStoryFactory } from "./getStory";
4+
import { action } from "@storybook/addon-actions";
5+
import React from "react";
6+
7+
const { meta, getStory } = getStoryFactory<FollowProps>({
8+
sectionName,
9+
wrappedComponent: { Follow },
10+
description: `
11+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/lettre-d-information-et-reseaux-sociaux)
12+
- [See DSFR demos](https://main--ds-gouv.netlify.app/example/component/follow/)
13+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Follow.tsx)`,
14+
argTypes: {
15+
classes: {
16+
control: { type: null },
17+
description:
18+
'Add custom classes for various inner elements. Possible keys are "root", "container", "row", "newsletter-col", "newsletter", "newsletter-title", "newsletter-desc", "newsletter-form-wrapper", "newsletter-form-hint", "social-col", "social", "social-title", "social-buttons", "social-buttons-each"'
19+
}
20+
},
21+
disabledProps: ["lang"]
22+
});
23+
24+
export default meta;
25+
26+
const defaultSocialButtons: [FollowProps.SocialButton, ...FollowProps.SocialButton[]] = [
27+
{
28+
type: "facebook",
29+
linkProps: {
30+
href: "#facebook"
31+
}
32+
},
33+
{
34+
type: "twitter-x",
35+
linkProps: {
36+
href: "#twitter"
37+
}
38+
},
39+
{
40+
type: "linkedin",
41+
linkProps: {
42+
href: "#linkedin"
43+
}
44+
},
45+
{
46+
type: "instagram",
47+
linkProps: {
48+
href: "#instagram"
49+
}
50+
},
51+
{
52+
type: "youtube",
53+
linkProps: {
54+
href: "#youtube"
55+
}
56+
}
57+
];
58+
59+
export const Default = getStory({
60+
newsletter: {
61+
buttonProps: {
62+
onClick: action("Default onClick")
63+
},
64+
form: {
65+
formComponent: ({ children }) => <form action="#">{children}</form>,
66+
inputProps: {
67+
label: undefined
68+
},
69+
success: false
70+
}
71+
},
72+
social: {
73+
buttons: defaultSocialButtons
74+
}
75+
});
76+
77+
export const SocialOnly = getStory({
78+
social: {
79+
buttons: defaultSocialButtons
80+
}
81+
});
82+
83+
export const NewsletterOnly = getStory({
84+
newsletter: {
85+
buttonProps: {
86+
onClick: action("NewsletterOnly onClick")
87+
}
88+
}
89+
});
90+
91+
export const NewsletterOnlyButtonAsLink = getStory({
92+
newsletter: {
93+
buttonProps: {
94+
linkProps: {
95+
href: "#"
96+
}
97+
}
98+
}
99+
});
100+
101+
export const NewsletterOnlyWithDescription = getStory({
102+
newsletter: {
103+
buttonProps: {
104+
onClick: action("NewsletterOnlyWithDescription onClick")
105+
},
106+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et."
107+
}
108+
});
109+
110+
export const NewsletterOnlyWithForm = getStory({
111+
newsletter: {
112+
buttonProps: {
113+
onClick: action("NewsletterOnlyWithForm onClick")
114+
},
115+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et.",
116+
form: {
117+
formComponent: ({ children }) => <form action="#">{children}</form>,
118+
success: false
119+
}
120+
}
121+
});
122+
123+
export const SocialAndNewsletter = getStory({
124+
newsletter: {
125+
buttonProps: {
126+
onClick: action("SocialAndNewsletter onClick")
127+
},
128+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et."
129+
},
130+
social: {
131+
buttons: defaultSocialButtons
132+
}
133+
});
134+
135+
export const SocialAndNewsletterWithForm = getStory({
136+
newsletter: {
137+
buttonProps: {
138+
onClick: action("SocialAndNewsletterWithForm onClick")
139+
},
140+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et.",
141+
form: {
142+
formComponent: ({ children }) => <form action="#">{children}</form>,
143+
success: false
144+
}
145+
},
146+
social: {
147+
buttons: defaultSocialButtons
148+
}
149+
});
150+
151+
export const SocialAndNewsletterWithFormSuccess = getStory({
152+
newsletter: {
153+
buttonProps: {
154+
onClick: action("SocialAndNewsletterWithFormSuccess onClick")
155+
},
156+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et.",
157+
form: {
158+
formComponent: ({ children }) => <form action="#">{children}</form>,
159+
success: true
160+
}
161+
},
162+
social: {
163+
buttons: defaultSocialButtons
164+
}
165+
});
166+
167+
export const SocialAndNewsletterWithFormError = getStory({
168+
newsletter: {
169+
buttonProps: {
170+
onClick: action("SocialAndNewsletterWithFormError onClick")
171+
},
172+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et.",
173+
form: {
174+
formComponent: ({ children }) => <form action="#">{children}</form>,
175+
success: false,
176+
inputProps: {
177+
state: "error",
178+
stateRelatedMessage:
179+
"Le format de l’adresse electronique saisie n’est pas valide. Le format attendu est : nom@exemple.org"
180+
}
181+
}
182+
},
183+
social: {
184+
buttons: defaultSocialButtons
185+
}
186+
});

stories/Input.stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { sectionName } from "./sectionName";
33
import { getStoryFactory } from "./getStory";
44
import { assert } from "tsafe/assert";
55
import type { Equals } from "tsafe";
6+
import Button from "../dist/Button";
7+
import React from "react";
68

79
const { meta, getStory } = getStoryFactory({
810
sectionName,
@@ -131,3 +133,8 @@ export const WithPlaceholder = getStory({
131133
"placeholder": "https://"
132134
}
133135
});
136+
137+
export const WithButtonAddon = getStory({
138+
"label": "Label champs de saisie",
139+
"addon": <Button>Valider</Button>
140+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client";
2+
3+
import { Follow as BaseFollow } from "@codegouvfr/react-dsfr/Follow";
4+
import { useState } from 'react'
5+
6+
export const Follow = () => {
7+
const [success, setSuccess] = useState(false)
8+
return <BaseFollow
9+
newsletter={{
10+
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius tortor nibh, sit amet tempor nibh finibus et.",
11+
buttonProps: {
12+
onClick: () => setSuccess(true)
13+
},
14+
form: {
15+
formComponent: ({ children }) => <form action="#">{children}</form>,
16+
success,
17+
}
18+
}}
19+
social= {{
20+
buttons: [
21+
{
22+
type: "facebook",
23+
linkProps: {
24+
href: "#facebook"
25+
}
26+
},
27+
{
28+
type: "twitter-x",
29+
linkProps: {
30+
href: "#twitter"
31+
}
32+
},
33+
{
34+
type: "linkedin",
35+
linkProps: {
36+
href: "#linkedin"
37+
}
38+
},
39+
{
40+
type: "instagram",
41+
linkProps: {
42+
href: "#instagram"
43+
}
44+
},
45+
{
46+
type: "youtube",
47+
linkProps: {
48+
href: "#youtube"
49+
}
50+
}
51+
]
52+
}}
53+
/>
54+
}

test/integration/next-appdir/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { headers } from "next/headers";
1818
import { getScriptNonceFromHeader } from "next/dist/server/app-render/get-script-nonce-from-header"; // or use your own implementation
1919
import style from "./main.module.css";
2020
import { cx } from '@codegouvfr/react-dsfr/tools/cx';
21+
import { Follow } from './Follow';
2122

2223

2324
export default function RootLayout({ children }: { children: JSX.Element; }) {
@@ -81,6 +82,7 @@ export default function RootLayout({ children }: { children: JSX.Element; }) {
8182
<div className={cx(style.container)}>
8283
{children}
8384
</div>
85+
<Follow />
8486
<Footer
8587
accessibility="fully compliant"
8688
contentDescription={`

0 commit comments

Comments
 (0)