Skip to content

Commit c06e75b

Browse files
damienguezouGUEZOU Damiengarronej
authored
feat: Add password functional block (#91)
* feat: Add password functional block * #91 --------- Co-authored-by: GUEZOU Damien <dguezou@jouve.fr> Co-authored-by: garronej <joseph.garrone@data.gouv.fr>
1 parent 530bc10 commit c06e75b

File tree

7 files changed

+266
-9
lines changed

7 files changed

+266
-9
lines changed

.storybook/main.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
"stories": [
33
"../stories/*.stories.mdx",
44
"../stories/*.stories.@(ts|tsx)",
5+
"../stories/blocks/*.stories.@(ts|tsx)"
56
],
67
"addons": [
78
"@storybook/addon-links",
@@ -10,9 +11,7 @@ module.exports = {
1011
"@storybook/addon-a11y"
1112
],
1213
"core": {
13-
"builder": "webpack5",
14+
"builder": "webpack5"
1415
},
15-
"staticDirs": [
16-
"./static"
17-
]
16+
"staticDirs": ["./static"]
1817
};

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
6464
},
6565
"dependencies": {
66-
"tsafe": "^1.4.0"
66+
"tsafe": "^1.6.0"
6767
},
6868
"devDependencies": {
6969
"@gouvfr/dsfr": "1.9.0",
@@ -131,13 +131,15 @@
131131
"./tss": "./dist/tss.js",
132132
"./tools/cx": "./dist/tools/cx.js",
133133
"./dsfr/*": "./dsfr/*",
134+
"./block/*": "./block/*",
134135
"./ToggleSwitchGroup": "./dist/ToggleSwitchGroup.js",
135136
"./ToggleSwitch": "./dist/ToggleSwitch.js",
136137
"./Tile": "./dist/Tile.js",
137138
"./Tabs": "./dist/Tabs.js",
138139
"./Summary": "./dist/Summary.js",
139140
"./Stepper": "./dist/Stepper.js",
140141
"./SkipLinks": "./dist/SkipLinks.js",
142+
"./SideMenu": "./dist/SideMenu.js",
141143
"./Select": "./dist/Select.js",
142144
"./SearchBar": "./dist/SearchBar.js",
143145
"./RadioButtons": "./dist/RadioButtons.js",
@@ -160,7 +162,6 @@
160162
"./Breadcrumb": "./dist/Breadcrumb.js",
161163
"./Badge": "./dist/Badge.js",
162164
"./Alert": "./dist/Alert.js",
163-
"./Accordion": "./dist/Accordion.js",
164-
"./SideMenu": "./dist/SideMenu.js"
165+
"./Accordion": "./dist/Accordion.js"
165166
}
166167
}

src/blocks/PasswordInput.tsx

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import React, {
2+
type DetailedHTMLProps,
3+
forwardRef,
4+
type InputHTMLAttributes,
5+
memo,
6+
type ReactNode,
7+
useId
8+
} from "react";
9+
import { assert, type Equals } from "tsafe/assert";
10+
import { symToStr } from "tsafe/symToStr";
11+
import { fr } from "../fr";
12+
import { createComponentI18nApi } from "../i18n";
13+
import type { InputProps } from "../Input";
14+
import { cx } from "../tools/cx";
15+
import type { FrClassName } from "../fr/generatedFromCss/classNames";
16+
17+
export type PasswordInputProps = Omit<
18+
InputProps.Common,
19+
"state" | "stateRelatedMessage" | "iconId" | "classes"
20+
> & {
21+
classes?: Partial<Record<"root" | "input" | "label" | "checkbox", string>>;
22+
messages?: {
23+
severity: PasswordInputProps.Severity;
24+
message: ReactNode;
25+
}[];
26+
nativeInputProps?: Omit<
27+
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
28+
"type"
29+
>;
30+
};
31+
32+
export namespace PasswordInputProps {
33+
type ExtractSeverity<ClassName extends string> =
34+
ClassName extends `fr-message--${infer Severity}` ? Severity : never;
35+
36+
export type Severity = ExtractSeverity<FrClassName>;
37+
}
38+
39+
/**
40+
* @see <https://react-dsfr-components.etalab.studio/?path=/docs/blocks-passwordinput
41+
* */
42+
export const PasswordInput = memo(
43+
forwardRef<HTMLDivElement, PasswordInputProps>((props, ref) => {
44+
const {
45+
className,
46+
label,
47+
hintText,
48+
hideLabel,
49+
disabled = false,
50+
classes = {},
51+
style,
52+
messages = [],
53+
nativeInputProps,
54+
...rest
55+
} = props;
56+
57+
assert<Equals<keyof typeof rest, never>>();
58+
59+
const { t } = useTranslation();
60+
61+
const inputId = (function useClosure() {
62+
const id = useId();
63+
64+
return nativeInputProps?.id ?? `password-${id}`;
65+
})();
66+
const containerId = `${inputId}-container`;
67+
const togglePasswordShowId = `${inputId}-toggle-show`;
68+
const messagesGroupId = `${inputId}-messages-group`;
69+
const messageGroupId = `${inputId}-message-group`;
70+
71+
const hasError = messages.find(({ severity }) => severity === "error") !== undefined;
72+
const isSuccess =
73+
messages.length !== 0 &&
74+
messages.find(({ severity }) => severity !== "valid") === undefined;
75+
76+
return (
77+
<div
78+
className={cx(
79+
fr.cx(
80+
"fr-password",
81+
disabled && "fr-input-group--disabled",
82+
hasError && "fr-input-group--error",
83+
isSuccess && "fr-input-group--valid"
84+
),
85+
classes.root,
86+
className
87+
)}
88+
id={containerId}
89+
style={style}
90+
ref={ref}
91+
{...rest}
92+
>
93+
<label
94+
className={cx(fr.cx("fr-label", hideLabel && "fr-sr-only"), classes.label)}
95+
htmlFor={inputId}
96+
>
97+
{label}
98+
{hintText !== undefined && <span className="fr-hint-text">{hintText}</span>}
99+
</label>
100+
<div className={fr.cx("fr-input-wrap")}>
101+
<input
102+
{...nativeInputProps}
103+
className={cx(fr.cx("fr-password__input", "fr-input"), classes.input)}
104+
id={inputId}
105+
type="password"
106+
disabled={disabled}
107+
{...(messages.length !== 0 && { "aria-describedby": messagesGroupId })}
108+
/>
109+
</div>
110+
{messages.length !== 0 && (
111+
<div
112+
className={fr.cx("fr-messages-group")}
113+
id={messagesGroupId}
114+
aria-live="assertive"
115+
>
116+
<p className={fr.cx("fr-message")} id={messageGroupId}>
117+
{t("your password must contain")}
118+
</p>
119+
{messages.map(({ severity, message }, index) => (
120+
<p
121+
key={index}
122+
className={fr.cx("fr-message", `fr-message--${severity}`)}
123+
id={`${messageGroupId}-${index}`}
124+
>
125+
{message}
126+
</p>
127+
))}
128+
</div>
129+
)}
130+
<div
131+
className={cx(
132+
fr.cx(
133+
"fr-password__checkbox",
134+
"fr-checkbox-group",
135+
"fr-checkbox-group--sm"
136+
),
137+
classes.checkbox
138+
)}
139+
>
140+
<input
141+
aria-label={t("show password")}
142+
id={togglePasswordShowId}
143+
type="checkbox"
144+
disabled={disabled || undefined}
145+
/>
146+
<label
147+
className={cx(fr.cx("fr-password__checkbox", "fr-label"), classes.checkbox)}
148+
htmlFor={togglePasswordShowId}
149+
>
150+
{t("show")}
151+
</label>
152+
</div>
153+
</div>
154+
);
155+
})
156+
);
157+
158+
const { useTranslation, addPasswordInputTranslations } = createComponentI18nApi({
159+
"componentName": symToStr({ PasswordInput }),
160+
"frMessages": {
161+
/* spell-checker: disable */
162+
"show": "Afficher",
163+
"show password": "Afficher le mot de passe",
164+
"your password must contain": "Votre mot de passe doit contenir :"
165+
/* spell-checker: enable */
166+
}
167+
});
168+
169+
addPasswordInputTranslations({
170+
"lang": "en",
171+
"messages": {
172+
"show": "Show",
173+
"show password": "Show password",
174+
"your password must contain": "Your password must contain:"
175+
}
176+
});
177+
178+
addPasswordInputTranslations({
179+
"lang": "es",
180+
"messages": {
181+
/* spell-checker: disable */
182+
"show": "Mostrar",
183+
"show password": "Mostrar contraseña",
184+
"your password must contain": "Su contraseña debe contener:"
185+
/* spell-checker: enable */
186+
}
187+
});
188+
189+
PasswordInput.displayName = symToStr({ PasswordInput });
190+
191+
export default PasswordInput;

src/scripts/list-exports.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const newExports = {
2929
"./tss": "./dist/tss.js",
3030
"./tools/cx": "./dist/tools/cx.js",
3131
"./dsfr/*": "./dsfr/*",
32+
"./block/*": "./block/*",
3233
...Object.fromEntries(
3334
fs
3435
.readdirSync(srcDirPath)
@@ -49,7 +50,7 @@ const newExports = {
4950
continue;
5051
}
5152

52-
return [basename, relativePath];
53+
return [basename, relativePath.replace(new RegExp(/\\/, "g"), "/")];
5354
}
5455

5556
return undefined;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import PasswordInput from "../../dist/blocks/PasswordInput";
2+
import { getStoryFactory } from "../getStory";
3+
import { sectionName } from "./sectionName";
4+
5+
const { meta, getStory } = getStoryFactory({
6+
sectionName,
7+
"wrappedComponent": { PasswordInput },
8+
"description": `- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mot-de-passe)
9+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/blocks/PasswordInput.tsx) `,
10+
"argTypes": {
11+
"disabled": {
12+
"control": { "type": "boolean" }
13+
},
14+
15+
"nativeInputProps": {
16+
"description": `An object that is forwarded as props to te underlying native \`<input />\` element.
17+
This is where you pass the \`name\` prop or \`onChange\` for example.`,
18+
"control": { "type": null }
19+
}
20+
}
21+
});
22+
23+
export default meta;
24+
25+
export const Default = getStory({
26+
"label": "Mot de passe"
27+
});
28+
29+
export const WithHint = getStory({
30+
"label": "Mot de passe",
31+
/* spell-checker: disable */
32+
"hintText": "Texte de description additionnel"
33+
/* spell-checker: english */
34+
});
35+
36+
export const WithMessagesGroup = getStory({
37+
"label": "Mot de passe",
38+
"messages": [
39+
/* spell-checker: disable */
40+
{
41+
"message": "12 caractères minimum",
42+
"severity": "info"
43+
},
44+
{
45+
"message": "1 caractère spécial minimum",
46+
"severity": "valid"
47+
},
48+
{
49+
"message": "1 chiffre minimum",
50+
"severity": "error"
51+
}
52+
/* spell-checker: enabled */
53+
]
54+
});
55+
56+
export const Disabled = getStory({
57+
"label": "Mot de passe",
58+
"disabled": true
59+
});

stories/blocks/sectionName.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const sectionName = "blocks";

yarn.lock

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11133,11 +11133,16 @@ ts-pnp@^1.1.6:
1113311133
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
1113411134
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
1113511135

11136-
tsafe@^1.4.0, tsafe@^1.4.1:
11136+
tsafe@^1.4.1:
1113711137
version "1.4.1"
1113811138
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.4.1.tgz#59cdad8ac41babf88e56dcd1a683ae2fb145d059"
1113911139
integrity sha512-3IDBalvf6SyvHFS14UiwCWzqdSdo+Q0k2J7DZyJYaHW/iraW9DJpaBKDJpry3yQs3o/t/A+oGaRW3iVt2lKxzA==
1114011140

11141+
tsafe@^1.6.0:
11142+
version "1.6.0"
11143+
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.0.tgz#48a9bd0a4c43df43d289bdfc1d89f0d7fffbd612"
11144+
integrity sha512-wlUeRBnyN3EN2chXznpLm7vBEvJLEOziDU+MN6NRlD99AkwmXgtChNQhp+V97VyRa3Bp05IaL4Cocsc7JlyEUg==
11145+
1114111146
tslib@^1.8.1, tslib@^1.9.3:
1114211147
version "1.14.1"
1114311148
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"

0 commit comments

Comments
 (0)