@@ -5,9 +5,11 @@ import SelectionList from '@components/SelectionList';
55import SingleSelectListItem from '@components/SelectionList/SingleSelectListItem' ;
66import type { ListItem } from '@components/SelectionList/types' ;
77import Text from '@components/Text' ;
8+ import useDebouncedState from '@hooks/useDebouncedState' ;
89import useLocalize from '@hooks/useLocalize' ;
910import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
1011import useThemeStyles from '@hooks/useThemeStyles' ;
12+ import useWindowDimensions from '@hooks/useWindowDimensions' ;
1113
1214type SingleSelectItem < T > = {
1315 text : string ;
@@ -29,22 +31,71 @@ type SingleSelectPopupProps<T> = {
2931
3032 /** Function to call when changes are applied */
3133 onChange : ( item : SingleSelectItem < T > | null ) => void ;
34+
35+ /** Whether the search input should be displayed */
36+ isSearchable ?: boolean ;
37+
38+ /** Search input place holder */
39+ searchPlaceholder ?: string ;
3240} ;
3341
34- function SingleSelectPopup < T extends string > ( { label, value, items, closeOverlay, onChange} : SingleSelectPopupProps < T > ) {
42+ function SingleSelectPopup < T extends string > ( { label, value, items, closeOverlay, onChange, isSearchable , searchPlaceholder } : SingleSelectPopupProps < T > ) {
3543 const { translate} = useLocalize ( ) ;
3644 const styles = useThemeStyles ( ) ;
3745 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
3846 const { isSmallScreenWidth} = useResponsiveLayout ( ) ;
47+ const { windowHeight} = useWindowDimensions ( ) ;
3948 const [ selectedItem , setSelectedItem ] = useState ( value ) ;
49+ const [ searchTerm , debouncedSearchTerm , setSearchTerm ] = useDebouncedState ( '' ) ;
50+
51+ const { sections, noResultsFound} = useMemo ( ( ) => {
52+ // If the selection is searchable, we push the initially selected item into its own section and display it at the top
53+ if ( isSearchable ) {
54+ const initiallySelectedItemSection = value ?. text . toLowerCase ( ) . includes ( debouncedSearchTerm ?. toLowerCase ( ) )
55+ ? [ { text : value . text , keyForList : value . value , isSelected : selectedItem ?. value === value . value } ]
56+ : [ ] ;
57+ const remainingItemsSection = items
58+ . filter ( ( item ) => item ?. value !== value ?. value && item ?. text ?. toLowerCase ( ) . includes ( debouncedSearchTerm ?. toLowerCase ( ) ) )
59+ . map ( ( item ) => ( {
60+ text : item . text ,
61+ keyForList : item . value ,
62+ isSelected : selectedItem ?. value === item . value ,
63+ } ) ) ;
64+ const isEmpty = ! initiallySelectedItemSection . length && ! remainingItemsSection . length ;
65+ return {
66+ sections : isEmpty
67+ ? [ ]
68+ : [
69+ {
70+ data : initiallySelectedItemSection ,
71+ shouldShow : initiallySelectedItemSection . length > 0 ,
72+ indexOffset : 0 ,
73+ } ,
74+ {
75+ data : remainingItemsSection ,
76+ shouldShow : remainingItemsSection . length > 0 ,
77+ indexOffset : initiallySelectedItemSection . length ,
78+ } ,
79+ ] ,
80+ noResultsFound : isEmpty ,
81+ } ;
82+ }
83+
84+ return {
85+ sections : [
86+ {
87+ data : items . map ( ( item ) => ( {
88+ text : item . text ,
89+ keyForList : item . value ,
90+ isSelected : item . value === selectedItem ?. value ,
91+ } ) ) ,
92+ } ,
93+ ] ,
94+ noResultsFound : false ,
95+ } ;
96+ } , [ isSearchable , items , value , selectedItem , debouncedSearchTerm ] ) ;
4097
41- const listData : ListItem [ ] = useMemo ( ( ) => {
42- return items . map ( ( item ) => ( {
43- text : item . text ,
44- keyForList : item . value ,
45- isSelected : item . value === selectedItem ?. value ,
46- } ) ) ;
47- } , [ items , selectedItem ] ) ;
98+ const dataLength = useMemo ( ( ) => sections . flatMap ( ( section ) => section . data ) . length , [ sections ] ) ;
4899
49100 const updateSelectedItem = useCallback (
50101 ( item : ListItem ) => {
@@ -68,12 +119,19 @@ function SingleSelectPopup<T extends string>({label, value, items, closeOverlay,
68119 < View style = { [ ! isSmallScreenWidth && styles . pv4 , styles . gap2 ] } >
69120 { isSmallScreenWidth && < Text style = { [ styles . textLabel , styles . textSupporting , styles . ph5 , styles . pv1 ] } > { label } </ Text > }
70121
71- < View style = { [ styles . getSelectionListPopoverHeight ( items . length ) ] } >
122+ < View style = { [ styles . getSelectionListPopoverHeight ( dataLength || 1 , windowHeight , isSearchable ?? false ) ] } >
72123 < SelectionList
73124 shouldSingleExecuteRowSelect
74- sections = { [ { data : listData } ] }
125+ sections = { sections }
75126 ListItem = { SingleSelectListItem }
76127 onSelectRow = { updateSelectedItem }
128+ textInputValue = { searchTerm }
129+ onChangeText = { setSearchTerm }
130+ textInputLabel = { isSearchable ? ( searchPlaceholder ?? translate ( 'common.search' ) ) : undefined }
131+ shouldUpdateFocusedIndex = { isSearchable }
132+ initiallyFocusedOptionKey = { isSearchable ? value ?. value : undefined }
133+ headerMessage = { noResultsFound ? translate ( 'common.noResultsFound' ) : undefined }
134+ showLoadingPlaceholder = { ! noResultsFound }
77135 />
78136 </ View >
79137 < View style = { [ styles . flexRow , styles . gap2 , styles . ph5 ] } >
0 commit comments