diff --git a/src/components/SidePane/Help/HelpContent.tsx b/src/components/SidePane/Help/HelpContent.tsx new file mode 100644 index 0000000000000..9080dd84cc320 --- /dev/null +++ b/src/components/SidePane/Help/HelpContent.tsx @@ -0,0 +1,61 @@ +import {findFocusedRoute} from '@react-navigation/native'; +import React, {useEffect, useRef} from 'react'; +import HeaderGap from '@components/HeaderGap'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScrollView from '@components/ScrollView'; +import getHelpContent from '@components/SidePane/getHelpContent'; +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useRootNavigationState from '@hooks/useRootNavigationState'; +import useSidePane from '@hooks/useSidePane'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {substituteRouteParameters} from '@libs/SidePaneUtils'; + +function HelpContent() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isProduction} = useEnvironment(); + const {isExtraLargeScreenWidth} = useResponsiveLayout(); + const {closeSidePane} = useSidePane(); + const route = useRootNavigationState((state) => { + const params = (findFocusedRoute(state)?.params as Record) ?? {}; + const activeRoute = Navigation.getActiveRouteWithoutParams(); + return substituteRouteParameters(activeRoute, params); + }); + + const wasPreviousNarrowScreen = useRef(!isExtraLargeScreenWidth); + useEffect(() => { + // Close the side pane when the screen size changes from large to small + if (!isExtraLargeScreenWidth && !wasPreviousNarrowScreen.current) { + closeSidePane(true); + wasPreviousNarrowScreen.current = true; + } + + // Reset the trigger when the screen size changes back to large + if (isExtraLargeScreenWidth) { + wasPreviousNarrowScreen.current = false; + } + }, [isExtraLargeScreenWidth, closeSidePane]); + + return ( + <> + + closeSidePane(false)} + onCloseButtonPress={() => closeSidePane(false)} + shouldShowBackButton={!isExtraLargeScreenWidth} + shouldShowCloseButton={isExtraLargeScreenWidth} + shouldDisplayHelpButton={false} + /> + {getHelpContent(styles, route, isProduction)} + + ); +} + +HelpContent.displayName = 'HelpContent'; + +export default HelpContent; diff --git a/src/components/SidePane/Help/index.android.tsx b/src/components/SidePane/Help/index.android.tsx new file mode 100644 index 0000000000000..e907643f66974 --- /dev/null +++ b/src/components/SidePane/Help/index.android.tsx @@ -0,0 +1,38 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {Animated, BackHandler} from 'react-native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import HelpContent from './HelpContent'; +import type HelpProps from './types'; + +function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { + const styles = useThemeStyles(); + const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {paddingTop, paddingBottom} = useStyledSafeAreaInsets(); + + // SidePane isn't a native screen, this handles the back button press on Android + useFocusEffect( + useCallback(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { + closeSidePane(); + // Return true to indicate that the back button press is handled here + return true; + }); + + return () => backHandler.remove(); + }, [closeSidePane]), + ); + + return ( + + + + ); +} + +Help.displayName = 'Help'; + +export default Help; diff --git a/src/components/SidePane/Help/index.ios.tsx b/src/components/SidePane/Help/index.ios.tsx new file mode 100644 index 0000000000000..7b9e5b65849fe --- /dev/null +++ b/src/components/SidePane/Help/index.ios.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import {Animated, Dimensions} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import HelpContent from './HelpContent'; +import type HelpProps from './types'; + +const SCREEN_WIDTH = Dimensions.get('window').width; + +function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { + const styles = useThemeStyles(); + const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {paddingTop, paddingBottom} = useStyledSafeAreaInsets(); + + // SidePane isn't a native screen, this simulates the 'close swipe gesture' on iOS + const panGesture = Gesture.Pan() + .runOnJS(true) + .hitSlop({left: 0, width: 20}) + .onUpdate((event) => { + if (event.translationX <= 0) { + return; + } + sidePaneTranslateX.current.setValue(event.translationX); + }) + .onEnd((event) => { + if (event.translationX > 100) { + // If swiped far enough, animate out and close + Animated.timing(sidePaneTranslateX.current, { + toValue: SCREEN_WIDTH, + duration: CONST.ANIMATED_TRANSITION, + useNativeDriver: false, + }).start(() => closeSidePane()); + } else { + // Otherwise, animate back to original position + Animated.spring(sidePaneTranslateX.current, { + toValue: 0, + useNativeDriver: false, + }).start(); + } + }); + + return ( + + + + + + ); +} + +Help.displayName = 'Help'; +export default Help; diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx new file mode 100644 index 0000000000000..08567e2f1fab0 --- /dev/null +++ b/src/components/SidePane/Help/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import {Animated} from 'react-native'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import HelpContent from './HelpContent'; +import type HelpProps from './types'; + +function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { + const styles = useThemeStyles(); + const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {paddingTop, paddingBottom} = useStyledSafeAreaInsets(); + + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth}); + + return ( + + + + ); +} + +Help.displayName = 'Help'; + +export default Help; diff --git a/src/components/SidePane/Help/types.ts b/src/components/SidePane/Help/types.ts new file mode 100644 index 0000000000000..8054cc6cd43d6 --- /dev/null +++ b/src/components/SidePane/Help/types.ts @@ -0,0 +1,10 @@ +import type {MutableRefObject} from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {Animated} from 'react-native'; + +type HelpProps = { + sidePaneTranslateX: MutableRefObject; + closeSidePane: (shouldUpdateNarrow?: boolean) => void; +}; + +export default HelpProps; diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx index ec2aa5a959986..4a2f7adba8d34 100644 --- a/src/components/SidePane/index.tsx +++ b/src/components/SidePane/index.tsx @@ -1,74 +1,14 @@ -import {findFocusedRoute} 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 HeaderGap from '@components/HeaderGap'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScrollView from '@components/ScrollView'; -import useEnvironment from '@hooks/useEnvironment'; -import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import React from 'react'; +import {View} from 'react-native'; import useRootNavigationState from '@hooks/useRootNavigationState'; import useSidePane from '@hooks/useSidePane'; -import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {triggerSidePane} from '@libs/actions/SidePane'; -import Navigation from '@libs/Navigation/Navigation'; -import {substituteRouteParameters} from '@libs/SidePaneUtils'; -import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import getHelpContent from './getHelpContent'; +import Help from './Help'; import SidePaneOverlay from './SidePaneOverlay'; function SidePane() { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isProduction} = useEnvironment(); - const {route, isInNarrowPaneModal} = useRootNavigationState((state) => { - const params = (findFocusedRoute(state)?.params as Record) ?? {}; - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - return { - route: substituteRouteParameters(activeRoute, params), - isInNarrowPaneModal: state?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR, - }; - }); - - const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {sidePaneTranslateX, shouldHideSidePane, shouldHideSidePaneBackdrop, sidePane} = useSidePane(); - const {paddingTop, paddingBottom} = useStyledSafeAreaInsets(); - - const onClose = useCallback( - (shouldUpdateNarrow = false) => { - if (!sidePane) { - return; - } - - const shouldOnlyUpdateNarrowLayout = !isExtraLargeScreenWidth || shouldUpdateNarrow; - triggerSidePane({ - isOpen: shouldOnlyUpdateNarrowLayout ? undefined : false, - isOpenNarrowScreen: shouldOnlyUpdateNarrowLayout ? false : undefined, - }); - }, - [isExtraLargeScreenWidth, sidePane], - ); - - 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]); - - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => onClose(), {shouldBubble: shouldHideSidePane, isActive: !isExtraLargeScreenWidth}); + const {shouldHideSidePane, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); + const isInNarrowPaneModal = useRootNavigationState((state) => state?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); if (shouldHideSidePane) { return null; @@ -79,26 +19,15 @@ function SidePane() { {!shouldHideSidePaneBackdrop && ( )} - - - onClose(false)} - onCloseButtonPress={() => onClose(false)} - shouldShowBackButton={!isExtraLargeScreenWidth} - shouldShowCloseButton={isExtraLargeScreenWidth} - shouldDisplayHelpButton={false} - /> - {getHelpContent(styles, route, isProduction)} - + ); } diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts index b7f928c8a28a8..912857fbe14e1 100644 --- a/src/hooks/useSidePane.ts +++ b/src/hooks/useSidePane.ts @@ -1,9 +1,10 @@ -import {useEffect, useRef, useState} from 'react'; +import {useCallback, 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 {triggerSidePane} from '@libs/actions/SidePane'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -67,6 +68,21 @@ function useSidePane() { }); }, [isPaneHidden, shouldApplySidePaneOffset, shouldUseNarrowLayout, sidePaneWidth]); + const closeSidePane = useCallback( + (shouldUpdateNarrow = false) => { + if (!sidePaneNVP) { + return; + } + + const shouldOnlyUpdateNarrowLayout = !isExtraLargeScreenWidth || shouldUpdateNarrow; + triggerSidePane({ + isOpen: shouldOnlyUpdateNarrowLayout ? undefined : false, + isOpenNarrowScreen: shouldOnlyUpdateNarrowLayout ? false : undefined, + }); + }, + [isExtraLargeScreenWidth, sidePaneNVP], + ); + return { sidePane: sidePaneNVP, shouldHideSidePane, @@ -74,6 +90,7 @@ function useSidePane() { shouldHideHelpButton, sidePaneOffset, sidePaneTranslateX, + closeSidePane, }; }