Skip to content
61 changes: 61 additions & 0 deletions src/components/SidePane/Help/HelpContent.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>) ?? {};
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 (
<>
<HeaderGap />
<HeaderWithBackButton
title={translate('common.help')}
style={styles.headerBarDesktopHeight}
onBackButtonPress={() => closeSidePane(false)}
onCloseButtonPress={() => closeSidePane(false)}
shouldShowBackButton={!isExtraLargeScreenWidth}
shouldShowCloseButton={isExtraLargeScreenWidth}
shouldDisplayHelpButton={false}
/>
<ScrollView style={[styles.ph5, styles.pb5]}>{getHelpContent(styles, route, isProduction)}</ScrollView>
</>
);
}

HelpContent.displayName = 'HelpContent';

export default HelpContent;
38 changes: 38 additions & 0 deletions src/components/SidePane/Help/index.android.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Animated.View style={[styles.sidePaneContainer(shouldUseNarrowLayout, isExtraLargeScreenWidth), {transform: [{translateX: sidePaneTranslateX.current}], paddingTop, paddingBottom}]}>
<HelpContent />
</Animated.View>
);
}

Help.displayName = 'Help';

export default Help;
58 changes: 58 additions & 0 deletions src/components/SidePane/Help/index.ios.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<GestureDetector gesture={panGesture}>
<Animated.View
style={[styles.sidePaneContainer(shouldUseNarrowLayout, isExtraLargeScreenWidth), {transform: [{translateX: sidePaneTranslateX.current}], paddingTop, paddingBottom}]}
>
<HelpContent />
</Animated.View>
</GestureDetector>
);
}

Help.displayName = 'Help';
export default Help;
28 changes: 28 additions & 0 deletions src/components/SidePane/Help/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Animated.View style={[styles.sidePaneContainer(shouldUseNarrowLayout, isExtraLargeScreenWidth), {transform: [{translateX: sidePaneTranslateX.current}], paddingTop, paddingBottom}]}>
<HelpContent />
</Animated.View>
);
}

Help.displayName = 'Help';

export default Help;
10 changes: 10 additions & 0 deletions src/components/SidePane/Help/types.ts
Original file line number Diff line number Diff line change
@@ -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<Animated.Value>;
closeSidePane: (shouldUpdateNarrow?: boolean) => void;
};

export default HelpProps;
91 changes: 10 additions & 81 deletions src/components/SidePane/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>) ?? {};
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;
Expand All @@ -79,26 +19,15 @@ function SidePane() {
<View>
{!shouldHideSidePaneBackdrop && (
<SidePaneOverlay
onBackdropPress={onClose}
onBackdropPress={closeSidePane}
isInNarrowPaneModal={isInNarrowPaneModal}
/>
)}
</View>
<Animated.View
style={[styles.sidePaneContainer(shouldUseNarrowLayout, isExtraLargeScreenWidth), {transform: [{translateX: sidePaneTranslateX.current}], paddingTop, paddingBottom}]}
>
<HeaderGap />
<HeaderWithBackButton
title={translate('common.help')}
style={styles.headerBarDesktopHeight}
onBackButtonPress={() => onClose(false)}
onCloseButtonPress={() => onClose(false)}
shouldShowBackButton={!isExtraLargeScreenWidth}
shouldShowCloseButton={isExtraLargeScreenWidth}
shouldDisplayHelpButton={false}
/>
<ScrollView style={[styles.ph5, styles.pb5]}>{getHelpContent(styles, route, isProduction)}</ScrollView>
</Animated.View>
<Help
sidePaneTranslateX={sidePaneTranslateX}
closeSidePane={closeSidePane}
/>
</>
);
}
Expand Down
19 changes: 18 additions & 1 deletion src/hooks/useSidePane.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -67,13 +68,29 @@ 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,
shouldHideSidePaneBackdrop,
shouldHideHelpButton,
sidePaneOffset,
sidePaneTranslateX,
closeSidePane,
};
}

Expand Down