Skip to content

Commit 4e17a1b

Browse files
authored
Merge pull request Expensify#89584 from marufsharifi/fix/preselected-items-top-of-list
Move pre-selected list items to top of list throughout the app
2 parents bc77c84 + 62d3a85 commit 4e17a1b

29 files changed

Lines changed: 1429 additions & 41 deletions

src/CONST/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ const CONST = {
261261
POPOVER_DROPDOWN_MAX_HEIGHT: 416,
262262
POPOVER_MENU_MAX_HEIGHT: 496,
263263
POPOVER_MENU_MAX_HEIGHT_MOBILE: 432,
264+
MOVE_SELECTED_ITEMS_TO_TOP_OF_LIST_THRESHOLD: 8,
264265
POPOVER_DATE_WIDTH: 338,
265266
POPOVER_DATE_RANGE_WIDTH: 672,
266267
POPOVER_DATE_MAX_HEIGHT: 366,

src/components/CountryPicker/CountrySelectorModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import ScreenWrapper from '@components/ScreenWrapper';
55
import SelectionList from '@components/SelectionList';
66
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
77
import useDebouncedState from '@hooks/useDebouncedState';
8+
import useInitialSelection from '@hooks/useInitialSelection';
89
import useLocalize from '@hooks/useLocalize';
910
import useThemeStyles from '@hooks/useThemeStyles';
1011
import searchOptions from '@libs/searchOptions';
1112
import type {Option} from '@libs/searchOptions';
13+
import moveInitialSelectionToTop from '@libs/SelectionListOrderUtils';
1214
import StringUtils from '@libs/StringUtils';
1315
import CONST from '@src/CONST';
1416
import type {TranslationPaths} from '@src/languages/types';
@@ -36,6 +38,8 @@ type CountrySelectorModalProps = {
3638
function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onClose, label, onBackdropPress}: CountrySelectorModalProps) {
3739
const {translate} = useLocalize();
3840
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
41+
const initialSelectedValue = useInitialSelection(currentCountry || undefined, {isVisible});
42+
const initialSelectedValues = initialSelectedValue ? [initialSelectedValue] : [];
3943

4044
const countries = useMemo(
4145
() =>
@@ -51,8 +55,8 @@ function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onC
5155
}),
5256
[translate, currentCountry],
5357
);
54-
55-
const searchResults = searchOptions(debouncedSearchValue, countries);
58+
const orderedCountries = moveInitialSelectionToTop(countries, initialSelectedValues);
59+
const searchResults = searchOptions(debouncedSearchValue, debouncedSearchValue ? countries : orderedCountries);
5660
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
5761

5862
const styles = useThemeStyles();
@@ -89,9 +93,10 @@ function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onC
8993
<SelectionList
9094
data={searchResults}
9195
textInputOptions={textInputOptions}
96+
searchValueForFocusSync={debouncedSearchValue}
9297
onSelectRow={onCountrySelected}
9398
ListItem={SingleSelectListItem}
94-
initiallyFocusedItemKey={currentCountry}
99+
initiallyFocusedItemKey={initialSelectedValue}
95100
shouldSingleExecuteRowSelect
96101
shouldStopPropagation
97102
/>

src/components/PushRowWithModal/PushRowModal.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import ScreenWrapper from '@components/ScreenWrapper';
55
import SelectionList from '@components/SelectionList';
66
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
77
import useDebouncedState from '@hooks/useDebouncedState';
8+
import useInitialSelection from '@hooks/useInitialSelection';
89
import useLocalize from '@hooks/useLocalize';
910
import searchOptions from '@libs/searchOptions';
11+
import moveInitialSelectionToTop from '@libs/SelectionListOrderUtils';
1012
import StringUtils from '@libs/StringUtils';
1113
import CONST from '@src/CONST';
1214

@@ -44,6 +46,8 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
4446
const {translate} = useLocalize();
4547

4648
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
49+
const initialSelectedValue = useInitialSelection(selectedOption || undefined, {isVisible});
50+
const initialSelectedValues = initialSelectedValue ? [initialSelectedValue] : [];
4751

4852
const options = useMemo(
4953
() =>
@@ -57,6 +61,8 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
5761
[optionsList, selectedOption],
5862
);
5963

64+
const orderedOptions = moveInitialSelectionToTop(options, initialSelectedValues);
65+
6066
const handleSelectRow = (option: ListItemType) => {
6167
onOptionChange(option.value);
6268
onClose();
@@ -67,7 +73,7 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
6773
setSearchValue('');
6874
};
6975

70-
const searchResults = searchOptions(debouncedSearchValue, options);
76+
const searchResults = searchOptions(debouncedSearchValue, debouncedSearchValue ? options : orderedOptions);
7177

7278
const textInputOptions = useMemo(
7379
() => ({
@@ -102,7 +108,8 @@ function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optio
102108
ListItem={SingleSelectListItem}
103109
onSelectRow={handleSelectRow}
104110
textInputOptions={textInputOptions}
105-
initiallyFocusedItemKey={selectedOption}
111+
searchValueForFocusSync={debouncedSearchValue}
112+
initiallyFocusedItemKey={initialSelectedValue}
106113
disableMaintainingScrollPosition
107114
shouldShowTooltips={false}
108115
showScrollIndicator

src/components/SelectionList/BaseSelectionList.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function BaseSelectionList<TItem extends ListItem>({
4444
ref,
4545
ListItem,
4646
textInputOptions,
47+
searchValueForFocusSync,
4748
initiallyFocusedItemKey,
4849
onSelectRow,
4950
onSelectAll,
@@ -242,6 +243,7 @@ function BaseSelectionList<TItem extends ListItem>({
242243
// Including data.length ensures FlashList resets its layout cache when the list size changes
243244
// This prevents "index out of bounds" errors when filtering reduces the list size
244245
const extraData = useMemo(() => [data.length], [data.length]);
246+
const syncedSearchValue = searchValueForFocusSync ?? textInputOptions?.value;
245247

246248
const selectRow = useCallback(
247249
(item: TItem, indexToFocus?: number) => {
@@ -541,7 +543,7 @@ function BaseSelectionList<TItem extends ListItem>({
541543
initiallyFocusedItemKey,
542544
isItemSelected,
543545
focusedIndex,
544-
searchValue: textInputOptions?.value,
546+
searchValue: syncedSearchValue,
545547
setFocusedIndex,
546548
});
547549

@@ -550,7 +552,7 @@ function BaseSelectionList<TItem extends ListItem>({
550552
}, []);
551553

552554
useSearchFocusSync({
553-
searchValue: textInputOptions?.value,
555+
searchValue: syncedSearchValue,
554556
data,
555557
selectedOptionsCount: dataDetails.selectedOptions.length,
556558
isItemSelected,

src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
4343
ref,
4444
ListItem,
4545
textInputOptions,
46+
searchValueForFocusSync,
4647
initiallyFocusedItemKey,
4748
confirmButtonOptions,
4849
initialScrollIndex,
@@ -268,6 +269,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
268269

269270
// Disable `Enter` shortcut if the active element is a button, checkbox, or switch
270271
const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX, CONST.ROLE.SWITCH].includes(activeElementRole as InteractiveElementRoles);
272+
const syncedSearchValue = searchValueForFocusSync ?? textInputOptions?.value;
271273

272274
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedItem, {
273275
captureOnInputs: true,
@@ -306,7 +308,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
306308
initiallyFocusedItemKey,
307309
isItemSelected,
308310
focusedIndex,
309-
searchValue: textInputOptions?.value,
311+
searchValue: syncedSearchValue,
310312
setFocusedIndex,
311313
});
312314

@@ -315,7 +317,7 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
315317
};
316318

317319
useSearchFocusSync({
318-
searchValue: textInputOptions?.value,
320+
searchValue: syncedSearchValue,
319321
data: flattenedData,
320322
selectedOptionsCount: selectedItems.length,
321323
isItemSelected,

src/components/SelectionList/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type BaseSelectionListProps<TItem extends ListItem> = {
5858
/** Configuration options for the text input */
5959
textInputOptions?: TextInputOptions;
6060

61+
/** Search value used for focus synchronization. Defaults to textInputOptions.value */
62+
searchValueForFocusSync?: string;
63+
6164
/** Whether to show the text input */
6265
shouldShowTextInput?: boolean;
6366

src/components/StatePicker/StateSelectorModal.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import ScreenWrapper from '@components/ScreenWrapper';
66
import SelectionList from '@components/SelectionList';
77
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
88
import useDebouncedState from '@hooks/useDebouncedState';
9+
import useInitialSelection from '@hooks/useInitialSelection';
910
import useLocalize from '@hooks/useLocalize';
1011
import useThemeStyles from '@hooks/useThemeStyles';
1112
import searchOptions from '@libs/searchOptions';
1213
import type {Option} from '@libs/searchOptions';
14+
import moveInitialSelectionToTop from '@libs/SelectionListOrderUtils';
1315
import StringUtils from '@libs/StringUtils';
1416
import CONST from '@src/CONST';
1517

@@ -39,6 +41,8 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose,
3941
const {translate} = useLocalize();
4042
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
4143
const styles = useThemeStyles();
44+
const initialSelectedValue = useInitialSelection(currentState || undefined, {isVisible});
45+
const initialSelectedValues = initialSelectedValue ? [initialSelectedValue] : [];
4246

4347
const countryStates = useMemo(
4448
() =>
@@ -57,7 +61,8 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose,
5761
[translate, currentState],
5862
);
5963

60-
const searchResults = searchOptions(debouncedSearchValue, countryStates);
64+
const orderedCountryStates = moveInitialSelectionToTop(countryStates, initialSelectedValues);
65+
const searchResults = searchOptions(debouncedSearchValue, debouncedSearchValue ? countryStates : orderedCountryStates);
6166

6267
const textInputOptions = useMemo(
6368
() => ({
@@ -93,7 +98,8 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose,
9398
ListItem={SingleSelectListItem}
9499
onSelectRow={onStateSelected}
95100
textInputOptions={textInputOptions}
96-
initiallyFocusedItemKey={currentState}
101+
searchValueForFocusSync={debouncedSearchValue}
102+
initiallyFocusedItemKey={initialSelectedValue}
97103
disableMaintainingScrollPosition
98104
shouldSingleExecuteRowSelect
99105
shouldStopPropagation

src/components/ValuePicker/ValueSelectionList.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, {useMemo} from 'react';
22
import SelectionList from '@components/SelectionList';
33
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
4+
import useInitialSelection from '@hooks/useInitialSelection';
5+
import moveInitialSelectionToTop from '@libs/SelectionListOrderUtils';
46
import type {ValueSelectionListProps} from './types';
57

68
function ValueSelectionList({
@@ -11,20 +13,26 @@ function ValueSelectionList({
1113
addBottomSafeAreaPadding = true,
1214
disableKeyboardShortcuts = false,
1315
alternateNumberOfSupportedLines,
16+
isVisible,
1417
}: ValueSelectionListProps) {
15-
const options = useMemo(
16-
() => items.map((item) => ({value: item.value, alternateText: item.description, text: item.label ?? '', isSelected: item === selectedItem, keyForList: item.value ?? ''})),
17-
[items, selectedItem],
18-
);
18+
const initialSelectedValue = useInitialSelection(selectedItem?.value ? selectedItem.value : undefined, isVisible === undefined ? {resetOnFocus: true} : {isVisible});
19+
20+
const options = useMemo(() => {
21+
const mappedOptions = items.map((item) => ({value: item.value ?? '', alternateText: item.description, text: item.label ?? '', keyForList: item.value ?? ''}));
22+
const orderedOptions = moveInitialSelectionToTop(mappedOptions, initialSelectedValue ? [initialSelectedValue] : []);
23+
24+
return orderedOptions.map((item) => ({...item, isSelected: item.value === selectedItem?.value}));
25+
}, [initialSelectedValue, items, selectedItem?.value]);
1926

2027
return (
2128
<SelectionList
2229
data={options}
2330
onSelectRow={(item) => onItemSelected?.(item)}
24-
initiallyFocusedItemKey={selectedItem?.value}
31+
initiallyFocusedItemKey={initialSelectedValue}
2532
shouldStopPropagation
2633
shouldShowTooltips={shouldShowTooltips}
27-
shouldUpdateFocusedIndex
34+
shouldScrollToFocusedIndex={false}
35+
shouldScrollToFocusedIndexOnMount={false}
2836
ListItem={SingleSelectListItem}
2937
addBottomSafeAreaPadding={addBottomSafeAreaPadding}
3038
disableKeyboardShortcuts={disableKeyboardShortcuts}

src/components/ValuePicker/ValueSelectorModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function ValueSelectorModal({
4242
<ValueSelectionList
4343
items={items}
4444
selectedItem={selectedItem}
45+
isVisible={isVisible}
4546
onItemSelected={onItemSelected}
4647
shouldShowTooltips={shouldShowTooltips}
4748
disableKeyboardShortcuts={disableKeyboardShortcuts}

src/components/ValuePicker/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ type ValueSelectorModalProps = {
5454
type ValueSelectionListProps = Pick<
5555
ValueSelectorModalProps,
5656
'items' | 'selectedItem' | 'onItemSelected' | 'shouldShowTooltips' | 'addBottomSafeAreaPadding' | 'disableKeyboardShortcuts' | 'alternateNumberOfSupportedLines'
57-
>;
57+
> & {
58+
/** Whether the parent modal is visible */
59+
isVisible?: boolean;
60+
};
5861

5962
type ValuePickerProps = ForwardedFSClassProps & {
6063
/** Item to display */

0 commit comments

Comments
 (0)