Skip to content

Commit 3fbebde

Browse files
committed
Release new select as SelectNext
1 parent e2c4b32 commit 3fbebde

File tree

12 files changed

+15600
-371
lines changed

12 files changed

+15600
-371
lines changed

.eslintrc.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ 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"],
76
"rules": {
87
"no-extra-boolean-cast": "off",
98
"@typescript-eslint/explicit-module-boundary-types": "off",
109
"@typescript-eslint/no-explicit-any": "off",
1110
"@typescript-eslint/no-namespace": "off",
1211
"@typescript-eslint/ban-types": "off",
13-
"@typescript-eslint/ban-ts-comment": "off"
12+
"@typescript-eslint/ban-ts-comment": "off",
1413
},
1514

1615
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"./Stepper": "./dist/Stepper.js",
149149
"./SkipLinks": "./dist/SkipLinks.js",
150150
"./SideMenu": "./dist/SideMenu.js",
151+
"./SelectNext": "./dist/SelectNext.js",
151152
"./Select": "./dist/Select.js",
152153
"./SearchBar": "./dist/SearchBar.js",
153154
"./RadioButtons": "./dist/RadioButtons.js",

src/Select.tsx

+88-157
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,113 @@
11
"use client";
22

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";
144
import { symToStr } from "tsafe/symToStr";
155
import { assert } from "tsafe/assert";
166
import type { Equals } from "tsafe";
177
import { fr } from "./fr";
188
import { cx } from "./tools/cx";
19-
import type { FrClassName } from "./fr/generatedFromCss/classNames";
20-
import { createComponentI18nApi } from "./i18n";
219

22-
export type SelectProps<Options extends SelectProps.Option[]> = {
23-
options: Options;
10+
export type SelectProps = {
2411
className?: string;
2512
label: ReactNode;
2613
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;
4419
/** Default: false */
4520
disabled?: boolean;
4621
/** Default: "default" */
47-
state?: SelectProps.State | "default";
22+
state?: "success" | "error" | "default";
4823
/** The message won't be displayed if state is "default" */
4924
stateRelatedMessage?: ReactNode;
5025
style?: CSSProperties;
51-
placeholder?: string;
5226
};
5327

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-
7028
/**
7129
* @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-select>
7230
* */
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}
13174
>
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 });
163112

164113
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

Comments
 (0)