Skip to content

Commit e2c4b32

Browse files
Feature/select with options prop (#93)
* added placeholder, options and updated stories * update Select with Option typesafe * update Select stories * more readable example with typed option values * fixed mistake in controlled exampe of Select * WIP: added typesafe value / defaultValue, need rework on Select stories * updated Select stories according to new type * fix displayName * fixed exports, option key and placeholder checks on Select component * fix ColorHelper stories * ids in closure, add storybook example + fixed stories with values * nativeSelectProps?.id as selectIdExplicitelyProvided * selectIdExplicitelyProvided in closure and check vs undefined * Release candidate Signed-off-by: Joseph Garrone <[email protected]> * Extra work on select * Make clearer recommendations * fixed some typos in french doc * fixed import of type Equals, ignored eslint on .eslintrc.js * added selected prop on placeholder, added real life example to vite demo project, fixed event.target.value type * small comment on useState * Realize there is still some more work needed to be done * Peer programing on the Select component --------- Signed-off-by: Joseph Garrone <[email protected]> Co-authored-by: Enguerran Weiss <[email protected]>
1 parent 01444ed commit e2c4b32

File tree

12 files changed

+802
-15156
lines changed

12 files changed

+802
-15156
lines changed

.eslintrc.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ module.exports = {
33
"parser": "@typescript-eslint/parser",
44
"plugins": ["@typescript-eslint"],
55
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:storybook/recommended"],
6+
"ignorePatterns": [".eslintrc.js"],
67
"rules": {
78
"no-extra-boolean-cast": "off",
89
"@typescript-eslint/explicit-module-boundary-types": "off",
910
"@typescript-eslint/no-explicit-any": "off",
1011
"@typescript-eslint/no-namespace": "off",
11-
"@typescript-eslint/ban-types": "off"
12-
}
12+
"@typescript-eslint/ban-types": "off",
13+
"@typescript-eslint/ban-ts-comment": "off"
14+
},
15+
1316
};

README.fr.md

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
-
2424
<a href="https://react-dsfr.etalab.studio/">guides</a>
2525
-
26-
<a href="https://stackblitz.com/edit/nextjs-j2wba3?file=pages/index.tsx">essaie immédiat</a>
26+
<a href="https://stackblitz.com/edit/nextjs-j2wba3?file=pages/index.tsx">essai immédiat</a>
2727
</p>
2828

2929
> ATTENTION: Ce design système a uniquement vocation à être utilisé pour des sites officiels de l'état.
@@ -38,7 +38,7 @@ DSFR en pur JavaScript/CSS.
3838
<img width="1712" alt="image" src="https://user-images.githubusercontent.com/6702424/224423044-c1823249-eab6-4844-af43-d059c01416af.png">
3939
</a>
4040

41-
> Bien que cette bibliothèque soit écrit en TypeScript, l'utilisation de TypeScript dans votre application est facultative (mais recommandée car elle présente des avantages exceptionnels pour vous et votre base de code).
41+
> Bien que cette bibliothèque soit écrite en TypeScript, l'utilisation de TypeScript dans votre application est facultative (mais recommandée car elle présente des avantages exceptionnels pour vous et votre base de code).
4242
4343
- [x] une interface de programmation strictement typée et bien documentée.
4444
- [x] Garantie d'être toujours à jour avec les [dernières évolutions du DSFR](https://www.systeme-de-design.gouv.fr/).
@@ -51,10 +51,10 @@ DSFR en pur JavaScript/CSS.
5151
- [ ] tout [les composants de référence implémentés](https://www.systeme-de-design.gouv.fr/elements-d-interface). À ce jour 20/41, [see details](COMPONENTS.md)
5252
- [x] seulement le code des composants que vous utilisez effectivement sera inclus dans votre projet final.
5353
- [x] Intégration facultative avec [MUI](https://mui.com/). Si vous utilisez des composants MUI ils seront automatiquement adaptés pour ressembler à des composants DSFR.
54-
Voir [documentation](https://react-dsfr.etalab.studio/mui-integration).
54+
Voir la [documentation](https://react-dsfr.etalab.studio/mui-integration).
5555
- [x] permet de développer à l'aide d'outil de CSS-in-JS comme [Styled component](https://styled-components.com/), [Emotion](https://emotion.sh/docs/introduction) ou [TSS](https://www.tss-react.dev/).
56-
- [x] prévois un système de traduction pour les textes présents dans les composants (i18n).
57-
- [x] [s'intègre avec les librairies de routing](https://react-dsfr.etalab.studio/routing) like [React Router](https://reactrouter.com/en/main), [TanStack Router](https://tanstack.com/router/v1) ou encore [Type route](https://type-route.zilch.dev/).
56+
- [x] prévoit un système de traduction pour les textes présents dans les composants (i18n).
57+
- [x] [s'intègre avec les librairies de routing](https://react-dsfr.etalab.studio/routing) comme [React Router](https://reactrouter.com/en/main), [TanStack Router](https://tanstack.com/router/v1) ou encore [Type route](https://type-route.zilch.dev/).
5858

5959
Ce travail est un produit de [CodeGouvFr](https://communs.numerique.gouv.fr/), la mission logiciel libre de [la direction interministérielle du numérique](https://www.numerique.gouv.fr/dinum/) (DINUM).
6060

@@ -64,22 +64,15 @@ Ce travail est un produit de [CodeGouvFr](https://communs.numerique.gouv.fr/), l
6464

6565
## À propos [`@dataesr/react-dsfr`](https://github.com/dataesr/react-dsfr)?
6666

67-
`@codegouvfr/react-dsfr` (ce projet) est un projet TypeScript ayant pour priorité de fournir une bonne intégration
68-
avec l’écosystème React, notamment avec Next.js.
69-
70-
Ce projet a été démarré en octobre 2022, c'est une initiative récente et, malgré le fait qu'il soit activement développé, aujourd'hui
71-
`@dataesr/react-dsfr` est plus stable et fournit [une couverture de composant plus exhaustive](https://github.com/dataesr/react-dsfr/tree/master/src/components/interface).
72-
Si vous travaillez sur une SPA (Create React App, Vite) `@dataesr/react-dsfr` est probablement l'option la plus viable à ce jour.
73-
74-
Cela étant dit, vous pouvez bénéficier de plusieurs des fonctionnalités de `@codegouvfr/react-dsfr` sans migrer de `@dataesr/react-dsfr`:
67+
Si votre projet utilise [`@dataesr/react-dsfr`](https://github.com/dataesr/react-dsfr) et que vous n'êtes pas enclin a migrer ver `@codegouvfr/react-dsfr` vous pouvez tout de même profiter de plusieurs fonctionalités de ce dernier:
7568

7669
- Profitez de [l'auto complétion des classes en `fr-*`](https://react-dsfr.etalab.studio/class-names-type-safety).
7770
- Utilisez [le système de couleur strictement typer](https://react-dsfr.etalab.studio/css-in-js#colors).
7871
- Utilisez le thème MUI.
7972
- Utilisez [le système d'espacement](https://react-dsfr.etalab.studio/css-in-js#fr.spacing) et de
8073
[point de rupture](https://react-dsfr.etalab.studio/css-in-js#fr.breakpoints).
8174

82-
[Voici un bac a sable pour expérimenter](https://stackblitz.com/edit/react-ts-fph9bh?file=App.tsx).
75+
[Voici un bac à sable pour expérimenter](https://stackblitz.com/edit/react-ts-fph9bh?file=App.tsx).
8376

8477
## Development
8578

@@ -105,7 +98,7 @@ npx vitest -t "Resolution of CSS variables"
10598

10699
### Vous cherchez comment contribuer?
107100

108-
Tout d'abord, merci! Voici [le guide de contribution](https://github.com/codegouvfr/react-dsfr/blob/main/CONTRIBUTING.md).
101+
Tout d'abord, merci ! Voici [le guide de contribution](https://github.com/codegouvfr/react-dsfr/blob/main/CONTRIBUTING.md).
109102

110103
### Comment publier une nouvelle version sur NPM
111104

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ I'm working full time on this project. You can expect rapid development.
6666

6767
# What about [`@dataesr/react-dsfr`](https://github.com/dataesr/react-dsfr)?
6868

69-
Many of `@codegouvfr/react-dsfr`'s features can be enjoyed without migrating away from `@dataesr/react-dsfr`.
70-
You can, as standalone feature:
69+
If your project is using [`@dataesr/react-dsfr`](https://github.com/dataesr/react-dsfr) and you're not willing to migrate to `@codegouvfr/react-dsfr` you can still benefit from some of this project features:
7170

72-
- Enjoy the [`fr-*` classes autocompletion and type safety](https://react-dsfr.etalab.studio/class-names-type-safety).
71+
- The [`fr-*` classes autocompletion and type safety](https://react-dsfr.etalab.studio/class-names-type-safety).
7372
- Use [the type safe color system](https://react-dsfr.etalab.studio/css-in-js#colors).
7473
- Use the MUI theme.
7574
- The [the spacing system](https://react-dsfr.etalab.studio/css-in-js#fr.spacing) and

src/Select.tsx

Lines changed: 157 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,182 @@
11
"use client";
22

3-
import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react";
3+
import React, {
4+
memo,
5+
forwardRef,
6+
type ReactNode,
7+
useId,
8+
type CSSProperties,
9+
type ForwardedRef,
10+
type DetailedHTMLProps,
11+
type SelectHTMLAttributes,
12+
type ChangeEvent
13+
} from "react";
414
import { symToStr } from "tsafe/symToStr";
515
import { assert } from "tsafe/assert";
616
import type { Equals } from "tsafe";
717
import { fr } from "./fr";
818
import { cx } from "./tools/cx";
19+
import type { FrClassName } from "./fr/generatedFromCss/classNames";
20+
import { createComponentI18nApi } from "./i18n";
921

10-
export type SelectProps = {
22+
export type SelectProps<Options extends SelectProps.Option[]> = {
23+
options: Options;
1124
className?: string;
1225
label: ReactNode;
1326
hint?: ReactNode;
14-
nativeSelectProps: React.DetailedHTMLProps<
15-
React.SelectHTMLAttributes<HTMLSelectElement>,
16-
HTMLSelectElement
17-
>;
18-
children: ReactNode;
27+
nativeSelectProps?: Omit<
28+
DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
29+
"value" | "onChange"
30+
> & {
31+
// Overriding the type of value and defaultValue to only accept the value type of the options
32+
value?: Options[number]["value"];
33+
onChange?: (
34+
e: Omit<ChangeEvent<HTMLSelectElement>, "target" | "currentTarget"> & {
35+
target: Omit<ChangeEvent<HTMLSelectElement>, "value"> & {
36+
value: Options[number]["value"];
37+
};
38+
currentTarget: Omit<ChangeEvent<HTMLSelectElement>, "value"> & {
39+
value: Options[number]["value"];
40+
};
41+
}
42+
) => void;
43+
};
1944
/** Default: false */
2045
disabled?: boolean;
2146
/** Default: "default" */
22-
state?: "success" | "error" | "default";
47+
state?: SelectProps.State | "default";
2348
/** The message won't be displayed if state is "default" */
2449
stateRelatedMessage?: ReactNode;
2550
style?: CSSProperties;
51+
placeholder?: string;
2652
};
2753

54+
export namespace SelectProps {
55+
export type Option<T extends string = string> = {
56+
value: T;
57+
label: string;
58+
disabled?: boolean;
59+
/** Default: false, should be used only in uncontrolled mode */
60+
selected?: boolean;
61+
};
62+
63+
type ExtractState<FrClassName> = FrClassName extends `fr-select-group--${infer State}`
64+
? Exclude<State, "disabled">
65+
: never;
66+
67+
export type State = ExtractState<FrClassName>;
68+
}
69+
2870
/**
2971
* @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-select>
3072
* */
31-
export const Select = memo(
32-
forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
33-
const {
34-
className,
35-
label,
36-
hint,
37-
nativeSelectProps,
38-
disabled = false,
39-
children,
40-
state = "default",
41-
stateRelatedMessage,
42-
style,
43-
...rest
44-
} = props;
45-
46-
assert<Equals<keyof typeof rest, never>>();
47-
48-
const selectId = `select-${useId()}`;
49-
const stateDescriptionId = `select-${useId()}-desc`;
50-
51-
return (
52-
<div
53-
className={cx(
54-
fr.cx(
55-
"fr-select-group",
56-
disabled && "fr-select-group--disabled",
57-
(() => {
58-
switch (state) {
59-
case "error":
60-
return "fr-select-group--error";
61-
case "success":
62-
return "fr-select-group--valid";
63-
case "default":
64-
return undefined;
65-
}
66-
assert<Equals<typeof state, never>>(false);
67-
})()
68-
),
69-
className
70-
)}
71-
ref={ref}
72-
style={style}
73-
{...rest}
73+
function NonMemoizedNonForwardedSelect<T extends SelectProps.Option[]>(
74+
props: SelectProps<T>,
75+
ref: React.LegacyRef<HTMLDivElement>
76+
) {
77+
const {
78+
className,
79+
label,
80+
hint,
81+
nativeSelectProps,
82+
disabled = false,
83+
options,
84+
state = "default",
85+
stateRelatedMessage,
86+
placeholder,
87+
style,
88+
...rest
89+
} = props;
90+
91+
assert<Equals<keyof typeof rest, never>>();
92+
93+
const { selectId, stateDescriptionId } = (function useClosure() {
94+
const selectIdExplicitlyProvided = nativeSelectProps?.id;
95+
const elementId = useId();
96+
const selectId = selectIdExplicitlyProvided ?? `select-${elementId}`;
97+
const stateDescriptionId =
98+
selectIdExplicitlyProvided !== undefined
99+
? `${selectIdExplicitlyProvided}-desc`
100+
: `select-${elementId}-desc`;
101+
102+
return { selectId, stateDescriptionId };
103+
})();
104+
105+
const { t } = useTranslation();
106+
107+
return (
108+
<div
109+
className={cx(
110+
fr.cx(
111+
"fr-select-group",
112+
disabled && "fr-select-group--disabled",
113+
state !== "default" && `fr-select-group--${state}`
114+
),
115+
className
116+
)}
117+
ref={ref}
118+
style={style}
119+
{...rest}
120+
>
121+
<label className={fr.cx("fr-label")} htmlFor={selectId}>
122+
{label}
123+
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
124+
</label>
125+
<select
126+
{...(nativeSelectProps as any)}
127+
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
128+
id={selectId}
129+
aria-describedby={stateDescriptionId}
130+
disabled={disabled}
74131
>
75-
<label className={fr.cx("fr-label")} htmlFor={selectId}>
76-
{label}
77-
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
78-
</label>
79-
<select
80-
{...nativeSelectProps}
81-
className={cx(fr.cx("fr-select"), nativeSelectProps.className)}
82-
id={selectId}
83-
aria-describedby={stateDescriptionId}
84-
disabled={disabled}
85-
>
86-
{children}
87-
</select>
88-
{state !== "default" && (
89-
<p
90-
id={stateDescriptionId}
91-
className={fr.cx(
92-
(() => {
93-
switch (state) {
94-
case "error":
95-
return "fr-error-text";
96-
case "success":
97-
return "fr-valid-text";
98-
}
99-
assert<Equals<typeof state, never>>(false);
100-
})()
101-
)}
102-
>
103-
{stateRelatedMessage}
104-
</p>
105-
)}
106-
</div>
107-
);
108-
})
109-
);
110-
111-
Select.displayName = symToStr({ Select });
132+
{[
133+
{
134+
"label": placeholder === undefined ? t("select an option") : placeholder,
135+
"selected": true,
136+
"value": "",
137+
"disabled": true,
138+
"hidden": true
139+
},
140+
...options
141+
].map((option, index) => (
142+
<option {...option} key={`${option.value}-${index}`}>
143+
{option.label}
144+
</option>
145+
))}
146+
</select>
147+
{state !== "default" && (
148+
<p id={stateDescriptionId} className={fr.cx(`fr-${state}-text`)}>
149+
{stateRelatedMessage}
150+
</p>
151+
)}
152+
</div>
153+
);
154+
}
155+
156+
export const Select = memo(forwardRef(NonMemoizedNonForwardedSelect)) as <
157+
T extends SelectProps.Option[]
158+
>(
159+
props: SelectProps<T> & { ref?: ForwardedRef<HTMLDivElement> }
160+
) => ReturnType<typeof NonMemoizedNonForwardedSelect>;
161+
162+
(Select as any).displayName = symToStr({ Select });
112163

113164
export default Select;
165+
166+
const { useTranslation, addSelectTranslations } = createComponentI18nApi({
167+
"componentName": symToStr({ Select }),
168+
"frMessages": {
169+
/* spell-checker: disable */
170+
"select an option": "Selectioner une option",
171+
/* spell-checker: enable */
172+
}
173+
});
174+
175+
addSelectTranslations({
176+
"lang": "en",
177+
"messages": {
178+
"select an option": "Select an option"
179+
}
180+
});
181+
182+
export { addSelectTranslations };

0 commit comments

Comments
 (0)