|
1 | | -import React, {useState} from 'react'; |
| 1 | +import React, {useEffect, useState} from 'react'; |
2 | 2 | import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem'; |
3 | 3 | import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; |
4 | 4 | import useDebouncedState from '@hooks/useDebouncedState'; |
@@ -33,36 +33,49 @@ function SearchMultipleSelectionPicker<T extends string | string[]>({ |
33 | 33 | const {translate, localeCompare} = useLocalize(); |
34 | 34 | const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); |
35 | 35 |
|
36 | | - const [selectedItemIDs, setSelectedItemIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); |
| 36 | + const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); |
| 37 | + const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); |
| 38 | + // Clear after mount to prevent FlashList from auto-scrolling when data changes |
| 39 | + // cause the key to transition from "not found" to "found" (e.g., clearing a search). |
| 40 | + // Deferred by one frame so FlashList processes the initial scroll first. |
| 41 | + const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(() => { |
| 42 | + let minItem: SearchMultipleSelectionPickerItem<T> | undefined; |
| 43 | + for (const item of items) { |
| 44 | + if (initialSelectedIDs.has(item.value.toString())) { |
| 45 | + if (!minItem || sortOptionsWithEmptyValue(item.value.toString(), minItem.value.toString(), localeCompare) < 0) { |
| 46 | + minItem = item; |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | + return minItem?.name; |
| 51 | + }); |
| 52 | + useEffect(() => { |
| 53 | + const id = requestAnimationFrame(() => { |
| 54 | + setInitiallyFocusedKey(undefined); |
| 55 | + }); |
| 56 | + return () => cancelAnimationFrame(id); |
| 57 | + }, []); |
37 | 58 |
|
38 | 59 | const searchLower = debouncedSearchTerm.toLowerCase(); |
39 | | - const selectedSectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; |
40 | | - const remainingSectionData: typeof selectedSectionData = []; |
| 60 | + const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; |
41 | 61 | for (const item of items) { |
42 | 62 | if (!item.name.toLowerCase().includes(searchLower)) { |
43 | 63 | continue; |
44 | 64 | } |
45 | 65 | const isSelected = selectedItemIDs.has(item.value.toString()); |
46 | | - (isSelected ? selectedSectionData : remainingSectionData).push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement}); |
| 66 | + sectionData.push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement}); |
47 | 67 | } |
48 | 68 |
|
49 | | - const sortByValue = (a: {value: string | string[]}, b: {value: string | string[]}) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare); |
50 | | - selectedSectionData.sort(sortByValue); |
51 | | - remainingSectionData.sort(sortByValue); |
| 69 | + sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); |
52 | 70 |
|
53 | | - const noResultsFound = !selectedSectionData.length && !remainingSectionData.length; |
| 71 | + const noResultsFound = !sectionData.length; |
54 | 72 | const sections = noResultsFound |
55 | 73 | ? [] |
56 | 74 | : [ |
57 | | - { |
58 | | - title: undefined, |
59 | | - data: selectedSectionData, |
60 | | - sectionIndex: 0, |
61 | | - }, |
62 | 75 | { |
63 | 76 | title: pickerTitle, |
64 | | - data: remainingSectionData, |
65 | | - sectionIndex: 1, |
| 77 | + data: sectionData, |
| 78 | + sectionIndex: 0, |
66 | 79 | }, |
67 | 80 | ]; |
68 | 81 |
|
@@ -101,6 +114,8 @@ function SearchMultipleSelectionPicker<T extends string | string[]>({ |
101 | 114 | <SelectionListWithSections |
102 | 115 | sections={sections} |
103 | 116 | ListItem={MultiSelectListItem} |
| 117 | + initiallyFocusedItemKey={initiallyFocusedKey} |
| 118 | + shouldClearInputOnSelect={false} |
104 | 119 | shouldShowTextInput={shouldShowTextInput} |
105 | 120 | textInputOptions={textInputOptions} |
106 | 121 | onSelectRow={onSelectItem} |
|
0 commit comments