diff --git a/patches/@react-navigation+core+6.4.11+001+getStateFromPath-getPathFromState-configs-caching.patch b/patches/@react-navigation+core+6.4.11+002+getStateFromPath-getPathFromState-configs-caching.patch
similarity index 100%
rename from patches/@react-navigation+core+6.4.11+001+getStateFromPath-getPathFromState-configs-caching.patch
rename to patches/@react-navigation+core+6.4.11+002+getStateFromPath-getPathFromState-configs-caching.patch
diff --git a/patches/@react-navigation+core+6.4.11+002+platform-navigation-stack-types.patch b/patches/@react-navigation+core+6.4.11+003+platform-navigation-stack-types.patch
similarity index 100%
rename from patches/@react-navigation+core+6.4.11+002+platform-navigation-stack-types.patch
rename to patches/@react-navigation+core+6.4.11+003+platform-navigation-stack-types.patch
diff --git a/patches/@react-navigation+core+6.4.11+004+side-pane.patch b/patches/@react-navigation+core+6.4.11+004+side-pane.patch
new file mode 100644
index 000000000000..995c37eedd04
--- /dev/null
+++ b/patches/@react-navigation+core+6.4.11+004+side-pane.patch
@@ -0,0 +1,92 @@
+diff --git a/node_modules/@react-navigation/core/lib/module/useDescriptors.js b/node_modules/@react-navigation/core/lib/module/useDescriptors.js
+index 76fdab1..75f315c 100644
+--- a/node_modules/@react-navigation/core/lib/module/useDescriptors.js
++++ b/node_modules/@react-navigation/core/lib/module/useDescriptors.js
+@@ -112,6 +112,19 @@ export default function useDescriptors(_ref, convertCustomScreenOptions) {
+ }
+ return o;
+ });
++ const SidePane = customOptions.sidePane;
++ let element = /*#__PURE__*/React.createElement(React.Fragment, {
++ children: [/*#__PURE__*/React.createElement(SceneView, {
++ navigation: navigation,
++ route: route,
++ screen: screen,
++ routeState: state.routes[i].state,
++ getState: getState,
++ setState: setState,
++ options: customOptions,
++ clearOptions: clearOptions
++ }), SidePane && /*#__PURE__*/React.createElement(SidePane, {})]
++ });
+ acc[route.key] = {
+ route,
+ // @ts-expect-error: it's missing action helpers, fix later
+@@ -123,17 +136,10 @@ export default function useDescriptors(_ref, convertCustomScreenOptions) {
+ }, /*#__PURE__*/React.createElement(NavigationContext.Provider, {
+ value: navigation
+ }, /*#__PURE__*/React.createElement(NavigationRouteContext.Provider, {
+- value: route
+- }, /*#__PURE__*/React.createElement(SceneView, {
+- navigation: navigation,
+- route: route,
+- screen: screen,
+- routeState: state.routes[i].state,
+- getState: getState,
+- setState: setState,
+- options: mergedOptions,
+- clearOptions: clearOptions
+- }))));
++ value: route,
++ children: element
++ },
++ )));
+ },
+ options: mergedOptions
+ };
+diff --git a/node_modules/@react-navigation/core/src/useDescriptors.tsx b/node_modules/@react-navigation/core/src/useDescriptors.tsx
+index 2e4ee0f..11ece43 100644
+--- a/node_modules/@react-navigation/core/src/useDescriptors.tsx
++++ b/node_modules/@react-navigation/core/src/useDescriptors.tsx
+@@ -238,6 +238,23 @@ export default function useDescriptors<
+ return o;
+ });
+
++ const SidePane = (customOptions as any).sidePane;
++ let element = (
++ <>
++
++ {SidePane && }
++ >
++ );
++
+ acc[route.key] = {
+ route,
+ // @ts-expect-error: it's missing action helpers, fix later
+@@ -247,16 +264,7 @@ export default function useDescriptors<
+
+
+
+-
++ {element}
+
+
+
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 7ee5660b9da6..72cbcee20e34 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -479,6 +479,9 @@ const ONYXKEYS = {
/** Information about travel provisioning process */
TRAVEL_PROVISIONING: 'travelProvisioning',
+ /** Stores the information about the state of side panel */
+ NVP_SIDE_PANE: 'nvp_sidePaneExpanded',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -1081,6 +1084,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
[ONYXKEYS.LAST_FULL_RECONNECT_TIME]: string;
[ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning;
+ [ONYXKEYS.NVP_SIDE_PANE]: OnyxTypes.SidePane;
};
type OnyxDerivedValuesMapping = {
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 363fe238e9f4..184d7b62c054 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import PinButton from '@components/PinButton';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
+import HelpButton from '@components/SidePane/HelpButton';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
@@ -65,6 +66,7 @@ function HeaderWithBackButton({
shouldOverlayDots = false,
shouldOverlay = false,
shouldNavigateToTopMostReport = false,
+ shouldDisplayHelpButton = true,
shouldDisplaySearchRouter = false,
progressBarPercentage,
style,
@@ -275,6 +277,7 @@ function HeaderWithBackButton({
)}
+ {shouldDisplayHelpButton && }
{shouldDisplaySearchRouter && }
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index d2d4ba9e4e0f..a4a0dc084c07 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -143,6 +143,9 @@ type HeaderWithBackButtonProps = Partial & {
/** Whether we should overlay the 3 dots menu */
shouldOverlayDots?: boolean;
+ /** Whether we should display the button that opens the help pane */
+ shouldDisplayHelpButton?: boolean;
+
/** Whether we should display the button that opens new SearchRouter */
shouldDisplaySearchRouter?: boolean;
diff --git a/src/components/Navigation/TopBar.tsx b/src/components/Navigation/TopBar.tsx
index ef2d2adabe3b..60633c4929b4 100644
--- a/src/components/Navigation/TopBar.tsx
+++ b/src/components/Navigation/TopBar.tsx
@@ -5,6 +5,7 @@ import Breadcrumbs from '@components/Breadcrumbs';
import LoadingBar from '@components/LoadingBar';
import {PressableWithoutFeedback} from '@components/Pressable';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
+import HelpButton from '@components/SidePane/HelpButton';
import Text from '@components/Text';
import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton';
import useLocalize from '@hooks/useLocalize';
@@ -19,10 +20,11 @@ type TopBarProps = {
breadcrumbLabel: string;
activeWorkspaceID?: string;
shouldDisplaySearch?: boolean;
+ shouldDisplaySidePane?: boolean;
cancelSearch?: () => void;
};
-function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, cancelSearch}: TopBarProps) {
+function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplaySidePane = true, cancelSearch}: TopBarProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const policy = usePolicy(activeWorkspaceID);
@@ -71,6 +73,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true,
{translate('common.cancel')}
)}
+ {shouldDisplaySidePane && }
{displaySearch && }
diff --git a/src/components/Navigation/TopLevelBottomTabBar/index.tsx b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
index 9da5f63d306b..2f99c4936116 100644
--- a/src/components/Navigation/TopLevelBottomTabBar/index.tsx
+++ b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
@@ -4,6 +4,7 @@ import {InteractionManager, View} from 'react-native';
import {FullScreenBlockingViewContext} from '@components/FullScreenBlockingViewContextProvider';
import BottomTabBar from '@components/Navigation/BottomTabBar';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSidePane from '@hooks/useSidePane';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -30,11 +31,12 @@ function TopLevelBottomTabBar({state}: TopLevelBottomTabBarProps) {
const [isAfterClosingTransition, setIsAfterClosingTransition] = useState(false);
const cancelAfterInteractions = useRef | undefined>();
const {isBlockingViewVisible} = useContext(FullScreenBlockingViewContext);
+ const {shouldHideTopLevelBottomBar} = useSidePane();
// That means it's visible and it's not covered by the overlay.
- const isBottomTabVisibleDirectly = getIsBottomTabVisibleDirectly(state);
+ const isBottomTabVisibleDirectly = getIsBottomTabVisibleDirectly(state) && !shouldHideTopLevelBottomBar;
+ const isScreenWithBottomTabFocused = getIsScreenWithBottomTabFocused(state) && !shouldHideTopLevelBottomBar;
const selectedTab = getSelectedTab(state);
- const isScreenWithBottomTabFocused = getIsScreenWithBottomTabFocused(state);
const shouldDisplayBottomBar = shouldUseNarrowLayout ? isScreenWithBottomTabFocused : isBottomTabVisibleDirectly;
const isReadyToDisplayBottomBar = isAfterClosingTransition && shouldDisplayBottomBar && !isBlockingViewVisible;
diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx
index 472987cc6ffe..208ea3ebc858 100644
--- a/src/components/Search/SearchAutocompleteInput.tsx
+++ b/src/components/Search/SearchAutocompleteInput.tsx
@@ -13,6 +13,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {parseFSAttributes} from '@libs/Fullstory';
@@ -104,6 +105,7 @@ function SearchAutocompleteInput(
const {isOffline} = useNetwork();
const {activeWorkspaceID} = useActiveWorkspace();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST);
const currencyAutocompleteList = Object.keys(currencyList ?? {});
@@ -191,7 +193,7 @@ function SearchAutocompleteInput(
;
+};
+
+function HelpButton({style}: HelpButtonProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const [sidePane] = useOnyx(ONYXKEYS.NVP_SIDE_PANE);
+ const [language] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE);
+ const {isExtraLargeScreenWidth} = useResponsiveLayout();
+
+ if (!sidePane || language !== CONST.LOCALES.EN) {
+ return null;
+ }
+
+ return (
+
+ triggerSidePane(isExtraLargeScreenWidth ? !sidePane?.open : !sidePane?.openNarrowScreen, {shouldUpdateNarrowLayout: !isExtraLargeScreenWidth})}
+ >
+
+
+
+ );
+}
+
+HelpButton.displayName = 'HelpButton';
+
+export default HelpButton;
diff --git a/src/components/SidePane/getHelpContent.tsx b/src/components/SidePane/getHelpContent.tsx
new file mode 100644
index 000000000000..f72d5ebe9cc8
--- /dev/null
+++ b/src/components/SidePane/getHelpContent.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import {View} from 'react-native';
+import Text from '@components/Text';
+import type {ThemeStyles} from '@styles/index';
+
+const getHelpContent = (styles: ThemeStyles, route: string) => {
+ return (
+
+ Missing page for route
+ {route}
+
+ );
+};
+
+export default getHelpContent;
diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx
new file mode 100644
index 000000000000..5caa4925dd54
--- /dev/null
+++ b/src/components/SidePane/index.tsx
@@ -0,0 +1,91 @@
+import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
+import React, {useCallback, useEffect, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import {Animated, View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Backdrop from '@components/Modal/BottomDockedModal/Backdrop';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSidePane from '@hooks/useSidePane';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {triggerSidePane} from '@libs/actions/SidePane';
+import Navigation from '@libs/Navigation/Navigation';
+import {substituteRouteParameters} from '@libs/SidePaneUtils';
+import NAVIGATORS from '@src/NAVIGATORS';
+import getHelpContent from './getHelpContent';
+
+function SidePane({shouldShowOverlay = false}: {shouldShowOverlay?: boolean}) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
+ const {sidePaneTranslateX, shouldHideSidePane, shouldHideSidePaneBackdrop} = useSidePane();
+
+ const {route, isInNarrowPaneModal} = useNavigationState((state) => {
+ const params = (findFocusedRoute(state)?.params as Record) ?? {};
+ const activeRoute = Navigation.getActiveRouteWithoutParams();
+ return {route: substituteRouteParameters(activeRoute, params), isInNarrowPaneModal: state.routes.some((r) => r.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR)};
+ });
+
+ const onClose = useCallback(
+ (shouldUpdateNarrow = false) => {
+ triggerSidePane(false, {shouldOnlyUpdateNarrowLayout: !isExtraLargeScreenWidth || shouldUpdateNarrow});
+ },
+ [isExtraLargeScreenWidth],
+ );
+
+ const sizeChangedFromLargeToNarrow = useRef(!isExtraLargeScreenWidth);
+ useEffect(() => {
+ // Close the side pane when the screen size changes from large to small
+ if (!isExtraLargeScreenWidth && !sizeChangedFromLargeToNarrow.current) {
+ onClose(true);
+ sizeChangedFromLargeToNarrow.current = true;
+ }
+
+ // Reset the trigger when the screen size changes back to large
+ if (isExtraLargeScreenWidth) {
+ sizeChangedFromLargeToNarrow.current = false;
+ }
+ }, [isExtraLargeScreenWidth, onClose]);
+
+ if (shouldHideSidePane) {
+ return null;
+ }
+
+ return (
+ <>
+
+ {shouldShowOverlay && !shouldHideSidePaneBackdrop && !isInNarrowPaneModal && (
+
+ )}
+
+
+
+ onClose(false)}
+ onCloseButtonPress={() => onClose(false)}
+ shouldShowBackButton={!isExtraLargeScreenWidth}
+ shouldShowCloseButton={isExtraLargeScreenWidth}
+ shouldDisplayHelpButton={false}
+ />
+ {getHelpContent(styles, route)}
+
+
+ >
+ );
+}
+
+function SidePaneWithOverlay() {
+ return ;
+}
+
+SidePane.displayName = 'SidePane';
+
+export default SidePane;
+export {SidePaneWithOverlay, useSidePane as useAnimatedPaddingRight};
diff --git a/src/hooks/useResponsiveLayout/index.native.ts b/src/hooks/useResponsiveLayout/index.native.ts
index 10f8506caf4f..454e798622a5 100644
--- a/src/hooks/useResponsiveLayout/index.native.ts
+++ b/src/hooks/useResponsiveLayout/index.native.ts
@@ -29,6 +29,7 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult {
const isSmallScreenWidth = true;
const isMediumScreenWidth = false;
const isLargeScreenWidth = false;
+ const isExtraLargeScreenWidth = false;
const isExtraSmallScreenWidth = windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint;
const isSmallScreen = true;
@@ -74,5 +75,6 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult {
onboardingIsMediumOrLargerScreenWidth,
isLargeScreenWidth,
isSmallScreen,
+ isExtraLargeScreenWidth,
};
}
diff --git a/src/hooks/useResponsiveLayout/index.ts b/src/hooks/useResponsiveLayout/index.ts
index 3aa698e6d654..76a57f6838cb 100644
--- a/src/hooks/useResponsiveLayout/index.ts
+++ b/src/hooks/useResponsiveLayout/index.ts
@@ -33,6 +33,7 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult {
const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint;
const onboardingIsMediumOrLargerScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint;
const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint;
+ const isExtraLargeScreenWidth = windowWidth > variables.sidePanelResponsiveWidthBreakpoint;
const isExtraSmallScreenWidth = windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint;
const lowerScreenDimmension = Math.min(windowWidth, windowHeight);
@@ -76,6 +77,7 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult {
isMediumScreenWidth,
onboardingIsMediumOrLargerScreenWidth,
isLargeScreenWidth,
+ isExtraLargeScreenWidth,
isSmallScreen,
};
}
diff --git a/src/hooks/useResponsiveLayout/types.ts b/src/hooks/useResponsiveLayout/types.ts
index c7bb021389f2..f9867d4a6600 100644
--- a/src/hooks/useResponsiveLayout/types.ts
+++ b/src/hooks/useResponsiveLayout/types.ts
@@ -5,6 +5,7 @@ type ResponsiveLayoutResult = {
isExtraSmallScreenHeight: boolean;
isMediumScreenWidth: boolean;
isLargeScreenWidth: boolean;
+ isExtraLargeScreenWidth: boolean;
isExtraSmallScreenWidth: boolean;
isSmallScreen: boolean;
onboardingIsMediumOrLargerScreenWidth: boolean;
diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts
new file mode 100644
index 000000000000..7b6f4186437f
--- /dev/null
+++ b/src/hooks/useSidePane.ts
@@ -0,0 +1,73 @@
+import {useEffect, useRef, useState} from 'react';
+// Import Animated directly from 'react-native' as animations are used with navigation.
+// eslint-disable-next-line no-restricted-imports
+import {Animated} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxTypes from '@src/types/onyx';
+import useResponsiveLayout from './useResponsiveLayout';
+import useWindowDimensions from './useWindowDimensions';
+
+function isSidePaneHidden(sidePane: OnyxEntry, isExtraLargeScreenWidth: boolean) {
+ if (!isExtraLargeScreenWidth && !sidePane?.openNarrowScreen) {
+ return true;
+ }
+
+ return isExtraLargeScreenWidth && !sidePane?.open;
+}
+
+/**
+ * Hook to get the animated position of the side pane and the margin of the navigator
+ */
+function useSidePane() {
+ const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
+ const {windowWidth} = useWindowDimensions();
+
+ const [sidePane] = useOnyx(ONYXKEYS.NVP_SIDE_PANE);
+ const isPaneHidden = isSidePaneHidden(sidePane, isExtraLargeScreenWidth);
+
+ const sidePaneWidth = shouldUseNarrowLayout ? windowWidth : variables.sideBarWidth;
+ const shouldApplySidePaneOffset = isExtraLargeScreenWidth && !isPaneHidden;
+
+ const [shouldHideSidePane, setShouldHideSidePane] = useState(true);
+ const shouldHideSidePaneBackdrop = isPaneHidden || isExtraLargeScreenWidth || shouldUseNarrowLayout;
+ const shouldHideTopLevelBottomBar = !shouldHideSidePaneBackdrop || (!isPaneHidden && shouldUseNarrowLayout);
+
+ const sidePaneOffset = useRef(new Animated.Value(shouldApplySidePaneOffset ? variables.sideBarWidth : 0));
+ const sidePaneTranslateX = useRef(new Animated.Value(isPaneHidden ? sidePaneWidth : 0));
+
+ useEffect(() => {
+ if (!isPaneHidden) {
+ setShouldHideSidePane(false);
+ }
+
+ Animated.parallel([
+ Animated.timing(sidePaneOffset.current, {
+ toValue: shouldApplySidePaneOffset ? variables.sideBarWidth : 0,
+ duration: CONST.ANIMATED_TRANSITION,
+ useNativeDriver: false,
+ }),
+ Animated.timing(sidePaneTranslateX.current, {
+ toValue: isPaneHidden ? sidePaneWidth : 0,
+ duration: CONST.ANIMATED_TRANSITION,
+ useNativeDriver: false,
+ }),
+ ]).start(() => {
+ setShouldHideSidePane(isPaneHidden);
+ });
+ }, [isPaneHidden, shouldApplySidePaneOffset, shouldUseNarrowLayout, sidePaneWidth]);
+
+ return {
+ sidePane,
+ shouldHideSidePane,
+ shouldHideSidePaneBackdrop,
+ shouldHideTopLevelBottomBar,
+ sidePaneOffset,
+ sidePaneTranslateX,
+ };
+}
+
+export default useSidePane;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 42b3c99d476d..4dfb9f5e3bd9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -523,6 +523,7 @@ const translations = {
subrate: 'Subrate',
perDiem: 'Per diem',
validate: 'Validate',
+ help: 'Help',
expenseReports: 'Expense Reports',
rateOutOfPolicy: 'Rate out of policy',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index e526ad136a20..28b2bceedc5d 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -514,6 +514,7 @@ const translations = {
subrate: 'Subtasa',
perDiem: 'Per diem',
validate: 'Validar',
+ help: 'Ayuda',
expenseReports: 'Informes de Gastos',
rateOutOfPolicy: 'Tasa fuera de póliza',
},
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 4ed6f22f3f3b..e33ef15cfdcd 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -11,6 +11,7 @@ import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext';
import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal';
+import {SidePaneWithOverlay} from '@components/SidePane';
import TestToolsModal from '@components/TestToolsModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnboardingFlowRouter from '@hooks/useOnboardingFlow';
@@ -453,7 +454,11 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
return (
-
+
{/* This has to be the first navigator in auth screens. */}
St
const useModalCardStyleInterpolator = (): ModalCardStyleInterpolator => {
const {shouldUseNarrowLayout, onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
const StyleUtils = useStyleUtils();
+ const {sidePaneOffset} = useSidePane();
const modalCardStyleInterpolator: ModalCardStyleInterpolator = ({
props: {
@@ -28,13 +32,12 @@ const useModalCardStyleInterpolator = (): ModalCardStyleInterpolator => {
isOnboardingModal = false,
isFullScreenModal = false,
shouldFadeScreen = false,
+ shouldAnimateSidePane = false,
outputRangeMultiplier = 1,
}) => {
if (isOnboardingModal ? onboardingIsMediumOrLargerScreenWidth : shouldFadeScreen) {
return {
- cardStyle: {
- opacity: progress,
- },
+ cardStyle: {opacity: progress},
};
}
@@ -53,6 +56,10 @@ const useModalCardStyleInterpolator = (): ModalCardStyleInterpolator => {
cardStyle.transform = [{translateX}];
}
+ if (shouldAnimateSidePane) {
+ cardStyle.paddingRight = sidePaneOffset.current;
+ }
+
return {
containerStyle: {
overflow: 'hidden',
diff --git a/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts
index 15ca8ea2265a..454aee75cd03 100644
--- a/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts
+++ b/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts
@@ -1,4 +1,5 @@
import type {StackCardInterpolationProps} from '@react-navigation/stack';
+import SidePane from '@components/SidePane';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
@@ -35,15 +36,10 @@ const useRootNavigatorScreenOptions = () => {
animationTypeForReplace: 'pop',
web: {
presentation: Presentation.TRANSPARENT_MODAL,
- cardStyle: {
- ...StyleUtils.getNavigationModalCardStyle(),
- // This is necessary to cover translated sidebar with overlay.
- width: shouldUseNarrowLayout ? '100%' : '200%',
- // Excess space should be on the left so we need to position from right.
- right: 0,
- },
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props}),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, shouldAnimateSidePane: true}),
},
+ // @ts-expect-error SidePane is a custom screen option that was added in a patch (when we migrate to react-navigation v7 we can use screenLayout instead)
+ sidePane: SidePane,
},
basicModalNavigator: {
presentation: Presentation.TRANSPARENT_MODAL,
@@ -95,10 +91,7 @@ const useRootNavigatorScreenOptions = () => {
// We need to turn off animation for the full screen to avoid delay when closing screens.
animation: Animations.NONE,
web: {
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true}),
- cardStyle: {
- ...StyleUtils.getNavigationModalCardStyle(),
- },
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true, shouldAnimateSidePane: true}),
},
},
} satisfies RootNavigatorScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts
index 28df27454ca0..4b8a69c16895 100644
--- a/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts
+++ b/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts
@@ -52,11 +52,8 @@ const useSplitNavigatorScreenOptions = () => {
title: CONFIG.SITE_TITLE,
animation: shouldUseNarrowLayout ? undefined : Animations.NONE,
web: {
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true}),
- cardStyle: {
- ...StyleUtils.getNavigationModalCardStyle(),
- paddingRight: shouldUseNarrowLayout ? 0 : variables.sideBarWidth,
- },
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true, shouldAnimateSidePane: true}),
+ cardStyle: shouldUseNarrowLayout ? StyleUtils.getNavigationModalCardStyle() : undefined,
},
},
} satisfies SplitNavigatorScreenOptions;
diff --git a/src/libs/SidePaneUtils.ts b/src/libs/SidePaneUtils.ts
new file mode 100644
index 000000000000..d9b84edae1a7
--- /dev/null
+++ b/src/libs/SidePaneUtils.ts
@@ -0,0 +1,26 @@
+function substituteRouteParameters(route: string, params: Record) {
+ let updatedRoute = route;
+
+ function searchAndReplace(obj: Record) {
+ for (const key in obj) {
+ if (key === 'path') {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const value = obj[key];
+ if (typeof value === 'object' && value !== null) {
+ searchAndReplace(value as Record);
+ } else if (typeof value === 'string' && route.includes(value)) {
+ updatedRoute = updatedRoute.replace(value, `:${key}`);
+ }
+ }
+ }
+
+ searchAndReplace(params);
+
+ return updatedRoute;
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export {substituteRouteParameters};
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index f5fa451c59a4..53229f6df6ca 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -31,6 +31,7 @@ import {getAll, rollbackOngoingRequest, save} from './PersistedRequests';
import {createDraftInitialWorkspace, createWorkspace, generatePolicyID} from './Policy/Policy';
import {resolveDuplicationConflictAction} from './RequestConflictUtils';
import {isAnonymousUser} from './Session';
+import {triggerSidePane} from './SidePane';
import Timing from './Timing';
type PolicyParamsForOpenOrReconnect = {
@@ -196,6 +197,7 @@ function setLocale(locale: Locale) {
function setLocaleAndNavigate(locale: Locale) {
setLocale(locale);
+ triggerSidePane(false, {shouldUpdateNarrowLayout: true});
Navigation.goBack();
}
diff --git a/src/libs/actions/SidePane.ts b/src/libs/actions/SidePane.ts
new file mode 100644
index 000000000000..bda6a27a0b74
--- /dev/null
+++ b/src/libs/actions/SidePane.ts
@@ -0,0 +1,33 @@
+import type {OnyxMergeInput} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type Options = {
+ /** Whether to update the narrow layout along with the large screen layout */
+ shouldUpdateNarrowLayout?: boolean;
+
+ /** Whether to update only the narrow layout without affecting the large screen layout */
+ shouldOnlyUpdateNarrowLayout?: boolean;
+};
+
+/**
+ * Updates the side pane state in Onyx.
+ *
+ * @param isOpen - Determines whether the side pane should be open or closed.
+ * @param [options] - Additional options for updating the layout.
+ */
+function triggerSidePane(isOpen: boolean, {shouldUpdateNarrowLayout = false, shouldOnlyUpdateNarrowLayout = false}: Options = {}) {
+ const value: OnyxMergeInput = {};
+
+ if (!shouldOnlyUpdateNarrowLayout) {
+ value.open = isOpen;
+ }
+ if (shouldUpdateNarrowLayout || shouldOnlyUpdateNarrowLayout) {
+ value.openNarrowScreen = isOpen;
+ }
+
+ Onyx.merge(ONYXKEYS.NVP_SIDE_PANE, value);
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export {triggerSidePane};
diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx
index 763ee3b10039..30257ea9ae38 100644
--- a/src/pages/home/HeaderView.tsx
+++ b/src/pages/home/HeaderView.tsx
@@ -16,6 +16,7 @@ import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
+import HelpButton from '@components/SidePane/HelpButton';
import SubscriptAvatar from '@components/SubscriptAvatar';
import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import Text from '@components/Text';
@@ -111,6 +112,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked,
const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`);
+ const [sidePane] = useOnyx(ONYXKEYS.NVP_SIDE_PANE);
const [isDismissedDiscountBanner, setIsDismissedDiscountBanner] = useState(false);
const {translate} = useLocalize();
@@ -201,6 +203,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked,
const isParentReportLoading = !!report?.parentReportID && !parentReport;
const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP;
+ const shouldDisplaySidePane = !!sidePane;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, onboardingPurposeSelected);
@@ -349,7 +352,8 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked,
{!shouldUseNarrowLayout && isOpenTaskReport(report, parentReportAction) && }
{!isParentReportLoading && canJoin && !shouldUseNarrowLayout && joinButton}
- {shouldDisplaySearchRouter && }
+ {shouldDisplaySidePane && }
+ {shouldDisplaySearchRouter && }
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 1c808291f98a..f81b890c3dac 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5443,6 +5443,22 @@ const styles = (theme: ThemeColors) =>
marginHorizontal: 8,
alignSelf: 'center',
},
+
+ sidePaneOverlay: {
+ ...positioning.pFixed,
+ right: -variables.sideBarWidth,
+ backgroundColor: theme.overlay,
+ opacity: variables.overlayOpacity,
+ },
+ sidePaneContainer: (shouldUseNarrowLayout: boolean, isExtraLargeScreenWidth: boolean): ViewStyle => ({
+ position: Platform.OS === 'web' ? 'fixed' : 'absolute',
+ right: 0,
+ width: shouldUseNarrowLayout ? '100%' : variables.sideBarWidth,
+ height: '100%',
+ backgroundColor: theme.modalBackground,
+ borderLeftWidth: isExtraLargeScreenWidth ? 1 : 0,
+ borderLeftColor: theme.border,
+ }),
} satisfies Styles);
type ThemeStyles = ReturnType;
diff --git a/src/styles/utils/getNavigationModalCardStyles/index.desktop.ts b/src/styles/utils/getNavigationModalCardStyles/index.desktop.ts
deleted file mode 100644
index df47e76379c5..000000000000
--- a/src/styles/utils/getNavigationModalCardStyles/index.desktop.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import positioning from '@styles/utils/positioning';
-import type GetNavigationModalCardStyles from './types';
-
-const getNavigationModalCardStyles: GetNavigationModalCardStyles = () => ({
- // position: fixed is set instead of position absolute to workaround Safari known issues of updating heights in DOM.
- // Safari issues:
- // https://github.com/Expensify/App/issues/12005
- // https://github.com/Expensify/App/issues/17824
- // https://github.com/Expensify/App/issues/20709
- width: '100%',
- height: '100%',
-
- ...positioning.pFixed,
-});
-
-export default getNavigationModalCardStyles;
diff --git a/src/styles/utils/getNavigationModalCardStyles/index.native.ts b/src/styles/utils/getNavigationModalCardStyles/index.native.ts
new file mode 100644
index 000000000000..1614690dbbcd
--- /dev/null
+++ b/src/styles/utils/getNavigationModalCardStyles/index.native.ts
@@ -0,0 +1,7 @@
+import type GetNavigationModalCardStyles from './types';
+
+const getNavigationModalCardStyles: GetNavigationModalCardStyles = () => ({
+ height: '100%',
+});
+
+export default getNavigationModalCardStyles;
diff --git a/src/styles/utils/getNavigationModalCardStyles/index.ts b/src/styles/utils/getNavigationModalCardStyles/index.ts
index 1614690dbbcd..df47e76379c5 100644
--- a/src/styles/utils/getNavigationModalCardStyles/index.ts
+++ b/src/styles/utils/getNavigationModalCardStyles/index.ts
@@ -1,7 +1,17 @@
+// eslint-disable-next-line no-restricted-imports
+import positioning from '@styles/utils/positioning';
import type GetNavigationModalCardStyles from './types';
const getNavigationModalCardStyles: GetNavigationModalCardStyles = () => ({
+ // position: fixed is set instead of position absolute to workaround Safari known issues of updating heights in DOM.
+ // Safari issues:
+ // https://github.com/Expensify/App/issues/12005
+ // https://github.com/Expensify/App/issues/17824
+ // https://github.com/Expensify/App/issues/20709
+ width: '100%',
height: '100%',
+
+ ...positioning.pFixed,
});
export default getNavigationModalCardStyles;
diff --git a/src/styles/utils/getNavigationModalCardStyles/index.website.ts b/src/styles/utils/getNavigationModalCardStyles/index.website.ts
deleted file mode 100644
index df47e76379c5..000000000000
--- a/src/styles/utils/getNavigationModalCardStyles/index.website.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import positioning from '@styles/utils/positioning';
-import type GetNavigationModalCardStyles from './types';
-
-const getNavigationModalCardStyles: GetNavigationModalCardStyles = () => ({
- // position: fixed is set instead of position absolute to workaround Safari known issues of updating heights in DOM.
- // Safari issues:
- // https://github.com/Expensify/App/issues/12005
- // https://github.com/Expensify/App/issues/17824
- // https://github.com/Expensify/App/issues/20709
- width: '100%',
- height: '100%',
-
- ...positioning.pFixed,
-});
-
-export default getNavigationModalCardStyles;
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index f087a9a19373..516b1d10a0fe 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -92,6 +92,7 @@ export default {
extraSmallMobileResponsiveHeightBreakpoint: 667,
mobileResponsiveWidthBreakpoint: 800,
tabletResponsiveWidthBreakpoint: 1024,
+ sidePanelResponsiveWidthBreakpoint: 1300,
iosSafeAreaInsetsPercentage: 0.7,
androidSafeAreaInsetsPercentage: 1,
sideBarWidth: 375,
diff --git a/src/types/onyx/SidePane.tsx b/src/types/onyx/SidePane.tsx
new file mode 100644
index 000000000000..7029c1c5e397
--- /dev/null
+++ b/src/types/onyx/SidePane.tsx
@@ -0,0 +1,9 @@
+type SidePane = {
+ /** Whether the side pane is open on large screens */
+ open: boolean;
+
+ /** Whether the side pane is open on small screens */
+ openNarrowScreen: boolean;
+};
+
+export default SidePane;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index f5a9acb5f3ea..46d6a2108fc7 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -95,6 +95,7 @@ import type SearchResults from './SearchResults';
import type SecurityGroup from './SecurityGroup';
import type SelectedTabRequest from './SelectedTabRequest';
import type Session from './Session';
+import type SidePane from './SidePane';
import type StripeCustomerID from './StripeCustomerID';
import type Task from './Task';
import type Transaction from './Transaction';
@@ -250,5 +251,6 @@ export type {
JoinablePolicies,
DismissedProductTraining,
TravelProvisioning,
+ SidePane,
LastPaymentMethodType,
};
diff --git a/tests/ui/components/ProductTrainingContextProvider.tsx b/tests/ui/components/ProductTrainingContextProvider.tsx
index 2cdb64c11824..6bc131f80be2 100644
--- a/tests/ui/components/ProductTrainingContextProvider.tsx
+++ b/tests/ui/components/ProductTrainingContextProvider.tsx
@@ -33,6 +33,7 @@ const DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE = {
isExtraSmallScreenWidth: false,
isSmallScreen: false,
onboardingIsMediumOrLargerScreenWidth: false,
+ isExtraLargeScreenWidth: false,
};
const TEST_USER_ACCOUNT_ID = 1;