diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index b522b9b7528b..948bac85a5e2 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import {createStackNavigator} from '@react-navigation/stack'; - import {getNavigationModalCardStyle} from '../../../styles/styles'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import CONST from '../../../CONST'; @@ -25,6 +23,8 @@ import KeyboardShortcut from '../../KeyboardShortcut'; import Navigation from '../Navigation'; import * as User from '../../actions/User'; import NameValuePair from '../../actions/NameValuePair'; +import modalCardStyleInterpolator from './modalCardStyleInterpolator'; +import createCustomModalStackNavigator from './createCustomModalStackNavigator'; // Main drawer navigator import MainDrawerNavigator from './MainDrawerNavigator'; @@ -40,7 +40,7 @@ import { SettingsModalStackNavigator, } from './ModalStackNavigators'; -const RootStack = createStackNavigator(); +const RootStack = createCustomModalStackNavigator(); const propTypes = { network: PropTypes.shape({isOffline: PropTypes.bool}), @@ -116,8 +116,14 @@ class AuthScreens extends React.Component { const modalScreenOptions = { headerShown: false, cardStyle: getNavigationModalCardStyle(this.props.isSmallScreenWidth), - }; + cardStyleInterpolator: modalCardStyleInterpolator, + animationEnabled: true, + gestureDirection: 'horizontal', + // This is a custom prop we are passing to custom navigator so that we will know to add a Pressable overlay + // when displaying a modal. This allows us to dismiss by clicking outside on web / large screens. + isModal: true, + }; return ( { + if (!props.isDisplayingModal) { + return null; + } + + return ( + + ); +}; + +ClickAwayHandler.propTypes = propTypes; +ClickAwayHandler.displayName = 'ClickAwayHandler'; +export default ClickAwayHandler; diff --git a/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.js b/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.js new file mode 100644 index 000000000000..837018cba06c --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.js @@ -0,0 +1,3 @@ +import ClickAwayHandler from './ClickAwayHandler'; + +export default ClickAwayHandler; diff --git a/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.native.js b/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.native.js new file mode 100644 index 000000000000..565061dadb5f --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ClickAwayHandler/index.native.js @@ -0,0 +1,20 @@ +import React from 'react'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions'; +import ClickAwayHandler from './ClickAwayHandler'; + +const propTypes = { + ...windowDimensionsPropTypes, +}; + +const ClickAwayHandlerWithWindowDimensions = (props) => { + if (props.isSmallScreenWidth) { + return null; + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +}; + +ClickAwayHandlerWithWindowDimensions.propTypes = propTypes; +ClickAwayHandlerWithWindowDimensions.displayName = 'ClickAwayHandlerWithWindowDimensions'; +export default withWindowDimensions(ClickAwayHandlerWithWindowDimensions); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index bcb6abbce011..b7aae33daec5 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -1,16 +1,7 @@ import React from 'react'; - +import {createStackNavigator} from '@react-navigation/stack'; import styles from '../../../styles/styles'; import ROUTES from '../../../ROUTES'; -import { - SettingsModalStack, - NewChatModalStack, - NewGroupModalStack, - SearchModalStack, - DetailsModalStack, - IOURequestModalStack, - IOUBillModalStack, -} from './ModalStacks'; import NewChatPage from '../../../pages/NewChatPage'; import NewGroupPage from '../../../pages/NewGroupPage'; import SearchPage from '../../../pages/SearchPage'; @@ -23,6 +14,15 @@ import SettingsPreferencesPage from '../../../pages/settings/PreferencesPage'; import SettingsPasswordPage from '../../../pages/settings/PasswordPage'; import SettingsPaymentsPage from '../../../pages/settings/PaymentsPage'; +// Setup the modal stack navigators so we only have to create them once +const SettingsModalStack = createStackNavigator(); +const NewChatModalStack = createStackNavigator(); +const NewGroupModalStack = createStackNavigator(); +const SearchModalStack = createStackNavigator(); +const DetailsModalStack = createStackNavigator(); +const IOURequestModalStack = createStackNavigator(); +const IOUBillModalStack = createStackNavigator(); + const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, headerShown: false, diff --git a/src/libs/Navigation/AppNavigator/ModalStacks/index.js b/src/libs/Navigation/AppNavigator/ModalStacks/index.js deleted file mode 100644 index 89a8cf1ee59a..000000000000 --- a/src/libs/Navigation/AppNavigator/ModalStacks/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import createCustomModalStackNavigator from '../createCustomModalStackNavigator'; - -// Setup the modal stack navigators so we only have to create them once -const SettingsModalStack = createCustomModalStackNavigator(); -const NewChatModalStack = createCustomModalStackNavigator(); -const NewGroupModalStack = createCustomModalStackNavigator(); -const SearchModalStack = createCustomModalStackNavigator(); -const DetailsModalStack = createCustomModalStackNavigator(); -const IOURequestModalStack = createCustomModalStackNavigator(); -const IOUBillModalStack = createCustomModalStackNavigator(); - -export { - SettingsModalStack, - NewChatModalStack, - NewGroupModalStack, - SearchModalStack, - DetailsModalStack, - IOURequestModalStack, - IOUBillModalStack, -}; diff --git a/src/libs/Navigation/AppNavigator/ModalStacks/index.native.js b/src/libs/Navigation/AppNavigator/ModalStacks/index.native.js deleted file mode 100644 index 3b26a3efcb4e..000000000000 --- a/src/libs/Navigation/AppNavigator/ModalStacks/index.native.js +++ /dev/null @@ -1,26 +0,0 @@ -import {Dimensions} from 'react-native'; -import {createStackNavigator} from '@react-navigation/stack'; - -import variables from '../../../../styles/variables'; -import createCustomModalStackNavigator from '../createCustomModalStackNavigator'; - -const shouldUseCustomModalStack = Dimensions.get('window').width > variables.mobileResponsiveWidthBreakpoint; - -// Setup the modal stack navigators so we only have to create them once -const SettingsModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const NewChatModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const NewGroupModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const SearchModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const DetailsModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const IOURequestModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); -const IOUBillModalStack = shouldUseCustomModalStack ? createCustomModalStackNavigator() : createStackNavigator(); - -export { - SettingsModalStack, - NewChatModalStack, - NewGroupModalStack, - SearchModalStack, - DetailsModalStack, - IOURequestModalStack, - IOUBillModalStack, -}; diff --git a/src/libs/Navigation/AppNavigator/createCustomModalStackNavigator.js b/src/libs/Navigation/AppNavigator/createCustomModalStackNavigator.js index cd9d0e632f7a..5c3951e8751f 100644 --- a/src/libs/Navigation/AppNavigator/createCustomModalStackNavigator.js +++ b/src/libs/Navigation/AppNavigator/createCustomModalStackNavigator.js @@ -1,114 +1,40 @@ +import _ from 'underscore'; import React from 'react'; import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/core'; import {StackRouter} from '@react-navigation/routers'; -import withWindowDimensions from '../../../components/withWindowDimensions'; -import Modal from '../../../components/Modal'; -import themeColors from '../../../styles/themes/default'; -import ONYXKEYS from '../../../ONYXKEYS'; -import Navigation from '../Navigation'; -import compose from '../../compose'; -import CONST from '../../../CONST'; +import {StackView} from '@react-navigation/stack'; +import ClickAwayHandler from './ClickAwayHandler'; -const propTypes = { - // Navigation state for this navigator - // See: https://reactnavigation.org/docs/navigation-state/ - state: PropTypes.shape({ - // Index of the focused route object in the routes array - index: PropTypes.number, - - // List of route objects (screens) which are rendered in the navigator. It also represents the history in a - // stack navigator. There should be at least one item present in this array. - routes: PropTypes.arrayOf(PropTypes.shape({ - - // A unique key name for a screen. Created automatically by react-nav. - key: PropTypes.string, - })), - }).isRequired, - - // Object containing descriptors for each route with the route keys as its properties - // See: https://reactnavigation.org/docs/custom-navigators/#usenavigationbuilder - // eslint-disable-next-line react/no-unused-prop-types - descriptors: PropTypes.objectOf(PropTypes.shape({ - - // A function which can be used to render the actual screen. Calling descriptors[route.key].render() will return - // a React element containing the screen content. - render: PropTypes.func, - })).isRequired, - - // Current url we are navigated to - currentURL: PropTypes.string, - - // Path for the modal parent to match on - path: PropTypes.string.isRequired, -}; - -const defaultProps = { - currentURL: '', -}; - -/** - * Returns the current descriptor for the focused screen in this navigators state. The descriptor has a function - * called render() that we must call each time this navigator updates. It's important to use this method to render - * a screen, otherwise any child navigators won't be connected to the navigation tree properly. - * - * @param {Object} props - * @returns {Object} - */ -function getCurrentViewDescriptor(props) { - const currentRoute = props.state.routes[props.state.index]; - const currentRouteKey = currentRoute.key; - const currentDescriptor = props.descriptors[currentRouteKey]; - return currentDescriptor; -} - -const ResponsiveView = props => ( - - {getCurrentViewDescriptor(props).render()} - -); - -ResponsiveView.propTypes = propTypes; -ResponsiveView.defaultProps = defaultProps; -ResponsiveView.displayName = 'ResponsiveView'; - -const ResponsiveViewWithHOCs = compose( - withWindowDimensions, - withOnyx({ - currentURL: { - key: ONYXKEYS.CURRENT_URL, - }, - }), -)(ResponsiveView); - -const ResponsiveNavigator = ({ +const CustomRootStackNavigator = ({ children, ...rest }) => { const {state, navigation, descriptors} = useNavigationBuilder(StackRouter, { children, }); - + const isDisplayingModal = Boolean(_.find(descriptors, descriptor => descriptor.options.isModal)); return ( - + <> + + + {/* We need to superimpose a clickaway handler when showing modals so that they can be dismissed. Capturing + press events on the cardOverlay element in react-navigation is not yet supported on web */} + + ); }; -ResponsiveNavigator.propTypes = { +CustomRootStackNavigator.propTypes = { children: PropTypes.node.isRequired, }; -export default createNavigatorFactory(ResponsiveNavigator); +export default createNavigatorFactory(CustomRootStackNavigator); diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js new file mode 100644 index 000000000000..28b85c68b7ec --- /dev/null +++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js @@ -0,0 +1,31 @@ +import {Animated} from 'react-native'; + +export default ({ + current: {progress}, + inverted, + layouts: { + screen, + }, +}) => { + const translateX = Animated.multiply(progress.interpolate({ + inputRange: [0, 1], + outputRange: [screen.width, 0], + extrapolate: 'clamp', + }), inverted); + + return ({ + containerStyle: { + overflow: 'hidden', + }, + cardStyle: { + transform: [{translateX}], + }, + overlayStyle: { + opacity: progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.3], + extrapolate: 'clamp', + }), + }, + }); +}; diff --git a/src/styles/styles.js b/src/styles/styles.js index 7205fb337d2b..cbfbc83e7007 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -811,6 +811,15 @@ const styles = { zIndex: 2, }, + navigationModalOverlay: { + position: 'absolute', + width: '100%', + height: '100%', + transform: [{ + translateX: -variables.sideBarWidth, + }], + }, + sidebarVisible: { borderRightWidth: 1, },