From 1f1bcac694512c6f38cb64c291a49a517aa8539c Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 12 Mar 2024 18:46:24 +0100 Subject: [PATCH 1/6] save scroll position on HOME screen --- src/App.tsx | 2 + .../LHNOptionsList/LHNOptionsList.tsx | 28 +++++- .../ScrollOffsetContextProvider.tsx | 93 +++++++++++++++++++ src/libs/Navigation/NavigationRoot.tsx | 7 +- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/components/ScrollOffsetContextProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index 0e247d5faa53..b9d1129c1067 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; +import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; @@ -69,6 +70,7 @@ function App({url}: AppProps) { KeyboardStateProvider, PopoverContextProvider, CurrentReportIDContextProvider, + ScrollOffsetContextProvider, ReportAttachmentsProvider, PickerStateProvider, EnvironmentProvider, diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index f5545f402b14..22df45901fc2 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,8 +1,11 @@ +import {useRoute} from '@react-navigation/native'; +import type {FlashListProps} from '@shopify/flash-list'; import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; +import React, {memo, useCallback, useContext, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import withCurrentReportID from '@components/withCurrentReportID'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -34,6 +37,10 @@ function LHNOptionsList({ onFirstItemRendered = () => {}, reportIDsWithErrors = {}, }: LHNOptionsListProps) { + const {saveScrollOffset, scrollToOffset} = useContext(ScrollOffsetContext); + const flashListRef = useRef>(null); + const route = useRoute(); + const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); @@ -116,9 +123,26 @@ function LHNOptionsList({ ], ); + const onScroll = useCallback['onScroll']>>( + (e) => { + // If the layout measurement is 0, it means the flashlist is not displayed but the onScroll may be triggered with offset value 0. + // We should ignore this case. + if (e.nativeEvent.layoutMeasurement.height === 0) { + return; + } + saveScrollOffset(route, e.nativeEvent.contentOffset.y); + }, + [route, saveScrollOffset], + ); + + const onLayout = useCallback(() => { + scrollToOffset(route, flashListRef); + }, [route, flashListRef, scrollToOffset]); + return ( ); diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx new file mode 100644 index 000000000000..d56ce7793983 --- /dev/null +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -0,0 +1,93 @@ +import type {ParamListBase, RouteProp} from '@react-navigation/native'; +import type {FlashList} from '@shopify/flash-list'; +import React, {createContext, useCallback, useMemo, useRef} from 'react'; +import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; + +type ScrollOffsetContextValue = { + /** Save scroll offset of flashlist on given screen */ + saveScrollOffset: (route: RouteProp, scrollOffset: number) => void; + + /** Scroll to saved offset of given screen */ + scrollToOffset: (route: RouteProp, flashListRef: React.RefObject>) => void; + + /** Clean scroll offsets of screen that aren't anymore in the state */ + cleanStaleScrollOffsets: (state: State) => void; +}; + +type ScrollOffsetContextProviderProps = { + /** Actual content wrapped by this component */ + children: React.ReactNode; +}; + +const defaultValue: ScrollOffsetContextValue = { + saveScrollOffset: () => {}, + scrollToOffset: () => {}, + cleanStaleScrollOffsets: () => {}, +}; + +const ScrollOffsetContext = createContext(defaultValue); + +/** This function is prepared to work with HOME screens. May need modification if we want to handle other types of screens. */ +function getKey(route: RouteProp | NavigationPartialRoute): string { + if (route.params && 'policyID' in route.params && typeof route.params.policyID === 'string') { + return `${route.name}-${route.params.policyID}`; + } + return `${route.name}-global`; +} + +export default function ScrollOffsetContextProvider(props: ScrollOffsetContextProviderProps) { + const scrollOffsetsRef = useRef>({}); + + const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => { + scrollOffsetsRef.current[getKey(route)] = scrollOffset; + }, []); + + const scrollToOffset: ScrollOffsetContextValue['scrollToOffset'] = useCallback((route, flashListRef) => { + const offset = scrollOffsetsRef.current[getKey(route)]; + + if (!(offset && flashListRef.current)) { + return; + } + + // We need to use requestAnimationFrame to make sure it will scroll properly on iOS. + requestAnimationFrame(() => { + if (!(offset && flashListRef.current)) { + return; + } + flashListRef.current.scrollToOffset({offset}); + }); + }, []); + + const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback((state) => { + const bottomTabNavigator = state.routes.find((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); + if (bottomTabNavigator?.state && 'routes' in bottomTabNavigator.state) { + const bottomTabNavigatorRoutes = bottomTabNavigator.state.routes; + const scrollOffsetkeysOfExistingScreens = bottomTabNavigatorRoutes.map((route) => getKey(route)); + for (const key of Object.keys(scrollOffsetsRef.current)) { + if (!scrollOffsetkeysOfExistingScreens.includes(key)) { + delete scrollOffsetsRef.current[key]; + } + } + } + }, []); + + /** + * The context this component exposes to child components + * @returns currentReportID to share between central pane and LHN + */ + const contextValue = useMemo( + (): ScrollOffsetContextValue => ({ + saveScrollOffset, + scrollToOffset, + cleanStaleScrollOffsets, + }), + [saveScrollOffset, scrollToOffset, cleanStaleScrollOffsets], + ); + + return {props.children}; +} + +export {ScrollOffsetContext}; + +export type {ScrollOffsetContextProviderProps, ScrollOffsetContextValue}; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 20c426a74c71..265a710667bd 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,6 +1,7 @@ import type {NavigationState} from '@react-navigation/native'; import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native'; -import React, {useEffect, useMemo, useRef} from 'react'; +import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useFlipper from '@hooks/useFlipper'; @@ -63,6 +64,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N useFlipper(navigationRef); const firstRenderRef = useRef(true); const theme = useTheme(); + const {cleanStaleScrollOffsets} = useContext(ScrollOffsetContext); const currentReportIDValue = useCurrentReportID(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -125,6 +127,9 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N setActiveWorkspaceID(activeWorkspaceID); }, 0); parseAndLogRoute(state); + + // We want to clean saved scroll offsets for screens that aren't anymore in the state. + cleanStaleScrollOffsets(state); }; return ( From 6246bc8dff9d034f803f740e5e8d9b42292ce399 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 13 Mar 2024 15:31:29 +0100 Subject: [PATCH 2/6] fix tests --- tests/utils/LHNTestUtils.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 80f28002f975..602397f62672 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -39,6 +39,7 @@ jest.mock('@react-navigation/native', (): typeof Navigation => { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, + useRoute: jest.fn(), useFocusEffect: jest.fn(), useIsFocused: () => ({ navigate: mockedNavigate, From a792bb18969491ac1c25735945ed69763fe54b72 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 14 Mar 2024 15:11:59 +0100 Subject: [PATCH 3/6] save scroll position for settings tab --- .../LHNOptionsList/LHNOptionsList.tsx | 18 ++++++-- .../ScrollOffsetContextProvider.tsx | 26 ++++------- src/pages/settings/InitialSettingsPage.tsx | 46 +++++++++++++++++-- 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index c110582ad615..4b2cfcff35f6 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -36,7 +36,7 @@ function LHNOptionsList({ transactionViolations = {}, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { - const {saveScrollOffset, scrollToOffset} = useContext(ScrollOffsetContext); + const {saveScrollOffset, getScrollOffset} = useContext(ScrollOffsetContext); const flashListRef = useRef>(null); const route = useRoute(); @@ -132,8 +132,20 @@ function LHNOptionsList({ ); const onLayout = useCallback(() => { - scrollToOffset(route, flashListRef); - }, [route, flashListRef, scrollToOffset]); + const offset = getScrollOffset(route); + + if (!(offset && flashListRef.current)) { + return; + } + + // We need to use requestAnimationFrame to make sure it will scroll properly on iOS. + requestAnimationFrame(() => { + if (!(offset && flashListRef.current)) { + return; + } + flashListRef.current.scrollToOffset({offset}); + }); + }, [route, flashListRef, getScrollOffset]); return ( diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index d56ce7793983..1d5c237860ef 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -1,5 +1,4 @@ import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import type {FlashList} from '@shopify/flash-list'; import React, {createContext, useCallback, useMemo, useRef} from 'react'; import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -8,8 +7,8 @@ type ScrollOffsetContextValue = { /** Save scroll offset of flashlist on given screen */ saveScrollOffset: (route: RouteProp, scrollOffset: number) => void; - /** Scroll to saved offset of given screen */ - scrollToOffset: (route: RouteProp, flashListRef: React.RefObject>) => void; + /** Get scroll offset value for given screen */ + getScrollOffset: (route: RouteProp) => number | undefined; /** Clean scroll offsets of screen that aren't anymore in the state */ cleanStaleScrollOffsets: (state: State) => void; @@ -22,7 +21,7 @@ type ScrollOffsetContextProviderProps = { const defaultValue: ScrollOffsetContextValue = { saveScrollOffset: () => {}, - scrollToOffset: () => {}, + getScrollOffset: () => undefined, cleanStaleScrollOffsets: () => {}, }; @@ -43,20 +42,11 @@ export default function ScrollOffsetContextProvider(props: ScrollOffsetContextPr scrollOffsetsRef.current[getKey(route)] = scrollOffset; }, []); - const scrollToOffset: ScrollOffsetContextValue['scrollToOffset'] = useCallback((route, flashListRef) => { - const offset = scrollOffsetsRef.current[getKey(route)]; - - if (!(offset && flashListRef.current)) { + const getScrollOffset: ScrollOffsetContextValue['getScrollOffset'] = useCallback((route) => { + if (!scrollOffsetsRef.current) { return; } - - // We need to use requestAnimationFrame to make sure it will scroll properly on iOS. - requestAnimationFrame(() => { - if (!(offset && flashListRef.current)) { - return; - } - flashListRef.current.scrollToOffset({offset}); - }); + return scrollOffsetsRef.current[getKey(route)]; }, []); const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback((state) => { @@ -79,10 +69,10 @@ export default function ScrollOffsetContextProvider(props: ScrollOffsetContextPr const contextValue = useMemo( (): ScrollOffsetContextValue => ({ saveScrollOffset, - scrollToOffset, + getScrollOffset, cleanStaleScrollOffsets, }), - [saveScrollOffset, scrollToOffset, cleanStaleScrollOffsets], + [saveScrollOffset, getScrollOffset, cleanStaleScrollOffsets], ); return {props.children}; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 811ce38bdd1a..4f47b9949bff 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,5 +1,7 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import {useRoute} from '@react-navigation/native'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, ScrollView as RNScrollView, ScrollViewProps, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -13,6 +15,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; +import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; @@ -437,13 +440,50 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa ); + const {saveScrollOffset, getScrollOffset} = useContext(ScrollOffsetContext); + const route = useRoute(); + const scrollViewRef = useRef(null); + + const onScroll = useCallback>( + (e) => { + // If the layout measurement is 0, it means the flashlist is not displayed but the onScroll may be triggered with offset value 0. + // We should ignore this case. + if (e.nativeEvent.layoutMeasurement.height === 0) { + return; + } + saveScrollOffset(route, e.nativeEvent.contentOffset.y); + }, + [route, saveScrollOffset], + ); + + const [isAfterOnLayout, setIsAfterOnLayout] = useState(false); + + const onLayout = useCallback(() => { + const scrollOffset = getScrollOffset(route); + setIsAfterOnLayout(true); + if (!scrollOffset || !scrollViewRef.current) { + return; + } + scrollViewRef.current.scrollTo({y: scrollOffset, animated: false}); + }, [getScrollOffset, route]); + + const scrollOffset = getScrollOffset(route); + return ( - + {headerContent} {accountMenuItems} {workspaceMenuItems} From 0c68a22add83cda0adad0e79231870c87305f58a Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 21 Mar 2024 12:20:44 +0100 Subject: [PATCH 4/6] reset saved scroll positions for HOME if the priority mode changed --- .../ScrollOffsetContextProvider.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 1d5c237860ef..6ecd7750bd8f 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -1,7 +1,11 @@ import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import React, {createContext, useCallback, useMemo, useRef} from 'react'; +import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type {PriorityMode} from '@src/types/onyx'; type ScrollOffsetContextValue = { /** Save scroll offset of flashlist on given screen */ @@ -14,7 +18,12 @@ type ScrollOffsetContextValue = { cleanStaleScrollOffsets: (state: State) => void; }; -type ScrollOffsetContextProviderProps = { +type ScrollOffsetContextProviderOnyxProps = { + /** The chat priority mode */ + priorityMode: PriorityMode; +}; + +type ScrollOffsetContextProviderProps = ScrollOffsetContextProviderOnyxProps & { /** Actual content wrapped by this component */ children: React.ReactNode; }; @@ -35,8 +44,24 @@ function getKey(route: RouteProp | NavigationPartialRoute): strin return `${route.name}-global`; } -export default function ScrollOffsetContextProvider(props: ScrollOffsetContextProviderProps) { +function ScrollOffsetContextProvider({children, priorityMode}: ScrollOffsetContextProviderProps) { const scrollOffsetsRef = useRef>({}); + const previousPriorityMode = useRef(priorityMode); + + useEffect(() => { + if (previousPriorityMode.current === priorityMode) { + return; + } + + // 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. + for (const key of Object.keys(scrollOffsetsRef.current)) { + if (key.includes(SCREENS.HOME)) { + delete scrollOffsetsRef.current[key]; + } + } + + previousPriorityMode.current = priorityMode; + }, [priorityMode]); const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => { scrollOffsetsRef.current[getKey(route)] = scrollOffset; @@ -75,9 +100,15 @@ export default function ScrollOffsetContextProvider(props: ScrollOffsetContextPr [saveScrollOffset, getScrollOffset, cleanStaleScrollOffsets], ); - return {props.children}; + return {children}; } +export default withOnyx({ + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + }, +})(ScrollOffsetContextProvider); + export {ScrollOffsetContext}; export type {ScrollOffsetContextProviderProps, ScrollOffsetContextValue}; From 873add6328c61fdd5eac72e45930226dff4876e3 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 22 Mar 2024 11:12:35 +0100 Subject: [PATCH 5/6] use usePrevious instead useRef --- src/components/ScrollOffsetContextProvider.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 6ecd7750bd8f..d7815d7a65a0 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -1,6 +1,7 @@ import type {ParamListBase, RouteProp} from '@react-navigation/native'; import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; +import usePrevious from '@hooks/usePrevious'; import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -46,10 +47,10 @@ function getKey(route: RouteProp | NavigationPartialRoute): strin function ScrollOffsetContextProvider({children, priorityMode}: ScrollOffsetContextProviderProps) { const scrollOffsetsRef = useRef>({}); - const previousPriorityMode = useRef(priorityMode); + const previousPriorityMode = usePrevious(priorityMode); useEffect(() => { - if (previousPriorityMode.current === priorityMode) { + if (previousPriorityMode === null || previousPriorityMode === priorityMode) { return; } @@ -59,9 +60,7 @@ function ScrollOffsetContextProvider({children, priorityMode}: ScrollOffsetConte delete scrollOffsetsRef.current[key]; } } - - previousPriorityMode.current = priorityMode; - }, [priorityMode]); + }, [priorityMode, previousPriorityMode]); const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => { scrollOffsetsRef.current[getKey(route)] = scrollOffset; @@ -87,10 +86,6 @@ function ScrollOffsetContextProvider({children, priorityMode}: ScrollOffsetConte } }, []); - /** - * The context this component exposes to child components - * @returns currentReportID to share between central pane and LHN - */ const contextValue = useMemo( (): ScrollOffsetContextValue => ({ saveScrollOffset, From 830fb279e66f3a943257f7c480e25200ac3d188a Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 22 Mar 2024 17:09:46 +0100 Subject: [PATCH 6/6] reset scroll position on option mode change --- .../LHNOptionsList/LHNOptionsList.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 9af552b151d0..04573c8bccac 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -2,11 +2,12 @@ import {useRoute} from '@react-navigation/native'; import type {FlashListProps} from '@shopify/flash-list'; import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback, useContext, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; @@ -118,6 +119,21 @@ function LHNOptionsList({ const extraData = useMemo(() => [reportActions, reports, policy, personalDetails, data.length], [reportActions, reports, policy, personalDetails, data.length]); + const previousOptionMode = usePrevious(optionMode); + + useEffect(() => { + if (previousOptionMode === null || previousOptionMode === optionMode || !flashListRef.current) { + return; + } + + if (!flashListRef.current) { + return; + } + + // If the option mode changes want to scroll to the top of the list because rendered items will have different height. + flashListRef.current.scrollToOffset({offset: 0}); + }, [previousOptionMode, optionMode]); + const onScroll = useCallback['onScroll']>>( (e) => { // If the layout measurement is 0, it means the flashlist is not displayed but the onScroll may be triggered with offset value 0.