Skip to content

Commit ddee0e0

Browse files
Feature / select fix (#76)
* 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 <joseph.garrone.gj@gmail.com> * 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 --------- Signed-off-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Co-authored-by: garronej <joseph.garrone@data.gouv.fr>
1 parent 7dda4b5 commit ddee0e0

File tree

9 files changed

+584
-231
lines changed

9 files changed

+584
-231
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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codegouvfr/react-dsfr",
3-
"version": "0.41.0",
3+
"version": "0.42.0",
44
"description": "French State Design System React integration library",
55
"repository": {
66
"type": "git",

src/Select.tsx

Lines changed: 136 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,165 @@
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";
920

10-
export type SelectProps = {
21+
export type SelectProps<Options extends SelectProps.Option[]> = {
22+
options: Options;
1123
className?: string;
1224
label: ReactNode;
1325
hint?: ReactNode;
14-
nativeSelectProps: React.DetailedHTMLProps<
15-
React.SelectHTMLAttributes<HTMLSelectElement>,
16-
HTMLSelectElement
17-
>;
18-
children: ReactNode;
26+
nativeSelectProps?: Omit<
27+
DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
28+
"value" | "defaultValue" | "onChange"
29+
> & {
30+
// Overriding the type of value and defaultValue to only accept the value type of the options
31+
value?: Options[number]["value"];
32+
defaultValue?: 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+
hidden?: boolean;
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;
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;
4590

46-
assert<Equals<keyof typeof rest, never>>();
91+
assert<Equals<keyof typeof rest, never>>();
4792

48-
const selectId = `select-${useId()}`;
49-
const stateDescriptionId = `select-${useId()}-desc`;
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`;
50101

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}
102+
return { selectId, stateDescriptionId };
103+
})();
104+
105+
return (
106+
<div
107+
className={cx(
108+
fr.cx(
109+
"fr-select-group",
110+
disabled && "fr-select-group--disabled",
111+
state !== "default" && `fr-select-group--${state}`
112+
),
113+
className
114+
)}
115+
ref={ref}
116+
style={style}
117+
{...rest}
118+
>
119+
<label className={fr.cx("fr-label")} htmlFor={selectId}>
120+
{label}
121+
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
122+
</label>
123+
<select
124+
{...(nativeSelectProps as any)}
125+
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
126+
id={selectId}
127+
aria-describedby={stateDescriptionId}
128+
disabled={disabled}
74129
>
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-
);
130+
{[
131+
...(placeholder === undefined
132+
? []
133+
: [
134+
{
135+
label: placeholder,
136+
selected: true,
137+
value: "",
138+
disabled: true
139+
}
140+
]),
141+
...options
142+
].map((option, index) => (
143+
<option {...option} key={`${option.value}-${index}`}>
144+
{option.label}
145+
</option>
146+
))}
147+
</select>
148+
{state !== "default" && (
149+
<p id={stateDescriptionId} className={fr.cx(`fr-${state}-text`)}>
150+
{stateRelatedMessage}
151+
</p>
152+
)}
153+
</div>
154+
);
155+
}
156+
157+
export const Select = memo(forwardRef(NonMemoizedNonForwardedSelect)) as <
158+
T extends SelectProps.Option[]
159+
>(
160+
props: SelectProps<T> & { ref?: ForwardedRef<HTMLDivElement> }
161+
) => ReturnType<typeof NonMemoizedNonForwardedSelect>;
110162

111-
Select.displayName = symToStr({ Select });
163+
(Select as any).displayName = symToStr({ Select });
112164

113165
export default Select;

0 commit comments

Comments
 (0)