diff -ruN node_modules/@react-navigation/stack/src/index.tsx src/vendor/index.tsx --- node_modules/@react-navigation/stack/src/index.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/index.tsx 1970-01-01 01:00:00.000000000 +0100 @@ -1,57 +0,0 @@ -import * as CardStyleInterpolators from './TransitionConfigs/CardStyleInterpolators'; -import * as HeaderStyleInterpolators from './TransitionConfigs/HeaderStyleInterpolators'; -import * as TransitionSpecs from './TransitionConfigs/TransitionSpecs'; -import * as TransitionPresets from './TransitionConfigs/TransitionPresets'; - -/** - * Navigators - */ -export { default as createStackNavigator } from './navigators/createStackNavigator'; - -export const Assets = [ - // eslint-disable-next-line import/no-commonjs - require('./views/assets/back-icon.png'), - // eslint-disable-next-line import/no-commonjs - require('./views/assets/back-icon-mask.png'), -]; - -/** - * Views - */ -export { default as StackView } from './views/Stack/StackView'; -export { default as Header } from './views/Header/Header'; -export { default as HeaderTitle } from './views/Header/HeaderTitle'; -export { default as HeaderBackButton } from './views/Header/HeaderBackButton'; - -/** - * Transition presets - */ -export { - CardStyleInterpolators, - HeaderStyleInterpolators, - TransitionSpecs, - TransitionPresets, -}; - -/** - * Utilities - */ -export { default as StackGestureContext } from './utils/StackGestureContext'; -export { default as StackCardAnimationContext } from './utils/StackCardAnimationContext'; - -/** - * Types - */ -export { - StackNavigationOptions, - StackNavigationProp, - StackHeaderProps, - StackHeaderLeftButtonProps, - StackHeaderTitleProps, - StackCardInterpolatedStyle, - StackCardInterpolationProps, - StackCardStyleInterpolator, - StackHeaderInterpolatedStyle, - StackHeaderInterpolationProps, - StackHeaderStyleInterpolator, -} from './types'; diff -ruN node_modules/@react-navigation/stack/src/navigators/createStackNavigator.tsx src/vendor/navigators/createStackNavigator.tsx --- node_modules/@react-navigation/stack/src/navigators/createStackNavigator.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/navigators/createStackNavigator.tsx 1970-01-01 01:00:00.000000000 +0100 @@ -1,77 +0,0 @@ -import * as React from 'react'; -import { - useNavigationBuilder, - createNavigatorFactory, - DefaultNavigatorOptions, - EventArg, -} from '@react-navigation/native'; -import { - StackRouter, - StackRouterOptions, - StackNavigationState, - StackActions, -} from '@react-navigation/routers'; -import StackView from '../views/Stack/StackView'; -import { - StackNavigationConfig, - StackNavigationOptions, - StackNavigationEventMap, -} from '../types'; - -type Props = DefaultNavigatorOptions & - StackRouterOptions & - StackNavigationConfig; - -function StackNavigator({ - initialRouteName, - children, - screenOptions, - ...rest -}: Props) { - const { state, descriptors, navigation } = useNavigationBuilder< - StackNavigationState, - StackRouterOptions, - StackNavigationOptions, - StackNavigationEventMap - >(StackRouter, { - initialRouteName, - children, - screenOptions, - }); - - React.useEffect( - () => - navigation.addListener && - navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => { - const isFocused = navigation.isFocused(); - - // Run the operation in the next frame so we're sure all listeners have been run - // This is necessary to know if preventDefault() has been called - requestAnimationFrame(() => { - if (state.index > 0 && isFocused && !e.defaultPrevented) { - // When user taps on already focused tab and we're inside the tab, - // reset the stack to replicate native behaviour - navigation.dispatch({ - ...StackActions.popToTop(), - target: state.key, - }); - } - }); - }), - [navigation, state.index, state.key] - ); - - return ( - - ); -} - -export default createNavigatorFactory< - StackNavigationOptions, - typeof StackNavigator ->(StackNavigator); diff -ruN node_modules/@react-navigation/stack/src/types.tsx src/vendor/types.tsx --- node_modules/@react-navigation/stack/src/types.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/types.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -8,13 +8,28 @@ } from 'react-native'; import { EdgeInsets } from 'react-native-safe-area-context'; import { + NavigationRoute, + NavigationState, + NavigationScreenProp, NavigationProp, - ParamListBase, - Descriptor, - Route, - NavigationHelpers, -} from '@react-navigation/native'; -import { StackNavigationState } from '@react-navigation/routers'; + NavigationParams, + NavigationNavigateAction, + NavigationAction, + NavigationEventCallback, + NavigationEventSubscription, + NavigationDescriptor, +} from 'react-navigation'; + +// @ts-ignore +export type Route = NavigationRoute; + +export type NavigationStackState = NavigationState; + +export type NavigationStackEventName = + | 'willFocus' + | 'didFocus' + | 'willBlur' + | 'didBlur'; export type StackNavigationEventMap = { /** @@ -27,42 +42,29 @@ transitionEnd: { closing: boolean }; }; -export type StackNavigationHelpers = NavigationHelpers< - ParamListBase, - StackNavigationEventMap ->; +export type StackNavigationHelpers = NavigationProp export type StackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = string -> = NavigationProp< - ParamList, - RouteName, - StackNavigationState, - StackNavigationOptions, - StackNavigationEventMap -> & { - /** - * Push a new screen onto the stack. - * - * @param name Name of the route for the tab. - * @param [params] Params object for the route. - */ - push( - ...args: ParamList[RouteName] extends undefined | any - ? [RouteName] | [RouteName, ParamList[RouteName]] - : [RouteName, ParamList[RouteName]] - ): void; - - /** - * Pop a screen from the stack. - */ - pop(count?: number): void; - - /** - * Pop to the first route in the stack, dismissing all other screens. - */ - popToTop(): void; +State = NavigationRoute, +Params = NavigationParams +> = NavigationScreenProp & { + push: ( + routeName: string, + params?: NavigationParams, + action?: NavigationNavigateAction + ) => boolean; + replace: ( + routeName: string, + params?: NavigationParams, + action?: NavigationNavigateAction + ) => boolean; + reset: (actions: NavigationAction[], index: number) => boolean; + pop: (n?: number, params?: { immediate?: boolean }) => boolean; + popToTop: (params?: { immediate?: boolean }) => boolean; + addListener: ( + event: NavigationStackEventName, + callback: NavigationEventCallback + ) => NavigationEventSubscription; }; export type Layout = { width: number; height: number }; @@ -232,24 +234,27 @@ /** * Navigation prop for the header. */ - navigation: StackNavigationProp; + navigation: StackNavigationProp; /** * Interpolated styles for various elements in the header. */ styleInterpolator: StackHeaderStyleInterpolator; }; -export type StackDescriptor = Descriptor< - ParamListBase, - string, - StackNavigationState, - StackNavigationOptions ->; +export type StackDescriptor = NavigationDescriptor< + NavigationParams, + StackNavigationOptions, + StackNavigationProp +> export type StackDescriptorMap = { [key: string]: StackDescriptor; }; +export type TransitionCallbackProps = { + closing: boolean; +}; + export type StackNavigationOptions = StackHeaderOptions & Partial & { /** @@ -322,6 +327,8 @@ bottom?: number; left?: number; }; + onTransitionStart?: (props: TransitionCallbackProps) => void; + onTransitionEnd?: (props: TransitionCallbackProps) => void; }; export type StackNavigationConfig = { diff -ruN node_modules/@react-navigation/stack/src/views/Header/Header.tsx src/vendor/views/Header/Header.tsx --- node_modules/@react-navigation/stack/src/views/Header/Header.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/Header.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StackActions } from '@react-navigation/routers'; +import { StackActions } from 'react-navigation'; import HeaderSegment from './HeaderSegment'; import { StackHeaderProps, StackHeaderTitleProps } from '../../types'; @@ -21,7 +21,7 @@ ? options.headerTitle : options.title !== undefined ? options.title - : scene.route.name; + : scene.route.routeName; let leftLabel; @@ -37,7 +37,7 @@ ? o.headerTitle : o.title !== undefined ? o.title - : previous.route.name; + : previous.route.routeName; } return ( @@ -55,11 +55,8 @@ } onGoBack={ previous - ? () => - navigation.dispatch({ - ...StackActions.pop(), - source: scene.route.key, - }) + // @ts-ignore + ? () => navigation.dispatch(StackActions.pop({ key: scene.route.key })) : undefined } styleInterpolator={styleInterpolator} diff -ruN node_modules/@react-navigation/stack/src/views/Header/HeaderBackButton.tsx src/vendor/views/Header/HeaderBackButton.tsx --- node_modules/@react-navigation/stack/src/views/Header/HeaderBackButton.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/HeaderBackButton.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -8,9 +8,9 @@ StyleSheet, LayoutChangeEvent, } from 'react-native'; -import { useTheme } from '@react-navigation/native'; import MaskedView from '../MaskedView'; import TouchableItem from '../TouchableItem'; +import useTheme from '../../../utils/useTheme'; import { StackHeaderLeftButtonProps } from '../../types'; type Props = StackHeaderLeftButtonProps; diff -ruN node_modules/@react-navigation/stack/src/views/Header/HeaderBackground.tsx src/vendor/views/Header/HeaderBackground.tsx --- node_modules/@react-navigation/stack/src/views/Header/HeaderBackground.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/HeaderBackground.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -1,6 +1,6 @@ import * as React from 'react'; import { Animated, StyleSheet, Platform, ViewProps } from 'react-native'; -import { useTheme } from '@react-navigation/native'; +import useTheme from '../../../utils/useTheme'; export default function HeaderBackground({ style, ...rest }: ViewProps) { const { colors } = useTheme(); diff -ruN node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx src/vendor/views/Header/HeaderContainer.tsx --- node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/HeaderContainer.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -1,16 +1,13 @@ import * as React from 'react'; import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; -import { - NavigationContext, - Route, - ParamListBase, -} from '@react-navigation/native'; -import { StackNavigationState } from '@react-navigation/routers'; +import { NavigationContext } from 'react-navigation'; +import { NavigationState as StackNavigationState } from 'react-navigation'; import { EdgeInsets } from 'react-native-safe-area-context'; import Header from './Header'; import { forStatic } from '../../TransitionConfigs/HeaderStyleInterpolators'; import { + Route, Layout, Scene, StackHeaderStyleInterpolator, @@ -93,9 +90,7 @@ insets, scene, previous, - navigation: scene.descriptor.navigation as StackNavigationProp< - ParamListBase - >, + navigation: scene.descriptor.navigation as StackNavigationProp, styleInterpolator: isHeaderStatic ? forStatic : styleInterpolator, }; diff -ruN node_modules/@react-navigation/stack/src/views/Header/HeaderSegment.tsx src/vendor/views/Header/HeaderSegment.tsx --- node_modules/@react-navigation/stack/src/views/Header/HeaderSegment.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/HeaderSegment.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -8,7 +8,7 @@ ViewStyle, } from 'react-native'; import { EdgeInsets } from 'react-native-safe-area-context'; -import { Route } from '@react-navigation/native'; +import { NavigationRoute } from 'react-navigation'; import HeaderBackButton from './HeaderBackButton'; import HeaderBackground from './HeaderBackground'; import memoize from '../../utils/memoize'; @@ -28,7 +28,7 @@ onGoBack?: () => void; title?: string; leftLabel?: string; - scene: Scene>; + scene: Scene; styleInterpolator: StackHeaderStyleInterpolator; }; diff -ruN node_modules/@react-navigation/stack/src/views/Header/HeaderTitle.tsx src/vendor/views/Header/HeaderTitle.tsx --- node_modules/@react-navigation/stack/src/views/Header/HeaderTitle.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Header/HeaderTitle.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -1,6 +1,6 @@ import * as React from 'react'; import { Animated, StyleSheet, Platform, TextProps } from 'react-native'; -import { useTheme } from '@react-navigation/native'; +import useTheme from '../../../utils/useTheme'; type Props = TextProps & { tintColor?: string; diff -ruN node_modules/@react-navigation/stack/src/views/Stack/Card.tsx src/vendor/views/Stack/Card.tsx --- node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Stack/Card.tsx 2020-01-03 21:48:42.000000000 +0100 @@ -453,7 +453,7 @@ pointerEvents="none" /> ) : null} - + {children} diff -ruN node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx src/vendor/views/Stack/CardContainer.tsx --- node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Stack/CardContainer.tsx 2020-01-03 21:49:43.000000000 +0100 @@ -1,10 +1,16 @@ import * as React from 'react'; import { Animated, View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; -import { StackNavigationState } from '@react-navigation/routers'; -import { Route, useTheme } from '@react-navigation/native'; +import { NavigationState as StackNavigationState } from 'react-navigation'; import { Props as HeaderContainerProps } from '../Header/HeaderContainer'; import Card from './Card'; -import { Scene, Layout, StackHeaderMode, TransitionPreset } from '../../types'; +import useTheme from '../../../utils/useTheme'; +import { + Route, + Scene, + Layout, + StackHeaderMode, + TransitionPreset, +} from '../../types'; type Props = TransitionPreset & { index: number; @@ -152,7 +158,7 @@ ? { marginTop: floatingHeaderHeight } : null } - contentStyle={[{ backgroundColor: colors.background }, cardStyle]} + contentStyle={[{ backgroundColor: colors.background }, cardStyle] as any} style={StyleSheet.absoluteFill} > diff -ruN node_modules/@react-navigation/stack/src/views/Stack/CardStack.tsx src/vendor/views/Stack/CardStack.tsx --- node_modules/@react-navigation/stack/src/views/Stack/CardStack.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Stack/CardStack.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -11,8 +11,7 @@ import { EdgeInsets } from 'react-native-safe-area-context'; // eslint-disable-next-line import/no-unresolved import { ScreenContainer, Screen, screensEnabled } from 'react-native-screens'; // Import with * as to prevent getters being called -import { Route } from '@react-navigation/native'; -import { StackNavigationState } from '@react-navigation/routers'; +import { NavigationState as StackNavigationState } from 'react-navigation'; import { getDefaultHeaderHeight } from '../Header/HeaderSegment'; import { Props as HeaderContainerProps } from '../Header/HeaderContainer'; @@ -25,6 +24,7 @@ import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators'; import getDistanceForDirection from '../../utils/getDistanceForDirection'; import { + Route, Layout, StackHeaderMode, StackCardMode, diff -ruN node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx src/vendor/views/Stack/StackView.tsx --- node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx 2020-01-03 21:46:12.000000000 +0100 +++ src/vendor/views/Stack/StackView.tsx 2020-01-03 21:46:29.000000000 +0100 @@ -1,8 +1,11 @@ import * as React from 'react'; import { Platform } from 'react-native'; import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context'; -import { Route } from '@react-navigation/native'; -import { StackActions, StackNavigationState } from '@react-navigation/routers'; +import { + StackActions, + NavigationState as StackNavigationState, + SceneView, +} from 'react-navigation'; import CardStack from './CardStack'; import KeyboardManager from '../KeyboardManager'; @@ -11,6 +14,7 @@ } from '../Header/HeaderContainer'; import SafeAreaProviderCompat from '../SafeAreaProviderCompat'; import { + Route, StackNavigationHelpers, StackNavigationConfig, StackDescriptorMap, @@ -20,6 +24,7 @@ state: StackNavigationState; navigation: StackNavigationHelpers; descriptors: StackDescriptorMap; + screenProps: unknown; }; type State = { @@ -257,14 +262,31 @@ return null; } - return descriptor.render(); + const { navigation, getComponent } = descriptor; + const SceneComponent = getComponent(); + + return ( + + ); }; private renderHeader = (props: HeaderContainerProps) => { return ; }; + private handleTransitionComplete = ({ route }: { route: Route }) => { + // TODO: remove when the new event system lands + this.props.navigation.dispatch( + StackActions.completeTransition({ toChildKey: route.key }) + ); + }; + private handleOpenRoute = ({ route }: { route: Route }) => { + this.handleTransitionComplete({ route }); this.setState(state => ({ routes: state.replacingRouteKeys.length ? state.routes.filter(r => !state.replacingRouteKeys.includes(r.key)) @@ -283,15 +305,19 @@ // This will happen in when the route was closed from the card component // e.g. When the close animation triggered from a gesture ends // For the cleanup, the card needs to call this function again from its componentDidUpdate - navigation.dispatch({ - ...StackActions.pop(), - source: route.key, - target: state.key, - }); + // @ts-ignore + navigation.dispatch(StackActions.pop({ key: route.key })); } else { // Otherwise, the animation was triggered due to a route removal // In this case, we need to clean up any state tracking the route and pop it immediately + // While closing route we need to point to the previous one assuming that + // this previous one in routes array + const index = this.state.routes.findIndex(r => r.key === route.key); + this.handleTransitionComplete({ + route: this.state.routes[Math.max(index - 1, 0)], + }); + // @ts-ignore this.setState(state => ({ routes: state.routes.filter(r => r.key !== route.key), @@ -308,22 +334,26 @@ private handleTransitionStart = ( { route }: { route: Route }, closing: boolean - ) => - this.props.navigation.emit({ - type: 'transitionStart', - data: { closing }, - target: route.key, - }); + ) => { + const { descriptors } = this.props; + const descriptor = descriptors[route.key]; + + descriptor && + descriptor.options.onTransitionStart && + descriptor.options.onTransitionStart({ closing }); + }; private handleTransitionEnd = ( { route }: { route: Route }, closing: boolean - ) => - this.props.navigation.emit({ - type: 'transitionEnd', - data: { closing }, - target: route.key, - }); + ) => { + const { descriptors } = this.props; + const descriptor = descriptors[route.key]; + + descriptor && + descriptor.options.onTransitionStart && + descriptor.options.onTransitionStart({ closing }); + }; render() { const { diff -ruN node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx.orig src/vendor/views/Stack/StackView.tsx.orig --- node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx.orig 1970-01-01 01:00:00.000000000 +0100 +++ src/vendor/views/Stack/StackView.tsx.orig 2020-01-03 21:46:29.000000000 +0100 @@ -0,0 +1,383 @@ +import * as React from 'react'; +import { Platform } from 'react-native'; +import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context'; +import { Route } from '@react-navigation/native'; +import { StackActions, StackNavigationState } from '@react-navigation/routers'; + +import CardStack from './CardStack'; +import KeyboardManager from '../KeyboardManager'; +import HeaderContainer, { + Props as HeaderContainerProps, +} from '../Header/HeaderContainer'; +import SafeAreaProviderCompat from '../SafeAreaProviderCompat'; +import { + StackNavigationHelpers, + StackNavigationConfig, + StackDescriptorMap, +} from '../../types'; + +type Props = StackNavigationConfig & { + state: StackNavigationState; + navigation: StackNavigationHelpers; + descriptors: StackDescriptorMap; +}; + +type State = { + // Local copy of the routes which are actually rendered + routes: Route[]; + // Previous routes, to compare whether routes have changed or not + previousRoutes: Route[]; + // Previous descriptors, to compare whether descriptors have changed or not + previousDescriptors: StackDescriptorMap; + // List of routes being opened, we need to animate pushing of these new routes + openingRouteKeys: string[]; + // List of routes being closed, we need to animate popping of these routes + closingRouteKeys: string[]; + // List of routes being replaced, we need to keep a copy until the new route animates in + replacingRouteKeys: string[]; + // Since the local routes can vary from the routes from props, we need to keep the descriptors for old routes + // Otherwise we won't be able to access the options for routes that were removed + descriptors: StackDescriptorMap; +}; + +class StackView extends React.Component { + static getDerivedStateFromProps( + props: Readonly, + state: Readonly + ) { + // If there was no change in routes, we don't need to compute anything + if (props.state.routes === state.previousRoutes && state.routes.length) { + if (props.descriptors !== state.previousDescriptors) { + const descriptors = state.routes.reduce( + (acc, route) => { + acc[route.key] = + props.descriptors[route.key] || state.descriptors[route.key]; + + return acc; + }, + {} + ); + + return { + previousDescriptors: props.descriptors, + descriptors, + }; + } + + return null; + } + + // Here we determine which routes were added or removed to animate them + // We keep a copy of the route being removed in local state to be able to animate it + + let routes = + props.state.index < props.state.routes.length - 1 + ? // Remove any extra routes from the state + // The last visible route should be the focused route, i.e. at current index + props.state.routes.slice(0, props.state.index + 1) + : props.state.routes; + + // Now we need to determine which routes were added and removed + let { + openingRouteKeys, + closingRouteKeys, + replacingRouteKeys, + previousRoutes, + } = state; + + const previousFocusedRoute = previousRoutes[previousRoutes.length - 1] as + | Route + | undefined; + const nextFocusedRoute = routes[routes.length - 1]; + + const isAnimationEnabled = (key: string) => { + const descriptor = props.descriptors[key] || state.descriptors[key]; + + return descriptor ? descriptor.options.animationEnabled !== false : true; + }; + + if ( + previousFocusedRoute && + previousFocusedRoute.key !== nextFocusedRoute.key + ) { + // We only need to animate routes if the focused route changed + // Animating previous routes won't be visible coz the focused route is on top of everything + + if (!previousRoutes.find(r => r.key === nextFocusedRoute.key)) { + // A new route has come to the focus, we treat this as a push + // A replace can also trigger this, the animation should look like push + + if ( + isAnimationEnabled(nextFocusedRoute.key) && + !openingRouteKeys.includes(nextFocusedRoute.key) + ) { + // In this case, we need to animate pushing the focused route + // We don't care about animating any other added routes because they won't be visible + openingRouteKeys = [...openingRouteKeys, nextFocusedRoute.key]; + + closingRouteKeys = closingRouteKeys.filter( + key => key !== nextFocusedRoute.key + ); + replacingRouteKeys = replacingRouteKeys.filter( + key => key !== nextFocusedRoute.key + ); + + if (!routes.find(r => r.key === previousFocusedRoute.key)) { + // The previous focused route isn't present in state, we treat this as a replace + + replacingRouteKeys = [ + ...replacingRouteKeys, + previousFocusedRoute.key, + ]; + + openingRouteKeys = openingRouteKeys.filter( + key => key !== previousFocusedRoute.key + ); + closingRouteKeys = closingRouteKeys.filter( + key => key !== previousFocusedRoute.key + ); + + // Keep the old route in state because it's visible under the new route, and removing it will feel abrupt + // We need to insert it just before the focused one (the route being pushed) + // After the push animation is completed, routes being replaced will be removed completely + routes = routes.slice(); + routes.splice(routes.length - 1, 0, previousFocusedRoute); + } + } + } else if (!routes.find(r => r.key === previousFocusedRoute.key)) { + // The previously focused route was removed, we treat this as a pop + + if ( + isAnimationEnabled(previousFocusedRoute.key) && + !closingRouteKeys.includes(previousFocusedRoute.key) + ) { + // Sometimes a route can be closed before the opening animation finishes + // So we also need to remove it from the opening list + closingRouteKeys = [...closingRouteKeys, previousFocusedRoute.key]; + + openingRouteKeys = openingRouteKeys.filter( + key => key !== previousFocusedRoute.key + ); + replacingRouteKeys = replacingRouteKeys.filter( + key => key !== previousFocusedRoute.key + ); + + // Keep a copy of route being removed in the state to be able to animate it + routes = [...routes, previousFocusedRoute]; + } + } else { + // Looks like some routes were re-arranged and no focused routes were added/removed + // i.e. the currently focused route already existed and the previously focused route still exists + // We don't know how to animate this + } + } else if (replacingRouteKeys.length || closingRouteKeys.length) { + // Keep the routes we are closing or replacing if animation is enabled for them + routes = routes.slice(); + routes.splice( + routes.length - 1, + 0, + ...state.routes.filter(({ key }) => + isAnimationEnabled(key) + ? replacingRouteKeys.includes(key) || closingRouteKeys.includes(key) + : false + ) + ); + } + + if (!routes.length) { + throw new Error(`There should always be at least one route.`); + } + + const descriptors = routes.reduce((acc, route) => { + acc[route.key] = + props.descriptors[route.key] || state.descriptors[route.key]; + + return acc; + }, {}); + + return { + routes, + previousRoutes: props.state.routes, + previousDescriptors: props.descriptors, + openingRouteKeys, + closingRouteKeys, + replacingRouteKeys, + descriptors, + }; + } + + state: State = { + routes: [], + previousRoutes: [], + previousDescriptors: {}, + openingRouteKeys: [], + closingRouteKeys: [], + replacingRouteKeys: [], + descriptors: {}, + }; + + private getGesturesEnabled = ({ route }: { route: Route }) => { + const descriptor = this.state.descriptors[route.key]; + + if (descriptor) { + const { gestureEnabled, animationEnabled } = descriptor.options; + + if (animationEnabled === false) { + // When animation is disabled, also disable gestures + // The gesture to dismiss a route will look weird when not animated + return false; + } + + return gestureEnabled !== undefined + ? gestureEnabled + : Platform.OS !== 'android'; + } + + return false; + }; + + private getPreviousRoute = ({ route }: { route: Route }) => { + const { closingRouteKeys, replacingRouteKeys } = this.state; + const routes = this.state.routes.filter( + r => + r.key === route.key || + (!closingRouteKeys.includes(r.key) && + !replacingRouteKeys.includes(r.key)) + ); + const index = routes.findIndex(r => r.key === route.key); + + return routes[index - 1]; + }; + + private renderScene = ({ route }: { route: Route }) => { + const descriptor = + this.state.descriptors[route.key] || this.props.descriptors[route.key]; + + if (!descriptor) { + return null; + } + + return descriptor.render(); + }; + + private renderHeader = (props: HeaderContainerProps) => { + return ; + }; + + private handleOpenRoute = ({ route }: { route: Route }) => { + this.setState(state => ({ + routes: state.replacingRouteKeys.length + ? state.routes.filter(r => !state.replacingRouteKeys.includes(r.key)) + : state.routes, + openingRouteKeys: state.openingRouteKeys.filter(key => key !== route.key), + closingRouteKeys: state.closingRouteKeys.filter(key => key !== route.key), + replacingRouteKeys: [], + })); + }; + + private handleCloseRoute = ({ route }: { route: Route }) => { + const { state, navigation } = this.props; + + if (state.routes.find(r => r.key === route.key)) { + // If a route exists in state, trigger a pop + // This will happen in when the route was closed from the card component + // e.g. When the close animation triggered from a gesture ends + // For the cleanup, the card needs to call this function again from its componentDidUpdate + navigation.dispatch({ + ...StackActions.pop(), + source: route.key, + target: state.key, + }); + } else { + // Otherwise, the animation was triggered due to a route removal + // In this case, we need to clean up any state tracking the route and pop it immediately + + // @ts-ignore + this.setState(state => ({ + routes: state.routes.filter(r => r.key !== route.key), + openingRouteKeys: state.openingRouteKeys.filter( + key => key !== route.key + ), + closingRouteKeys: state.closingRouteKeys.filter( + key => key !== route.key + ), + })); + } + }; + + private handleTransitionStart = ( + { route }: { route: Route }, + closing: boolean + ) => + this.props.navigation.emit({ + type: 'transitionStart', + data: { closing }, + target: route.key, + }); + + private handleTransitionEnd = ( + { route }: { route: Route }, + closing: boolean + ) => + this.props.navigation.emit({ + type: 'transitionEnd', + data: { closing }, + target: route.key, + }); + + render() { + const { + state, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + navigation, + keyboardHandlingEnabled, + mode = 'card', + ...rest + } = this.props; + + const { + routes, + descriptors, + openingRouteKeys, + closingRouteKeys, + } = this.state; + + const headerMode = + mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen'; + + return ( + + + {insets => ( + + {props => ( + + )} + + )} + + + ); + } +} + +export default StackView;