Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
886bf0a
added placeholder, options and updated stories
enguerranws Feb 16, 2023
53ef21c
update Select with Option typesafe
enguerranws Feb 21, 2023
eac8438
update Select stories
enguerranws Feb 21, 2023
67405d5
more readable example with typed option values
enguerranws Feb 21, 2023
5d0b444
fixed mistake in controlled exampe of Select
enguerranws Feb 21, 2023
393cdd3
WIP: added typesafe value / defaultValue, need rework on Select stories
enguerranws Mar 1, 2023
97e36ff
updated Select stories according to new type
enguerranws Mar 2, 2023
78ff4e7
fix displayName
enguerranws Mar 2, 2023
51c71db
fixed exports, option key and placeholder checks on Select component
enguerranws Mar 8, 2023
baaa9d3
fix ColorHelper stories
enguerranws Mar 8, 2023
24e743b
ids in closure, add storybook example + fixed stories with values
enguerranws Mar 9, 2023
5e13c9e
nativeSelectProps?.id as selectIdExplicitelyProvided
enguerranws Mar 9, 2023
835cccc
selectIdExplicitelyProvided in closure and check vs undefined
enguerranws Mar 10, 2023
756de24
Release candidate
garronej Mar 10, 2023
c407c2f
Extra work on select
garronej Mar 24, 2023
970fa12
Make clearer recommendations
garronej Mar 24, 2023
0a3ede8
fixed some typos in french doc
enguerranws Mar 25, 2023
5a98bec
fixed import of type Equals, ignored eslint on .eslintrc.js
enguerranws Mar 25, 2023
df29526
added selected prop on placeholder, added real life example to vite d…
enguerranws Mar 25, 2023
2bf97b8
small comment on useState
enguerranws Mar 25, 2023
15534c7
Merge branch 'main' into feature/select-with-options-prop
garronej Mar 25, 2023
b274f1c
Realize there is still some more work needed to be done
garronej Mar 26, 2023
17efaa8
Peer programing on the Select component
garronej Mar 31, 2023
4c18266
Merge branch 'main' into feature/select-with-options-prop
garronej Apr 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ module.exports = {
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:storybook/recommended"],
"ignorePatterns": [".eslintrc.js"],
"rules": {
"no-extra-boolean-cast": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/ban-types": "off"
}
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off"
},

};
23 changes: 8 additions & 15 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
-
<a href="https://react-dsfr.etalab.studio/">guides</a>
-
<a href="https://stackblitz.com/edit/nextjs-j2wba3?file=pages/index.tsx">essaie immédiat</a>
<a href="https://stackblitz.com/edit/nextjs-j2wba3?file=pages/index.tsx">essai immédiat</a>
</p>

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

> 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).
> 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).

- [x] une interface de programmation strictement typée et bien documentée.
- [x] Garantie d'être toujours à jour avec les [dernières évolutions du DSFR](https://www.systeme-de-design.gouv.fr/).
Expand All @@ -51,10 +51,10 @@ DSFR en pur JavaScript/CSS.
- [ ] 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)
- [x] seulement le code des composants que vous utilisez effectivement sera inclus dans votre projet final.
- [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.
Voir [documentation](https://react-dsfr.etalab.studio/mui-integration).
Voir la [documentation](https://react-dsfr.etalab.studio/mui-integration).
- [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/).
- [x] prévois un système de traduction pour les textes présents dans les composants (i18n).
- [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/).
- [x] prévoit un système de traduction pour les textes présents dans les composants (i18n).
- [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/).

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).

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

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

`@codegouvfr/react-dsfr` (ce projet) est un projet TypeScript ayant pour priorité de fournir une bonne intégration
avec l’écosystème React, notamment avec Next.js.

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
`@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).
Si vous travaillez sur une SPA (Create React App, Vite) `@dataesr/react-dsfr` est probablement l'option la plus viable à ce jour.

Cela étant dit, vous pouvez bénéficier de plusieurs des fonctionnalités de `@codegouvfr/react-dsfr` sans migrer de `@dataesr/react-dsfr`:
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:

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

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

## Development

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

### Vous cherchez comment contribuer?

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

### Comment publier une nouvelle version sur NPM

Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,9 @@ I'm working full time on this project. You can expect rapid development.

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

Many of `@codegouvfr/react-dsfr`'s features can be enjoyed without migrating away from `@dataesr/react-dsfr`.
You can, as standalone feature:
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:

- Enjoy the [`fr-*` classes autocompletion and type safety](https://react-dsfr.etalab.studio/class-names-type-safety).
- The [`fr-*` classes autocompletion and type safety](https://react-dsfr.etalab.studio/class-names-type-safety).
- Use [the type safe color system](https://react-dsfr.etalab.studio/css-in-js#colors).
- Use the MUI theme.
- The [the spacing system](https://react-dsfr.etalab.studio/css-in-js#fr.spacing) and
Expand Down
245 changes: 157 additions & 88 deletions src/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,182 @@
"use client";

import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react";
import React, {
memo,
forwardRef,
type ReactNode,
useId,
type CSSProperties,
type ForwardedRef,
type DetailedHTMLProps,
type SelectHTMLAttributes,
type ChangeEvent
} from "react";
import { symToStr } from "tsafe/symToStr";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { fr } from "./fr";
import { cx } from "./tools/cx";
import type { FrClassName } from "./fr/generatedFromCss/classNames";
import { createComponentI18nApi } from "./i18n";

export type SelectProps = {
export type SelectProps<Options extends SelectProps.Option[]> = {
options: Options;
className?: string;
label: ReactNode;
hint?: ReactNode;
nativeSelectProps: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
>;
children: ReactNode;
nativeSelectProps?: Omit<
DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
"value" | "onChange"
> & {
// Overriding the type of value and defaultValue to only accept the value type of the options
value?: Options[number]["value"];
onChange?: (
e: Omit<ChangeEvent<HTMLSelectElement>, "target" | "currentTarget"> & {
target: Omit<ChangeEvent<HTMLSelectElement>, "value"> & {
value: Options[number]["value"];
};
currentTarget: Omit<ChangeEvent<HTMLSelectElement>, "value"> & {
value: Options[number]["value"];
};
}
) => void;
};
/** Default: false */
disabled?: boolean;
/** Default: "default" */
state?: "success" | "error" | "default";
state?: SelectProps.State | "default";
/** The message won't be displayed if state is "default" */
stateRelatedMessage?: ReactNode;
style?: CSSProperties;
placeholder?: string;
};

export namespace SelectProps {
export type Option<T extends string = string> = {
value: T;
label: string;
disabled?: boolean;
/** Default: false, should be used only in uncontrolled mode */
selected?: boolean;
};

type ExtractState<FrClassName> = FrClassName extends `fr-select-group--${infer State}`
? Exclude<State, "disabled">
: never;

export type State = ExtractState<FrClassName>;
}

/**
* @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-select>
* */
export const Select = memo(
forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
const {
className,
label,
hint,
nativeSelectProps,
disabled = false,
children,
state = "default",
stateRelatedMessage,
style,
...rest
} = props;

assert<Equals<keyof typeof rest, never>>();

const selectId = `select-${useId()}`;
const stateDescriptionId = `select-${useId()}-desc`;

return (
<div
className={cx(
fr.cx(
"fr-select-group",
disabled && "fr-select-group--disabled",
(() => {
switch (state) {
case "error":
return "fr-select-group--error";
case "success":
return "fr-select-group--valid";
case "default":
return undefined;
}
assert<Equals<typeof state, never>>(false);
})()
),
className
)}
ref={ref}
style={style}
{...rest}
function NonMemoizedNonForwardedSelect<T extends SelectProps.Option[]>(
props: SelectProps<T>,
ref: React.LegacyRef<HTMLDivElement>
) {
const {
className,
label,
hint,
nativeSelectProps,
disabled = false,
options,
state = "default",
stateRelatedMessage,
placeholder,
style,
...rest
} = props;

assert<Equals<keyof typeof rest, never>>();

const { selectId, stateDescriptionId } = (function useClosure() {
const selectIdExplicitlyProvided = nativeSelectProps?.id;
const elementId = useId();
const selectId = selectIdExplicitlyProvided ?? `select-${elementId}`;
const stateDescriptionId =
selectIdExplicitlyProvided !== undefined
? `${selectIdExplicitlyProvided}-desc`
: `select-${elementId}-desc`;

return { selectId, stateDescriptionId };
})();

const { t } = useTranslation();

return (
<div
className={cx(
fr.cx(
"fr-select-group",
disabled && "fr-select-group--disabled",
state !== "default" && `fr-select-group--${state}`
),
className
)}
ref={ref}
style={style}
{...rest}
>
<label className={fr.cx("fr-label")} htmlFor={selectId}>
{label}
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
</label>
<select
{...(nativeSelectProps as any)}
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
id={selectId}
aria-describedby={stateDescriptionId}
disabled={disabled}
>
<label className={fr.cx("fr-label")} htmlFor={selectId}>
{label}
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
</label>
<select
{...nativeSelectProps}
className={cx(fr.cx("fr-select"), nativeSelectProps.className)}
id={selectId}
aria-describedby={stateDescriptionId}
disabled={disabled}
>
{children}
</select>
{state !== "default" && (
<p
id={stateDescriptionId}
className={fr.cx(
(() => {
switch (state) {
case "error":
return "fr-error-text";
case "success":
return "fr-valid-text";
}
assert<Equals<typeof state, never>>(false);
})()
)}
>
{stateRelatedMessage}
</p>
)}
</div>
);
})
);

Select.displayName = symToStr({ Select });
{[
{
"label": placeholder === undefined ? t("select an option") : placeholder,
"selected": true,
"value": "",
"disabled": true,
"hidden": true
},
...options
].map((option, index) => (
<option {...option} key={`${option.value}-${index}`}>
{option.label}
</option>
))}
</select>
{state !== "default" && (
<p id={stateDescriptionId} className={fr.cx(`fr-${state}-text`)}>
{stateRelatedMessage}
</p>
)}
</div>
);
}

export const Select = memo(forwardRef(NonMemoizedNonForwardedSelect)) as <
T extends SelectProps.Option[]
>(
props: SelectProps<T> & { ref?: ForwardedRef<HTMLDivElement> }
) => ReturnType<typeof NonMemoizedNonForwardedSelect>;

(Select as any).displayName = symToStr({ Select });

export default Select;

const { useTranslation, addSelectTranslations } = createComponentI18nApi({
"componentName": symToStr({ Select }),
"frMessages": {
/* spell-checker: disable */
"select an option": "Selectioner une option",
/* spell-checker: enable */
}
});

addSelectTranslations({
"lang": "en",
"messages": {
"select an option": "Select an option"
}
});

export { addSelectTranslations };
Loading