From 7b93ea8db9299721d814e46b478b84311ce8d8db Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Fri, 1 May 2026 13:57:41 +0000 Subject: [PATCH 01/13] Stop scroll jump when selecting items in SearchMultipleSelectionPicker Merge selected and remaining items into a single sorted section instead of splitting them across two sections. This prevents the scroll-to-top triggered by BaseSelectionListWithSections when sections.length > 1. Also set initiallyFocusedItemKey to the first selected item so the list scrolls to the right position on initial render. Co-authored-by: mkhutornyi --- .../Search/SearchMultipleSelectionPicker.tsx | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index cae3e8802839..f3eb158e8551 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -36,33 +36,26 @@ function SearchMultipleSelectionPicker({ const [selectedItemIDs, setSelectedItemIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); const searchLower = debouncedSearchTerm.toLowerCase(); - const selectedSectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; - const remainingSectionData: typeof selectedSectionData = []; + const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; for (const item of items) { if (!item.name.toLowerCase().includes(searchLower)) { continue; } const isSelected = selectedItemIDs.has(item.value.toString()); - (isSelected ? selectedSectionData : remainingSectionData).push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement}); + sectionData.push({text: item.name, keyForList: item.name, isSelected, value: item.value, leftElement: item.leftElement}); } - const sortByValue = (a: {value: string | string[]}, b: {value: string | string[]}) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare); - selectedSectionData.sort(sortByValue); - remainingSectionData.sort(sortByValue); + sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - const noResultsFound = !selectedSectionData.length && !remainingSectionData.length; + const firstSelectedKey = sectionData.find((item) => item.isSelected)?.keyForList; + const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] : [ - { - title: undefined, - data: selectedSectionData, - sectionIndex: 0, - }, { title: pickerTitle, - data: remainingSectionData, - sectionIndex: 1, + data: sectionData, + sectionIndex: 0, }, ]; @@ -101,6 +94,7 @@ function SearchMultipleSelectionPicker({ Date: Sun, 3 May 2026 21:45:36 +0000 Subject: [PATCH 02/13] Set shouldClearInputOnSelect to false for SearchMultipleSelectionPicker Move shouldClearInputOnSelect from SelectionListProps to BaseSelectionListProps so it can be used by SelectionListWithSections. Set it to false in SearchMultipleSelectionPicker to preserve the search filter text when selecting/deselecting items. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 1 + .../BaseSelectionListWithSections.tsx | 3 ++- src/components/SelectionList/types.ts | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index f3eb158e8551..ba0f4da08560 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -95,6 +95,7 @@ function SearchMultipleSelectionPicker({ sections={sections} ListItem={MultiSelectListItem} initiallyFocusedItemKey={firstSelectedKey} + shouldClearInputOnSelect={false} shouldShowTextInput={shouldShowTextInput} textInputOptions={textInputOptions} onSelectRow={onSelectItem} diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index e8cd7d10162d..865fa4517db2 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -71,6 +71,7 @@ function BaseSelectionListWithSections({ shouldDebounceScrolling = false, shouldUpdateFocusedIndex = false, shouldScrollToFocusedIndex = true, + shouldClearInputOnSelect = true, shouldSingleExecuteRowSelect = false, shouldPreventDefaultFocusOnSelectRow = false, isRowMultilineSupported = false, @@ -164,7 +165,7 @@ function BaseSelectionListWithSections({ scrollToIndex(0); } - if (shouldShowTextInput) { + if (shouldShowTextInput && shouldClearInputOnSelect) { textInputOptions?.onChangeText?.(''); } } diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 7d8cfe24def5..7d24527f3ace 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -110,6 +110,9 @@ type BaseSelectionListProps = { /** Configuration for the confirm button */ confirmButtonOptions?: ConfirmButtonOptions; + /** Whether to clear the text input when a row is selected */ + shouldClearInputOnSelect?: boolean; + /** Whether hover style should be disabled */ shouldDisableHoverStyle?: boolean; @@ -172,9 +175,6 @@ type SelectionListProps = Partial & /** Whether to place customListHeader in the list so it scrolls with data */ shouldHeaderBeInsideList?: boolean; - /** Whether to clear the text input when a row is selected */ - shouldClearInputOnSelect?: boolean; - /** Whether to highlight the selected item */ shouldHighlightSelectedItem?: boolean; From edc56c38f2d7a6774af8fd819347fc21074c4606 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Sun, 3 May 2026 22:00:06 +0000 Subject: [PATCH 03/13] Remove initiallyFocusedItemKey to prevent auto-scroll on mount Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index ba0f4da08560..6629d32d809c 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -47,7 +47,6 @@ function SearchMultipleSelectionPicker({ sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - const firstSelectedKey = sectionData.find((item) => item.isSelected)?.keyForList; const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] @@ -94,7 +93,6 @@ function SearchMultipleSelectionPicker({ Date: Sun, 3 May 2026 22:10:06 +0000 Subject: [PATCH 04/13] Restore initiallyFocusedItemKey for auto-scroll on mount Re-adds initiallyFocusedItemKey so the list scrolls to the first preselected item when opening the page. This does not cause scroll jumps on item selection because FlashList's initialScrollIndex only applies on mount and useArrowKeyFocusManager ignores prop changes after its initial useState call. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 6629d32d809c..ba0f4da08560 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -47,6 +47,7 @@ function SearchMultipleSelectionPicker({ sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); + const firstSelectedKey = sectionData.find((item) => item.isSelected)?.keyForList; const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] @@ -93,6 +94,7 @@ function SearchMultipleSelectionPicker({ Date: Mon, 4 May 2026 08:09:51 +0000 Subject: [PATCH 05/13] Fix: stabilize initiallyFocusedItemKey to prevent scroll on selection Compute the initial focused key once on mount using useState lazy initializer, so it doesn't change when the user toggles selections. This prevents FlashList's initialScrollIndex from updating and triggering auto-scroll on each item click. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index ba0f4da08560..478f107c167c 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -34,6 +34,11 @@ function SearchMultipleSelectionPicker({ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedItemIDs, setSelectedItemIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); + const [initiallyFocusedKey] = useState(() => { + const initialIDs = new Set((initiallySelectedItems ?? []).map((item) => item.value.toString())); + const sorted = [...items].sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); + return sorted.find((item) => initialIDs.has(item.value.toString()))?.name; + }); const searchLower = debouncedSearchTerm.toLowerCase(); const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; @@ -47,7 +52,6 @@ function SearchMultipleSelectionPicker({ sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - const firstSelectedKey = sectionData.find((item) => item.isSelected)?.keyForList; const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] @@ -94,7 +98,7 @@ function SearchMultipleSelectionPicker({ Date: Mon, 4 May 2026 08:24:13 +0000 Subject: [PATCH 06/13] Fix: eliminate duplicate sort and duplicate Set creation Store initialSelectedIDs once in state and reuse it for both selectedItemIDs initialization and initiallyFocusedKey lookup. Compute initiallyFocusedKey from the already-sorted sectionData instead of sorting a second time in a separate useState initializer. Co-authored-by: mkhutornyi --- .../Search/SearchMultipleSelectionPicker.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 478f107c167c..de9d523ba706 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -33,12 +33,8 @@ function SearchMultipleSelectionPicker({ const {translate, localeCompare} = useLocalize(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedItemIDs, setSelectedItemIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); - const [initiallyFocusedKey] = useState(() => { - const initialIDs = new Set((initiallySelectedItems ?? []).map((item) => item.value.toString())); - const sorted = [...items].sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - return sorted.find((item) => initialIDs.has(item.value.toString()))?.name; - }); + const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); + const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); const searchLower = debouncedSearchTerm.toLowerCase(); const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; @@ -52,6 +48,8 @@ function SearchMultipleSelectionPicker({ sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); + const initiallyFocusedKey = sectionData.find((item) => initialSelectedIDs.has(item.value.toString()))?.keyForList; + const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] From b797082e6594b691d37659c34504e6bf6055cc0a Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 09:56:18 +0000 Subject: [PATCH 07/13] Stabilize initiallyFocusedKey across search state changes Compute initiallyFocusedKey once on mount via useState lazy initializer instead of recomputing from sectionData every render. This prevents the key from flipping between undefined (empty search) and a real key (search cleared), which was triggering initialScrollIndex changes and scroll jumps. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index de9d523ba706..4a85f8fa5cd1 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -35,6 +35,10 @@ function SearchMultipleSelectionPicker({ const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); + const [initiallyFocusedKey] = useState(() => { + const sorted = [...items].sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); + return sorted.find((item) => initialSelectedIDs.has(item.value.toString()))?.name; + }); const searchLower = debouncedSearchTerm.toLowerCase(); const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; @@ -48,8 +52,6 @@ function SearchMultipleSelectionPicker({ sectionData.sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - const initiallyFocusedKey = sectionData.find((item) => initialSelectedIDs.has(item.value.toString()))?.keyForList; - const noResultsFound = !sectionData.length; const sections = noResultsFound ? [] From 230c1c65207934473a9f7a773719f60f00873217 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 10:06:14 +0000 Subject: [PATCH 08/13] Fix: avoid duplicate sort in initiallyFocusedKey computation Replace the full array sort in the useState initializer with a linear scan that finds the minimum selected item using the comparator directly. This eliminates the duplicate sort while keeping initiallyFocusedKey stable across re-renders (no search-clear scroll bug). Co-authored-by: mkhutornyi --- .../Search/SearchMultipleSelectionPicker.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 4a85f8fa5cd1..80b56fd5b830 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -36,8 +36,15 @@ function SearchMultipleSelectionPicker({ const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); const [initiallyFocusedKey] = useState(() => { - const sorted = [...items].sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)); - return sorted.find((item) => initialSelectedIDs.has(item.value.toString()))?.name; + let minItem: SearchMultipleSelectionPickerItem | undefined; + for (const item of items) { + if (initialSelectedIDs.has(item.value.toString())) { + if (!minItem || sortOptionsWithEmptyValue(item.value.toString(), minItem.value.toString(), localeCompare) < 0) { + minItem = item; + } + } + } + return minItem?.name; }); const searchLower = debouncedSearchTerm.toLowerCase(); From 5e2028bcb2fe7dae4501476938b89c71b0290eb2 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 10:33:10 +0000 Subject: [PATCH 09/13] Disable scroll animation when typing in search Search-triggered scrolls in useSearchFocusSync now pass animated: false so the list jumps instantly instead of smooth-scrolling when filtering. Co-authored-by: mkhutornyi --- src/components/SelectionList/BaseSelectionList.tsx | 4 ++-- .../BaseSelectionListWithSections.tsx | 4 ++-- src/components/SelectionList/hooks/useSearchFocusSync.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index b39a608c0ead..6a09fe2c4963 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -158,7 +158,7 @@ function BaseSelectionList({ }, []); const scrollToIndex = useCallback( - (index: number) => { + (index: number, animated = true) => { // Bounds check: ensure index is valid for current data if (index < 0 || index >= data.length) { return; @@ -168,7 +168,7 @@ function BaseSelectionList({ return; } try { - listRef.current.scrollToIndex({index}); + listRef.current.scrollToIndex({index, animated}); } catch (error) { // FlashList may throw if layout for this index doesn't exist yet // This can happen when data changes rapidly (e.g., during search filtering) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 865fa4517db2..4f6bb2a1f020 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -104,7 +104,7 @@ function BaseSelectionListWithSections({ hasKeyBeenPressed.current = true; }; - const scrollToIndex = (index: number) => { + const scrollToIndex = (index: number, animated = true) => { if (index < 0 || index >= flattenedData.length || !listRef.current) { return; } @@ -113,7 +113,7 @@ function BaseSelectionListWithSections({ return; } try { - listRef.current.scrollToIndex({index}); + listRef.current.scrollToIndex({index, animated}); } catch (error) { // FlashList may throw if layout for this index doesn't exist yet // This can happen when data changes rapidly (e.g., during search filtering) diff --git a/src/components/SelectionList/hooks/useSearchFocusSync.ts b/src/components/SelectionList/hooks/useSearchFocusSync.ts index ed1fb22822fa..9a7fe7aefe4a 100644 --- a/src/components/SelectionList/hooks/useSearchFocusSync.ts +++ b/src/components/SelectionList/hooks/useSearchFocusSync.ts @@ -22,7 +22,7 @@ type UseSearchFocusSyncParams = { shouldUpdateFocusedIndex: boolean; /** Function to scroll to an index */ - scrollToIndex: (index: number) => void; + scrollToIndex: (index: number, animated?: boolean) => void; /** Function to set the focused index */ setFocusedIndex: (index: number) => void; @@ -71,7 +71,7 @@ function useSearchFocusSync({ const foundSelectedItemIndex = data.findIndex(isItemSelected); if (foundSelectedItemIndex !== -1 && !canSelectMultiple) { - scrollToIndex(foundSelectedItemIndex); + scrollToIndex(foundSelectedItemIndex, false); setFocusedIndex(foundSelectedItemIndex); return; } @@ -90,7 +90,7 @@ function useSearchFocusSync({ } // Scroll to top of list and focus on first focusable item (not header) - scrollToIndex(0); + scrollToIndex(0, false); setFocusedIndex(firstFocusableIndex); }, [ canSelectMultiple, From ce775249150c5d216529381dfc24f7740bec882b Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 10:51:43 +0000 Subject: [PATCH 10/13] Clear initiallyFocusedItemKey after first render to prevent FlashList auto-scroll FlashList internally auto-scrolls when initialScrollIndex changes from -1 to a valid index. This happens when search is cleared and the focused item key transitions from "not found" to "found" in the data. Using a ref to track post-mount state ensures the key is only passed on the initial render. Co-authored-by: mkhutornyi --- .../Search/SearchMultipleSelectionPicker.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 80b56fd5b830..7d9c13de577f 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -35,7 +35,7 @@ function SearchMultipleSelectionPicker({ const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); - const [initiallyFocusedKey] = useState(() => { + const [initiallyFocusedKeyComputed] = useState(() => { let minItem: SearchMultipleSelectionPickerItem | undefined; for (const item of items) { if (initialSelectedIDs.has(item.value.toString())) { @@ -47,6 +47,15 @@ function SearchMultipleSelectionPicker({ return minItem?.name; }); + // Clear after first render to prevent FlashList from auto-scrolling when data changes + // cause the key to transition from "not found" to "found" (e.g., clearing a search). + const initialFocusAppliedRef = useRef(false); + useEffect(() => { + initialFocusAppliedRef.current = true; + }, []); + // eslint-disable-next-line react-hooks/refs -- Reading ref to detect post-mount state; intentional one-time prop pattern + const initiallyFocusedKey = initialFocusAppliedRef.current ? undefined : initiallyFocusedKeyComputed; + const searchLower = debouncedSearchTerm.toLowerCase(); const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; for (const item of items) { From 67a2437d27cf07c60fe1f06523b2903a2e5ae6eb Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 11:08:42 +0000 Subject: [PATCH 11/13] Fix React Compiler compliance: replace ref-during-render with state Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 7d9c13de577f..c7928540b037 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem'; import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -49,12 +49,11 @@ function SearchMultipleSelectionPicker({ // Clear after first render to prevent FlashList from auto-scrolling when data changes // cause the key to transition from "not found" to "found" (e.g., clearing a search). - const initialFocusAppliedRef = useRef(false); + const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(initiallyFocusedKeyComputed); useEffect(() => { - initialFocusAppliedRef.current = true; + // eslint-disable-next-line react-hooks/set-state-in-effect -- One-time clear after mount; prevents FlashList auto-scrolling when data changes cause the key to re-match + setInitiallyFocusedKey(undefined); }, []); - // eslint-disable-next-line react-hooks/refs -- Reading ref to detect post-mount state; intentional one-time prop pattern - const initiallyFocusedKey = initialFocusAppliedRef.current ? undefined : initiallyFocusedKeyComputed; const searchLower = debouncedSearchTerm.toLowerCase(); const sectionData: Array<{text: string; keyForList: string; isSelected: boolean; value: T; leftElement?: React.ReactNode}> = []; From 71ee5be15799706bb2f744be34d4bf7ab0a02cff Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 11:26:25 +0000 Subject: [PATCH 12/13] Fix: defer initiallyFocusedKey clear by one frame to let FlashList scroll The immediate setState in useEffect caused FlashList to see initialScrollIndex change from truthy to falsy before it processed the initial scroll. Using requestAnimationFrame gives FlashList one frame to complete the scroll before the key is cleared. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index c7928540b037..95a020ec2713 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -47,12 +47,15 @@ function SearchMultipleSelectionPicker({ return minItem?.name; }); - // Clear after first render to prevent FlashList from auto-scrolling when data changes + // Clear after mount to prevent FlashList from auto-scrolling when data changes // cause the key to transition from "not found" to "found" (e.g., clearing a search). + // Deferred by one frame so FlashList processes the initial scroll first. const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(initiallyFocusedKeyComputed); useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect -- One-time clear after mount; prevents FlashList auto-scrolling when data changes cause the key to re-match - setInitiallyFocusedKey(undefined); + const id = requestAnimationFrame(() => { + setInitiallyFocusedKey(undefined); + }); + return () => cancelAnimationFrame(id); }, []); const searchLower = debouncedSearchTerm.toLowerCase(); From 7c662c0142d15d0eee919864ef1caf5d974c20cc Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 15:43:25 +0000 Subject: [PATCH 13/13] Inline initiallyFocusedKeyComputed into initiallyFocusedKey useState initializer Co-authored-by: mkhutornyi --- .../Search/SearchMultipleSelectionPicker.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 95a020ec2713..a58e4510c0fd 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -35,7 +35,10 @@ function SearchMultipleSelectionPicker({ const [initialSelectedIDs] = useState(() => new Set((initiallySelectedItems ?? []).map((item) => item.value.toString()))); const [selectedItemIDs, setSelectedItemIDs] = useState(() => initialSelectedIDs); - const [initiallyFocusedKeyComputed] = useState(() => { + // Clear after mount to prevent FlashList from auto-scrolling when data changes + // cause the key to transition from "not found" to "found" (e.g., clearing a search). + // Deferred by one frame so FlashList processes the initial scroll first. + const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(() => { let minItem: SearchMultipleSelectionPickerItem | undefined; for (const item of items) { if (initialSelectedIDs.has(item.value.toString())) { @@ -46,11 +49,6 @@ function SearchMultipleSelectionPicker({ } return minItem?.name; }); - - // Clear after mount to prevent FlashList from auto-scrolling when data changes - // cause the key to transition from "not found" to "found" (e.g., clearing a search). - // Deferred by one frame so FlashList processes the initial scroll first. - const [initiallyFocusedKey, setInitiallyFocusedKey] = useState(initiallyFocusedKeyComputed); useEffect(() => { const id = requestAnimationFrame(() => { setInitiallyFocusedKey(undefined);