Skip to content

Commit

Permalink
Merge pull request Expensify#53466 from software-mansion-labs/fix/use…
Browse files Browse the repository at this point in the history
…-navigation-reset-on-layout-change

The central page page is not rendered when resized to narrow and then to wide view again
  • Loading branch information
mountiny authored Dec 11, 2024
2 parents c1454f7 + de1a3d3 commit b49e1bd
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ParamListBase} from '@react-navigation/native';
import {createNavigatorFactory} from '@react-navigation/native';
import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange';
import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
Expand All @@ -12,7 +12,7 @@ const ResponsiveStackNavigatorComponent = createPlatformStackNavigatorComponent(
createRouter: CustomRouter,
defaultScreenOptions: defaultPlatformStackScreenOptions,
useCustomState: useStateWithSearch,
useCustomEffects: useNavigationResetOnLayoutChange,
useCustomEffects: useNavigationResetRootOnLayoutChange,
ExtraContent: RenderSearchRoute,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import type {ParamListBase} from '@react-navigation/native';
import {useEffect} from 'react';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import navigationRef from '@libs/Navigation/navigationRef';
import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types';

function useNavigationResetOnLayoutChange() {
/**
* This hook resets the navigation root state when changing the layout size, resetting the state calls the getRehydredState method in CustomFullScreenRouter.tsx.
* When the screen size is changed, it is necessary to check whether the application displays the content correctly.
* When the app is opened on a small layout and the user resizes it to wide, a second screen has to be present in the navigation state to fill the space.
*/
function useNavigationResetOnLayoutChange({navigation}: CustomEffectsHookProps<ParamListBase>) {
const {shouldUseNarrowLayout} = useResponsiveLayout();

useEffect(() => {
if (!navigationRef.isReady()) {
return;
}
navigationRef.resetRoot(navigationRef.getRootState());
navigation.reset(navigation.getState());
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [shouldUseNarrowLayout]);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {useEffect} from 'react';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import navigationRef from '@libs/Navigation/navigationRef';

/**
* This hook resets the navigation root state when changing the layout size, resetting the state calls the getRehydredState method in CustomRouter.ts.
* When the screen size is changed, it is necessary to check whether the application displays the content correctly.
* When the app is opened on a small layout and the user resizes it to wide, a second screen has to be present in the navigation state to fill the space.
*/
function useNavigationResetRootOnLayoutChange() {
const {shouldUseNarrowLayout} = useResponsiveLayout();

useEffect(() => {
if (!navigationRef.isReady()) {
return;
}
navigationRef.resetRoot(navigationRef.getRootState());
}, [shouldUseNarrowLayout]);
}

export default useNavigationResetRootOnLayoutChange;
95 changes: 95 additions & 0 deletions tests/ui/ResizeScreenTests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {NavigationContainer} from '@react-navigation/native';
import {render, renderHook} from '@testing-library/react-native';
import React from 'react';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import createResponsiveStackNavigator from '@libs/Navigation/AppNavigator/createResponsiveStackNavigator';
import BottomTabNavigator from '@libs/Navigation/AppNavigator/Navigators/BottomTabNavigator';
import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange';
import navigationRef from '@libs/Navigation/navigationRef';
import type {AuthScreensParamList} from '@libs/Navigation/types';
import ProfilePage from '@pages/settings/Profile/ProfilePage';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';

const RootStack = createResponsiveStackNavigator<AuthScreensParamList>();

jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
jest.mock('@libs/getIsNarrowLayout', () => jest.fn());

jest.mock('@pages/settings/InitialSettingsPage');
jest.mock('@pages/settings/Profile/ProfilePage');
jest.mock('@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar');

const DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE: ResponsiveLayoutResult = {
shouldUseNarrowLayout: true,
isSmallScreenWidth: true,
isInNarrowPaneModal: false,
isExtraSmallScreenHeight: false,
isMediumScreenWidth: false,
isLargeScreenWidth: false,
isExtraSmallScreenWidth: false,
isSmallScreen: false,
onboardingIsMediumOrLargerScreenWidth: false,
};

const INITIAL_STATE = {
routes: [
{
name: NAVIGATORS.BOTTOM_TAB_NAVIGATOR,
state: {
index: 1,
routes: [{name: SCREENS.HOME}, {name: SCREENS.SETTINGS.ROOT}],
},
},
],
};

const mockedGetIsNarrowLayout = getIsNarrowLayout as jest.MockedFunction<typeof getIsNarrowLayout>;
const mockedUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction<typeof useResponsiveLayout>;

describe('Resize screen', () => {
it('Should display the settings profile after resizing the screen with the settings page opened to the wide layout', () => {
// Given the initialized navigation on the narrow layout with the settings screen
mockedGetIsNarrowLayout.mockReturnValue(true);
mockedUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: true});

const {rerender} = renderHook(() => useNavigationResetRootOnLayoutChange());

render(
<NavigationContainer
ref={navigationRef}
initialState={INITIAL_STATE}
>
<RootStack.Navigator>
<RootStack.Screen
name={NAVIGATORS.BOTTOM_TAB_NAVIGATOR}
component={BottomTabNavigator}
/>

<RootStack.Screen
name={SCREENS.SETTINGS.PROFILE.ROOT}
component={ProfilePage}
/>
</RootStack.Navigator>
</NavigationContainer>,
);

const rootStateBeforeResize = navigationRef.current?.getRootState();

expect(rootStateBeforeResize?.routes.at(0)?.name).toBe(NAVIGATORS.BOTTOM_TAB_NAVIGATOR);
expect(rootStateBeforeResize?.routes.at(1)).toBeUndefined();

// When resizing the screen to the wide layout
mockedGetIsNarrowLayout.mockReturnValue(false);
mockedUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: false});
rerender({});

const rootStateAfterResize = navigationRef.current?.getRootState();

// Then the settings profile page should be displayed on the screen
expect(rootStateAfterResize?.routes.at(0)?.name).toBe(NAVIGATORS.BOTTOM_TAB_NAVIGATOR);
expect(rootStateAfterResize?.routes.at(1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT);
});
});

0 comments on commit b49e1bd

Please sign in to comment.