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;