Skip to content

Commit e9a3ad1

Browse files
authored
Merge pull request Expensify#67610 from software-mansion-labs/nav/side-panel-custom-history
An additional entry is added to the browser history when opening the app
2 parents a55542a + e1dd0bf commit e9a3ad1

9 files changed

Lines changed: 137 additions & 14 deletions

File tree

src/CONST/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5161,6 +5161,7 @@ const CONST = {
51615161
SF_COORDINATES: [-122.4194, 37.7749],
51625162

51635163
NAVIGATION: {
5164+
CUSTOM_HISTORY_ENTRY_SIDE_PANEL: 'CUSTOM_HISTORY-SIDE_PANEL',
51645165
ACTION_TYPE: {
51655166
REPLACE: 'REPLACE',
51665167
PUSH: 'PUSH',
@@ -5174,6 +5175,7 @@ const CONST = {
51745175
OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT',
51755176
SET_HISTORY_PARAM: 'SET_HISTORY_PARAM',
51765177
REPLACE_PARAMS: 'REPLACE_PARAMS',
5178+
TOGGLE_SIDE_PANEL_WITH_HISTORY: 'TOGGLE_SIDE_PANEL_WITH_HISTORY',
51775179
},
51785180
},
51795181
TIME_PERIOD: {

src/components/SidePanel/HelpModal/index.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,7 @@ function Help({sidePanelTranslateX, closeSidePanel, shouldHideSidePanelBackdrop}
4747
// Web back button: push history state and close Side Panel on popstate
4848
useEffect(() => {
4949
ComposerFocusManager.resetReadyToFocus(uniqueModalId);
50-
window.history.pushState({isSidePanelOpen: true}, '', null);
51-
const handlePopState = () => {
52-
if (isExtraLargeScreenWidth) {
53-
return;
54-
}
55-
56-
closeSidePanel();
57-
};
58-
59-
window.addEventListener('popstate', handlePopState);
6050
return () => {
61-
window.removeEventListener('popstate', handlePopState);
6251
ComposerFocusManager.setReadyToFocus(uniqueModalId);
6352
};
6453
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps

src/components/SidePanel/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import React from 'react';
22
import useSidePanel from '@hooks/useSidePanel';
33
import Help from './HelpModal';
4+
import useSyncSidePanelWithHistory from './useSyncSidePanelWithHistory';
45

56
function SidePanel() {
67
const {isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel} = useSidePanel();
78

9+
// This hook synchronizes the side panel visibility with the browser history when it is displayed as RHP.
10+
// This means when you open or close the side panel, an entry connected with it is added to or removed from the browser history,
11+
// allowing this modal to be toggled using browser's "go back" and "go forward" buttons.
12+
useSyncSidePanelWithHistory();
13+
814
if (isSidePanelTransitionEnded && shouldHideSidePanel) {
915
return null;
1016
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Side panel synchronization with the browser history is only supported for web
2+
export default function useSyncSidePanelWithHistory() {}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {useNavigationState} from '@react-navigation/native';
2+
import {useEffect} from 'react';
3+
import usePrevious from '@hooks/usePrevious';
4+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
5+
import useSidePanel from '@hooks/useSidePanel';
6+
import navigationRef from '@libs/Navigation/navigationRef';
7+
import CONST from '@src/CONST';
8+
9+
export default function useSyncSidePanelWithHistory() {
10+
const {closeSidePanel, openSidePanel, shouldHideSidePanel} = useSidePanel();
11+
const {isExtraLargeScreenWidth} = useResponsiveLayout();
12+
const lastHistoryEntry = useNavigationState((state) => state?.history?.at(-1));
13+
const previousLastHistoryEntry = usePrevious(lastHistoryEntry);
14+
15+
useEffect(() => {
16+
// If the window width has been expanded and the modal is displayed, remove its history entry.
17+
// The side panel is only synced with the history when it's displayed as RHP.
18+
if (!shouldHideSidePanel && isExtraLargeScreenWidth) {
19+
navigationRef.dispatch({
20+
type: CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY,
21+
payload: {isVisible: false},
22+
});
23+
return;
24+
}
25+
26+
// When shouldHideSidePanel changes, synchronize the side panel with the browser history.
27+
navigationRef.dispatch({
28+
type: CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY,
29+
payload: {isVisible: !shouldHideSidePanel},
30+
});
31+
}, [shouldHideSidePanel, isExtraLargeScreenWidth]);
32+
33+
useEffect(() => {
34+
// The side panel is synced with the browser history only when displayed in RHP.
35+
if (isExtraLargeScreenWidth) {
36+
return;
37+
}
38+
39+
const hasHistoryChanged = previousLastHistoryEntry !== lastHistoryEntry;
40+
41+
// If nothing has changed in the browser history, do nothing.
42+
if (!hasHistoryChanged) {
43+
return;
44+
}
45+
46+
const hasSidePanelBeenClosed = previousLastHistoryEntry === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL;
47+
48+
// If the side panel history entry is not the last one and the modal is displayed, close it.
49+
if (hasSidePanelBeenClosed && !shouldHideSidePanel) {
50+
closeSidePanel();
51+
return;
52+
}
53+
54+
const hasSidePanelBeenOpened = lastHistoryEntry === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL;
55+
56+
// If the side panel history entry is the last one and the modal is not displayed, open it.
57+
if (hasSidePanelBeenOpened && shouldHideSidePanel) {
58+
openSidePanel();
59+
}
60+
}, [closeSidePanel, lastHistoryEntry, previousLastHistoryEntry, openSidePanel, shouldHideSidePanel, isExtraLargeScreenWidth]);
61+
}

src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import {StackActions} from '@react-navigation/native';
33
import type {ParamListBase, Router} from '@react-navigation/routers';
44
import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR';
55
import Log from '@libs/Log';
6+
import CONST from '@src/CONST';
67
import NAVIGATORS from '@src/NAVIGATORS';
78
import SCREENS from '@src/SCREENS';
8-
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType} from './types';
9+
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types';
910

1011
const MODAL_ROUTES_TO_DISMISS: string[] = [
1112
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
@@ -154,6 +155,26 @@ function handleNavigatingToModalFromModal(
154155
return stackRouter.getStateForAction(modifiedState, action, configOptions);
155156
}
156157

158+
function handleToggleSidePanelWithHistoryAction(state: StackNavigationState<ParamListBase>, action: ToggleSidePanelWithHistoryActionType) {
159+
// This shouldn't ever happen as the history should be always defined. It's for type safety.
160+
if (!state?.history) {
161+
return state;
162+
}
163+
164+
// If it's set to true, we need to add the side panel history entry if it's not already there.
165+
if (action.payload.isVisible && state.history.at(-1) !== CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL) {
166+
return {...state, history: [...state.history, CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL]};
167+
}
168+
169+
// If it's set to false, we need to remove the side panel history entry if it's there.
170+
if (!action.payload.isVisible) {
171+
return {...state, history: state.history.filter((entry) => entry !== CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL)};
172+
}
173+
174+
// Else, do not change history.
175+
return state;
176+
}
177+
157178
export {
158179
handleDismissModalAction,
159180
handleNavigatingToModalFromModal,
@@ -162,4 +183,5 @@ export {
162183
handleReplaceReportsSplitNavigatorAction,
163184
screensWithEnteringAnimation,
164185
workspaceSplitsWithoutEnteringAnimation,
186+
handleToggleSidePanelWithHistoryAction,
165187
};

src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@ import {
1313
handleOpenWorkspaceSplitAction,
1414
handlePushFullscreenAction,
1515
handleReplaceReportsSplitNavigatorAction,
16+
handleToggleSidePanelWithHistoryAction,
1617
} from './GetStateForActionHandlers';
1718
import syncBrowserHistory from './syncBrowserHistory';
18-
import type {DismissModalActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions} from './types';
19+
import type {
20+
DismissModalActionType,
21+
OpenWorkspaceSplitActionType,
22+
PushActionType,
23+
ReplaceActionType,
24+
RootStackNavigatorAction,
25+
RootStackNavigatorRouterOptions,
26+
ToggleSidePanelWithHistoryActionType,
27+
} from './types';
1928

2029
function isOpenWorkspaceSplitAction(action: RootStackNavigatorAction): action is OpenWorkspaceSplitActionType {
2130
return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
@@ -33,6 +42,10 @@ function isDismissModalAction(action: RootStackNavigatorAction): action is Dismi
3342
return action.type === CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
3443
}
3544

45+
function isToggleSidePanelWithHistoryAction(action: RootStackNavigatorAction): action is ToggleSidePanelWithHistoryActionType {
46+
return action.type === CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
47+
}
48+
3649
function shouldPreventReset(state: StackNavigationState<ParamListBase>, action: CommonActions.Action | StackActionType) {
3750
if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
3851
return false;
@@ -67,6 +80,10 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) {
6780
return {
6881
...stackRouter,
6982
getStateForAction(state: StackNavigationState<ParamListBase>, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) {
83+
if (isToggleSidePanelWithHistoryAction(action)) {
84+
return handleToggleSidePanelWithHistoryAction(state, action);
85+
}
86+
7087
if (isOpenWorkspaceSplitAction(action)) {
7188
return handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
7289
}

src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import type {WorkspaceScreenName} from '@libs/Navigation/types';
33
import type CONST from '@src/CONST';
44

55
type RootStackNavigatorActionType =
6+
| {
7+
type: typeof CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
8+
payload: {
9+
isVisible: boolean;
10+
};
11+
}
612
| {
713
type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
814
}
@@ -18,6 +24,10 @@ type OpenWorkspaceSplitActionType = RootStackNavigatorActionType & {
1824
type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
1925
};
2026

27+
type ToggleSidePanelWithHistoryActionType = RootStackNavigatorActionType & {
28+
type: typeof CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
29+
};
30+
2131
type PushActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.PUSH};
2232

2333
type ReplaceActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.REPLACE};
@@ -30,4 +40,12 @@ type RootStackNavigatorRouterOptions = StackRouterOptions;
3040

3141
type RootStackNavigatorAction = CommonActions.Action | StackActionType | RootStackNavigatorActionType;
3242

33-
export type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, DismissModalActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions};
43+
export type {
44+
OpenWorkspaceSplitActionType,
45+
PushActionType,
46+
ReplaceActionType,
47+
DismissModalActionType,
48+
RootStackNavigatorAction,
49+
RootStackNavigatorRouterOptions,
50+
ToggleSidePanelWithHistoryActionType,
51+
};

src/libs/Navigation/AppNavigator/customHistory/addCustomHistoryRouterExtension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ function addCustomHistoryRouterExtension<RouterOptions extends PlatformStackRout
6262
return stateWithInitialHistory;
6363
}
6464

65+
// Custom history param used to show the side panel is handled here
66+
if (state.history?.at(-1) === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL) {
67+
stateWithInitialHistory.history = [...stateWithInitialHistory.history, CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL];
68+
return stateWithInitialHistory;
69+
}
70+
6571
// @ts-expect-error focusedRoute.key is always defined because it is a route from a rehydrated state. Find focused route isn't correctly typed in this case.
6672
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
6773
const customHistoryEntry = getCustomHistoryEntry(focusedRoute.key);

0 commit comments

Comments
 (0)