Skip to content

Commit 393cdd3

Browse files
committed
WIP: added typesafe value / defaultValue, need rework on Select stories
1 parent 5d0b444 commit 393cdd3

File tree

2 files changed

+118
-119
lines changed

2 files changed

+118
-119
lines changed

src/Select.tsx

Lines changed: 107 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
"use client";
22

3-
import React, { memo, forwardRef, ReactNode, useId, type CSSProperties } from "react";
3+
import React, { memo, forwardRef, ReactNode, useId, type CSSProperties, ForwardedRef } from "react";
44
import { symToStr } from "tsafe/symToStr";
55
import { assert } from "tsafe/assert";
66
import type { Equals } from "tsafe";
77
import { fr } from "./fr";
88
import { cx } from "./tools/cx";
99

10-
export type SelectProps<OptionValue> = {
10+
export type SelectProps<Options extends GenericOption<DefaultOptionValue>[]> = {
11+
options: Options;
1112
className?: string;
1213
label: ReactNode;
1314
hint?: ReactNode;
14-
nativeSelectProps?: React.DetailedHTMLProps<
15-
React.SelectHTMLAttributes<HTMLSelectElement>,
16-
HTMLSelectElement
17-
>;
15+
nativeSelectProps?: Omit<
16+
React.DetailedHTMLProps<React.SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
17+
"value" | "defaultValue"
18+
> & {
19+
// Overriding the type of value and defaultValue to only accept the value type of the options
20+
value?: Options[number]["value"];
21+
defaultValue?: Options[number]["value"];
22+
};
1823
/** Default: false */
1924
disabled?: boolean;
2025
/** Default: "default" */
2126
state?: "success" | "error" | "default";
2227
/** The message won't be displayed if state is "default" */
2328
stateRelatedMessage?: ReactNode;
2429
style?: CSSProperties;
25-
options: GenericOption<OptionValue>[];
2630
placeholder?: string;
2731
};
2832
export type GenericOption<OptionValue> = {
@@ -38,105 +42,105 @@ type DefaultOptionValue = string | number | readonly string[] | undefined;
3842
/**
3943
* @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-select>
4044
* */
41-
export const Select = memo(
42-
forwardRef(
43-
<T extends DefaultOptionValue>(
44-
props: SelectProps<T>,
45-
ref: React.LegacyRef<HTMLDivElement>
46-
) => {
47-
const {
48-
className,
49-
label,
50-
hint,
51-
nativeSelectProps,
52-
disabled = false,
53-
options,
54-
state = "default",
55-
stateRelatedMessage,
56-
placeholder,
57-
style,
58-
...rest
59-
} = props;
45+
export const Select = <T extends GenericOption<DefaultOptionValue>[]>(
46+
props: SelectProps<T>,
47+
ref: React.LegacyRef<HTMLDivElement>
48+
) => {
49+
const {
50+
className,
51+
label,
52+
hint,
53+
nativeSelectProps,
54+
disabled = false,
55+
options,
56+
state = "default",
57+
stateRelatedMessage,
58+
placeholder,
59+
style,
60+
...rest
61+
} = props;
6062

61-
assert<Equals<keyof typeof rest, never>>();
62-
const elementId = nativeSelectProps?.id || useId();
63-
const selectId = `select-${elementId}`;
64-
const stateDescriptionId = `select-${elementId}-desc`;
65-
const displayedOptions = placeholder
66-
? [
67-
{
68-
label: placeholder,
69-
value: "",
70-
disabled: true
71-
},
72-
...options
73-
]
74-
: options;
75-
return (
76-
<div
77-
className={cx(
78-
fr.cx(
79-
"fr-select-group",
80-
disabled && "fr-select-group--disabled",
81-
(() => {
82-
switch (state) {
83-
case "error":
84-
return "fr-select-group--error";
85-
case "success":
86-
return "fr-select-group--valid";
87-
case "default":
88-
return undefined;
89-
}
90-
assert<Equals<typeof state, never>>(false);
91-
})()
92-
),
93-
className
63+
assert<Equals<keyof typeof rest, never>>();
64+
const elementId = nativeSelectProps?.id || useId();
65+
const selectId = `select-${elementId}`;
66+
const stateDescriptionId = `select-${elementId}-desc`;
67+
const displayedOptions = placeholder
68+
? [
69+
{
70+
label: placeholder,
71+
value: "",
72+
disabled: true
73+
},
74+
...options
75+
]
76+
: options;
77+
return (
78+
<div
79+
className={cx(
80+
fr.cx(
81+
"fr-select-group",
82+
disabled && "fr-select-group--disabled",
83+
(() => {
84+
switch (state) {
85+
case "error":
86+
return "fr-select-group--error";
87+
case "success":
88+
return "fr-select-group--valid";
89+
case "default":
90+
return undefined;
91+
}
92+
assert<Equals<typeof state, never>>(false);
93+
})()
94+
),
95+
className
96+
)}
97+
ref={ref}
98+
style={style}
99+
{...rest}
100+
>
101+
<label className={fr.cx("fr-label")} htmlFor={selectId}>
102+
{label}
103+
{hint !== undefined && <span className={fr.cx("fr-hint-text")}>{hint}</span>}
104+
</label>
105+
<select
106+
{...nativeSelectProps}
107+
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
108+
id={selectId}
109+
aria-describedby={stateDescriptionId}
110+
disabled={disabled}
111+
>
112+
{displayedOptions.map(option => (
113+
<option {...option}>{option.label}</option>
114+
))}
115+
</select>
116+
{state !== "default" && (
117+
<p
118+
id={stateDescriptionId}
119+
className={fr.cx(
120+
(() => {
121+
switch (state) {
122+
case "error":
123+
return "fr-error-text";
124+
case "success":
125+
return "fr-valid-text";
126+
}
127+
assert<Equals<typeof state, never>>(false);
128+
})()
94129
)}
95-
ref={ref}
96-
style={style}
97-
{...rest}
98130
>
99-
<label className={fr.cx("fr-label")} htmlFor={selectId}>
100-
{label}
101-
{hint !== undefined && (
102-
<span className={fr.cx("fr-hint-text")}>{hint}</span>
103-
)}
104-
</label>
105-
<select
106-
{...nativeSelectProps}
107-
className={cx(fr.cx("fr-select"), nativeSelectProps?.className)}
108-
id={selectId}
109-
aria-describedby={stateDescriptionId}
110-
disabled={disabled}
111-
>
112-
{displayedOptions.map(option => (
113-
<option {...option}>{option.label}</option>
114-
))}
115-
</select>
116-
{state !== "default" && (
117-
<p
118-
id={stateDescriptionId}
119-
className={fr.cx(
120-
(() => {
121-
switch (state) {
122-
case "error":
123-
return "fr-error-text";
124-
case "success":
125-
return "fr-valid-text";
126-
}
127-
assert<Equals<typeof state, never>>(false);
128-
})()
129-
)}
130-
>
131-
{stateRelatedMessage}
132-
</p>
133-
)}
134-
</div>
135-
);
136-
}
137-
)
138-
);
131+
{stateRelatedMessage}
132+
</p>
133+
)}
134+
</div>
135+
);
136+
};
139137

140138
Select.displayName = symToStr({ Select });
141139

142-
export default Select;
140+
const ForwardedSelect = forwardRef(Select) as <T extends GenericOption<DefaultOptionValue>[]>(
141+
props: SelectProps<T> & { ref?: ForwardedRef<HTMLDivElement> }
142+
) => ReturnType<typeof Select>;
143+
144+
const MemoizedSelect = memo(ForwardedSelect) as typeof ForwardedSelect;
145+
146+
export default MemoizedSelect;

stories/Select.stories.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Select, type SelectProps, type GenericOption } from "../dist/Select";
1+
import Select, { type SelectProps, type GenericOption } from "../dist/Select";
22
import { sectionName } from "./sectionName";
33
import { getStoryFactory } from "./getStory";
44
import { assert } from "tsafe/assert";
@@ -133,27 +133,22 @@ const defaultOptions = [
133133
}
134134
];
135135

136-
type MyFakeValueSet =
137-
| "dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf"
138-
| "1bda4f79-a199-40ce-985b-fa217809d568"
139-
| "e91b2cac-48f6-4d60-b86f-ece02f076837"
140-
| "66a9d7ac-9b25-4e52-9de3-4b7238135b39";
141-
142-
const myFakeValueSet: MyFakeValueSet[] = [
136+
const myFakeValueSet = [
143137
"dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf",
144138
"1bda4f79-a199-40ce-985b-fa217809d568",
145139
"e91b2cac-48f6-4d60-b86f-ece02f076837",
146140
"66a9d7ac-9b25-4e52-9de3-4b7238135b39"
147-
];
141+
] as const;
148142

149-
const optionsWithTypedValues: GenericOption<MyFakeValueSet>[] = myFakeValueSet.map(fakeValue => ({
150-
value: fakeValue,
151-
label: fakeValue
152-
}));
143+
const optionsWithTypedValues: GenericOption<typeof myFakeValueSet[number]>[] = myFakeValueSet.map(
144+
fakeValue => ({
145+
value: fakeValue,
146+
label: fakeValue
147+
})
148+
);
153149

154150
export const Default = getStory({
155151
"label": "Label pour liste déroulante",
156-
"nativeSelectProps": {},
157152
"options": defaultOptions
158153
});
159154

@@ -170,7 +165,6 @@ export const ErrorState = getStory({
170165
"label": "Label pour liste déroulante",
171166
"state": "error",
172167
"stateRelatedMessage": "Texte d’erreur obligatoire",
173-
"nativeSelectProps": {},
174168
"options": defaultOptions
175169
});
176170

@@ -210,6 +204,7 @@ export const TypedSelect = getStory({
210204
"placeholder": "Sélectionnez une option",
211205
"options": optionsWithTypedValues,
212206
"nativeSelectProps": {
213-
"defaultValue": "dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf"
207+
"defaultValue": "dc9d15ee-7794-470e-9dcf-a8d1dd1a6fcf",
208+
"value": "dc9ee-7794-470e-9dcf-a8d1dd1a6fcf"
214209
}
215210
});

0 commit comments

Comments
 (0)