@@ -5,44 +5,47 @@ import * as ScrollPrimitive from '@radix-ui/react-scroll-area';
5
5
import * as SelectPrimitive from '@radix-ui/react-select' ;
6
6
import classNames from 'classnames' ;
7
7
import { useEffect , useId , useMemo , useState } from 'react' ;
8
- import type { FC } from 'react' ;
8
+ import type { ReactElement , ReactNode } from 'react' ;
9
9
10
10
import Skeleton from '@/components/Common/Skeleton' ;
11
11
import type { FormattedMessage } from '@/types' ;
12
12
13
13
import styles from './index.module.css' ;
14
14
15
- type SelectValue = {
16
- label : FormattedMessage ;
17
- value : string ;
18
- iconImage ?: React . ReactNode ;
15
+ export type SelectValue < T extends string > = {
16
+ label : FormattedMessage | string ;
17
+ value : T ;
18
+ iconImage ?: ReactElement < SVGSVGElement > ;
19
19
disabled ?: boolean ;
20
20
} ;
21
21
22
- type SelectGroup = {
23
- label ?: FormattedMessage ;
24
- items : Array < SelectValue > ;
22
+ export type SelectGroup < T extends string > = {
23
+ label ?: FormattedMessage | string ;
24
+ items : Array < SelectValue < T > > ;
25
25
} ;
26
26
27
27
const isStringArray = ( values : Array < unknown > ) : values is Array < string > =>
28
28
Boolean ( values [ 0 ] && typeof values [ 0 ] === 'string' ) ;
29
29
30
- const isValuesArray = ( values : Array < unknown > ) : values is Array < SelectValue > =>
30
+ const isValuesArray = < T extends string > (
31
+ values : Array < unknown >
32
+ ) : values is Array < SelectValue < T > > =>
31
33
Boolean ( values [ 0 ] && typeof values [ 0 ] === 'object' && 'value' in values [ 0 ] ) ;
32
34
33
- type SelectProps = {
34
- values : Array < SelectGroup | string | SelectValue > ;
35
- defaultValue ?: string ;
35
+ type SelectProps < T extends string > = {
36
+ values : Array < SelectGroup < T > > | Array < T > | Array < SelectValue < T > > ;
37
+ defaultValue ?: T ;
36
38
placeholder ?: string ;
37
39
label ?: string ;
38
40
inline ?: boolean ;
39
- onChange ?: ( value : string ) => void ;
41
+ onChange ?: ( value : T ) => void ;
40
42
className ?: string ;
41
43
ariaLabel ?: string ;
42
44
loading ?: boolean ;
45
+ disabled ?: boolean ;
43
46
} ;
44
47
45
- const Select : FC < SelectProps > = ( {
48
+ const Select = < T extends string > ( {
46
49
values = [ ] ,
47
50
defaultValue,
48
51
placeholder,
@@ -52,7 +55,8 @@ const Select: FC<SelectProps> = ({
52
55
className,
53
56
ariaLabel,
54
57
loading = false ,
55
- } ) => {
58
+ disabled = false ,
59
+ } : SelectProps < T > ) : ReactNode => {
56
60
const id = useId ( ) ;
57
61
const [ value , setValue ] = useState ( defaultValue ) ;
58
62
@@ -69,7 +73,7 @@ const Select: FC<SelectProps> = ({
69
73
return [ { items : mappedValues } ] ;
70
74
}
71
75
72
- return mappedValues as Array < SelectGroup > ;
76
+ return mappedValues as Array < SelectGroup < T > > ;
73
77
} , [ values ] ) ;
74
78
75
79
// We render the actual item slotted to fix/prevent the issue
@@ -82,8 +86,39 @@ const Select: FC<SelectProps> = ({
82
86
[ mappedValues , value ]
83
87
) ;
84
88
89
+ const memoizedMappedValues = useMemo ( ( ) => {
90
+ return mappedValues . map ( ( { label, items } , key ) => (
91
+ < SelectPrimitive . Group key = { label ?. toString ( ) ?? key } >
92
+ { label && (
93
+ < SelectPrimitive . Label
94
+ className = { classNames ( styles . item , styles . label ) }
95
+ >
96
+ { label }
97
+ </ SelectPrimitive . Label >
98
+ ) }
99
+
100
+ { items . map ( ( { value, label, iconImage, disabled } ) => (
101
+ < SelectPrimitive . Item
102
+ key = { value }
103
+ value = { value }
104
+ disabled = { disabled }
105
+ className = { classNames ( styles . item , styles . text ) }
106
+ >
107
+ < SelectPrimitive . ItemText >
108
+ { iconImage }
109
+ < span > { label } </ span >
110
+ </ SelectPrimitive . ItemText >
111
+ </ SelectPrimitive . Item >
112
+ ) ) }
113
+ </ SelectPrimitive . Group >
114
+ ) ) ;
115
+ // We explicitly want to recalculate these values only when the values themselves changed
116
+ // This is to prevent re-rendering and re-calcukating the values on every render
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ } , [ JSON . stringify ( values ) ] ) ;
119
+
85
120
// Both change the internal state and emit the change event
86
- const handleChange = ( value : string ) => {
121
+ const handleChange = ( value : T ) => {
87
122
setValue ( value ) ;
88
123
89
124
if ( typeof onChange === 'function' ) {
@@ -106,15 +141,23 @@ const Select: FC<SelectProps> = ({
106
141
</ label >
107
142
) }
108
143
109
- < SelectPrimitive . Root value = { value } onValueChange = { handleChange } >
144
+ < SelectPrimitive . Root
145
+ value = { currentItem !== undefined ? value : undefined }
146
+ onValueChange = { handleChange }
147
+ disabled = { disabled }
148
+ >
110
149
< SelectPrimitive . Trigger
111
150
className = { styles . trigger }
112
151
aria-label = { ariaLabel }
113
152
id = { id }
114
153
>
115
154
< SelectPrimitive . Value placeholder = { placeholder } >
116
- { currentItem ?. iconImage }
117
- < span > { currentItem ?. label } </ span >
155
+ { currentItem !== undefined && (
156
+ < >
157
+ { currentItem . iconImage }
158
+ < span > { currentItem . label } </ span >
159
+ </ >
160
+ ) }
118
161
</ SelectPrimitive . Value >
119
162
< ChevronDownIcon className = { styles . icon } />
120
163
</ SelectPrimitive . Trigger >
@@ -129,31 +172,7 @@ const Select: FC<SelectProps> = ({
129
172
< ScrollPrimitive . Root type = "auto" >
130
173
< SelectPrimitive . Viewport >
131
174
< ScrollPrimitive . Viewport >
132
- { mappedValues . map ( ( { label, items } , key ) => (
133
- < SelectPrimitive . Group key = { label ?. toString ( ) ?? key } >
134
- { label && (
135
- < SelectPrimitive . Label
136
- className = { classNames ( styles . item , styles . label ) }
137
- >
138
- { label }
139
- </ SelectPrimitive . Label >
140
- ) }
141
-
142
- { items . map ( ( { value, label, iconImage, disabled } ) => (
143
- < SelectPrimitive . Item
144
- key = { value }
145
- value = { value }
146
- disabled = { disabled }
147
- className = { classNames ( styles . item , styles . text ) }
148
- >
149
- < SelectPrimitive . ItemText >
150
- { iconImage }
151
- < span > { label } </ span >
152
- </ SelectPrimitive . ItemText >
153
- </ SelectPrimitive . Item >
154
- ) ) }
155
- </ SelectPrimitive . Group >
156
- ) ) }
175
+ { memoizedMappedValues }
157
176
</ ScrollPrimitive . Viewport >
158
177
</ SelectPrimitive . Viewport >
159
178
< ScrollPrimitive . Scrollbar orientation = "vertical" >
0 commit comments