Skip to content

An additional entry is added to the browser history when opening the app #67610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5110,6 +5110,7 @@ const CONST = {
SF_COORDINATES: [-122.4194, 37.7749],

NAVIGATION: {
CUSTOM_HISTORY_ENTRY_SIDE_PANEL: 'CUSTOM_HISTORY-SIDE_PANEL',
ACTION_TYPE: {
REPLACE: 'REPLACE',
PUSH: 'PUSH',
Expand All @@ -5123,6 +5124,7 @@ const CONST = {
OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT',
SET_HISTORY_PARAM: 'SET_HISTORY_PARAM',
REPLACE_PARAMS: 'REPLACE_PARAMS',
TOGGLE_SIDE_PANEL_WITH_HISTORY: 'TOGGLE_SIDE_PANEL_WITH_HISTORY',
},
},
TIME_PERIOD: {
Expand Down
11 changes: 0 additions & 11 deletions src/components/SidePanel/HelpModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,7 @@ function Help({sidePanelTranslateX, closeSidePanel, shouldHideSidePanelBackdrop}
// Web back button: push history state and close Side Panel on popstate
useEffect(() => {
ComposerFocusManager.resetReadyToFocus(uniqueModalId);
window.history.pushState({isSidePanelOpen: true}, '', null);
const handlePopState = () => {
if (isExtraLargeScreenWidth) {
return;
}

closeSidePanel();
};

window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
ComposerFocusManager.setReadyToFocus(uniqueModalId);
};
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
Expand Down
3 changes: 3 additions & 0 deletions src/components/SidePanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from 'react';
import useSidePanel from '@hooks/useSidePanel';
import Help from './HelpModal';
import useSyncSidePanelWithHistory from './useSyncSidePanelWithHistory';

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

useSyncSidePanelWithHistory();
Copy link
Contributor

@blazejkustra blazejkustra Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is the right spot for this hook? It will be called even when side panel is not available/closed

Copy link
Contributor Author

@WojtekBoman WojtekBoman Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I need to open it when I go forward in the browser history, then the side panel is not rendered, so the hook will not be called if I call it from HelpModal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the described case

Screen.Recording.2025-08-01.at.11.02.42.mov


if (isSidePanelTransitionEnded && shouldHideSidePanel) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Side panel synchronization with the browser history is only supported for web
export default function useSyncSidePanelWithHistory() {}
61 changes: 61 additions & 0 deletions src/components/SidePanel/useSyncSidePanelWithHistory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {useNavigationState} from '@react-navigation/native';
import {useEffect} from 'react';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSidePanel from '@hooks/useSidePanel';
import navigationRef from '@libs/Navigation/navigationRef';
import CONST from '@src/CONST';

export default function useSyncSidePanelWithHistory() {
const {closeSidePanel, openSidePanel, shouldHideSidePanel} = useSidePanel();
const {isExtraLargeScreenWidth} = useResponsiveLayout();
const lastHistoryEntry = useNavigationState((state) => state.history?.at(-1));
const previousLastHistoryEntry = usePrevious(lastHistoryEntry);

useEffect(() => {
// If the window width has been expanded and the modal is displayed, remove its history entry.
// The side panel is only synced with the history when it's displayed as RHP.
if (!shouldHideSidePanel && isExtraLargeScreenWidth) {
navigationRef.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY,
payload: {isVisible: false},
});
return;
}

// When shouldHideSidePanel changes, synchronize the side panel with the browser history.
navigationRef.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY,
payload: {isVisible: !shouldHideSidePanel},
});
}, [shouldHideSidePanel, isExtraLargeScreenWidth]);

useEffect(() => {
// The side panel is synced with the browser history only when displayed in RHP.
if (isExtraLargeScreenWidth) {
return;
}

const hasHistoryChanged = previousLastHistoryEntry !== lastHistoryEntry;

// If nothing has changed in the browser history, do nothing.
if (!hasHistoryChanged) {
return;
}

const hasSidePanelBeenClosed = previousLastHistoryEntry === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL;

// If the side panel history entry is not the last one and the modal is displayed, close it.
if (hasSidePanelBeenClosed && !shouldHideSidePanel) {
closeSidePanel();
return;
}

const hasSidePanelBeenOpened = lastHistoryEntry === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL;

// If the side panel history entry is the last one and the modal is not displayed, open it.
if (hasSidePanelBeenOpened && shouldHideSidePanel) {
openSidePanel();
}
}, [closeSidePanel, lastHistoryEntry, previousLastHistoryEntry, openSidePanel, shouldHideSidePanel, isExtraLargeScreenWidth]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {StackActions} from '@react-navigation/native';
import type {ParamListBase, Router} from '@react-navigation/routers';
import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType} from './types';
import type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types';

const MODAL_ROUTES_TO_DISMISS: string[] = [
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
Expand Down Expand Up @@ -154,6 +155,26 @@ function handleNavigatingToModalFromModal(
return stackRouter.getStateForAction(modifiedState, action, configOptions);
}

function handleToggleSidePanelWithHistoryAction(state: StackNavigationState<ParamListBase>, action: ToggleSidePanelWithHistoryActionType) {
// This shouldn't ever happen as the history should be always defined. It's for type safety.
if (!state?.history) {
return state;
}

// If it's set to true, we need to add the side panel history entry if it's not already there.
if (action.payload.isVisible && state.history.at(-1) !== CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL) {
return {...state, history: [...state.history, CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL]};
}

// If it's set to false, we need to remove the side panel history entry if it's there.
if (!action.payload.isVisible) {
return {...state, history: state.history.filter((entry) => entry !== CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL)};
}

// Else, do not change history.
return state;
}

export {
handleDismissModalAction,
handleNavigatingToModalFromModal,
Expand All @@ -162,4 +183,5 @@ export {
handleReplaceReportsSplitNavigatorAction,
screensWithEnteringAnimation,
workspaceSplitsWithoutEnteringAnimation,
handleToggleSidePanelWithHistoryAction,
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ import {
handleOpenWorkspaceSplitAction,
handlePushFullscreenAction,
handleReplaceReportsSplitNavigatorAction,
handleToggleSidePanelWithHistoryAction,
} from './GetStateForActionHandlers';
import syncBrowserHistory from './syncBrowserHistory';
import type {DismissModalActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions} from './types';
import type {
DismissModalActionType,
OpenWorkspaceSplitActionType,
PushActionType,
ReplaceActionType,
RootStackNavigatorAction,
RootStackNavigatorRouterOptions,
ToggleSidePanelWithHistoryActionType,
} from './types';

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

function isToggleSidePanelWithHistoryAction(action: RootStackNavigatorAction): action is ToggleSidePanelWithHistoryActionType {
return action.type === CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
}

function shouldPreventReset(state: StackNavigationState<ParamListBase>, action: CommonActions.Action | StackActionType) {
if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
return false;
Expand Down Expand Up @@ -67,6 +80,10 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) {
return {
...stackRouter,
getStateForAction(state: StackNavigationState<ParamListBase>, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) {
if (isToggleSidePanelWithHistoryAction(action)) {
return handleToggleSidePanelWithHistoryAction(state, action);
}

if (isOpenWorkspaceSplitAction(action)) {
return handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type {WorkspaceScreenName} from '@libs/Navigation/types';
import type CONST from '@src/CONST';

type RootStackNavigatorActionType =
| {
type: typeof CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
payload: {
isVisible: boolean;
};
}
| {
type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
}
Expand All @@ -18,6 +24,10 @@ type OpenWorkspaceSplitActionType = RootStackNavigatorActionType & {
type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
};

type ToggleSidePanelWithHistoryActionType = RootStackNavigatorActionType & {
type: typeof CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
};

type PushActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.PUSH};

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

type RootStackNavigatorAction = CommonActions.Action | StackActionType | RootStackNavigatorActionType;

export type {OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, DismissModalActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions};
export type {
OpenWorkspaceSplitActionType,
PushActionType,
ReplaceActionType,
DismissModalActionType,
RootStackNavigatorAction,
RootStackNavigatorRouterOptions,
ToggleSidePanelWithHistoryActionType,
};
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ function addCustomHistoryRouterExtension<RouterOptions extends PlatformStackRout
return stateWithInitialHistory;
}

// Custom history param used to show the side panel is handled here
if (state.history?.at(-1) === CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL) {
stateWithInitialHistory.history = [...stateWithInitialHistory.history, CONST.NAVIGATION.CUSTOM_HISTORY_ENTRY_SIDE_PANEL];
return stateWithInitialHistory;
}

// @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.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const customHistoryEntry = getCustomHistoryEntry(focusedRoute.key);
Expand Down
Loading