From e4dfdf75ace941084747cfdd5437a95b51ee6acf Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 29 Jan 2026 12:52:24 +0100 Subject: [PATCH 1/5] Make SearchMultipleSelectionPicker use new SelectionListWithSections --- .../Search/SearchMultipleSelectionPicker.tsx | 149 +++++++++--------- 1 file changed, 71 insertions(+), 78 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 1cae120f349c6..e778204eee611 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -1,7 +1,6 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import SelectionList from '@components/SelectionListWithSections'; -import MultiSelectListItem from '@components/SelectionListWithSections/MultiSelectListItem'; +import React, {useEffect, useState} from 'react'; +import MultiSelectListItem from '@components/SelectionList/ListItem/MultiSelectListItem'; +import SelectionList from '@components/SelectionList/SelectionListWithSections'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; @@ -33,94 +32,88 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit setSelectedItems(initiallySelectedItems ?? []); }, [initiallySelectedItems]); - const {sections, noResultsFound} = useMemo(() => { - const selectedItemsSection = selectedItems - .filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) - .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)) - .map((item) => ({ - text: item.name, - keyForList: item.name, - isSelected: true, - value: item.value, - })); - const remainingItemsSection = items - .filter( - (item) => - !selectedItems.some((selectedItem) => selectedItem.value.toString() === item.value.toString()) && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()), - ) - .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)) - .map((item) => ({ - text: item.name, - keyForList: item.name, - isSelected: false, - value: item.value, - })); - const isEmpty = !selectedItemsSection.length && !remainingItemsSection.length; - return { - sections: isEmpty - ? [] - : [ - { - title: undefined, - data: selectedItemsSection, - shouldShow: selectedItemsSection.length > 0, - }, - { - title: pickerTitle, - data: remainingItemsSection, - shouldShow: remainingItemsSection.length > 0, - }, - ], - noResultsFound: isEmpty, - }; - }, [selectedItems, items, pickerTitle, debouncedSearchTerm, localeCompare]); + const selectedItemsSection = selectedItems + .filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) + .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)) + .map((item) => ({ + text: item.name, + keyForList: item.name, + isSelected: true, + value: item.value, + })); - const onSelectItem = useCallback( - (item: Partial) => { - if (!item.text || !item.keyForList || !item.value) { - return; - } - if (item.isSelected) { - setSelectedItems(selectedItems?.filter((selectedItem) => selectedItem.name !== item.keyForList)); - } else { - setSelectedItems([...(selectedItems ?? []), {name: item.text, value: item.value}]); - } - }, - [selectedItems], - ); + const remainingItemsSection = items + .filter( + (item) => + !selectedItems.some((selectedItem) => selectedItem.value.toString() === item.value.toString()) && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()), + ) + .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)) + .map((item) => ({ + text: item.name, + keyForList: item.name, + isSelected: false, + value: item.value, + })); - const resetChanges = useCallback(() => { + const noResultsFound = !selectedItemsSection.length && !remainingItemsSection.length; + const sections = noResultsFound + ? [] + : [ + { + title: undefined, + data: selectedItemsSection, + sectionIndex: 0, + }, + { + title: pickerTitle, + data: remainingItemsSection, + sectionIndex: 1, + }, + ]; + + const onSelectItem = (item: Partial) => { + if (!item.text || !item.keyForList || !item.value) { + return; + } + if (item.isSelected) { + setSelectedItems(selectedItems?.filter((selectedItem) => selectedItem.name !== item.keyForList)); + } else { + setSelectedItems([...(selectedItems ?? []), {name: item.text, value: item.value}]); + } + }; + + const resetChanges = () => { setSelectedItems([]); - }, []); + }; - const applyChanges = useCallback(() => { + const applyChanges = () => { onSaveSelection(selectedItems.map((item) => item.value).flat()); Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS.getRoute()); - }, [onSaveSelection, selectedItems]); + }; - const footerContent = useMemo( - () => ( - - ), - [resetChanges, applyChanges], - ); + const textInputOptions = { + value: searchTerm, + label: translate('common.search'), + onChangeText: setSearchTerm, + headerMessage: noResultsFound ? translate('common.noResultsFound') : undefined, + }; return ( + } /> ); } From 23453348591aebb55edabbd84ad9b10f87dc7a2a Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 29 Jan 2026 18:21:07 +0100 Subject: [PATCH 2/5] Add scrolling to the top if more than 1 section --- .../BaseSelectionListWithSections.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 0a9b170b10404..4a5a894bf92ae 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -140,8 +140,14 @@ function BaseSelectionListWithSections({ if (!isScreenFocused) { return; } - if (canSelectMultiple && shouldShowTextInput) { - textInputOptions?.onChangeText?.(''); + if (canSelectMultiple) { + if (sections.length > 1 && !isItemSelected(item)) { + scrollToIndex(0); + } + + if(shouldShowTextInput) { + textInputOptions?.onChangeText?.(''); + } } if (shouldUpdateFocusedIndex && typeof indexToFocus === 'number') { setFocusedIndex(indexToFocus); From 7a4a8e79d5537033a1fefc6fb5a596959b92ea69 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 29 Jan 2026 17:48:01 +0100 Subject: [PATCH 3/5] Fix scrolling with text input --- .../BaseSelectionListWithSections.tsx | 7 ++++--- .../hooks/useFlattenedSections.ts | 9 +++++++++ .../SelectionList/hooks/useSearchFocusSync.ts | 19 +++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 4a5a894bf92ae..3c96784965ad9 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -80,7 +80,7 @@ function BaseSelectionListWithSections({ const paddingBottomStyle = !isKeyboardShown && !footerContent && safeAreaPaddingBottomStyle; - const {flattenedData, disabledIndexes, itemsCount, selectedItems, initialFocusedIndex} = useFlattenedSections(sections, initiallyFocusedItemKey); + const {flattenedData, disabledIndexes, itemsCount, selectedItems, initialFocusedIndex, firstFocusableIndex} = useFlattenedSections(sections, initiallyFocusedItemKey); const setHasKeyBeenPressed = () => { if (hasKeyBeenPressed.current) { @@ -90,11 +90,11 @@ function BaseSelectionListWithSections({ }; const scrollToIndex = (index: number) => { - if (index < 0 || index >= flattenedData.length) { + if (index < 0 || index >= flattenedData.length || !listRef.current) { return; } const item = flattenedData.at(index); - if (!listRef.current || !item || getItemType(item) === CONST.SECTION_LIST_ITEM_TYPE.HEADER) { + if (!item) { return; } try { @@ -214,6 +214,7 @@ function BaseSelectionListWithSections({ shouldUpdateFocusedIndex, scrollToIndex, setFocusedIndex, + firstFocusableIndex, }); const textInputComponent = () => { diff --git a/src/components/SelectionList/hooks/useFlattenedSections.ts b/src/components/SelectionList/hooks/useFlattenedSections.ts index 6e0e78bb09e21..b3bea12d12109 100644 --- a/src/components/SelectionList/hooks/useFlattenedSections.ts +++ b/src/components/SelectionList/hooks/useFlattenedSections.ts @@ -31,6 +31,9 @@ type UseFlattenedSectionsResult = { /** Index of initially focused item in flattenedData, or -1 if none */ initialFocusedIndex: number; + + /** Index of the first focusable (non-header) item in flattenedData. Returns 0 if no items exist. */ + firstFocusableIndex: number; }; /** @@ -43,6 +46,7 @@ function useFlattenedSections(sections: Array(sections: Array; data.push(itemData); + if (firstNonHeaderIndex === -1) { + firstNonHeaderIndex = currentIndex; + } + if (item.keyForList === initiallyFocusedItemKey && focusedIndex === -1) { focusedIndex = currentIndex; } @@ -88,6 +96,7 @@ function useFlattenedSections(sections: Array = { /** Function to set the focused index */ setFocusedIndex: (index: number) => void; + + /** The first focusable index in the list (useful when index 0 is a header). Defaults to 0. */ + firstFocusableIndex?: number; }; /** @@ -44,6 +47,7 @@ function useSearchFocusSync({ shouldUpdateFocusedIndex, scrollToIndex, setFocusedIndex, + firstFocusableIndex = 0, }: UseSearchFocusSyncParams) { const prevSearchValue = usePrevious(searchValue); const prevSelectedOptionsCount = usePrevious(selectedOptionsCount); @@ -76,12 +80,18 @@ function useSearchFocusSync({ // Remove focus (set focused index to -1) if: // 1. If the search is idle or // 2. If the user is just toggling options without changing the list content - // Otherwise (e.g. when filtering/typing), focus on the first item (0) + // Otherwise (e.g. when filtering/typing), scroll to top and focus on the first focusable item const isSearchIdle = !prevSearchValue && !searchValue; - const newSelectedIndex = isSearchIdle || (selectedOptionsChanged && prevItemsLength === data.length) ? -1 : 0; + const shouldResetFocus = isSearchIdle || (selectedOptionsChanged && prevItemsLength === data.length); + + if (shouldResetFocus) { + setFocusedIndex(-1); + return; + } - scrollToIndex(newSelectedIndex); - setFocusedIndex(newSelectedIndex); + // Scroll to top of list and focus on first focusable item (not header) + scrollToIndex(0); + setFocusedIndex(firstFocusableIndex); }, [ canSelectMultiple, data, @@ -94,6 +104,7 @@ function useSearchFocusSync({ shouldUpdateFocusedIndex, searchValue, isItemSelected, + firstFocusableIndex, ]); } From af72c47eebcd3375706d0079740fc666c138aed8 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Thu, 29 Jan 2026 18:27:14 +0100 Subject: [PATCH 4/5] Fix lint --- .../BaseSelectionListWithSections.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index 3c96784965ad9..5d29ed7efcb78 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -144,8 +144,8 @@ function BaseSelectionListWithSections({ if (sections.length > 1 && !isItemSelected(item)) { scrollToIndex(0); } - - if(shouldShowTextInput) { + + if (shouldShowTextInput) { textInputOptions?.onChangeText?.(''); } } From ced611ac9b73251fd1c38a3933ee351479728408 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Fri, 30 Jan 2026 13:13:13 +0100 Subject: [PATCH 5/5] Add disableMaintainingScrollPosition --- src/components/Search/SearchMultipleSelectionPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index e778204eee611..e90ea0b2e532b 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -108,6 +108,7 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit shouldStopPropagation shouldShowTooltips canSelectMultiple + disableMaintainingScrollPosition footerContent={