From e1dd0bf2dc8dabcab44cbbfc7e356399a4f23a0a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 29 Jul 2025 13:42:49 +0200 Subject: [PATCH] Use custom history to handle showing side panel --- src/CONST/index.ts | 2 + src/components/SidePanel/HelpModal/index.tsx | 11 ---- src/components/SidePanel/index.tsx | 6 ++ .../index.native.ts | 2 + .../useSyncSidePanelWithHistory/index.ts | 61 +++++++++++++++++++ .../GetStateForActionHandlers.ts | 24 +++++++- .../RootStackRouter.ts | 19 +++++- .../createRootStackNavigator/types.ts | 20 +++++- .../addCustomHistoryRouterExtension.ts | 6 ++ 9 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 src/components/SidePanel/useSyncSidePanelWithHistory/index.native.ts create mode 100644 src/components/SidePanel/useSyncSidePanelWithHistory/index.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 29a75f6c09e7..a80a1da68bd8 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5143,6 +5143,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', @@ -5156,6 +5157,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: { diff --git a/src/components/SidePanel/HelpModal/index.tsx b/src/components/SidePanel/HelpModal/index.tsx index ae74c4df47f1..051a41eb2144 100644 --- a/src/components/SidePanel/HelpModal/index.tsx +++ b/src/components/SidePanel/HelpModal/index.tsx @@ -47,18 +47,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 diff --git a/src/components/SidePanel/index.tsx b/src/components/SidePanel/index.tsx index 9042c4eb2e95..e5e12c05c4c1 100644 --- a/src/components/SidePanel/index.tsx +++ b/src/components/SidePanel/index.tsx @@ -1,10 +1,16 @@ 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(); + // This hook synchronizes the side panel visibility with the browser history when it is displayed as RHP. + // This means when you open or close the side panel, an entry connected with it is added to or removed from the browser history, + // allowing this modal to be toggled using browser's "go back" and "go forward" buttons. + useSyncSidePanelWithHistory(); + if (isSidePanelTransitionEnded && shouldHideSidePanel) { return null; } diff --git a/src/components/SidePanel/useSyncSidePanelWithHistory/index.native.ts b/src/components/SidePanel/useSyncSidePanelWithHistory/index.native.ts new file mode 100644 index 000000000000..77d5762f362d --- /dev/null +++ b/src/components/SidePanel/useSyncSidePanelWithHistory/index.native.ts @@ -0,0 +1,2 @@ +// Side panel synchronization with the browser history is only supported for web +export default function useSyncSidePanelWithHistory() {} diff --git a/src/components/SidePanel/useSyncSidePanelWithHistory/index.ts b/src/components/SidePanel/useSyncSidePanelWithHistory/index.ts new file mode 100644 index 000000000000..3feef60766b0 --- /dev/null +++ b/src/components/SidePanel/useSyncSidePanelWithHistory/index.ts @@ -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]); +} diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 8324260ddfc9..1aee194a32b2 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -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, @@ -154,6 +155,26 @@ function handleNavigatingToModalFromModal( return stackRouter.getStateForAction(modifiedState, action, configOptions); } +function handleToggleSidePanelWithHistoryAction(state: StackNavigationState, 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, @@ -162,4 +183,5 @@ export { handleReplaceReportsSplitNavigatorAction, screensWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation, + handleToggleSidePanelWithHistoryAction, }; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts index 9402678712c9..408973aaa45e 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -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; @@ -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, action: CommonActions.Action | StackActionType) { if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) { return false; @@ -67,6 +80,10 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) { return { ...stackRouter, getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) { + if (isToggleSidePanelWithHistoryAction(action)) { + return handleToggleSidePanelWithHistoryAction(state, action); + } + if (isOpenWorkspaceSplitAction(action)) { return handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter); } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts index 16a8a9cc5316..1f5c962b2e36 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts @@ -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; } @@ -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}; @@ -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, +}; diff --git a/src/libs/Navigation/AppNavigator/customHistory/addCustomHistoryRouterExtension.ts b/src/libs/Navigation/AppNavigator/customHistory/addCustomHistoryRouterExtension.ts index ca2deafc7493..e8e9cd3a083b 100644 --- a/src/libs/Navigation/AppNavigator/customHistory/addCustomHistoryRouterExtension.ts +++ b/src/libs/Navigation/AppNavigator/customHistory/addCustomHistoryRouterExtension.ts @@ -62,6 +62,12 @@ function addCustomHistoryRouterExtension