From 9da4042d84630cd86ca0ac92fdc2b36de31aceff Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 11 Jun 2024 16:55:01 +0200 Subject: [PATCH 1/6] Fix wrong scrolling in Search list when navigating by keyboard --- src/components/Search.tsx | 22 +++++++++++++++++++ .../SelectionList/BaseSelectionList.tsx | 9 ++++++-- src/components/SelectionList/types.ts | 9 ++++++++ src/libs/SearchUtils.ts | 6 +++-- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 3d295e72c09e..3dd8bfc2e586 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,5 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; +import lodashMemoize from 'lodash/memoize'; import React, {useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -14,6 +15,7 @@ import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import EmptySearchView from '@pages/Search/EmptySearchView'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,12 +36,30 @@ type SearchProps = { }; const sortableSearchTabs: SearchQuery[] = [CONST.TAB_SEARCH.ALL]; +const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item function isTransactionListItemType(item: TransactionListItemType | ReportListItemType): item is TransactionListItemType { const transactionListItem = item as TransactionListItemType; return transactionListItem.transactionID !== undefined; } +const getItemHeight = lodashMemoize((item: TransactionListItemType | ReportListItemType) => { + if (isTransactionListItemType(item)) { + return variables.optionRowHeight + listItemPadding; + } + + if (item.transactions.length === 0) { + return 0; + } + + if (item.transactions.length === 1) { + return variables.optionRowHeight + listItemPadding; + } + + const baseReportItemHeight = 72; + return baseReportItemHeight + item.transactions.length * 52 + listItemPadding; +}); + function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -133,6 +153,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { sortBy={sortBy} /> } + customListHeaderHeight={54} // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, // we have configured a larger windowSize and a longer delay between batch renders. // The windowSize determines the number of items rendered before and after the currently visible items. @@ -147,6 +168,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { ListItem={ListItem} sections={[{data: sortedData, isDisabled: false}]} onSelectRow={(item) => openReport(item)} + getItemHeight={getItemHeight} shouldDebounceRowSelect shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 6bbe69a1e2c6..8e3647b7e535 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -30,6 +30,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import arraysEqual from '@src/utils/arraysEqual'; import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, SectionListDataType, SectionWithIndexOffset, SelectionListHandle} from './types'; +const getDefaultItemHeight = () => variables.optionRowHeight; + function BaseSelectionList( { sections, @@ -40,6 +42,7 @@ function BaseSelectionList( onCheckboxPress, onSelectAll, onDismissError, + getItemHeight = getDefaultItemHeight, textInputLabel = '', textInputPlaceholder = '', textInputValue = '', @@ -71,6 +74,7 @@ function BaseSelectionList( isLoadingNewOptions = false, onLayout, customListHeader, + customListHeaderHeight = 0, listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, @@ -124,7 +128,8 @@ function BaseSelectionList( const disabledArrowKeyOptionsIndexes: number[] = []; let disabledIndex = 0; - let offset = 0; + // need to account that the list might have some extra content above it + let offset = customListHeader ? customListHeaderHeight : 0; const itemLayouts = [{length: 0, offset}]; const selectedOptions: TItem[] = []; @@ -154,7 +159,7 @@ function BaseSelectionList( disabledIndex += 1; // Account for the height of the item in getItemLayout - const fullItemHeight = variables.optionRowHeight; + const fullItemHeight = getItemHeight(item); itemLayouts.push({length: fullItemHeight, offset}); offset += fullItemHeight; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 5d5e7fc3891b..11b0ca6a2cd8 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -278,6 +278,12 @@ type BaseSelectionListProps = Partial & { /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */ onSelectAll?: () => void; + /** + * Callback that should return height of the specific item + * Only use this if we're handling some non-standard items, most of the time the default value is correct + */ + getItemHeight?: (item: TItem) => number; + /** Callback to fire when an error is dismissed */ onDismissError?: (item: TItem) => void; @@ -386,6 +392,9 @@ type BaseSelectionListProps = Partial & { /** Custom header to show right above list */ customListHeader?: ReactNode; + /** When customListHeader is provided, this should be its height needed for correct list scrolling */ + customListHeaderHeight?: number; + /** Styles for the list header wrapper */ listHeaderWrapperStyle?: StyleProp; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index a5d2ac1d9df2..70225078149c 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -116,9 +116,11 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { const value = {...data[key]}; const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${value.reportID}`; + const transactions = reportIDToTransactions[reportKey]?.transactions ?? []; + reportIDToTransactions[reportKey] = { ...value, - transactions: reportIDToTransactions[reportKey]?.transactions ?? [], + transactions, }; } else if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { const transactionItem = {...data[key]}; @@ -155,7 +157,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte } } - return Object.values(reportIDToTransactions); + return Object.values(reportIDToTransactions).filter((item) => item.transactions?.length !== 0); } const searchTypeToItemMap: SearchTypeToItemMap = { From c41ca39869b854372245dae73124f91b0e842bf9 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 12 Jun 2024 09:45:21 +0200 Subject: [PATCH 2/6] Fix lint in SelectionList --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 8e3647b7e535..c48cf9a3bc0b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -191,7 +191,7 @@ function BaseSelectionList( itemLayouts, allSelected: selectedOptions.length > 0 && selectedOptions.length === allOptions.length - disabledOptionsIndexes.length, }; - }, [canSelectMultiple, sections]); + }, [canSelectMultiple, sections, customListHeader, customListHeaderHeight, getItemHeight]); const [slicedSections, ShowMoreButtonInstance] = useMemo(() => { let remainingOptionsLimit = CONST.MAX_SELECTION_LIST_PAGE_LENGTH * currentPage; From a97b14df55c68ee77944479338239b3ef7b9096b Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 12 Jun 2024 13:28:39 +0200 Subject: [PATCH 3/6] Make sure Search scrolling works on narrow layout --- src/components/Search.tsx | 44 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 3dd8bfc2e586..837ba98c8063 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,11 +1,11 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; -import lodashMemoize from 'lodash/memoize'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -36,6 +36,8 @@ type SearchProps = { }; const sortableSearchTabs: SearchQuery[] = [CONST.TAB_SEARCH.ALL]; +const transactionItemMobileHeight = 100; +const reportItemTransactionHeight = 52; const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item function isTransactionListItemType(item: TransactionListItemType | ReportListItemType): item is TransactionListItemType { @@ -43,29 +45,33 @@ function isTransactionListItemType(item: TransactionListItemType | ReportListIte return transactionListItem.transactionID !== undefined; } -const getItemHeight = lodashMemoize((item: TransactionListItemType | ReportListItemType) => { - if (isTransactionListItemType(item)) { - return variables.optionRowHeight + listItemPadding; - } - - if (item.transactions.length === 0) { - return 0; - } - - if (item.transactions.length === 1) { - return variables.optionRowHeight + listItemPadding; - } - - const baseReportItemHeight = 72; - return baseReportItemHeight + item.transactions.length * 52 + listItemPadding; -}); - function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); + const {isLargeScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); + const getItemHeight = useCallback( + (item: TransactionListItemType | ReportListItemType) => { + if (isTransactionListItemType(item)) { + return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding; + } + + if (item.transactions.length === 0) { + return 0; + } + + if (item.transactions.length === 1) { + return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding; + } + + const baseReportItemHeight = isLargeScreenWidth ? 72 : 108; + return baseReportItemHeight + item.transactions.length * reportItemTransactionHeight + listItemPadding; + }, + [isLargeScreenWidth], + ); + const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder); const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); From c8904da557970f7e047e798065c6411eaec90a3c Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 12 Jun 2024 16:53:37 +0200 Subject: [PATCH 4/6] Remove filtering empty reports in Search --- src/libs/SearchUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 70225078149c..709faf36959f 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -157,7 +157,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte } } - return Object.values(reportIDToTransactions).filter((item) => item.transactions?.length !== 0); + return Object.values(reportIDToTransactions); } const searchTypeToItemMap: SearchTypeToItemMap = { From 5f1a9bed742eac484bcba1e3b57531ceeb90706c Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 17 Jun 2024 11:39:13 +0200 Subject: [PATCH 5/6] Add small fixes to search --- src/components/Search.tsx | 3 ++- src/libs/SearchUtils.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index c885b6631d05..e4faa15bfb94 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -39,6 +39,7 @@ const sortableSearchTabs: SearchQuery[] = [CONST.TAB_SEARCH.ALL]; const transactionItemMobileHeight = 100; const reportItemTransactionHeight = 52; const listItemPadding = 12; // this is equivalent to 'mb3' on every transaction/report list item +const searchHeaderHeight = 54; function isTransactionListItemType(item: TransactionListItemType | ReportListItemType): item is TransactionListItemType { const transactionListItem = item as TransactionListItemType; @@ -162,7 +163,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { shouldShowYear={shouldShowYear} /> } - customListHeaderHeight={54} + customListHeaderHeight={searchHeaderHeight} // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, // we have configured a larger windowSize and a longer delay between batch renders. // The windowSize determines the number of items rendered before and after the currently visible items. diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 4be04ac48802..a47915e3bf35 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -199,7 +199,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte } } - return Object.values(reportIDToTransactions); + return Object.values(reportIDToTransactions).filter((item) => item.transactions?.length !== 0); } const searchTypeToItemMap: SearchTypeToItemMap = { From eca4735958d2fc0f5933cf29ccc3e3b9212822df Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 18 Jun 2024 09:30:39 +0200 Subject: [PATCH 6/6] Remove extra filtering of Search ReportItems --- src/libs/SearchUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index a47915e3bf35..4be04ac48802 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -199,7 +199,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte } } - return Object.values(reportIDToTransactions).filter((item) => item.transactions?.length !== 0); + return Object.values(reportIDToTransactions); } const searchTypeToItemMap: SearchTypeToItemMap = {