From 9180f58900a873ff5ec6ae67f35a86e79c78905d Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 13:34:45 +0000 Subject: [PATCH 1/5] Suppress redundant animated scroll in useSearchFocusSync When useSearchFocusSync calls scrollToIndex followed by setFocusedIndex, the setFocusedIndex triggers onFocusedIndexChange which fires another scrollToIndex with animation. This causes a visible animated scroll on top of the non-animated scroll that was already performed. Add suppressNextFocusScroll callback to useSearchFocusSync so BaseSelectionListWithSections can set suppressNextFocusScrollRef before setFocusedIndex, preventing the redundant animated scroll. Co-authored-by: mkhutornyi --- .../BaseSelectionListWithSections.tsx | 7 ++++++- src/components/SelectionList/hooks/useSearchFocusSync.ts | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index e8cd7d10162d..215e2e5d4a27 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {FlashList} from '@shopify/flash-list'; import type {FlashListRef, ListRenderItemInfo} from '@shopify/flash-list'; -import React, {useImperativeHandle, useRef} from 'react'; +import React, {useCallback, useImperativeHandle, useRef} from 'react'; import type {TextInputKeyPressEvent} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; @@ -267,6 +267,10 @@ function BaseSelectionListWithSections({ setFocusedIndex, }); + const suppressNextFocusScroll = useCallback(() => { + suppressNextFocusScrollRef.current = true; + }, []); + useSearchFocusSync({ searchValue: textInputOptions?.value, data: flattenedData, @@ -277,6 +281,7 @@ function BaseSelectionListWithSections({ scrollToIndex, setFocusedIndex, firstFocusableIndex, + suppressNextFocusScroll, }); const textInputComponent = () => { diff --git a/src/components/SelectionList/hooks/useSearchFocusSync.ts b/src/components/SelectionList/hooks/useSearchFocusSync.ts index ed1fb22822fa..4ed29223f4d8 100644 --- a/src/components/SelectionList/hooks/useSearchFocusSync.ts +++ b/src/components/SelectionList/hooks/useSearchFocusSync.ts @@ -29,6 +29,9 @@ type UseSearchFocusSyncParams = { /** The first focusable index in the list (useful when index 0 is a header). Defaults to 0. */ firstFocusableIndex?: number; + + /** Optional callback to suppress the scroll that onFocusedIndexChange would otherwise trigger when setFocusedIndex is called */ + suppressNextFocusScroll?: () => void; }; /** @@ -48,6 +51,7 @@ function useSearchFocusSync({ scrollToIndex, setFocusedIndex, firstFocusableIndex = 0, + suppressNextFocusScroll, }: UseSearchFocusSyncParams) { const prevSearchValue = usePrevious(searchValue); const prevSelectedOptionsCount = usePrevious(selectedOptionsCount); @@ -72,6 +76,7 @@ function useSearchFocusSync({ if (foundSelectedItemIndex !== -1 && !canSelectMultiple) { scrollToIndex(foundSelectedItemIndex); + suppressNextFocusScroll?.(); setFocusedIndex(foundSelectedItemIndex); return; } @@ -91,6 +96,7 @@ function useSearchFocusSync({ // Scroll to top of list and focus on first focusable item (not header) scrollToIndex(0); + suppressNextFocusScroll?.(); setFocusedIndex(firstFocusableIndex); }, [ canSelectMultiple, @@ -105,6 +111,7 @@ function useSearchFocusSync({ searchValue, isItemSelected, firstFocusableIndex, + suppressNextFocusScroll, ]); } From dc142e786e49198be512bd310a29cdea8066ae37 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Mon, 4 May 2026 13:44:24 +0000 Subject: [PATCH 2/5] Remove useCallback wrapper from suppressNextFocusScroll React Compiler auto-memoizes this, so the explicit useCallback is unnecessary. Co-authored-by: mkhutornyi --- .../BaseSelectionListWithSections.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 215e2e5d4a27..f58e284636a8 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {FlashList} from '@shopify/flash-list'; import type {FlashListRef, ListRenderItemInfo} from '@shopify/flash-list'; -import React, {useCallback, useImperativeHandle, useRef} from 'react'; +import React, {useImperativeHandle, useRef} from 'react'; import type {TextInputKeyPressEvent} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; @@ -267,9 +267,9 @@ function BaseSelectionListWithSections({ setFocusedIndex, }); - const suppressNextFocusScroll = useCallback(() => { + const suppressNextFocusScroll = () => { suppressNextFocusScrollRef.current = true; - }, []); + }; useSearchFocusSync({ searchValue: textInputOptions?.value, From fe0da6cac3c126a7264071ec878c5974aeca1eaa Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Tue, 5 May 2026 07:50:08 +0000 Subject: [PATCH 3/5] Suppress scroll on mouse click, enable shouldUpdateFocusedIndex in SearchMultipleSelectionPicker Set suppressNextFocusScrollRef before setFocusedIndex in selectRow so mouse clicks update the focused index without triggering auto-scroll. Arrow key navigation continues to scroll normally via onFocusedIndexChange. Also pass shouldUpdateFocusedIndex to SelectionListWithSections in SearchMultipleSelectionPicker so the focused index tracks the selected item. Co-authored-by: mkhutornyi --- src/components/Search/SearchMultipleSelectionPicker.tsx | 1 + .../SelectionListWithSections/BaseSelectionListWithSections.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index a58e4510c0fd..bd91307b0c82 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -115,6 +115,7 @@ function SearchMultipleSelectionPicker({ sections={sections} ListItem={MultiSelectListItem} initiallyFocusedItemKey={initiallyFocusedKey} + shouldUpdateFocusedIndex shouldClearInputOnSelect={false} shouldShowTextInput={shouldShowTextInput} textInputOptions={textInputOptions} diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 4a57ff01094e..4b7ea0be9860 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -170,6 +170,7 @@ function BaseSelectionListWithSections({ } } if (shouldUpdateFocusedIndex && typeof indexToFocus === 'number') { + suppressNextFocusScrollRef.current = true; setFocusedIndex(indexToFocus); } onSelectRow(item); From 698b9511c6fe988bd1bb9e4fae5943c4e42ea332 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Tue, 5 May 2026 09:07:26 +0000 Subject: [PATCH 4/5] Fix: only suppress focus scroll when index actually changes Guard suppressNextFocusScrollRef so it is only armed when the clicked row differs from the current focusedIndex. Previously, clicking the already-focused row would arm the flag without onFocusedIndexChange ever firing to clear it, causing the next arrow-key press to skip scrolling. Co-authored-by: mkhutornyi --- .../BaseSelectionListWithSections.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 4b7ea0be9860..f342d8505709 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -170,7 +170,9 @@ function BaseSelectionListWithSections({ } } if (shouldUpdateFocusedIndex && typeof indexToFocus === 'number') { - suppressNextFocusScrollRef.current = true; + if (indexToFocus !== focusedIndex) { + suppressNextFocusScrollRef.current = true; + } setFocusedIndex(indexToFocus); } onSelectRow(item); From ac2cf8b2e367eb97c25c8453d1a9d3b1dd721ae0 Mon Sep 17 00:00:00 2001 From: "mkhutornyi (via MelvinBot)" Date: Tue, 5 May 2026 10:36:18 +0000 Subject: [PATCH 5/5] Fix: guard suppressNextFocusScroll to only arm when focused index will change When the focused index is already the target value, React skips the state update and onFocusedIndexChange never fires, leaving the suppression flag stuck at true. This caused the next real arrow-key focus change to skip scrolling. Now we only arm the flag when the index will actually change. Co-authored-by: mkhutornyi --- .../BaseSelectionListWithSections.tsx | 1 + .../SelectionList/hooks/useSearchFocusSync.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index f342d8505709..f162b2d5e52a 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -284,6 +284,7 @@ function BaseSelectionListWithSections({ shouldUpdateFocusedIndex, scrollToIndex, setFocusedIndex, + focusedIndex, firstFocusableIndex, suppressNextFocusScroll, }); diff --git a/src/components/SelectionList/hooks/useSearchFocusSync.ts b/src/components/SelectionList/hooks/useSearchFocusSync.ts index 918017871f9a..3aaf46412b44 100644 --- a/src/components/SelectionList/hooks/useSearchFocusSync.ts +++ b/src/components/SelectionList/hooks/useSearchFocusSync.ts @@ -27,6 +27,9 @@ type UseSearchFocusSyncParams = { /** Function to set the focused index */ setFocusedIndex: (index: number) => void; + /** The current focused index — needed to avoid arming scroll suppression when the index won't actually change */ + focusedIndex?: number; + /** The first focusable index in the list (useful when index 0 is a header). Defaults to 0. */ firstFocusableIndex?: number; @@ -50,6 +53,7 @@ function useSearchFocusSync({ shouldUpdateFocusedIndex, scrollToIndex, setFocusedIndex, + focusedIndex, firstFocusableIndex = 0, suppressNextFocusScroll, }: UseSearchFocusSyncParams) { @@ -76,7 +80,9 @@ function useSearchFocusSync({ if (foundSelectedItemIndex !== -1 && !canSelectMultiple) { scrollToIndex(foundSelectedItemIndex, false); - suppressNextFocusScroll?.(); + if (foundSelectedItemIndex !== focusedIndex) { + suppressNextFocusScroll?.(); + } setFocusedIndex(foundSelectedItemIndex); return; } @@ -96,7 +102,9 @@ function useSearchFocusSync({ // Scroll to top of list and focus on first focusable item (not header) scrollToIndex(0, false); - suppressNextFocusScroll?.(); + if (firstFocusableIndex !== focusedIndex) { + suppressNextFocusScroll?.(); + } setFocusedIndex(firstFocusableIndex); }, [ canSelectMultiple, @@ -110,6 +118,7 @@ function useSearchFocusSync({ shouldUpdateFocusedIndex, searchValue, isItemSelected, + focusedIndex, firstFocusableIndex, suppressNextFocusScroll, ]);