Skip to content

Commit 4dc5672

Browse files
committed
Rework SelectNext #265 #280
1 parent 3041516 commit 4dc5672

File tree

3 files changed

+45
-43
lines changed

3 files changed

+45
-43
lines changed

src/SelectNext.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,29 @@ function NonMemoizedNonForwardedSelect<T extends SelectProps.Option[]>(
148148
</label>
149149
)}
150150
<select
151-
{...(nativeSelectProps as any)}
151+
{...nativeSelectProps}
152+
{...(() => {
153+
const isControlled =
154+
nativeSelectProps !== undefined && "value" in nativeSelectProps;
155+
156+
const isEmptyValueSelected = isControlled
157+
? nativeSelectProps.value === undefined
158+
: options.find(option => option.selected) === undefined;
159+
160+
if (isControlled) {
161+
return isEmptyValueSelected ? { "value": "" } : {};
162+
}
163+
164+
return {
165+
"defaultValue": isEmptyValueSelected
166+
? ""
167+
: (() => {
168+
const selectedOption = options.find(option => option.selected);
169+
assert(selectedOption !== undefined);
170+
return selectedOption.value;
171+
})()
172+
};
173+
})()}
152174
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
153175
id={selectId}
154176
aria-describedby={stateDescriptionId}
@@ -160,16 +182,16 @@ function NonMemoizedNonForwardedSelect<T extends SelectProps.Option[]>(
160182
: {
161183
"label":
162184
placeholder === undefined ? t("select an option") : placeholder,
163-
"selected": true,
164185
"value": "",
165186
"disabled": true
166187
},
167-
...options
188+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
189+
...options.map(({ selected, ...option }) => option)
168190
]
169191
.filter(exclude(undefined))
170-
.map((option, index) => (
171-
<option {...(option as any)} key={`${option.value}-${index}`}>
172-
{option.label}
192+
.map(({ label, ...option }, index) => (
193+
<option {...option} key={`${option.value}-${index}`}>
194+
{label}
173195
</option>
174196
))}
175197
</select>

stories/Select.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const { meta, getStory } = getStoryFactory({
1212
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bandeau-d-information-importante)
1313
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Notice.tsx)
1414
15-
> 🗣️ This is a legacy implementation. Try out [\`SelectNext\`](https://components.react-dsfr.codegouv.studio/?path=/docs/components-selectnext--default) it will eventually replace this component.
15+
> 🗣️ This implementation of the <Select /> component is headless. It matched very closely the behavior of a native select input.
16+
> Try out [\`SelectNext\`](https://components.react-dsfr.codegouv.studio/?path=/docs/components-selectnext--default) if you want a smarter component with better type inference.
1617
1718
## Controlled
1819

stories/SelectNext.stories.tsx

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { Select } from "@codegouvfr/react-dsfr/SelectNext";
1717
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/liste-deroulante)
1818
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/SelectNext.tsx)
1919
20-
> NOTE: This component is still in beta, it may change in the future.
2120
2221
## Controlled
2322
@@ -63,6 +62,10 @@ function MyComponent(){
6362
}
6463
\`\`\`
6564
65+
> NOTE: In this implementation, once the use has selected a value it can't be unselected.
66+
> If you need you want your users to be able to unselect please provide an option with an empty string as value
67+
> and use the next example as reference.
68+
6669
### Value pre-selected
6770
6871
"bar" selected by default.
@@ -92,22 +95,21 @@ function MyComponent(){
9295
}
9396
\`\`\`
9497
95-
96-
9798
## Uncontrolled
9899
99100
\`\`\`tsx
100101
import { useState } from "react";
101102
import { Select } from "@codegouvfr/react-dsfr/Select";
102103
103-
104104
function MyComponent(){
105105
106106
return (
107107
<form method="POST" action="...">
108108
{/*
109109
* With no value pre selected, if the user didn't select anything,
110-
* when submitted the value of "my-select" will be ""
110+
* when submitted the value of "my-select" will be "".
111+
* Here as well, the value once selected can't be unselected.
112+
* Use an explicit option with an empty string as value and set selected to true to allow unselecting.
111113
*/}
112114
<Select
113115
label="Label"
@@ -200,7 +202,7 @@ function MyComponent(){
200202

201203
export default meta;
202204

203-
const defaultOptions = [
205+
const options = [
204206
{
205207
"value": "1",
206208
"label": "Option 1"
@@ -215,73 +217,50 @@ const defaultOptions = [
215217
}
216218
];
217219

218-
const myFakeValueSet = [
219-
"dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf",
220-
"1bda4f79-a199-40ce-985b-fa217809d568",
221-
"e91b2cac-48f6-4d60-b86f-ece02f076837",
222-
"66a9d7ac-9b25-4e52-9de3-4b7238135b39"
223-
] as const;
224-
225-
type MyFakeValue = typeof myFakeValueSet[number];
226-
227-
const optionsWithTypedValues: SelectProps.Option<MyFakeValue>[] = myFakeValueSet.map(fakeValue => ({
228-
value: fakeValue,
229-
label: fakeValue
230-
}));
231-
232220
export const Default = getStory({
233221
"label": "Label pour liste déroulante",
234-
"options": defaultOptions
222+
options
235223
});
236224

237225
export const DefaultWithPlaceholder = getStory({
238226
"label": "Label pour liste déroulante",
239227
"placeholder": "Sélectionnez une option",
240-
"options": defaultOptions
228+
options
241229
});
242230

243231
export const ErrorState = getStory({
244232
"label": "Label pour liste déroulante",
245233
"state": "error",
246234
"stateRelatedMessage": "Texte d’erreur obligatoire",
247-
"options": defaultOptions
235+
options
248236
});
249237

250238
export const SuccessState = getStory({
251239
"label": "Label pour liste déroulante",
252240
"state": "valid",
253241
"stateRelatedMessage": "Texte de validation",
254242
"placeholder": "Sélectionnez une option",
255-
"options": defaultOptions
243+
options
256244
});
257245

258246
export const Disabled = getStory({
259247
"label": "Label pour liste déroulante",
260248
"disabled": true,
261249
"placeholder": "Sélectionnez une option",
262-
"options": defaultOptions
250+
options
263251
});
264252

265253
export const WithHint = getStory({
266254
"label": "Label pour liste déroulante",
267255
"hint": "Texte de description additionnel",
268256
"placeholder": "Sélectionnez une option",
269-
"options": defaultOptions
270-
});
271-
272-
export const TypedSelect = getStory({
273-
"label": "Label pour liste déroulante avec valeurs d'options typesafe",
274-
"placeholder": "Sélectionnez une option",
275-
"options": optionsWithTypedValues,
276-
"nativeSelectProps": {
277-
"value": "dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf"
278-
}
257+
options
279258
});
280259

281260
export const SelectWithCustomId = getStory({
282261
"label": "Label pour liste déroulante",
283262
"nativeSelectProps": {
284263
id: "my-unique-id"
285264
},
286-
"options": defaultOptions
265+
options
287266
});

0 commit comments

Comments
 (0)