diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index f3739c106841..3cf1971a1631 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -147,6 +147,8 @@ function MoneyRequestReportNavigationContent({reportID, shouldDisplayNarrowVersi }); Navigation.setParams({ reportID: reportId, + reportActionID: undefined, + referrer: undefined, }); }; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx index 68952caf8a0b..b4c41f0352ed 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx @@ -125,7 +125,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR backTo = params?.backTo ?? backTo; } const nextThreadReportID = nextParentReportAction?.childReportID; - const navigationParams = {reportID: nextThreadReportID, backTo}; + const navigationParams = {reportID: nextThreadReportID, reportActionID: undefined, backTo}; if (nextThreadReportID) { markReportIDAsExpense(nextThreadReportID); @@ -161,7 +161,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR backTo = params?.backTo ?? backTo; } const prevThreadReportID = prevParentReportAction?.childReportID; - const navigationParams = {reportID: prevThreadReportID, backTo}; + const navigationParams = {reportID: prevThreadReportID, reportActionID: undefined, backTo}; if (prevThreadReportID) { markReportIDAsExpense(prevThreadReportID); diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 085044839cbe..850d65b2f582 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -186,14 +186,12 @@ function ParentNavigationSubtitle({ // avoid stacking RHPs by going back to the search report if it's already there const previousRoute = currentFocusedNavigator?.state?.routes.at(-2); - if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { - if (previousRoute.params && 'reportID' in previousRoute.params) { - const reportIDFromParams = previousRoute.params.reportID; + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && previousRoute.params && 'reportID' in previousRoute.params) { + const reportIDFromParams = previousRoute.params.reportID; - if (reportIDFromParams === parentReportID) { - Navigation.goBack(); - return; - } + if (reportIDFromParams === parentReportID) { + Navigation.goBack(); + return; } } @@ -213,6 +211,14 @@ function ParentNavigationSubtitle({ return; } } + + // Stay in the Search tab when the parent link is tapped from a SEARCH_REPORT RHP + // and the parent isn't already in the stack — otherwise the REPORT_WITH_ID fallback + // would yank the user to Inbox. + if (isReportInRHP) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: isVisibleAction ? parentReportActionID : undefined})); + return; + } } // If the parent report is already the previous screen in the main stack, go back to it diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 81851f3303f4..26c80e3e4710 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -105,7 +105,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { const containerRef = useRef(null); const isExecutingRef = useRef(false); const screenOptions = useRHPScreenOptions(); - const {superWideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState(); + const {superWideRHPRouteKeys, wideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState(); const {clearWideRHPKeys, syncRHPKeys} = useWideRHPActions(); const {windowWidth} = useWindowDimensions(); const modalStackScreenOptions = useModalStackScreenOptions(); @@ -130,7 +130,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { // Animation should be disabled when we open the wide rhp from the narrow one. // When the wide rhp page is opened as first one, it will be animated with the entire RightModalNavigator. - const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || isSmallScreenWidth; + const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0 || isSmallScreenWidth; const animatedWidth = expandedRHPProgress.interpolate({ inputRange: [0, 1, 2], diff --git a/src/libs/Navigation/helpers/getTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/getTopmostFullScreenRoute.ts new file mode 100644 index 000000000000..9162613745f0 --- /dev/null +++ b/src/libs/Navigation/helpers/getTopmostFullScreenRoute.ts @@ -0,0 +1,24 @@ +import {navigationRef} from '@libs/Navigation/Navigation'; +import type {NavigationRoute, RootNavigatorParamList, State} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; + +/** + * Returns the active tab route of the topmost TAB_NAVIGATOR in the root navigation state. + * Use this to determine which full-screen tab (Search, Inbox, etc.) is currently focused. + */ +function getTopmostFullScreenRoute(): NavigationRoute | undefined { + const rootState = navigationRef.getRootState() as State; + + if (!rootState) { + return undefined; + } + + const topmostTabNavigatorRoute = rootState.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); + if (!topmostTabNavigatorRoute?.state) { + return undefined; + } + const index = topmostTabNavigatorRoute.state.index ?? 0; + return topmostTabNavigatorRoute.state.routes?.at(index); +} + +export default getTopmostFullScreenRoute; diff --git a/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts b/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts index 4b840387f45a..c9b7c33bf599 100644 --- a/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts +++ b/src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts @@ -1,18 +1,12 @@ -import {navigationRef} from '@libs/Navigation/Navigation'; -import type {RootNavigatorParamList, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; -import getActiveTabName from './getActiveTabName'; -import {isFullScreenName} from './isNavigatorName'; +import getTopmostFullScreenRoute from './getTopmostFullScreenRoute'; const isReportTopmostSplitNavigator = (): boolean => { - const rootState = navigationRef.getRootState() as State; - - if (!rootState) { + const topmostFullScreenRoute = getTopmostFullScreenRoute(); + if (!topmostFullScreenRoute) { return false; } - - const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name)); - return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; + return topmostFullScreenRoute.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; }; export default isReportTopmostSplitNavigator; diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts index 00631ae00e5e..9fc3b6a725f7 100644 --- a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -1,18 +1,12 @@ -import {navigationRef} from '@libs/Navigation/Navigation'; -import type {RootNavigatorParamList, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; -import getActiveTabName from './getActiveTabName'; -import {isFullScreenName} from './isNavigatorName'; +import getTopmostFullScreenRoute from './getTopmostFullScreenRoute'; const isSearchTopmostFullScreenRoute = (): boolean => { - const rootState = navigationRef.getRootState() as State; - - if (!rootState) { + const topmostFullScreenRoute = getTopmostFullScreenRoute(); + if (!topmostFullScreenRoute) { return false; } - - const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name)); - return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR; + return topmostFullScreenRoute.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR; }; export default isSearchTopmostFullScreenRoute; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d81bfb67e636..86a5a4e6da88 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10248,6 +10248,19 @@ function navigateToLinkedReportAction( visibleReportActionsData?: VisibleReportActionsDerivedValue, ) { if (isInNarrowPaneModal) { + const rootState = navigationRef.current?.getRootState(); + const topmostRHP = rootState?.routes.findLast((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + const rhpState = topmostRHP?.state; + const rhpIndex = rhpState?.index ?? (rhpState?.routes.length ? rhpState.routes.length - 1 : -1); + const routeBelow = rhpIndex > 0 ? rhpState?.routes.at(rhpIndex - 1) : undefined; + const routeBelowParams = routeBelow?.params as {reportID?: string} | undefined; + + if (routeBelow?.key && routeBelowParams?.reportID === ancestor.report.reportID) { + Navigation.setParams({reportActionID: ancestor.reportAction.reportActionID}, routeBelow.key, rhpState?.key); + Navigation.goBack(); + return; + } + Navigation.navigate( ROUTES.SEARCH_REPORT.getRoute({ reportID: ancestor.report.reportID, diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index d667f59fd4c1..1b91d15105a8 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -75,6 +75,7 @@ import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types'; import Navigation from '@libs/Navigation/Navigation'; import enhanceParameters from '@libs/Network/enhanceParameters'; @@ -2440,7 +2441,11 @@ function navigateToAndOpenChildReport( ) { const report = childReport ?? createChildReport(childReport, parentReportAction, parentReport, currentUserAccountID, introSelected, betas, isSelfTourViewed, personalDetails); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute())); + if (isSearchTopmostFullScreenRoute()) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()})); + } else { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute())); + } } /** @@ -2533,7 +2538,11 @@ function explain( // Check if explanation thread report already exists const report = childReport ?? createChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, isSelfTourViewed); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute())); + if (isSearchTopmostFullScreenRoute()) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()})); + } else { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute())); + } // Schedule adding the explanation comment on the next animation frame // so it runs immediately after navigation completes. requestAnimationFrame(() => { diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index 0a7476f7db9d..85e2ff34ba89 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -105,6 +105,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) const {isSmallScreenWidth, shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout(); const isInSidePanel = useIsInSidePanel(); const route = useRoute(); + const openParentReportInCurrentTab = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID) ?? getNonEmptyStringOnyxID(report?.reportID)}`); const [grandParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`); const grandParentReportAction = useParentReportAction(parentReport); @@ -346,6 +347,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) parentReportID={parentNavigationReport?.parentReportID} parentReportActionID={isParentOneTransactionThread ? undefined : parentNavigationReport?.parentReportActionID} pressableStyles={[styles.alignSelfStart, styles.mw100]} + openParentReportInCurrentTab={openParentReportInCurrentTab} humanAgentAccountID={humanAgentAccountID} humanAgentName={humanAgentName} /> diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx index 8dcfebdd6049..432ac45da15e 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx @@ -310,9 +310,9 @@ function ComposerWithSuggestions({ const commentRef = useRef(initialText); - const {superWideRHPRouteKeys} = useWideRHPState(); - // When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank - const shouldDelayAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + const {superWideRHPRouteKeys, wideRHPRouteKeys} = useWideRHPState(); + // When SearchReport is stacked above another RHP (wide or super-wide), delay autofocus until after the transition completes to avoid animation jank + const shouldDelayAutoFocus = (superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0) && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; const shouldDelayAutoFocusRef = useRef(shouldDelayAutoFocus); shouldDelayAutoFocusRef.current = shouldDelayAutoFocus; diff --git a/tests/unit/Navigation/getTopmostFullScreenRouteTest.ts b/tests/unit/Navigation/getTopmostFullScreenRouteTest.ts new file mode 100644 index 000000000000..6dc2760abe33 --- /dev/null +++ b/tests/unit/Navigation/getTopmostFullScreenRouteTest.ts @@ -0,0 +1,93 @@ +import getTopmostFullScreenRoute from '@libs/Navigation/helpers/getTopmostFullScreenRoute'; +import NAVIGATORS from '@src/NAVIGATORS'; + +const mockGetRootState = jest.fn(); + +jest.mock('@libs/Navigation/Navigation', () => ({ + navigationRef: { + getRootState: () => mockGetRootState() as unknown, + }, +})); + +describe('getTopmostFullScreenRoute', () => { + beforeEach(() => { + mockGetRootState.mockReset(); + }); + + it('returns undefined when there is no root state', () => { + mockGetRootState.mockReturnValue(undefined); + expect(getTopmostFullScreenRoute()).toBeUndefined(); + }); + + it('returns undefined when there is no TAB_NAVIGATOR route in the root state', () => { + mockGetRootState.mockReturnValue({ + routes: [{name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR}, {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR}], + }); + expect(getTopmostFullScreenRoute()).toBeUndefined(); + }); + + it('returns undefined when the TAB_NAVIGATOR has no nested state yet', () => { + mockGetRootState.mockReturnValue({ + routes: [{name: NAVIGATORS.TAB_NAVIGATOR}], + }); + expect(getTopmostFullScreenRoute()).toBeUndefined(); + }); + + it('returns the focused tab route based on state.index', () => { + const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}; + const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}; + mockGetRootState.mockReturnValue({ + routes: [ + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: { + index: 1, + routes: [reportsRoute, searchRoute], + }, + }, + ], + }); + expect(getTopmostFullScreenRoute()).toBe(searchRoute); + }); + + it('falls back to the first child route when state.index is missing', () => { + const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}; + const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}; + mockGetRootState.mockReturnValue({ + routes: [ + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: { + routes: [reportsRoute, searchRoute], + }, + }, + ], + }); + expect(getTopmostFullScreenRoute()).toBe(reportsRoute); + }); + + it('returns the focused tab of the topmost TAB_NAVIGATOR when multiple exist', () => { + const oldFocused = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}; + const newFocused = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}; + mockGetRootState.mockReturnValue({ + routes: [ + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: { + index: 0, + routes: [oldFocused], + }, + }, + {name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR}, + { + name: NAVIGATORS.TAB_NAVIGATOR, + state: { + index: 0, + routes: [newFocused], + }, + }, + ], + }); + expect(getTopmostFullScreenRoute()).toBe(newFocused); + }); +});