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" ;
44import { symToStr } from "tsafe/symToStr" ;
55import { assert } from "tsafe/assert" ;
66import type { Equals } from "tsafe" ;
77import { fr } from "./fr" ;
88import { 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} ;
2832export 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
140138Select . 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 ;
0 commit comments