@@ -15,18 +15,20 @@ import {
1515} from '@patternfly/react-core' ;
1616import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon' ;
1717
18- export interface TypeaheadSelectOption extends Omit < SelectOptionProps , 'content' > {
18+ export interface TypeaheadSelectOption extends Omit < SelectOptionProps , 'content' | 'isSelected' > {
1919 /** Content of the select option. */
2020 content : string | number ;
2121 /** Value of the select option. */
2222 value : string | number ;
23+ /** Indicator for option being selected */
24+ isSelected ?: boolean ;
2325}
2426
2527export interface TypeaheadSelectProps extends Omit < SelectProps , 'toggle' | 'onSelect' > {
2628 /** @hide Forwarded ref */
2729 innerRef ?: React . Ref < any > ;
28- /** Initial options of the select. */
29- initialOptions : TypeaheadSelectOption [ ] ;
30+ /** Options of the select */
31+ selectOptions : TypeaheadSelectOption [ ] ;
3032 /** Callback triggered on selection. */
3133 onSelect ?: (
3234 _event : React . MouseEvent < Element , MouseEvent > | React . KeyboardEvent < HTMLInputElement > | undefined ,
@@ -36,6 +38,8 @@ export interface TypeaheadSelectProps extends Omit<SelectProps, 'toggle' | 'onSe
3638 onToggle ?: ( nextIsOpen : boolean ) => void ;
3739 /** Callback triggered when the text in the input field changes. */
3840 onInputChange ?: ( newValue : string ) => void ;
41+ /** Function to return items matching the current filter value */
42+ filterFunction ?: ( filterValue : string , options : TypeaheadSelectOption [ ] ) => TypeaheadSelectOption [ ] ;
3943 /** Callback triggered when the clear button is selected */
4044 onClearSelection ?: ( ) => void ;
4145 /** Placeholder text for the select input. */
@@ -61,12 +65,16 @@ export interface TypeaheadSelectProps extends Omit<SelectProps, 'toggle' | 'onSe
6165const defaultNoOptionsFoundMessage = ( filter : string ) => `No results found for "${ filter } "` ;
6266const defaultCreateOptionMessage = ( newValue : string ) => `Create "${ newValue } "` ;
6367
68+ const defaultFilterFunction = ( filterValue : string , options : TypeaheadSelectOption [ ] ) =>
69+ options . filter ( ( o ) => String ( o . content ) . toLowerCase ( ) . includes ( filterValue . toLowerCase ( ) ) ) ;
70+
6471export const TypeaheadSelectBase : React . FunctionComponent < TypeaheadSelectProps > = ( {
6572 innerRef,
66- initialOptions ,
73+ selectOptions ,
6774 onSelect,
6875 onToggle,
6976 onInputChange,
77+ filterFunction = defaultFilterFunction ,
7078 onClearSelection,
7179 placeholder = 'Select an option' ,
7280 noOptionsAvailableMessage = 'No options are available' ,
@@ -80,31 +88,30 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
8088 ...props
8189} : TypeaheadSelectProps ) => {
8290 const [ isOpen , setIsOpen ] = React . useState ( false ) ;
83- const [ selected , setSelected ] = React . useState < string > ( String ( initialOptions . find ( ( o ) => o . selected ) ?. content ?? '' ) ) ;
84- const [ inputValue , setInputValue ] = React . useState < string > (
85- String ( initialOptions . find ( ( o ) => o . selected ) ?. content ?? '' )
86- ) ;
8791 const [ filterValue , setFilterValue ] = React . useState < string > ( '' ) ;
88- const [ selectOptions , setSelectOptions ] = React . useState < TypeaheadSelectOption [ ] > ( initialOptions ) ;
92+ const [ isFiltering , setIsFiltering ] = React . useState < boolean > ( false ) ;
8993 const [ focusedItemIndex , setFocusedItemIndex ] = React . useState < number | null > ( null ) ;
9094 const [ activeItemId , setActiveItemId ] = React . useState < string | null > ( null ) ;
9195 const textInputRef = React . useRef < HTMLInputElement > ( ) ;
9296
9397 const NO_RESULTS = 'no results' ;
9498
95- React . useEffect ( ( ) => {
96- let newSelectOptions : TypeaheadSelectOption [ ] = initialOptions ;
99+ const selected = React . useMemo (
100+ ( ) => selectOptions ?. find ( ( option ) => option . value === props . selected || option . isSelected ) ,
101+ [ props . selected , selectOptions ]
102+ ) ;
103+
104+ const filteredSelections = React . useMemo ( ( ) => {
105+ let newSelectOptions : TypeaheadSelectOption [ ] = selectOptions ;
97106
98107 // Filter menu items based on the text input value when one exists
99- if ( filterValue ) {
100- newSelectOptions = initialOptions . filter ( ( option ) =>
101- String ( option . content ) . toLowerCase ( ) . includes ( filterValue . toLowerCase ( ) )
102- ) ;
108+ if ( isFiltering && filterValue ) {
109+ newSelectOptions = filterFunction ( filterValue , selectOptions ) ;
103110
104111 if (
105112 isCreatable &&
106- filterValue &&
107- ! initialOptions . find ( ( o ) => String ( o . content ) . toLowerCase ( ) === filterValue . toLowerCase ( ) )
113+ filterValue . trim ( ) &&
114+ ! newSelectOptions . find ( ( o ) => String ( o . content ) . toLowerCase ( ) === filterValue . toLowerCase ( ) )
108115 ) {
109116 const createOption = {
110117 content : typeof createOptionMessage === 'string' ? createOptionMessage : createOptionMessage ( filterValue ) ,
@@ -126,9 +133,6 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
126133 }
127134 ] ;
128135 }
129-
130- // Open the menu when the input value changes and the new value is not empty
131- openMenu ( ) ;
132136 }
133137
134138 // When no options are available, display 'No options available'
@@ -142,10 +146,12 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
142146 ] ;
143147 }
144148
145- setSelectOptions ( newSelectOptions ) ;
149+ return newSelectOptions ;
146150 } , [
151+ isFiltering ,
147152 filterValue ,
148- initialOptions ,
153+ filterFunction ,
154+ selectOptions ,
149155 noOptionsFoundMessage ,
150156 isCreatable ,
151157 isCreateOptionOnTop ,
@@ -154,14 +160,12 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
154160 ] ) ;
155161
156162 React . useEffect ( ( ) => {
157- // If the selected option changed and the current input value is the previously selected item, update the displayed value.
158- const selectedOption = initialOptions . find ( ( o ) => o . selected ) ;
159- if ( inputValue === selected && selectedOption ?. value !== selected ) {
160- setInputValue ( String ( selectedOption ?. content ?? '' ) ) ;
163+ if ( isFiltering ) {
164+ openMenu ( ) ;
161165 }
162- // Only update when options change
166+ // Don't update on openMenu changes
163167 // eslint-disable-next-line react-hooks/exhaustive-deps
164- } , [ initialOptions ] ) ;
168+ } , [ isFiltering ] ) ;
165169
166170 const setActiveAndFocusedItem = ( itemIndex : number ) => {
167171 setFocusedItemIndex ( itemIndex ) ;
@@ -178,23 +182,24 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
178182 if ( ! isOpen ) {
179183 onToggle && onToggle ( true ) ;
180184 setIsOpen ( true ) ;
185+ setTimeout ( ( ) => {
186+ textInputRef . current ?. focus ( ) ;
187+ } , 100 ) ;
181188 }
182189 } ;
183190
184191 const closeMenu = ( ) => {
185192 onToggle && onToggle ( false ) ;
186193 setIsOpen ( false ) ;
187194 resetActiveAndFocusedItem ( ) ;
188- const option = initialOptions . find ( ( o ) => o . value === selected ) ;
189- if ( option ) {
190- setInputValue ( String ( option . content ) ) ;
191- }
195+ setIsFiltering ( false ) ;
196+ setFilterValue ( String ( selected ?. content ?? '' ) ) ;
192197 } ;
193198
194199 const onInputClick = ( ) => {
195200 if ( ! isOpen ) {
196201 openMenu ( ) ;
197- } else if ( ! inputValue ) {
202+ } else if ( isFiltering ) {
198203 closeMenu ( ) ;
199204 }
200205 } ;
@@ -204,25 +209,24 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
204209 option : TypeaheadSelectOption
205210 ) => {
206211 onSelect && onSelect ( _event , option . value ) ;
207-
208- setInputValue ( String ( option . content ) ) ;
209- setFilterValue ( '' ) ;
210- setSelected ( String ( option . value ) ) ;
211-
212212 closeMenu ( ) ;
213213 } ;
214214
215215 const _onSelect = ( _event : React . MouseEvent < Element , MouseEvent > | undefined , value : string | number | undefined ) => {
216216 if ( value && value !== NO_RESULTS ) {
217217 const optionToSelect = selectOptions . find ( ( option ) => option . value === value ) ;
218- selectOption ( _event , optionToSelect ) ;
218+ if ( optionToSelect ) {
219+ selectOption ( _event , optionToSelect ) ;
220+ } else if ( isCreatable ) {
221+ selectOption ( _event , { value, content : value } ) ;
222+ }
219223 }
220224 } ;
221225
222226 const onTextInputChange = ( _event : React . FormEvent < HTMLInputElement > , value : string ) => {
223- setInputValue ( value ) ;
227+ setIsFiltering ( true ) ;
228+ setFilterValue ( value || '' ) ;
224229 onInputChange && onInputChange ( value ) ;
225- setFilterValue ( value ) ;
226230
227231 resetActiveAndFocusedItem ( ) ;
228232 } ;
@@ -232,39 +236,39 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
232236
233237 openMenu ( ) ;
234238
235- if ( selectOptions . every ( ( option ) => option . isDisabled ) ) {
239+ if ( filteredSelections . every ( ( option ) => option . isDisabled ) ) {
236240 return ;
237241 }
238242
239243 if ( key === 'ArrowUp' ) {
240244 // When no index is set or at the first index, focus to the last, otherwise decrement focus index
241245 if ( focusedItemIndex === null || focusedItemIndex === 0 ) {
242- indexToFocus = selectOptions . length - 1 ;
246+ indexToFocus = filteredSelections . length - 1 ;
243247 } else {
244248 indexToFocus = focusedItemIndex - 1 ;
245249 }
246250
247251 // Skip disabled options
248- while ( selectOptions [ indexToFocus ] . isDisabled ) {
252+ while ( filteredSelections [ indexToFocus ] . isDisabled ) {
249253 indexToFocus -- ;
250254 if ( indexToFocus === - 1 ) {
251- indexToFocus = selectOptions . length - 1 ;
255+ indexToFocus = filteredSelections . length - 1 ;
252256 }
253257 }
254258 }
255259
256260 if ( key === 'ArrowDown' ) {
257261 // When no index is set or at the last index, focus to the first, otherwise increment focus index
258- if ( focusedItemIndex === null || focusedItemIndex === selectOptions . length - 1 ) {
262+ if ( focusedItemIndex === null || focusedItemIndex === filteredSelections . length - 1 ) {
259263 indexToFocus = 0 ;
260264 } else {
261265 indexToFocus = focusedItemIndex + 1 ;
262266 }
263267
264268 // Skip disabled options
265- while ( selectOptions [ indexToFocus ] . isDisabled ) {
269+ while ( filteredSelections [ indexToFocus ] . isDisabled ) {
266270 indexToFocus ++ ;
267- if ( indexToFocus === selectOptions . length ) {
271+ if ( indexToFocus === filteredSelections . length ) {
268272 indexToFocus = 0 ;
269273 }
270274 }
@@ -274,7 +278,7 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
274278 } ;
275279
276280 const onInputKeyDown = ( event : React . KeyboardEvent < HTMLInputElement > ) => {
277- const focusedItem = focusedItemIndex !== null ? selectOptions [ focusedItemIndex ] : null ;
281+ const focusedItem = focusedItemIndex !== null ? filteredSelections [ focusedItemIndex ] : null ;
278282
279283 switch ( event . key ) {
280284 case 'Enter' :
@@ -294,16 +298,21 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
294298 } ;
295299
296300 const onToggleClick = ( ) => {
297- onToggle && onToggle ( ! isOpen ) ;
298- setIsOpen ( ! isOpen ) ;
301+ if ( ! isOpen ) {
302+ openMenu ( ) ;
303+ } else {
304+ closeMenu ( ) ;
305+ }
299306 textInputRef . current ?. focus ( ) ;
300307 } ;
301308
302309 const onClearButtonClick = ( ) => {
303- setSelected ( '' ) ;
304- setInputValue ( '' ) ;
305- onInputChange && onInputChange ( '' ) ;
310+ if ( selected && onSelect ) {
311+ onSelect ( undefined , selected . value ) ;
312+ }
306313 setFilterValue ( '' ) ;
314+ onInputChange && onInputChange ( '' ) ;
315+ setIsFiltering ( false ) ;
307316 resetActiveAndFocusedItem ( ) ;
308317 textInputRef . current ?. focus ( ) ;
309318 onClearSelection && onClearSelection ( ) ;
@@ -327,7 +336,7 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
327336 >
328337 < TextInputGroup isPlain >
329338 < TextInputGroupMain
330- value = { inputValue }
339+ value = { isFiltering ? filterValue : ( selected ?. content ?? '' ) }
331340 onClick = { onInputClick }
332341 onChange = { onTextInputChange }
333342 onKeyDown = { onInputKeyDown }
@@ -339,8 +348,9 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
339348 isExpanded = { isOpen }
340349 aria-controls = "select-typeahead-listbox"
341350 />
342-
343- < TextInputGroupUtilities { ...( ! inputValue ? { style : { display : 'none' } } : { } ) } >
351+ < TextInputGroupUtilities
352+ { ...( ! ( isFiltering && filterValue ) && ! selected ? { style : { display : 'none' } } : { } ) }
353+ >
344354 < Button variant = "plain" onClick = { onClearButtonClick } aria-label = "Clear input value" >
345355 < TimesIcon aria-hidden />
346356 </ Button >
@@ -354,16 +364,14 @@ export const TypeaheadSelectBase: React.FunctionComponent<TypeaheadSelectProps>
354364 isOpen = { isOpen }
355365 selected = { selected }
356366 onSelect = { _onSelect }
357- onOpenChange = { ( isOpen ) => {
358- ! isOpen && closeMenu ( ) ;
359- } }
367+ onOpenChange = { ( isOpen ) => ! isOpen && closeMenu ( ) }
360368 toggle = { toggle }
361369 shouldFocusFirstItemOnOpen = { false }
362370 ref = { innerRef }
363371 { ...props }
364372 >
365373 < SelectList >
366- { selectOptions . map ( ( option , index ) => {
374+ { filteredSelections . map ( ( option , index ) => {
367375 const { content, value, ...props } = option ;
368376
369377 return (
0 commit comments