Skip to content

Commit 66b4e95

Browse files
authored
Merge pull request #68498 from huult/66751-fix-back-button-scroll-reset
Fix back button on report detail page resetting scroll position on re…
2 parents d1b5bdd + c4ced95 commit 66b4e95

2 files changed

Lines changed: 86 additions & 14 deletions

File tree

src/components/ScrollOffsetContextProvider.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {ParamListBase} from '@react-navigation/native';
2+
import {findFocusedRoute} from '@react-navigation/native';
23
import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react';
34
import useOnyx from '@hooks/useOnyx';
45
import usePrevious from '@hooks/usePrevious';
@@ -40,11 +41,21 @@ const defaultValue: ScrollOffsetContextValue = {
4041

4142
const ScrollOffsetContext = createContext<ScrollOffsetContextValue>(defaultValue);
4243

43-
/** This function is prepared to work with HOME screens. May need modification if we want to handle other types of screens. */
44+
/** This function is prepared to work with HOME and SEARCH screens. */
4445
function getKey(route: PlatformStackRouteProp<ParamListBase> | NavigationPartialRoute): string {
46+
// Handle routes with direct policyID parameter (HOME screens)
4547
if (route.params && 'policyID' in route.params && typeof route.params.policyID === 'string') {
4648
return `${route.name}-${route.params.policyID}`;
4749
}
50+
51+
// Handle SEARCH screens with query parameters
52+
if (route.name === SCREENS.SEARCH.ROOT && route.params && 'q' in route.params && typeof route.params.q === 'string') {
53+
// Encode the query to handle spaces and special characters
54+
const encodedQuery = encodeURIComponent(route.params.q);
55+
return `${route.name}-${encodedQuery}`;
56+
}
57+
58+
// For other routes, just use route name
4859
return `${route.name}-global`;
4960
}
5061

@@ -58,9 +69,9 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
5869
return;
5970
}
6071

61-
// If the priority mode changes, we need to clear the scroll offsets for the home screens because it affects the size of the elements and scroll positions wouldn't be correct.
72+
// If the priority mode changes, we need to clear the scroll offsets for the home and search screens because it affects the size of the elements and scroll positions wouldn't be correct.
6273
for (const key of Object.keys(scrollOffsetsRef.current)) {
63-
if (key.includes(SCREENS.HOME)) {
74+
if (key.includes(SCREENS.HOME) || key.includes(SCREENS.SEARCH.ROOT)) {
6475
delete scrollOffsetsRef.current[key];
6576
}
6677
}
@@ -77,16 +88,39 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
7788
return scrollOffsetsRef.current[getKey(route)];
7889
}, []);
7990

80-
const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback((state) => {
81-
const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
82-
const scrollOffsetKeysOfExistingScreens = sidebarRoutes.map((route) => getKey(route));
83-
for (const key of Object.keys(scrollOffsetsRef.current)) {
84-
if (!scrollOffsetKeysOfExistingScreens.includes(key)) {
85-
delete scrollOffsetsRef.current[key];
91+
const cleanScrollOffsets = useCallback((keys: string[], shouldDelete: (key: string) => boolean) => {
92+
keys.forEach((key) => {
93+
if (!shouldDelete(key)) {
94+
return;
8695
}
87-
}
96+
97+
delete scrollOffsetsRef.current[key];
98+
});
8899
}, []);
89100

101+
const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback(
102+
(state) => {
103+
const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
104+
const existingScreenKeys = sidebarRoutes.map(getKey);
105+
106+
const focusedRoute = findFocusedRoute(state);
107+
const routeName = focusedRoute?.name;
108+
109+
const isSearchScreen = routeName === SCREENS.SEARCH.ROOT;
110+
const isSearchMoneyRequestReport = routeName === SCREENS.SEARCH.MONEY_REQUEST_REPORT || routeName === SCREENS.SEARCH.REPORT_RHP;
111+
112+
const scrollOffsetKeys = Object.keys(scrollOffsetsRef.current);
113+
114+
if (isSearchScreen || isSearchMoneyRequestReport) {
115+
const currentKey = focusedRoute ? getKey(focusedRoute) : null;
116+
cleanScrollOffsets(scrollOffsetKeys, (key) => key.startsWith(SCREENS.SEARCH.ROOT) && key !== currentKey && !isSearchMoneyRequestReport);
117+
return;
118+
}
119+
cleanScrollOffsets(scrollOffsetKeys, (key) => !existingScreenKeys.includes(key));
120+
},
121+
[cleanScrollOffsets],
122+
);
123+
90124
const saveScrollIndex: ScrollOffsetContextValue['saveScrollIndex'] = useCallback((route, scrollIndex) => {
91125
scrollOffsetsRef.current[getKey(route)] = scrollIndex;
92126
}, []);
@@ -95,6 +129,7 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
95129
if (!scrollOffsetsRef.current) {
96130
return;
97131
}
132+
98133
return scrollOffsetsRef.current[getKey(route)];
99134
}, []);
100135

src/components/Search/SearchList.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {useIsFocused} from '@react-navigation/native';
1+
import {useIsFocused, useRoute} from '@react-navigation/native';
22
import {FlashList} from '@shopify/flash-list';
33
import type {FlashListProps, ViewToken} from '@shopify/flash-list';
4-
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
4+
import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
55
import type {ForwardedRef} from 'react';
66
import {View} from 'react-native';
77
import type {NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
@@ -12,6 +12,7 @@ import MenuItem from '@components/MenuItem';
1212
import Modal from '@components/Modal';
1313
import {usePersonalDetails} from '@components/OnyxListItemProvider';
1414
import {PressableWithFeedback} from '@components/Pressable';
15+
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
1516
import type ChatListItem from '@components/SelectionList/ChatListItem';
1617
import type TaskListItem from '@components/SelectionList/Search/TaskListItem';
1718
import type TransactionGroupListItem from '@components/SelectionList/Search/TransactionGroupListItem';
@@ -182,6 +183,9 @@ function SearchList(
182183
const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true});
183184
const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true});
184185

186+
const route = useRoute();
187+
const {saveScrollOffset, getScrollOffset} = useContext(ScrollOffsetContext);
188+
185189
const handleLongPressRow = useCallback(
186190
(item: SearchListItem) => {
187191
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -420,6 +424,39 @@ function SearchList(
420424
};
421425
}, [calculatedListHeight, calculatedListWidth]);
422426

427+
const handleScroll = useCallback<NonNullable<FlashListProps<SearchListItem>['onScroll']>>(
428+
(e) => {
429+
if (onScroll && typeof onScroll === 'function') {
430+
onScroll(e);
431+
}
432+
433+
if (e.nativeEvent.layoutMeasurement.height > 0) {
434+
saveScrollOffset(route, e.nativeEvent.contentOffset.y);
435+
}
436+
},
437+
[onScroll, route, saveScrollOffset],
438+
);
439+
440+
const handleLayout = useCallback(() => {
441+
if (onLayout && typeof onLayout === 'function') {
442+
onLayout();
443+
}
444+
445+
const offset = getScrollOffset(route);
446+
if (!offset || !listRef.current) {
447+
return;
448+
}
449+
450+
// Use requestAnimationFrame to ensure proper scrolling on iOS
451+
requestAnimationFrame(() => {
452+
if (!offset || !listRef.current) {
453+
return;
454+
}
455+
456+
listRef.current.scrollToOffset({offset});
457+
});
458+
}, [onLayout, getScrollOffset, route]);
459+
423460
return (
424461
<View style={[styles.flex1, !isKeyboardShown && safeAreaPaddingBottomStyle, containerStyle]}>
425462
{tableHeaderVisible && (
@@ -457,15 +494,15 @@ function SearchList(
457494
data={data}
458495
renderItem={renderItem}
459496
keyExtractor={keyExtractor}
460-
onScroll={onScroll}
497+
onScroll={handleScroll}
461498
showsVerticalScrollIndicator={false}
462499
ref={listRef}
463500
extraData={[focusedIndex, isFocused]}
464501
onEndReached={onEndReached}
465502
onEndReachedThreshold={onEndReachedThreshold}
466503
ListFooterComponent={ListFooterComponent}
467504
onViewableItemsChanged={onViewableItemsChanged}
468-
onLayout={onLayout}
505+
onLayout={handleLayout}
469506
removeClippedSubviews
470507
drawDistance={1000}
471508
estimatedItemSize={estimatedItemSize}

0 commit comments

Comments
 (0)