From 3f91b68c42da1c9f328c5b77d54b12dd937667cb Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Tue, 21 Jul 2026 08:11:06 +0430 Subject: [PATCH] Fix selection jump in rule and currency pickers --- .../CurrencySelectionList/index.tsx | 28 +++++++++++------- .../Search/SearchSingleSelectionPicker.tsx | 29 ++++++++++--------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 6150cfa34313..9ca8a25dace9 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -2,6 +2,7 @@ import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelec import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import {useCurrencyListActions, useCurrencyListState} from '@hooks/useCurrencyList'; +import useInitialSelection from '@hooks/useInitialSelection'; import useLocalize from '@hooks/useLocalize'; import getMatchScore from '@libs/getMatchScore'; @@ -9,16 +10,18 @@ import getMatchScore from '@libs/getMatchScore'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {Str} from 'expensify-common'; -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import type {CurrencyListItem, CurrencySelectionListProps} from './types'; +const EMPTY_SELECTED_CURRENCIES: string[] = []; + function CurrencySelectionList({ searchInputLabel, initiallySelectedCurrencyCode, onSelect, didScreenTransitionEnd = true, - selectedCurrencies = [], + selectedCurrencies = EMPTY_SELECTED_CURRENCIES, recentlyUsedCurrencies, excludedCurrencies = [], ...restProps @@ -26,8 +29,11 @@ function CurrencySelectionList({ const {currencyList} = useCurrencyListState(); const {getCurrencySymbol} = useCurrencyListActions(); const [searchValue, setSearchValue] = useState(''); + const selectedCurrencyCodes = useMemo(() => [initiallySelectedCurrencyCode, ...selectedCurrencies].filter(Boolean), [initiallySelectedCurrencyCode, selectedCurrencies]); + const initiallyPinnedCurrencyCodes = useInitialSelection(selectedCurrencyCodes, {resetOnFocus: true}); const {translate} = useLocalize(); - const getUnselectedOptions = (options: CurrencyListItem[]) => options.filter((option) => !option.isSelected); + const initiallyPinnedCurrencyCodeSet = new Set(initiallyPinnedCurrencyCodes); + const getUnpinnedOptions = (options: CurrencyListItem[]) => options.filter((option) => !initiallyPinnedCurrencyCodeSet.has(option.currencyCode)); const currencyOptions: CurrencyListItem[] = Object.entries(currencyList).reduce((acc, [currencyCode, currencyInfo]) => { const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode || selectedCurrencies.includes(currencyCode); @@ -64,15 +70,15 @@ function CurrencySelectionList({ const isEmpty = searchValue.trim() && !filteredCurrencies.length; const shouldDisplayRecentlyOptions = !isEmptyObject(recentlyUsedCurrencyOptions) && !searchValue; - const selectedOptions = filteredCurrencies.filter((option) => option.isSelected); - const shouldDisplaySelectedOptionOnTop = selectedOptions.length > 0; - const unselectedOptions = getUnselectedOptions(filteredCurrencies); + const initiallyPinnedOptions = filteredCurrencies.filter((option) => initiallyPinnedCurrencyCodeSet.has(option.currencyCode)); + const shouldDisplayInitiallyPinnedOptionsOnTop = initiallyPinnedOptions.length > 0; + const unpinnedOptions = getUnpinnedOptions(filteredCurrencies); const sections = []; - if (shouldDisplaySelectedOptionOnTop) { + if (shouldDisplayInitiallyPinnedOptionsOnTop) { sections.push({ title: undefined, - data: selectedOptions, + data: initiallyPinnedOptions, sectionIndex: 0, }); } @@ -82,15 +88,15 @@ function CurrencySelectionList({ sections.push( { title: translate('common.recents'), - data: shouldDisplaySelectedOptionOnTop ? getUnselectedOptions(recentlyUsedCurrencyOptions) : recentlyUsedCurrencyOptions, + data: shouldDisplayInitiallyPinnedOptionsOnTop ? getUnpinnedOptions(recentlyUsedCurrencyOptions) : recentlyUsedCurrencyOptions, sectionIndex: 1, }, - {title: translate('common.all'), data: shouldDisplayRecentlyOptions ? unselectedOptions : filteredCurrencies, sectionIndex: 2}, + {title: translate('common.all'), data: shouldDisplayRecentlyOptions ? unpinnedOptions : filteredCurrencies, sectionIndex: 2}, ); } } else if (!isEmpty) { sections.push({ - data: shouldDisplaySelectedOptionOnTop ? unselectedOptions : filteredCurrencies, + data: shouldDisplayInitiallyPinnedOptionsOnTop ? unpinnedOptions : filteredCurrencies, sectionIndex: 3, }); } diff --git a/src/components/Search/SearchSingleSelectionPicker.tsx b/src/components/Search/SearchSingleSelectionPicker.tsx index 3058001be328..c7e2f8ed8d9e 100644 --- a/src/components/Search/SearchSingleSelectionPicker.tsx +++ b/src/components/Search/SearchSingleSelectionPicker.tsx @@ -2,6 +2,7 @@ import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelec import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections'; import useDebouncedState from '@hooks/useDebouncedState'; +import useInitialSelection from '@hooks/useInitialSelection'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; @@ -47,6 +48,7 @@ function SearchSingleSelectionPicker({ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedItem, setSelectedItem] = useState(initiallySelectedItem); + const initialSelectedItem = useInitialSelection(initiallySelectedItem, {resetOnFocus: true}); useEffect(() => { setSelectedItem(initiallySelectedItem); @@ -54,21 +56,22 @@ function SearchSingleSelectionPicker({ const searchLower = debouncedSearchTerm?.toLowerCase(); const noneItem = allowNoneOption ? getNoneOption(debouncedSearchTerm, !selectedItem?.value, translate) : []; + const initialSelectedItemMatchesSearch = + !!initialSelectedItem && (initialSelectedItem.name.toLowerCase().includes(searchLower) || initialSelectedItem.searchableText?.toLowerCase().includes(searchLower)); - const initiallySelectedItemSection = - initiallySelectedItem?.name.toLowerCase().includes(searchLower) || initiallySelectedItem?.searchableText?.toLowerCase().includes(searchLower) - ? [ - { - text: initiallySelectedItem.name, - keyForList: initiallySelectedItem.value, - isSelected: selectedItem?.value === initiallySelectedItem.value, - value: initiallySelectedItem.value, - }, - ] - : []; + const initiallySelectedItemSection = initialSelectedItemMatchesSearch + ? [ + { + text: initialSelectedItem.name, + keyForList: initialSelectedItem.value, + isSelected: selectedItem?.value === initialSelectedItem.value, + value: initialSelectedItem.value, + }, + ] + : []; const remainingItemsSection = items - .filter((item) => item.value !== initiallySelectedItem?.value && (item.name.toLowerCase().includes(searchLower) || item.searchableText?.toLowerCase().includes(searchLower))) + .filter((item) => item.value !== initialSelectedItem?.value && (item.name.toLowerCase().includes(searchLower) || item.searchableText?.toLowerCase().includes(searchLower))) .sort((a, b) => sortOptionsWithEmptyValue(a.name.toString(), b.name.toString(), localeCompare)) .map((item) => ({ text: item.name, @@ -136,7 +139,7 @@ function SearchSingleSelectionPicker({ sections={sections} onSelectRow={onSelectItem} ListItem={SingleSelectListItem} - initiallyFocusedItemKey={initiallySelectedItem?.value} + initiallyFocusedItemKey={initialSelectedItem?.value} shouldShowTextInput={shouldShowTextInput} textInputOptions={textInputOptions} footerContent={shouldAutoSave ? undefined : footerContent}