|
1 | 1 | "use client";
|
2 | 2 |
|
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"; |
| 3 | +import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react"; |
14 | 4 | import { symToStr } from "tsafe/symToStr";
|
15 | 5 | import { assert } from "tsafe/assert";
|
16 | 6 | import type { Equals } from "tsafe";
|
17 | 7 | import { fr } from "./fr";
|
18 | 8 | import { cx } from "./tools/cx";
|
19 |
| -import type { FrClassName } from "./fr/generatedFromCss/classNames"; |
20 |
| -import { createComponentI18nApi } from "./i18n"; |
21 | 9 |
|
22 |
| -export type SelectProps<Options extends SelectProps.Option[]> = { |
23 |
| - options: Options; |
| 10 | +export type SelectProps = { |
24 | 11 | className?: string;
|
25 | 12 | label: ReactNode;
|
26 | 13 | hint?: 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 |
| - }; |
| 14 | + nativeSelectProps: React.DetailedHTMLProps< |
| 15 | + React.SelectHTMLAttributes<HTMLSelectElement>, |
| 16 | + HTMLSelectElement |
| 17 | + >; |
| 18 | + children: ReactNode; |
44 | 19 | /** Default: false */
|
45 | 20 | disabled?: boolean;
|
46 | 21 | /** Default: "default" */
|
47 |
| - state?: SelectProps.State | "default"; |
| 22 | + state?: "success" | "error" | "default"; |
48 | 23 | /** The message won't be displayed if state is "default" */
|
49 | 24 | stateRelatedMessage?: ReactNode;
|
50 | 25 | style?: CSSProperties;
|
51 |
| - placeholder?: string; |
52 | 26 | };
|
53 | 27 |
|
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 |
| - |
70 | 28 | /**
|
71 | 29 | * @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-select>
|
72 | 30 | * */
|
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} |
| 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} |
131 | 74 | >
|
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 }); |
| 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 }); |
163 | 112 |
|
164 | 113 | 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