Skip to content

Commit 6c3045b

Browse files
authored
Merge pull request #92045 from Expensify/claude-stopScrollJumpSearchFiltersParticipants
Stop scroll jump on search filter participant selection
2 parents 85844ee + 22d1ef8 commit 6c3045b

4 files changed

Lines changed: 426 additions & 195 deletions

File tree

src/components/Search/SearchFiltersChatsSelector.tsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import React, {useEffect, useState} from 'react';
33
import {usePersonalDetails} from '@components/OnyxListItemProvider';
44
import InviteMemberListItem from '@components/SelectionList/ListItem/InviteMemberListItem';
55
import SelectionListWithSections from '@components/SelectionList/SelectionListWithSections';
6+
import type {Section} from '@components/SelectionList/SelectionListWithSections/types';
67
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
78
import useDebouncedState from '@hooks/useDebouncedState';
89
import useFilteredOptions from '@hooks/useFilteredOptions';
10+
import useFrozenPreSelection from '@hooks/useFrozenPreSelection';
911
import useLocalize from '@hooks/useLocalize';
1012
import useOnyx from '@hooks/useOnyx';
1113
import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap';
@@ -14,9 +16,9 @@ import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionS
1416
import useSortedActions from '@hooks/useSortedActions';
1517
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
1618
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
17-
import {createOptionFromReport, filterAndOrderOptions, formatSectionsFromSearchTerm, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils';
19+
import {createOptionFromReport, filterAndOrderOptions, filterReports, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils';
1820
import type {Option} from '@libs/OptionsListUtils';
19-
import type {OptionWithKey, SelectionListSections} from '@libs/OptionsListUtils/types';
21+
import type {OptionWithKey, SearchOptionData} from '@libs/OptionsListUtils/types';
2022
import type {OptionData} from '@libs/ReportUtils';
2123
import Navigation from '@navigation/Navigation';
2224
import {searchInServer} from '@userActions/Report';
@@ -105,35 +107,27 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
105107
excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT,
106108
});
107109

108-
const sections: SelectionListSections = [];
110+
const selectedReportIDsSet = new Set(selectedReportIDs);
111+
// Mark selected rows in place so the checkmark moves with the toggle without reordering the list.
112+
const recentReportsWithSelection = chatOptions.recentReports.map((report) => (selectedReportIDsSet.has(report.reportID) ? getSelectedOptionData(report) : report));
109113

114+
// Selected reports that don't show up in Recents — surface them but respect the search term.
115+
const visibleReportIDsSet = new Set(chatOptions.recentReports.map((report) => report.reportID));
116+
const reportIDsMatchingSearch = cleanSearchTerm === '' ? null : new Set(filterReports(selectedOptions as SearchOptionData[], [cleanSearchTerm]).map((report) => report.reportID));
117+
const matchesSearchTerm = (report: OptionData) => reportIDsMatchingSearch === null || reportIDsMatchingSearch.has(report.reportID);
118+
const extraSelectedReports = selectedOptions.filter((report) => !visibleReportIDsSet.has(report.reportID) && matchesSearchTerm(report));
119+
120+
const baseSections: Array<Section<OptionData>> = [];
110121
if (!isLoading) {
111-
const formattedResults = formatSectionsFromSearchTerm(
112-
cleanSearchTerm,
113-
selectedOptions,
114-
chatOptions.recentReports,
115-
chatOptions.personalDetails,
116-
privateIsArchivedMap,
117-
currentUserAccountID,
118-
allPolicies,
119-
personalDetails,
120-
false,
121-
undefined,
122-
reportAttributesDerived,
123-
);
124-
125-
sections.push(formattedResults.section);
126-
127-
const visibleReportsWhenSearchTermNonEmpty = chatOptions.recentReports.map((report) => (selectedReportIDs.includes(report.reportID) ? getSelectedOptionData(report) : report));
128-
const visibleReportsWhenSearchTermEmpty = chatOptions.recentReports.filter((report) => !selectedReportIDs.includes(report.reportID));
129-
const reportsFiltered = cleanSearchTerm === '' ? visibleReportsWhenSearchTermEmpty : visibleReportsWhenSearchTermNonEmpty;
130-
131-
sections.push({
132-
data: reportsFiltered,
133-
sectionIndex: 1,
134-
});
122+
if (extraSelectedReports.length > 0) {
123+
baseSections.push({data: extraSelectedReports, sectionIndex: 1});
124+
}
125+
baseSections.push({data: recentReportsWithSelection, sectionIndex: 2});
135126
}
136-
const noResultsFound = didScreenTransitionEnd && sections.at(0)?.data.length === 0 && sections.at(1)?.data.length === 0;
127+
128+
const sections = useFrozenPreSelection<OptionData>(baseSections, {initialSelectedValues: initialReportIDs, canCapture: !isLoading});
129+
130+
const noResultsFound = didScreenTransitionEnd && !isLoading && sections.every((section) => section.data.length === 0);
137131
const headerMessage = noResultsFound ? translate('common.noResultsFound') : undefined;
138132

139133
useEffect(() => {
@@ -191,6 +185,9 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen
191185
footerContent={footerContent}
192186
canSelectMultiple
193187
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
188+
shouldUpdateFocusedIndex
189+
shouldPreventAutoScrollOnSelect
190+
shouldClearInputOnSelect={false}
194191
textInputOptions={textInputOptions}
195192
isLoadingNewOptions={isLoadingNewOptions}
196193
shouldShowLoadingPlaceholder={shouldShowLoadingPlaceholder}

0 commit comments

Comments
 (0)