@@ -19,7 +19,7 @@ import { Input } from './Input';
19
19
import { SvgIcon } from './SvgIcon' ;
20
20
import { Text } from './Text' ;
21
21
22
- export type ComboboxItem = {
22
+ export type ComboboxOption = {
23
23
group ?: string ;
24
24
hidden ?: boolean ;
25
25
label ?: string ;
@@ -34,7 +34,10 @@ type Props = {
34
34
error ?: string ;
35
35
icon ?: ReactElement ;
36
36
infoText ?: string ;
37
- items : ComboboxItem [ ] ;
37
+ /**
38
+ * @deprecated please use "options" prop instead
39
+ */
40
+ items : ComboboxOption [ ] ;
38
41
label : string ;
39
42
maxDropdownHeight ?: string ;
40
43
name : string ;
@@ -43,6 +46,7 @@ type Props = {
43
46
onChange : ( value : string | null ) => any ;
44
47
onCreateItem ?: ( inputText : string ) => any ;
45
48
onEditItem ?: ( selectedItemValue : string ) => any ;
49
+ options : ComboboxOption [ ] ;
46
50
placeholder ?: string ;
47
51
style ?: CSSProperties ;
48
52
success ?: string ;
@@ -51,33 +55,37 @@ type Props = {
51
55
52
56
export const Combobox = forwardRef < HTMLInputElement , Props > (
53
57
( { allowCustomInput, allowNone, noneLabel, ...props } , ref ) => {
54
- const noneItem = useMemo ( ( ) => {
55
- const item : ComboboxItem = { label : noneLabel || 'None' , value : '__NONE__' } ;
56
- return item ;
58
+ if ( props . items ) {
59
+ props . options = props . items ;
60
+ }
61
+
62
+ const noneOption = useMemo ( ( ) => {
63
+ const option : ComboboxOption = { label : noneLabel || 'None' , value : '__NONE__' } ;
64
+ return option ;
57
65
} , [ noneLabel ] ) ;
58
66
59
67
const inputValue = useRef < string > ( '' ) ;
60
68
const internalCurrentValue = useRef < string | null | undefined > ( undefined ) ;
61
69
const internalCurrentValueBeforeFocus = useRef < string | null | undefined > ( undefined ) ;
62
70
const onBlurTimeout = useRef < NodeJS . Timeout > ( ) ;
63
- const [ filteredItems , setFilteredItems ] = useState ( allowNone ? [ noneItem , ...props . items ] : props . items ) ;
64
- const defaultSelectedItem = props . items . find ( ( item ) => item . value === props . value ) ;
65
- const forceOverrideClosed = allowCustomInput && filteredItems . length === 0 ;
66
- const debouncedItems = useDebouncedValue ( props . items , 25 ) ; // This debounce avoids race condition where prop.items updates before props.value
71
+ const [ filteredOptions , setFilteredItems ] = useState ( allowNone ? [ noneOption , ...props . options ] : props . options ) ;
72
+ const defaultSelectedItem = props . options . find ( ( o ) => o . value === props . value ) ;
73
+ const forceOverrideClosed = allowCustomInput && filteredOptions . length === 0 ;
74
+ const debouncedOptions = useDebouncedValue ( props . options , 25 ) ; // This debounce avoids race condition where prop.items updates before props.value
67
75
68
76
const { selectItem, setInputValue, ...combobox } = useCombobox ( {
69
77
id : props . name ,
70
78
defaultSelectedItem,
71
- items : filteredItems ,
79
+ items : filteredOptions ,
72
80
itemToString ( item ) {
73
81
return item ? ( item . label ?? item . value . toString ( ) ) : '' ;
74
82
} ,
75
83
onInputValueChange ( event ) {
76
84
const query = event . inputValue ?. toLowerCase ( ) ?? '' ;
77
- const items = allowNone ? [ noneItem , ...props . items ] : props . items ;
78
- const results = items . filter ( ( item ) => {
79
- if ( item . hidden ) return false ;
80
- const label = ( item . label ?? item . value ) . toString ( ) . toLowerCase ( ) ;
85
+ const options = allowNone ? [ noneOption , ...props . options ] : props . options ;
86
+ const results = options . filter ( ( o ) => {
87
+ if ( o . hidden ) return false ;
88
+ const label = ( o . label ?? o . value ) . toString ( ) . toLowerCase ( ) ;
81
89
return label . includes ( query ) ;
82
90
} ) ;
83
91
setFilteredItems ( results ) ;
@@ -142,17 +150,17 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
142
150
const comboboxInputRef = ( combobox . getInputProps ( ) as any ) . ref ; // This value actually exists, the types are wrong
143
151
144
152
useEffect ( ( ) => {
145
- const items = allowNone ? [ noneItem , ...props . items ] : props . items ;
146
- setFilteredItems ( items . filter ( ( item ) => ! item . hidden ) ) ;
147
- } , [ allowNone , noneItem , props . items ] ) ;
153
+ const options = allowNone ? [ noneOption , ...props . options ] : props . options ;
154
+ setFilteredItems ( options . filter ( ( o ) => ! o . hidden ) ) ;
155
+ } , [ allowNone , noneOption , props . options ] ) ;
148
156
149
157
useEffect ( ( ) => {
150
- const selected = debouncedItems . find ( ( item ) => item . value === props . value ) ;
158
+ const selected = debouncedOptions . find ( ( o ) => o . value === props . value ) ;
151
159
152
160
if ( props . value !== internalCurrentValue . current ) {
153
161
if ( props . value === null && allowNone ) {
154
162
internalCurrentValue . current = null ;
155
- selectItem ( noneItem ) ;
163
+ selectItem ( noneOption ) ;
156
164
} else {
157
165
if ( selected ) {
158
166
internalCurrentValue . current = selected . value ;
@@ -166,14 +174,14 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
166
174
}
167
175
}
168
176
} else if ( props . value !== null ) {
169
- setInputValue ( selected ?. label ?? selected ?. value . toString ( ) ?? '' ) ; // In the case of the selected item 's label being updated, we need to update the input
177
+ setInputValue ( selected ?. label ?? selected ?. value . toString ( ) ?? '' ) ; // In the case of the selected option 's label being updated, we need to update the input
170
178
}
171
- } , [ allowCustomInput , allowNone , noneItem , selectItem , props . value , debouncedItems , setInputValue ] ) ;
179
+ } , [ allowCustomInput , allowNone , noneOption , selectItem , props . value , debouncedOptions , setInputValue ] ) ;
172
180
173
- const shouldRenderGroupLabel = ( item : ComboboxItem , index : number ) => {
174
- const previousItem = filteredItems [ Math . max ( 0 , index - 1 ) ] ;
175
- if ( item . group && index === 0 ) return true ;
176
- return item . group && item . group !== previousItem ?. group ;
181
+ const shouldRenderGroupLabel = ( option : ComboboxOption , index : number ) => {
182
+ const previousItem = filteredOptions [ Math . max ( 0 , index - 1 ) ] ;
183
+ if ( option . group && index === 0 ) return true ;
184
+ return option . group && option . group !== previousItem ?. group ;
177
185
} ;
178
186
179
187
return (
@@ -211,35 +219,35 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
211
219
</ li >
212
220
) }
213
221
214
- { filteredItems . map ( ( item , index ) => (
215
- < Fragment key = { item . value } >
216
- { shouldRenderGroupLabel ( item , index ) && (
222
+ { filteredOptions . map ( ( option , index ) => (
223
+ < Fragment key = { option . value } >
224
+ { shouldRenderGroupLabel ( option , index ) && (
217
225
< li className = { s . dropdownGroupLabel } >
218
- < Text as = "h5" > { item . group } </ Text >
226
+ < Text as = "h5" > { option . group } </ Text >
219
227
</ li >
220
228
) }
221
229
222
230
< li
223
231
className = { s . dropdownItem }
224
232
data-highlighted = { combobox . highlightedIndex === index }
225
- data-selected = { combobox . selectedItem ?. value === item . value }
226
- { ...combobox . getItemProps ( { item, index } ) }
233
+ data-selected = { combobox . selectedItem ?. value === option . value }
234
+ { ...combobox . getItemProps ( { item : option , index } ) }
227
235
>
228
- { combobox . selectedItem ?. value === item . value ? (
236
+ { combobox . selectedItem ?. value === option . value ? (
229
237
< SvgIcon icon = { < CheckCircle weight = "duotone" /> } color = "green-9" />
230
238
) : (
231
239
< SvgIcon icon = { < Circle weight = "duotone" /> } color = "sand-10" />
232
240
) }
233
241
234
- { item . label ?? item . value }
242
+ { option . label ?? option . value }
235
243
</ li >
236
244
</ Fragment >
237
245
) ) }
238
246
239
- { filteredItems . length === 0 && (
247
+ { filteredOptions . length === 0 && (
240
248
< li className = { s . content } >
241
249
< Text size = "text-s" >
242
- { props . items . length === 0
250
+ { props . options . length === 0
243
251
? 'No available options.'
244
252
: 'No matching options. Try a different search?' }
245
253
</ Text >
@@ -257,13 +265,13 @@ export const Combobox = forwardRef<HTMLInputElement, Props>(
257
265
) ;
258
266
Combobox . displayName = 'Combobox' ;
259
267
260
- export function useComboboxItemMapper < T extends unknown [ ] > (
268
+ export function useComboboxOptionMapper < T extends unknown [ ] > (
261
269
array : T | undefined ,
262
- mapItem : ( item : T [ number ] ) => ComboboxItem | ComboboxItem [ ] | null ,
270
+ mapper : ( item : T [ number ] ) => ComboboxOption | ComboboxOption [ ] | null ,
263
271
dependencies ?: unknown [ ] ,
264
272
) {
265
273
const options = useMemo ( ( ) => {
266
- return ( array ?. flatMap ( mapItem ) ?? [ ] ) . filter ( ( value ) => ! ! value ) as ComboboxItem [ ] ;
274
+ return ( array ?. flatMap ( mapper ) ?? [ ] ) . filter ( ( value ) => ! ! value ) as ComboboxOption [ ] ;
267
275
// eslint-disable-next-line react-hooks/exhaustive-deps
268
276
} , [ array , ...( dependencies ?? [ ] ) ] ) ;
269
277
0 commit comments