@@ -5,44 +5,47 @@ import * as ScrollPrimitive from '@radix-ui/react-scroll-area';
55import * as SelectPrimitive from '@radix-ui/react-select' ;
66import classNames from 'classnames' ;
77import { useEffect , useId , useMemo , useState } from 'react' ;
8- import type { FC } from 'react' ;
8+ import type { ReactElement , ReactNode } from 'react' ;
99
1010import Skeleton from '@/components/Common/Skeleton' ;
1111import type { FormattedMessage } from '@/types' ;
1212
1313import styles from './index.module.css' ;
1414
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 > ;
1919 disabled ?: boolean ;
2020} ;
2121
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 > > ;
2525} ;
2626
2727const isStringArray = ( values : Array < unknown > ) : values is Array < string > =>
2828 Boolean ( values [ 0 ] && typeof values [ 0 ] === 'string' ) ;
2929
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 > > =>
3133 Boolean ( values [ 0 ] && typeof values [ 0 ] === 'object' && 'value' in values [ 0 ] ) ;
3234
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 ;
3638 placeholder ?: string ;
3739 label ?: string ;
3840 inline ?: boolean ;
39- onChange ?: ( value : string ) => void ;
41+ onChange ?: ( value : T ) => void ;
4042 className ?: string ;
4143 ariaLabel ?: string ;
4244 loading ?: boolean ;
45+ disabled ?: boolean ;
4346} ;
4447
45- const Select : FC < SelectProps > = ( {
48+ const Select = < T extends string > ( {
4649 values = [ ] ,
4750 defaultValue,
4851 placeholder,
@@ -52,7 +55,8 @@ const Select: FC<SelectProps> = ({
5255 className,
5356 ariaLabel,
5457 loading = false ,
55- } ) => {
58+ disabled = false ,
59+ } : SelectProps < T > ) : ReactNode => {
5660 const id = useId ( ) ;
5761 const [ value , setValue ] = useState ( defaultValue ) ;
5862
@@ -69,7 +73,7 @@ const Select: FC<SelectProps> = ({
6973 return [ { items : mappedValues } ] ;
7074 }
7175
72- return mappedValues as Array < SelectGroup > ;
76+ return mappedValues as Array < SelectGroup < T > > ;
7377 } , [ values ] ) ;
7478
7579 // We render the actual item slotted to fix/prevent the issue
@@ -82,8 +86,39 @@ const Select: FC<SelectProps> = ({
8286 [ mappedValues , value ]
8387 ) ;
8488
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+
85120 // Both change the internal state and emit the change event
86- const handleChange = ( value : string ) => {
121+ const handleChange = ( value : T ) => {
87122 setValue ( value ) ;
88123
89124 if ( typeof onChange === 'function' ) {
@@ -106,15 +141,23 @@ const Select: FC<SelectProps> = ({
106141 </ label >
107142 ) }
108143
109- < SelectPrimitive . Root value = { value } onValueChange = { handleChange } >
144+ < SelectPrimitive . Root
145+ value = { currentItem !== undefined ? value : undefined }
146+ onValueChange = { handleChange }
147+ disabled = { disabled }
148+ >
110149 < SelectPrimitive . Trigger
111150 className = { styles . trigger }
112151 aria-label = { ariaLabel }
113152 id = { id }
114153 >
115154 < 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+ ) }
118161 </ SelectPrimitive . Value >
119162 < ChevronDownIcon className = { styles . icon } />
120163 </ SelectPrimitive . Trigger >
@@ -129,31 +172,7 @@ const Select: FC<SelectProps> = ({
129172 < ScrollPrimitive . Root type = "auto" >
130173 < SelectPrimitive . Viewport >
131174 < 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 }
157176 </ ScrollPrimitive . Viewport >
158177 </ SelectPrimitive . Viewport >
159178 < ScrollPrimitive . Scrollbar orientation = "vertical" >
0 commit comments