diff --git a/examples/NavigationPlayground/js/App.js b/examples/NavigationPlayground/js/App.js index 9db6a2cbb3..6435338e82 100644 --- a/examples/NavigationPlayground/js/App.js +++ b/examples/NavigationPlayground/js/App.js @@ -322,7 +322,7 @@ const AppNavigator = createStackNavigator( } ); -export default () => ; +export default AppNavigator; const styles = StyleSheet.create({ item: { diff --git a/examples/NavigationPlayground/js/Drawer.js b/examples/NavigationPlayground/js/Drawer.js index e80b070735..5c4c5a0386 100644 --- a/examples/NavigationPlayground/js/Drawer.js +++ b/examples/NavigationPlayground/js/Drawer.js @@ -32,14 +32,7 @@ const InboxScreen = ({ navigation }) => ( ); InboxScreen.navigationOptions = { - drawerLabel: 'Inbox', - drawerIcon: ({ tintColor }) => ( - - ), + headerTitle: 'Inbox', }; const EmailScreen = ({ navigation }) => ( @@ -50,10 +43,7 @@ const DraftsScreen = ({ navigation }) => ( ); DraftsScreen.navigationOptions = { - drawerLabel: 'Drafts', - drawerIcon: ({ tintColor }) => ( - - ), + headerTitle: 'Drafts', }; const InboxStack = createStackNavigator({ @@ -61,11 +51,29 @@ const InboxStack = createStackNavigator({ Email: { screen: EmailScreen }, }); +InboxStack.navigationOptions = { + drawerLabel: 'Inbox', + drawerIcon: ({ tintColor }) => ( + + ), +}; + const DraftsStack = createStackNavigator({ Drafts: { screen: DraftsScreen }, Email: { screen: EmailScreen }, }); +DraftsStack.navigationOptions = { + drawerLabel: 'Drafts', + drawerIcon: ({ tintColor }) => ( + + ), +}; + const DrawerExample = createDrawerNavigator( { Inbox: { diff --git a/examples/NavigationPlayground/js/KeyboardHandlingExample.js b/examples/NavigationPlayground/js/KeyboardHandlingExample.js index 7a0674a514..01e2ce765d 100644 --- a/examples/NavigationPlayground/js/KeyboardHandlingExample.js +++ b/examples/NavigationPlayground/js/KeyboardHandlingExample.js @@ -24,9 +24,9 @@ class ScreenOne extends React.Component { } class ScreenTwo extends React.Component { - static navigationOptions = { - title: 'Screen w/ Input', - }; + static navigationOptions = ({ navigation }) => ({ + title: navigation.getParam('inputValue', 'Screen w/ Input'), + }); componentDidMount() { InteractionManager.runAfterInteractions(() => { @@ -41,6 +41,7 @@ class ScreenTwo extends React.Component { (this._textInput = c)} + onChangeText={inputValue => navigation.setParams({ inputValue })} style={{ backgroundColor: 'white', height: 24, diff --git a/examples/NavigationPlayground/js/TabsWithNavigationFocus.js b/examples/NavigationPlayground/js/TabsWithNavigationFocus.js index 79effb2c97..e40b3125bd 100644 --- a/examples/NavigationPlayground/js/TabsWithNavigationFocus.js +++ b/examples/NavigationPlayground/js/TabsWithNavigationFocus.js @@ -4,10 +4,8 @@ import React from 'react'; import { SafeAreaView, StatusBar, Text, View } from 'react-native'; -import { - createBottomTabNavigator, - withNavigationFocus, -} from 'react-navigation'; +import { withNavigationFocus } from 'react-navigation'; +import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { Button } from './commonComponents/ButtonWithMargin'; @@ -80,16 +78,26 @@ const createTabScreen = (name, icon, focusedIcon, tintColor = '#673ab7') => { return withNavigationFocus(TabScreen); }; -const TabsWithNavigationFocus = createBottomTabNavigator({ - One: { - screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'), - }, - Two: { - screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'), - }, - Three: { - screen: createTabScreen('Three', 'numeric-3-box-outline', 'numeric-3-box'), +const TabsWithNavigationFocus = createMaterialBottomTabNavigator( + { + One: { + screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'), + }, + Two: { + screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'), + }, + Three: { + screen: createTabScreen( + 'Three', + 'numeric-3-box-outline', + 'numeric-3-box' + ), + }, }, -}); + { + shifting: false, + activeTintColor: '#F44336', + } +); export default TabsWithNavigationFocus; diff --git a/examples/NavigationPlayground/package.json b/examples/NavigationPlayground/package.json index e60490a6a7..7a8da1126c 100644 --- a/examples/NavigationPlayground/package.json +++ b/examples/NavigationPlayground/package.json @@ -16,7 +16,8 @@ "react-native": "^0.54.0", "react-native-iphone-x-helper": "^1.0.2", "react-navigation": "link:../..", - "react-navigation-header-buttons": "^0.0.4" + "react-navigation-header-buttons": "^0.0.4", + "react-navigation-material-bottom-tabs": "0.1.3" }, "devDependencies": { "babel-jest": "^22.4.1", diff --git a/examples/NavigationPlayground/yarn.lock b/examples/NavigationPlayground/yarn.lock index 08ee41e75f..590a1b0011 100644 --- a/examples/NavigationPlayground/yarn.lock +++ b/examples/NavigationPlayground/yarn.lock @@ -2110,6 +2110,13 @@ create-react-context@^0.2.1: fbjs "^0.8.0" gud "^1.0.0" +create-react-context@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" + dependencies: + fbjs "^0.8.0" + gud "^1.0.0" + cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2220,7 +2227,7 @@ deepmerge@^1.3.0, deepmerge@^1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" -deepmerge@^2.0.1: +deepmerge@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" @@ -5550,6 +5557,10 @@ react-lifecycles-compat@^1.0.2: version "1.1.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-1.1.4.tgz#fc005c72849b7ed364de20a0f64ff58ebdc2009a" +react-lifecycles-compat@^3, react-lifecycles-compat@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz#7279047275bd727a912e25f734c0559527e84eff" + react-native-branch@2.0.0-beta.3: version "2.0.0-beta.3" resolved "https://registry.yarnpkg.com/react-native-branch/-/react-native-branch-2.0.0-beta.3.tgz#2167af86bbc9f964bd45bd5f37684e5b54965e32" @@ -5589,14 +5600,15 @@ react-native-iphone-x-helper@^1.0.2: babel-plugin-module-resolver "^2.3.0" babel-preset-react-native "1.9.0" -react-native-paper@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-1.2.5.tgz#3a4480ca967aae4b3e65a987116973dd012d1fa8" +react-native-paper@=1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-1.4.0.tgz#33bd477cde1468b63e60bdb0c9a500822d03cdca" dependencies: color "^2.0.1" - create-react-context "^0.2.1" - deepmerge "^2.0.1" + create-react-context "^0.2.2" + deepmerge "^2.1.0" hoist-non-react-statics "^2.5.0" + react-lifecycles-compat "^3.0.2" react-native-platform-touchable@^1.1.1: version "1.1.1" @@ -5642,9 +5654,9 @@ react-native-svg@6.2.2: lodash "^4.16.6" pegjs "^0.10.0" -"react-native-tab-view@github:react-navigation/react-native-tab-view": +react-native-tab-view@^0.0.74: version "0.0.74" - resolved "https://codeload.github.com/react-navigation/react-native-tab-view/tar.gz/36ebd834d78b841fc19778c966465d02fd1213bb" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-0.0.74.tgz#62c0c882d9232b461ce181d440d683b4f99d1bd8" dependencies: prop-types "^15.6.0" @@ -5725,11 +5737,11 @@ react-native@^0.54.0: xmldoc "^0.4.0" yargs "^9.0.0" -react-navigation-deprecated-tab-navigator@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-navigation-deprecated-tab-navigator/-/react-navigation-deprecated-tab-navigator-1.0.1.tgz#a869d749d16116630411d6c7761c51484ba90175" +react-navigation-deprecated-tab-navigator@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-navigation-deprecated-tab-navigator/-/react-navigation-deprecated-tab-navigator-1.2.0.tgz#e0d969c196dcd3a4a440770a7bd97fa058eb4aaf" dependencies: - react-native-tab-view react-navigation/react-native-tab-view + react-native-tab-view "^0.0.74" react-navigation-header-buttons@^0.0.4: version "0.0.4" @@ -5737,14 +5749,32 @@ react-navigation-header-buttons@^0.0.4: dependencies: react-native-platform-touchable "^1.1.1" -react-navigation-tabs@0.1.0-alpha.5: - version "0.1.0-alpha.5" - resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.1.0-alpha.5.tgz#51cae3ea5f0f77bdf0deb50a855a5dcaa2f9bf1e" +react-navigation-material-bottom-tabs@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/react-navigation-material-bottom-tabs/-/react-navigation-material-bottom-tabs-0.1.3.tgz#8e11c3d57ebb72d19adf5e4764390729ed964072" + dependencies: + hoist-non-react-statics "^2.5.0" + prop-types "^15.6.0" + react-native-paper "=1.4.0" + react-navigation-tabs "0.2.0-rc.0" + +react-navigation-tabs@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.2.0.tgz#d25be64e01a728c893a93cfeee6b176ab74d67de" + dependencies: + hoist-non-react-statics "^2.5.0" + prop-types "^15.6.0" + react-lifecycles-compat "^1.0.2" + react-native-safe-area-view "^0.7.0" + react-native-tab-view "~0.0.77" + +react-navigation-tabs@0.2.0-rc.0: + version "0.2.0-rc.0" + resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.2.0-rc.0.tgz#671e8c9914b915796879329752f7240147494038" dependencies: hoist-non-react-statics "^2.5.0" prop-types "^15.6.0" react-lifecycles-compat "^1.0.2" - react-native-paper "^1.2.5" react-native-safe-area-view "^0.7.0" react-native-tab-view "~0.0.77" diff --git a/flow/react-navigation.js b/flow/react-navigation.js index 57df21db1c..30ea9dd477 100644 --- a/flow/react-navigation.js +++ b/flow/react-navigation.js @@ -809,10 +809,6 @@ declare module 'react-navigation' { routeConfigs: NavigationRouteConfigMap, config?: _TabNavigatorConfig ): NavigationContainer<*, *, *>; - declare export function createMaterialBottomTabNavigator( - routeConfigs: NavigationRouteConfigMap, - config?: _TabNavigatorConfig - ): NavigationContainer<*, *, *>; declare export function createMaterialTopTabNavigator( routeConfigs: NavigationRouteConfigMap, config?: _TabNavigatorConfig diff --git a/package.json b/package.json index fd35f536af..1766744d68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-navigation", - "version": "2.0.0-rc.7", + "version": "2.0.2", "description": "Routing and navigation for your React Native apps", "main": "src/react-navigation.js", "repository": { @@ -34,11 +34,11 @@ "hoist-non-react-statics": "^2.2.0", "path-to-regexp": "^1.7.0", "prop-types": "^15.5.10", - "react-lifecycles-compat": "^1.0.2", + "react-lifecycles-compat": "^3", "react-native-drawer-layout-polyfill": "^1.3.2", "react-native-safe-area-view": "^0.7.0", - "react-navigation-deprecated-tab-navigator": "1.1.0", - "react-navigation-tabs": "0.1.0-alpha.8" + "react-navigation-deprecated-tab-navigator": "1.2.0", + "react-navigation-tabs": "0.2.0" }, "devDependencies": { "babel-cli": "^6.24.1", diff --git a/src/StateUtils.js b/src/StateUtils.js index c743eac3a5..2fa2ab8e3b 100644 --- a/src/StateUtils.js +++ b/src/StateUtils.js @@ -21,7 +21,7 @@ const StateUtils = { * routes of the navigation state, or -1 if it is not present. */ indexOf(state, key) { - return state.routes.map(route => route.key).indexOf(key); + return state.routes.findIndex(route => route.key === key); }, /** diff --git a/src/__tests__/addNavigationHelpers-test.js b/src/__tests__/addNavigationHelpers-test.js deleted file mode 100644 index 642d7677da..0000000000 --- a/src/__tests__/addNavigationHelpers-test.js +++ /dev/null @@ -1,144 +0,0 @@ -import NavigationActions from '../NavigationActions'; -import addNavigationHelpers from '../addNavigationHelpers'; - -const dummyEventSubscriber = (name: string, handler: (*) => void) => ({ - remove: () => {}, -}); - -describe('addNavigationHelpers', () => { - it('handles dismiss action', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - const child = { key: 'A', routeName: 'Home' }; - expect( - addNavigationHelpers({ - state: child, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - dangerouslyGetParent: () => ({ - state: { - key: 'P', - routeName: 'Parent', - routes: [child], - }, - }), - }).dismiss() - ).toEqual(true); - expect(mockedDispatch).toBeCalledWith({ - type: NavigationActions.BACK, - key: 'P', - }); - expect(mockedDispatch.mock.calls.length).toBe(1); - }); - - it('handles Back action', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { key: 'A', routeName: 'Home' }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).goBack('A') - ).toEqual(true); - expect(mockedDispatch).toBeCalledWith({ - type: NavigationActions.BACK, - key: 'A', - }); - expect(mockedDispatch.mock.calls.length).toBe(1); - }); - - it('handles Back action when the key is not defined', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { routeName: 'Home' }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).goBack() - ).toEqual(true); - expect(mockedDispatch).toBeCalledWith({ type: NavigationActions.BACK }); - expect(mockedDispatch.mock.calls.length).toBe(1); - }); - - it('handles Navigate action', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { routeName: 'Home' }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).navigate('Profile', { name: 'Matt' }) - ).toEqual(true); - expect(mockedDispatch).toBeCalledWith({ - type: NavigationActions.NAVIGATE, - params: { name: 'Matt' }, - routeName: 'Profile', - }); - expect(mockedDispatch.mock.calls.length).toBe(1); - }); - - it('handles SetParams action', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { key: 'B', routeName: 'Settings' }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).setParams({ notificationsEnabled: 'yes' }) - ).toEqual(true); - expect(mockedDispatch).toBeCalledWith({ - type: NavigationActions.SET_PARAMS, - key: 'B', - params: { notificationsEnabled: 'yes' }, - }); - expect(mockedDispatch.mock.calls.length).toBe(1); - }); - - it('handles GetParams action', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { key: 'B', routeName: 'Settings', params: { name: 'Peter' } }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).getParam('name', 'Brent') - ).toEqual('Peter'); - }); - - it('handles GetParams action with default param', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { key: 'B', routeName: 'Settings' }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).getParam('name', 'Brent') - ).toEqual('Brent'); - }); - - it('handles GetParams action with param value as null', () => { - const mockedDispatch = jest - .fn(() => false) - .mockImplementationOnce(() => true); - expect( - addNavigationHelpers({ - state: { key: 'B', routeName: 'Settings', params: { name: null } }, - dispatch: mockedDispatch, - addListener: dummyEventSubscriber, - }).getParam('name') - ).toEqual(null); - }); -}); diff --git a/src/addNavigationHelpers.js b/src/addNavigationHelpers.js deleted file mode 100644 index 57bbd0f74e..0000000000 --- a/src/addNavigationHelpers.js +++ /dev/null @@ -1,105 +0,0 @@ -/* Helpers for navigation */ - -import NavigationActions from './NavigationActions'; -import invariant from './utils/invariant'; -export default function(navigation) { - return { - ...navigation, - // Go back from the given key, default to active key - goBack: key => { - let actualizedKey = key; - if (key === undefined && navigation.state.key) { - invariant( - typeof navigation.state.key === 'string', - 'key should be a string' - ); - actualizedKey = navigation.state.key; - } - return navigation.dispatch( - NavigationActions.back({ key: actualizedKey }) - ); - }, - // Go back from the parent key. If this is a nested stack, the entire - // stack will be dismissed. - dismiss: () => { - let parent = navigation.dangerouslyGetParent(); - if (parent && parent.state) { - return navigation.dispatch( - NavigationActions.back({ key: parent.state.key }) - ); - } else { - return false; - } - }, - navigate: (navigateTo, params, action) => { - if (typeof navigateTo === 'string') { - return navigation.dispatch( - NavigationActions.navigate({ routeName: navigateTo, params, action }) - ); - } - invariant( - typeof navigateTo === 'object', - 'Must navigateTo an object or a string' - ); - invariant( - params == null, - 'Params must not be provided to .navigate() when specifying an object' - ); - invariant( - action == null, - 'Child action must not be provided to .navigate() when specifying an object' - ); - return navigation.dispatch(NavigationActions.navigate(navigateTo)); - }, - pop: (n, params) => - navigation.dispatch( - NavigationActions.pop({ n, immediate: params && params.immediate }) - ), - popToTop: params => - navigation.dispatch( - NavigationActions.popToTop({ immediate: params && params.immediate }) - ), - /** - * For updating current route params. For example the nav bar title and - * buttons are based on the route params. - * This means `setParams` can be used to update nav bar for example. - */ - setParams: params => { - invariant( - navigation.state.key && typeof navigation.state.key === 'string', - 'setParams cannot be called by root navigator' - ); - const key = navigation.state.key; - return navigation.dispatch(NavigationActions.setParams({ params, key })); - }, - - getParam: (paramName, defaultValue) => { - const params = navigation.state.params; - - if (params && paramName in params) { - return params[paramName]; - } - - return defaultValue; - }, - - push: (routeName, params, action) => - navigation.dispatch( - NavigationActions.push({ routeName, params, action }) - ), - - replace: (routeName, params, action) => - navigation.dispatch( - NavigationActions.replace({ - routeName, - params, - action, - key: navigation.state.key, - }) - ), - - openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()), - closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()), - toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()), - }; -} diff --git a/src/createNavigationContainer.js b/src/createNavigationContainer.js index a13fc36b35..91aebb72e9 100644 --- a/src/createNavigationContainer.js +++ b/src/createNavigationContainer.js @@ -1,10 +1,11 @@ import React from 'react'; -import { Linking, AsyncStorage } from 'react-native'; -import withLifecyclePolyfill from 'react-lifecycles-compat'; +import { AsyncStorage, Linking, Platform } from 'react-native'; +import { polyfill } from 'react-lifecycles-compat'; import { BackHandler } from './PlatformHelpers'; import NavigationActions from './NavigationActions'; import invariant from './utils/invariant'; +import getNavigationActionCreators from './routers/getNavigationActionCreators'; import docsUrl from './utils/docsUrl'; function isStateful(props) { @@ -184,9 +185,9 @@ export default function createNavigationContainer(Component) { } componentDidUpdate() { - // Clear cached _nav every tick - if (this._nav === this.state.nav) { - this._nav = null; + // Clear cached _navState every tick + if (this._navState === this.state.nav) { + this._navState = null; } } @@ -198,11 +199,15 @@ export default function createNavigationContainer(Component) { if (__DEV__ && !this.props.detached) { if (_statefulContainerCount > 0) { - console.error( - `You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl( - 'common-mistakes.html#explicitly-rendering-more-than-one-navigator' - )}` - ); + // Temporarily only show this on iOS due to this issue: + // https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829 + if (Platform.OS === 'ios') { + console.warn( + `You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl( + 'common-mistakes.html#explicitly-rendering-more-than-one-navigator' + )}` + ); + } } } _statefulContainerCount++; @@ -252,12 +257,7 @@ export default function createNavigationContainer(Component) { } } - if (startupState === this.state.nav) { - return; - } - - this.setState({ nav: startupState }, () => { - _reactNavigationIsHydratingState = false; + const dispatchActions = () => this._actionEventSubscribers.forEach(subscriber => subscriber({ type: 'action', @@ -266,6 +266,15 @@ export default function createNavigationContainer(Component) { lastState: null, }) ); + + if (startupState === this.state.nav) { + dispatchActions(); + return; + } + + this.setState({ nav: startupState }, () => { + _reactNavigationIsHydratingState = false; + dispatchActions(); }); } @@ -303,32 +312,47 @@ export default function createNavigationContainer(Component) { if (this.props.navigation) { return this.props.navigation.dispatch(action); } - this._nav = this._nav || this.state.nav; - const oldNav = this._nav; - invariant(oldNav, 'should be set in constructor if stateful'); - const nav = Component.router.getStateForAction(action, oldNav); + + // navState will have the most up-to-date value, because setState sometimes behaves asyncronously + this._navState = this._navState || this.state.nav; + const lastNavState = this._navState; + invariant(lastNavState, 'should be set in constructor if stateful'); + const reducedState = Component.router.getStateForAction( + action, + lastNavState + ); + const navState = reducedState === null ? lastNavState : reducedState; + const dispatchActionEvents = () => { this._actionEventSubscribers.forEach(subscriber => subscriber({ type: 'action', action, - state: nav, - lastState: oldNav, + state: navState, + lastState: lastNavState, }) ); }; - if (nav && nav !== oldNav) { + + if (reducedState === null) { + // The router will return null when action has been handled and the state hasn't changed. + // dispatch returns true when something has been handled. + dispatchActionEvents(); + return true; + } + + if (navState !== lastNavState) { // Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change - this._nav = nav; - this.setState({ nav }, () => { - this._onNavigationStateChange(oldNav, nav, action); + this._navState = navState; + this.setState({ nav: navState }, () => { + this._onNavigationStateChange(lastNavState, navState, action); dispatchActionEvents(); - this._persistNavigationState(nav); + this._persistNavigationState(navState); }); return true; - } else { - dispatchActionEvents(); } + + dispatchActionEvents(); return false; }; @@ -355,6 +379,11 @@ export default function createNavigationContainer(Component) { }; }, }; + const actionCreators = getNavigationActionCreators(nav); + Object.keys(actionCreators).forEach(actionName => { + this._navigation[actionName] = (...args) => + this.dispatch(actionCreators[actionName](...args)); + }); } navigation = this._navigation; } @@ -363,5 +392,5 @@ export default function createNavigationContainer(Component) { } } - return withLifecyclePolyfill(NavigationContainer); + return polyfill(NavigationContainer); } diff --git a/src/getChildEventSubscriber.js b/src/getChildEventSubscriber.js index 0900bfe9f9..8839248a01 100644 --- a/src/getChildEventSubscriber.js +++ b/src/getChildEventSubscriber.js @@ -138,11 +138,14 @@ export default function getChildEventSubscriber(addListener, key) { emit((lastEmittedEvent = 'didBlur'), childPayload); } } + + if (lastEmittedEvent === 'didBlur' && !newRoute) { + removeAll(); + } }) ); return { - removeAll, addListener(eventName, eventHandler) { const subscribers = getChildSubscribers(eventName); if (!subscribers) { diff --git a/src/navigators/createKeyboardAwareNavigator.js b/src/navigators/createKeyboardAwareNavigator.js index 8da2d5bfe6..5b76a5acd2 100644 --- a/src/navigators/createKeyboardAwareNavigator.js +++ b/src/navigators/createKeyboardAwareNavigator.js @@ -39,10 +39,16 @@ export default Navigator => }; _handleTransitionStart = (transitionProps, prevTransitionProps) => { - const currentField = TextInput.State.currentlyFocusedField(); - if (currentField) { - TextInput.State.blurTextInput(currentField); + // TODO: We should not even have received the transition start event + // in the case where the index did not change, I believe. We + // should revisit this after 2.0 release. + if (transitionProps.index !== prevTransitionProps.index) { + const currentField = TextInput.State.currentlyFocusedField(); + if (currentField) { + TextInput.State.blurTextInput(currentField); + } } + this.props.onTransitionStart && this.props.onTransitionStart(transitionProps, prevTransitionProps); }; diff --git a/src/navigators/createNavigator.js b/src/navigators/createNavigator.js index 6401aeb6cf..35a1d4f5af 100644 --- a/src/navigators/createNavigator.js +++ b/src/navigators/createNavigator.js @@ -14,15 +14,14 @@ function createNavigator(NavigatorView, router, navigationConfig) { const activeKeys = this.props.navigation.state.routes.map(r => r.key); Object.keys(this.childEventSubscribers).forEach(key => { if (!activeKeys.includes(key)) { - this.childEventSubscribers[key].removeAll(); delete this.childEventSubscribers[key]; } }); } - // Remove all subscriptions + // Remove all subscription references componentWillUnmount() { - Object.values(this.childEventSubscribers).map(s => s.removeAll()); + this.childEventSubscribers = {}; } _isRouteFocused = route => { diff --git a/src/react-navigation.js b/src/react-navigation.js index e0d59c2eee..d93c0dcc79 100644 --- a/src/react-navigation.js +++ b/src/react-navigation.js @@ -57,9 +57,6 @@ module.exports = { get createBottomTabNavigator() { return require('react-navigation-tabs').createBottomTabNavigator; }, - get createMaterialBottomTabNavigator() { - return require('react-navigation-tabs').createMaterialBottomTabNavigator; - }, get createMaterialTopTabNavigator() { return require('react-navigation-tabs').createMaterialTopTabNavigator; }, diff --git a/src/routers/DrawerRouter.js b/src/routers/DrawerRouter.js index 03716a8dc4..a38a806096 100644 --- a/src/routers/DrawerRouter.js +++ b/src/routers/DrawerRouter.js @@ -33,44 +33,52 @@ export default (routeConfigs, config = {}) => { const isRouterTargeted = action.key == null || action.key === state.key; - if ( - isRouterTargeted && - action.type === DrawerActions.CLOSE_DRAWER && - state.isDrawerOpen - ) { - return { - ...state, - isDrawerOpen: false, - }; - } + if (isRouterTargeted) { + // Only handle actions that are meant for this drawer, as specified by action.key. + + if (action.type === DrawerActions.CLOSE_DRAWER && state.isDrawerOpen) { + return { + ...state, + isDrawerOpen: false, + }; + } + + if (action.type === DrawerActions.OPEN_DRAWER && !state.isDrawerOpen) { + return { + ...state, + isDrawerOpen: true, + }; + } - if ( - isRouterTargeted && - action.type === DrawerActions.OPEN_DRAWER && - !state.isDrawerOpen - ) { - return { - ...state, - isDrawerOpen: true, - }; + if (action.type === DrawerActions.TOGGLE_DRAWER) { + return { + ...state, + isDrawerOpen: !state.isDrawerOpen, + }; + } } - if (isRouterTargeted && action.type === DrawerActions.TOGGLE_DRAWER) { - return { - ...state, - isDrawerOpen: !state.isDrawerOpen, - }; + // Fall back on switch router for screen switching logic, and handling of child routers + const switchedState = switchRouter.getStateForAction(action, state); + + if (switchedState === null) { + // The switch router or a child router is attempting to swallow this action. We return null to allow this. + return null; } - // Fall back on tab router for screen switching logic - const childState = switchRouter.getStateForAction(action, state); - if (childState !== null && childState !== state) { - // If the tabs have changed, make sure to close the drawer - return { - ...childState, - isDrawerOpen: false, - }; + if (switchedState !== state) { + if (switchedState.index !== state.index) { + // If the tabs have changed, make sure to close the drawer + return { + ...switchedState, + isDrawerOpen: false, + }; + } + // Return the state new state, as returned by the switch router. + // The index hasn't changed, so this most likely means that a child router has returned a new state + return switchedState; } + return state; }, }; diff --git a/src/routers/StackRouter.js b/src/routers/StackRouter.js index 00ba9b2d11..2f94620c8a 100644 --- a/src/routers/StackRouter.js +++ b/src/routers/StackRouter.js @@ -164,7 +164,7 @@ export default (routeConfigs, stackConfig = {}) => { getActionCreators(route, navStateKey) { return { - ...getNavigationActionCreators(route, navStateKey), + ...getNavigationActionCreators(route), ...getCustomActionCreators(route, navStateKey), pop: (n, params) => StackActions.pop({ @@ -255,11 +255,14 @@ export default (routeConfigs, stackConfig = {}) => { // active route has the first opportunity, then the one before it, etc. for (let childRoute of state.routes.slice().reverse()) { let childRouter = childRouters[childRoute.routeName]; - let debug = action.params && action.params.debug; + let childAction = + action.routeName === childRoute.routeName && action.action + ? action.action + : action; if (childRouter) { const nextRouteState = childRouter.getStateForAction( - action, + childAction, childRoute ); @@ -324,7 +327,7 @@ export default (routeConfigs, stackConfig = {}) => { isTransitioning: state.index !== lastRouteIndex ? action.immediate !== true - : undefined, + : state.isTransitioning, index: lastRouteIndex, routes, }; diff --git a/src/routers/SwitchRouter.js b/src/routers/SwitchRouter.js index 01fb6195b3..cd375c1b3a 100644 --- a/src/routers/SwitchRouter.js +++ b/src/routers/SwitchRouter.js @@ -108,7 +108,7 @@ export default (routeConfigs, config = {}) => { getActionCreators(route, stateKey) { return { - ...getNavigationActionCreators(route, stateKey), + ...getNavigationActionCreators(route), ...getCustomActionCreators(route, stateKey), }; }, diff --git a/src/routers/__tests__/DrawerRouter-test.js b/src/routers/__tests__/DrawerRouter-test.js index f1a588c2cf..42f9bff5dc 100644 --- a/src/routers/__tests__/DrawerRouter-test.js +++ b/src/routers/__tests__/DrawerRouter-test.js @@ -91,3 +91,49 @@ describe('DrawerRouter', () => { expect(state3.isDrawerOpen).toEqual(true); }); }); + +test('Nested routers bubble up blocked actions', () => { + const ScreenA = () =>
; + ScreenA.router = { + getStateForAction(action, lastState) { + if (action.type === 'CHILD_ACTION') return null; + return lastState; + }, + }; + const ScreenB = () =>
; + const router = DrawerRouter({ + Foo: { screen: ScreenA }, + Bar: { screen: ScreenB }, + }); + const state = router.getStateForAction(INIT_ACTION); + + const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state); + expect(state2).toEqual(null); +}); + +test('Drawer stays open when child routers return new state', () => { + const ScreenA = () =>
; + ScreenA.router = { + getStateForAction(action, lastState = { changed: false }) { + if (action.type === 'CHILD_ACTION') + return { ...lastState, changed: true }; + return lastState; + }, + }; + const router = DrawerRouter({ + Foo: { screen: ScreenA }, + }); + + const state = router.getStateForAction(INIT_ACTION); + expect(state.isDrawerOpen).toEqual(false); + + const state2 = router.getStateForAction( + { type: DrawerActions.OPEN_DRAWER, key: state.key }, + state + ); + expect(state2.isDrawerOpen).toEqual(true); + + const state3 = router.getStateForAction({ type: 'CHILD_ACTION' }, state2); + expect(state3.isDrawerOpen).toEqual(true); + expect(state3.routes[0].changed).toEqual(true); +}); diff --git a/src/routers/__tests__/Routers-test.js b/src/routers/__tests__/Routers-test.js index 38e344577f..33e3a5af50 100644 --- a/src/routers/__tests__/Routers-test.js +++ b/src/routers/__tests__/Routers-test.js @@ -4,6 +4,7 @@ import React from 'react'; import StackRouter from '../StackRouter'; import TabRouter from '../TabRouter'; +import SwitchRouter from '../SwitchRouter'; import NavigationActions from '../../NavigationActions'; import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator'; @@ -89,6 +90,75 @@ Object.keys(ROUTERS).forEach(routerName => { }); }); +test('Nested navigate behavior test', () => { + const Leaf = () =>
; + + const First = () =>
; + First.router = StackRouter({ + First1: Leaf, + First2: Leaf, + }); + + const Second = () =>
; + Second.router = StackRouter({ + Second1: Leaf, + Second2: Leaf, + }); + + const Main = () =>
; + Main.router = StackRouter({ + First, + Second, + }); + const TestRouter = SwitchRouter({ + Login: Leaf, + Main, + }); + + const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT }); + + const state2 = TestRouter.getStateForAction( + { type: NavigationActions.NAVIGATE, routeName: 'First' }, + state1 + ); + expect(state2.index).toEqual(1); + expect(state2.routes[1].index).toEqual(0); + expect(state2.routes[1].routes[0].index).toEqual(0); + + const state3 = TestRouter.getStateForAction( + { type: NavigationActions.NAVIGATE, routeName: 'Second2' }, + state2 + ); + expect(state3.index).toEqual(1); + expect(state3.routes[1].index).toEqual(1); // second + expect(state3.routes[1].routes[1].index).toEqual(1); //second.second2 + + const state4 = TestRouter.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'First', + action: { type: NavigationActions.NAVIGATE, routeName: 'First2' }, + }, + state3, + true + ); + expect(state4.index).toEqual(1); // main + expect(state4.routes[1].index).toEqual(0); // first + expect(state4.routes[1].routes[0].index).toEqual(1); // first2 + + const state5 = TestRouter.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'First', + action: { type: NavigationActions.NAVIGATE, routeName: 'First1' }, + }, + state3 // second.second2 is active on state3 + ); + expect(state5.index).toEqual(1); // main + expect(state5.routes[1].index).toEqual(0); // first + expect(state5.routes[1].routes[0].index).toEqual(0); // first.first1 +}); + test('Handles no-op actions with tabs within stack router', () => { const BarView = () =>
; const FooTabNavigator = () =>
; diff --git a/src/routers/__tests__/StackRouter-test.js b/src/routers/__tests__/StackRouter-test.js index 1bb80792ba..b71dea8670 100644 --- a/src/routers/__tests__/StackRouter-test.js +++ b/src/routers/__tests__/StackRouter-test.js @@ -1850,3 +1850,120 @@ test('Handles deep navigate completion action', () => { expect(state3 && state3.routes[0].index).toEqual(1); expect(state3 && state3.routes[0].isTransitioning).toEqual(false); }); + +test('order of handling navigate action is correct for nested stackrouters', () => { + const Screen = () =>
; + const NestedStack = () =>
; + let nestedRouter = StackRouter({ + Foo: Screen, + Bar: Screen, + }); + + NestedStack.router = nestedRouter; + + let router = StackRouter( + { + NestedStack, + Bar: Screen, + Baz: Screen, + }, + { + initialRouteName: 'Baz', + } + ); + + const state = router.getStateForAction({ type: NavigationActions.INIT }); + expect(state.routes[state.index].routeName).toEqual('Baz'); + + const state2 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state + ); + expect(state2.routes[state2.index].routeName).toEqual('Bar'); + + const state3 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Baz', + }, + state2 + ); + expect(state3.routes[state3.index].routeName).toEqual('Baz'); + + const state4 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Foo', + }, + state3 + ); + let activeState4 = state4.routes[state4.index]; + expect(activeState4.routeName).toEqual('NestedStack'); + expect(activeState4.routes[activeState4.index].routeName).toEqual('Foo'); + + const state5 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state4 + ); + let activeState5 = state5.routes[state5.index]; + expect(activeState5.routeName).toEqual('NestedStack'); + expect(activeState5.routes[activeState5.index].routeName).toEqual('Bar'); +}); + +test('order of handling navigate action is correct for nested stackrouters', () => { + const Screen = () =>
; + const NestedStack = () =>
; + const OtherNestedStack = () =>
; + + let nestedRouter = StackRouter({ Foo: Screen, Bar: Screen }); + let otherNestedRouter = StackRouter({ Foo: Screen }); + NestedStack.router = nestedRouter; + OtherNestedStack.router = otherNestedRouter; + + let router = StackRouter( + { + NestedStack, + OtherNestedStack, + Bar: Screen, + }, + { + initialRouteName: 'OtherNestedStack', + } + ); + + const state = router.getStateForAction({ type: NavigationActions.INIT }); + expect(state.routes[state.index].routeName).toEqual('OtherNestedStack'); + + const state2 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state + ); + expect(state2.routes[state2.index].routeName).toEqual('Bar'); + + const state3 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'NestedStack', + }, + state2 + ); + const state4 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state3 + ); + let activeState4 = state4.routes[state4.index]; + expect(activeState4.routeName).toEqual('NestedStack'); + expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar'); +}); diff --git a/src/routers/__tests__/SwitchRouter-test.js b/src/routers/__tests__/SwitchRouter-test.js index 04929465d1..d22edde5f9 100644 --- a/src/routers/__tests__/SwitchRouter-test.js +++ b/src/routers/__tests__/SwitchRouter-test.js @@ -127,6 +127,93 @@ describe('SwitchRouter', () => { }, }); }); + + test('order of handling navigate action is correct for nested switchrouters', () => { + // router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar }) + // if we are focused on Other and navigate to Bar, what should happen? + + const Screen = () =>
; + const NestedSwitch = () =>
; + const OtherNestedSwitch = () =>
; + + let nestedRouter = SwitchRouter({ Foo: Screen, Bar: Screen }); + let otherNestedRouter = SwitchRouter({ Foo: Screen }); + NestedSwitch.router = nestedRouter; + OtherNestedSwitch.router = otherNestedRouter; + + let router = SwitchRouter( + { + NestedSwitch, + OtherNestedSwitch, + Bar: Screen, + }, + { + initialRouteName: 'OtherNestedSwitch', + } + ); + + const state = router.getStateForAction({ type: NavigationActions.INIT }); + expect(state.routes[state.index].routeName).toEqual('OtherNestedSwitch'); + + const state2 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state + ); + expect(state2.routes[state2.index].routeName).toEqual('Bar'); + + const state3 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'NestedSwitch', + }, + state2 + ); + const state4 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Bar', + }, + state3 + ); + let activeState4 = state4.routes[state4.index]; + expect(activeState4.routeName).toEqual('NestedSwitch'); + expect(activeState4.routes[activeState4.index].routeName).toEqual('Bar'); + }); + + // https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628 + test('order of handling navigate action is correct for nested stackrouters', () => { + const Screen = () =>
; + const MainStack = () =>
; + const LoginStack = () =>
; + MainStack.router = StackRouter({ Home: Screen, Profile: Screen }); + LoginStack.router = StackRouter({ Form: Screen, ForgotPassword: Screen }); + + let router = SwitchRouter( + { + Home: Screen, + Login: LoginStack, + Main: MainStack, + }, + { + initialRouteName: 'Login', + } + ); + + const state = router.getStateForAction({ type: NavigationActions.INIT }); + expect(state.routes[state.index].routeName).toEqual('Login'); + + const state2 = router.getStateForAction( + { + type: NavigationActions.NAVIGATE, + routeName: 'Home', + }, + state + ); + expect(state2.routes[state2.index].routeName).toEqual('Home'); + }); }); const getExampleRouter = (config = {}) => { diff --git a/src/routers/__tests__/createConfigGetter-test.js b/src/routers/__tests__/createConfigGetter-test.js index 3852598f7a..0a79d88642 100644 --- a/src/routers/__tests__/createConfigGetter-test.js +++ b/src/routers/__tests__/createConfigGetter-test.js @@ -1,6 +1,5 @@ import { Component } from 'react'; import createConfigGetter from '../createConfigGetter'; -import addNavigationHelpers from '../../addNavigationHelpers'; const dummyEventSubscriber = (name: string, handler: (*) => void) => ({ remove: () => {}, @@ -67,81 +66,81 @@ test('should get config for screen', () => { expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[0], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).title ).toEqual('Welcome anonymous'); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[1], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).title ).toEqual('Welcome jane'); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[0], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).gesturesEnabled ).toEqual(true); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[2], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).title ).toEqual('Settings!!!'); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[2], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).gesturesEnabled ).toEqual(false); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[3], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).title ).toEqual('10 new notifications'); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[3], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).gesturesEnabled ).toEqual(true); expect( getScreenOptions( - addNavigationHelpers({ + { state: routes[4], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ).gesturesEnabled ).toEqual(false); @@ -164,11 +163,11 @@ test('should throw if the route does not exist', () => { expect(() => getScreenOptions( - addNavigationHelpers({ + { state: routes[0], dispatch: () => false, addListener: dummyEventSubscriber, - }), + }, {} ) ).toThrowError( diff --git a/src/views/Drawer/DrawerSidebar.js b/src/views/Drawer/DrawerSidebar.js index eaf4ecf413..faf3adf0e0 100644 --- a/src/views/Drawer/DrawerSidebar.js +++ b/src/views/Drawer/DrawerSidebar.js @@ -2,6 +2,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import NavigationActions from '../../NavigationActions'; +import StackActions from '../../routers/StackActions'; import invariant from '../../utils/invariant'; /** @@ -46,9 +47,10 @@ class DrawerSidebar extends React.PureComponent { _onItemPress = ({ route, focused }) => { if (!focused) { let subAction; + // TODO (v3): Revisit and repeal this behavior: // if the child screen is a StackRouter then always navigate to its first screen (see #1914) - if (route.index !== undefined && route.index !== 0) { - subAction = NavigationActions.reset({ + if (route.index != null && route.index !== 0) { + subAction = StackActions.reset({ index: 0, actions: [ NavigationActions.navigate({ @@ -57,7 +59,12 @@ class DrawerSidebar extends React.PureComponent { ], }); } - this.props.navigation.navigate(route.routeName, undefined, subAction); + this.props.navigation.dispatch( + NavigationActions.navigate({ + routeName: route.routeName, + action: subAction, + }) + ); } }; diff --git a/src/views/Header/Header.js b/src/views/Header/Header.js index 9f31bd4f66..2319ec6c4a 100644 --- a/src/views/Header/Header.js +++ b/src/views/Header/Header.js @@ -7,6 +7,7 @@ import { Platform, StyleSheet, View, + I18nManager, ViewPropTypes, } from 'react-native'; import { MaskedViewIOS } from '../../PlatformHelpers'; @@ -565,6 +566,7 @@ const styles = StyleSheet.create({ marginTop: -0.5, // resizes down to 20.5 alignSelf: 'center', resizeMode: 'contain', + transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }], }, title: { bottom: 0, diff --git a/src/views/ResourceSavingSceneView.js b/src/views/ResourceSavingSceneView.js index 63b4f4ac03..fc7fa47530 100644 --- a/src/views/ResourceSavingSceneView.js +++ b/src/views/ResourceSavingSceneView.js @@ -1,7 +1,7 @@ import React from 'react'; import { Platform, StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; -import withLifecyclePolyfill from 'react-lifecycles-compat'; +import { polyfill } from 'react-lifecycles-compat'; import SceneView from './SceneView'; @@ -77,4 +77,4 @@ const styles = StyleSheet.create({ }, }); -export default withLifecyclePolyfill(ResourceSavingSceneView); +export default polyfill(ResourceSavingSceneView); diff --git a/src/views/StackView/StackView.js b/src/views/StackView/StackView.js index 2d1a61a7f7..4f4030e39f 100644 --- a/src/views/StackView/StackView.js +++ b/src/views/StackView/StackView.js @@ -3,7 +3,6 @@ import { NativeModules } from 'react-native'; import StackViewLayout from './StackViewLayout'; import Transitioner from '../Transitioner'; -import NavigationActions from '../../NavigationActions'; import StackActions from '../../routers/StackActions'; import TransitionConfigs from './StackViewTransitionConfigs'; @@ -24,20 +23,18 @@ class StackView extends React.Component { configureTransition={this._configureTransition} navigation={this.props.navigation} descriptors={this.props.descriptors} - onTransitionStart={this.props.onTransitionStart} + onTransitionStart={this.props.navigationConfig.onTransitionStart} onTransitionEnd={(transition, lastTransition) => { - const { onTransitionEnd, navigation } = this.props; - if ( - transition.navigation.state.isTransitioning && - !lastTransition.navigation.state.isTransitioning - ) { + const { navigationConfig, navigation } = this.props; + if (transition.navigation.state.isTransitioning) { navigation.dispatch( StackActions.completeTransition({ key: navigation.state.key, }) ); } - onTransitionEnd && onTransitionEnd(transition, lastTransition); + navigationConfig.onTransitionEnd && + navigationConfig.onTransitionEnd(transition, lastTransition); }} /> ); diff --git a/src/views/StackView/StackViewLayout.js b/src/views/StackView/StackViewLayout.js index 60de24302c..eb660c19f5 100644 --- a/src/views/StackView/StackViewLayout.js +++ b/src/views/StackView/StackViewLayout.js @@ -209,6 +209,186 @@ class StackViewLayout extends React.Component { } } + _panResponder = PanResponder.create({ + onPanResponderTerminate: () => { + this._isResponding = false; + this._reset(index, 0); + this.props.onGestureCanceled && this.props.onGestureCanceled(); + }, + onPanResponderGrant: () => { + const { + transitionProps: { navigation, position, scene }, + } = this.props; + const { index } = navigation.state; + + if (index !== scene.index) { + return false; + } + + position.stopAnimation((value: number) => { + this._isResponding = true; + this._gestureStartValue = value; + }); + this.props.onGestureBegin && this.props.onGestureBegin(); + }, + onMoveShouldSetPanResponder: (event, gesture) => { + const { + transitionProps: { navigation, position, layout, scene, scenes }, + mode, + } = this.props; + const { index } = navigation.state; + const isVertical = mode === 'modal'; + const { options } = scene.descriptor; + const gestureDirection = options.gestureDirection; + + const gestureDirectionInverted = + typeof gestureDirection === 'string' + ? gestureDirection === 'inverted' + : I18nManager.isRTL; + + if (index !== scene.index) { + return false; + } + + const immediateIndex = + this._immediateIndex == null ? index : this._immediateIndex; + const currentDragDistance = gesture[isVertical ? 'dy' : 'dx']; + const currentDragPosition = + event.nativeEvent[isVertical ? 'pageY' : 'pageX']; + const axisLength = isVertical + ? layout.height.__getValue() + : layout.width.__getValue(); + const axisHasBeenMeasured = !!axisLength; + + // Measure the distance from the touch to the edge of the screen + const screenEdgeDistance = gestureDirectionInverted + ? axisLength - (currentDragPosition - currentDragDistance) + : currentDragPosition - currentDragDistance; + // Compare to the gesture distance relavant to card or modal + + const { + gestureResponseDistance: userGestureResponseDistance = {}, + } = options; + const gestureResponseDistance = isVertical + ? userGestureResponseDistance.vertical || + GESTURE_RESPONSE_DISTANCE_VERTICAL + : userGestureResponseDistance.horizontal || + GESTURE_RESPONSE_DISTANCE_HORIZONTAL; + // GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals + if (screenEdgeDistance > gestureResponseDistance) { + // Reject touches that started in the middle of the screen + return false; + } + + const hasDraggedEnough = + Math.abs(currentDragDistance) > RESPOND_THRESHOLD; + + const isOnFirstCard = immediateIndex === 0; + const shouldSetResponder = + hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard; + return shouldSetResponder; + }, + onPanResponderMove: (event, gesture) => { + const { + transitionProps: { navigation, position, layout, scene }, + mode, + } = this.props; + const { index } = navigation.state; + const isVertical = mode === 'modal'; + const { options } = scene.descriptor; + const gestureDirection = options.gestureDirection; + + const gestureDirectionInverted = + typeof gestureDirection === 'string' + ? gestureDirection === 'inverted' + : I18nManager.isRTL; + + // Handle the moving touches for our granted responder + const startValue = this._gestureStartValue; + const axis = isVertical ? 'dy' : 'dx'; + const axisDistance = isVertical + ? layout.height.__getValue() + : layout.width.__getValue(); + const currentValue = + axis === 'dx' && gestureDirectionInverted + ? startValue + gesture[axis] / axisDistance + : startValue - gesture[axis] / axisDistance; + const value = clamp(index - 1, currentValue, index); + position.setValue(value); + }, + onPanResponderTerminationRequest: () => + // Returning false will prevent other views from becoming responder while + // the navigation view is the responder (mid-gesture) + false, + onPanResponderRelease: (event, gesture) => { + const { + transitionProps: { navigation, position, layout, scene }, + mode, + } = this.props; + const { index } = navigation.state; + const isVertical = mode === 'modal'; + const { options } = scene.descriptor; + const gestureDirection = options.gestureDirection; + + const gestureDirectionInverted = + typeof gestureDirection === 'string' + ? gestureDirection === 'inverted' + : I18nManager.isRTL; + + if (!this._isResponding) { + return; + } + this._isResponding = false; + + const immediateIndex = + this._immediateIndex == null ? index : this._immediateIndex; + + // Calculate animate duration according to gesture speed and moved distance + const axisDistance = isVertical + ? layout.height.__getValue() + : layout.width.__getValue(); + const movementDirection = gestureDirectionInverted ? -1 : 1; + const movedDistance = + movementDirection * gesture[isVertical ? 'dy' : 'dx']; + const gestureVelocity = + movementDirection * gesture[isVertical ? 'vy' : 'vx']; + const defaultVelocity = axisDistance / ANIMATION_DURATION; + const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity); + const resetDuration = gestureDirectionInverted + ? (axisDistance - movedDistance) / velocity + : movedDistance / velocity; + const goBackDuration = gestureDirectionInverted + ? movedDistance / velocity + : (axisDistance - movedDistance) / velocity; + + // To asyncronously get the current animated value, we need to run stopAnimation: + position.stopAnimation(value => { + // If the speed of the gesture release is significant, use that as the indication + // of intent + if (gestureVelocity < -0.5) { + this.props.onGestureCanceled && this.props.onGestureCanceled(); + this._reset(immediateIndex, resetDuration); + return; + } + if (gestureVelocity > 0.5) { + this.props.onGestureFinish && this.props.onGestureFinish(); + this._goBack(immediateIndex, goBackDuration); + return; + } + + // Then filter based on the distance the screen was moved. Over a third of the way swiped, + // and the back will happen. + if (value <= index - POSITION_THRESHOLD) { + this.props.onGestureFinish && this.props.onGestureFinish(); + this._goBack(immediateIndex, goBackDuration); + } else { + this.props.onGestureCanceled && this.props.onGestureCanceled(); + this._reset(immediateIndex, resetDuration); + } + }); + }, + }); + render() { let floatingHeader = null; const headerMode = this._getHeaderMode(); @@ -227,148 +407,19 @@ class StackViewLayout extends React.Component { const { index } = navigation.state; const isVertical = mode === 'modal'; const { options } = scene.descriptor; + const gestureDirection = options.gestureDirection; - const gestureDirectionInverted = options.gestureDirection === 'inverted'; + const gestureDirectionInverted = + typeof gestureDirection === 'string' + ? gestureDirection === 'inverted' + : I18nManager.isRTL; const gesturesEnabled = typeof options.gesturesEnabled === 'boolean' ? options.gesturesEnabled : Platform.OS === 'ios'; - const responder = !gesturesEnabled - ? null - : PanResponder.create({ - onPanResponderTerminate: () => { - this._isResponding = false; - this._reset(index, 0); - this.props.onGestureCanceled && this.props.onGestureCanceled(); - }, - onPanResponderGrant: () => { - position.stopAnimation((value: number) => { - this._isResponding = true; - this._gestureStartValue = value; - }); - this.props.onGestureBegin && this.props.onGestureBegin(); - }, - onMoveShouldSetPanResponder: (event, gesture) => { - if (index !== scene.index) { - return false; - } - const immediateIndex = - this._immediateIndex == null ? index : this._immediateIndex; - const currentDragDistance = gesture[isVertical ? 'dy' : 'dx']; - const currentDragPosition = - event.nativeEvent[isVertical ? 'pageY' : 'pageX']; - const axisLength = isVertical - ? layout.height.__getValue() - : layout.width.__getValue(); - const axisHasBeenMeasured = !!axisLength; - - // Measure the distance from the touch to the edge of the screen - const screenEdgeDistance = gestureDirectionInverted - ? axisLength - (currentDragPosition - currentDragDistance) - : currentDragPosition - currentDragDistance; - // Compare to the gesture distance relavant to card or modal - - const { options } = scene.descriptor; - - const { - gestureResponseDistance: userGestureResponseDistance = {}, - } = options; - const gestureResponseDistance = isVertical - ? userGestureResponseDistance.vertical || - GESTURE_RESPONSE_DISTANCE_VERTICAL - : userGestureResponseDistance.horizontal || - GESTURE_RESPONSE_DISTANCE_HORIZONTAL; - // GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals - if (screenEdgeDistance > gestureResponseDistance) { - // Reject touches that started in the middle of the screen - return false; - } - - const hasDraggedEnough = - Math.abs(currentDragDistance) > RESPOND_THRESHOLD; - - const isOnFirstCard = immediateIndex === 0; - const shouldSetResponder = - hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard; - return shouldSetResponder; - }, - onPanResponderMove: (event, gesture) => { - // Handle the moving touches for our granted responder - const startValue = this._gestureStartValue; - const axis = isVertical ? 'dy' : 'dx'; - const axisDistance = isVertical - ? layout.height.__getValue() - : layout.width.__getValue(); - const currentValue = - (I18nManager.isRTL && axis === 'dx') !== gestureDirectionInverted - ? startValue + gesture[axis] / axisDistance - : startValue - gesture[axis] / axisDistance; - const value = clamp(index - 1, currentValue, index); - position.setValue(value); - }, - onPanResponderTerminationRequest: () => - // Returning false will prevent other views from becoming responder while - // the navigation view is the responder (mid-gesture) - false, - onPanResponderRelease: (event, gesture) => { - if (!this._isResponding) { - return; - } - this._isResponding = false; - - const immediateIndex = - this._immediateIndex == null ? index : this._immediateIndex; - - // Calculate animate duration according to gesture speed and moved distance - const axisDistance = isVertical - ? layout.height.__getValue() - : layout.width.__getValue(); - const movementDirection = gestureDirectionInverted ? -1 : 1; - const movedDistance = - movementDirection * gesture[isVertical ? 'dy' : 'dx']; - const gestureVelocity = - movementDirection * gesture[isVertical ? 'vy' : 'vx']; - const defaultVelocity = axisDistance / ANIMATION_DURATION; - const velocity = Math.max( - Math.abs(gestureVelocity), - defaultVelocity - ); - const resetDuration = gestureDirectionInverted - ? (axisDistance - movedDistance) / velocity - : movedDistance / velocity; - const goBackDuration = gestureDirectionInverted - ? movedDistance / velocity - : (axisDistance - movedDistance) / velocity; - - // To asyncronously get the current animated value, we need to run stopAnimation: - position.stopAnimation(value => { - // If the speed of the gesture release is significant, use that as the indication - // of intent - if (gestureVelocity < -0.5) { - this.props.onGestureCanceled && this.props.onGestureCanceled(); - this._reset(immediateIndex, resetDuration); - return; - } - if (gestureVelocity > 0.5) { - this.props.onGestureFinish && this.props.onGestureFinish(); - this._goBack(immediateIndex, goBackDuration); - return; - } - - // Then filter based on the distance the screen was moved. Over a third of the way swiped, - // and the back will happen. - if (value <= index - POSITION_THRESHOLD) { - this.props.onGestureFinish && this.props.onGestureFinish(); - this._goBack(immediateIndex, goBackDuration); - } else { - this.props.onGestureCanceled && this.props.onGestureCanceled(); - this._reset(immediateIndex, resetDuration); - } - }); - }, - }); + const responder = !gesturesEnabled ? null : this._panResponder; const handlers = gesturesEnabled ? responder.panHandlers : {}; const containerStyle = [ diff --git a/src/views/__tests__/Transitioner-test.js b/src/views/__tests__/Transitioner-test.js new file mode 100644 index 0000000000..bd011b1f9a --- /dev/null +++ b/src/views/__tests__/Transitioner-test.js @@ -0,0 +1,49 @@ +/* eslint react/display-name:0 */ +import * as React from 'react'; +import renderer from 'react-test-renderer'; +import Transitioner from '../Transitioner'; + +describe('Transitioner', () => { + it('should not trigger onTransitionStart and onTransitionEnd when route params are changed', () => { + const onTransitionStartCallback = jest.fn(); + const onTransitionEndCallback = jest.fn(); + + const transitionerProps = { + configureTransition: (transitionProps, prevTransitionProps) => ({}), + navigation: { + state: { + index: 0, + routes: [ + { key: '1', routeName: 'Foo' }, + { key: '2', routeName: 'Bar' }, + ], + }, + goBack: () => false, + dispatch: () => false, + setParams: () => false, + navigate: () => false, + }, + render: () =>
, + onTransitionStart: onTransitionStartCallback, + onTransitionEnd: onTransitionEndCallback, + }; + + const nextTransitionerProps = { + ...transitionerProps, + navigation: { + ...transitionerProps.navigation, + state: { + index: 0, + routes: [ + { key: '1', routeName: 'Foo', params: { name: 'Zoom' } }, + { key: '2', routeName: 'Bar' }, + ], + }, + }, + }; + const component = renderer.create(); + component.update(); + expect(onTransitionStartCallback).not.toBeCalled(); + expect(onTransitionEndCallback).not.toBeCalled(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 31331a9b2a..0d613db603 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,7 +108,7 @@ ansi-gray@^0.1.1: dependencies: ansi-wrap "0.1.0" -ansi-regex@^2.0.0, ansi-regex@^2.1.1: +ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -120,7 +120,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.0.0, ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: @@ -1372,34 +1372,20 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" dependencies: color-name "^1.1.1" -color-name@^1.0.0, color-name@^1.1.1: +color-name@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" -color-string@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" -color@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839" - dependencies: - color-convert "^1.9.1" - color-string "^1.5.2" - combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" @@ -1594,13 +1580,6 @@ create-react-context@^0.2.1: fbjs "^0.8.0" gud "^1.0.0" -create-react-context@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" - dependencies: - fbjs "^0.8.0" - gud "^1.0.0" - cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1700,10 +1679,6 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" -deepmerge@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" - default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" @@ -1915,7 +1890,7 @@ escodegen@^1.9.0: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^2.3.0: +eslint-config-prettier@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz#5ecd65174d486c22dff389fe036febf502d468a3" dependencies: @@ -1962,7 +1937,7 @@ eslint-plugin-jsx-a11y@^6.0.2: emoji-regex "^6.1.0" jsx-ast-utils "^2.0.0" -eslint-plugin-prettier@^2.1.2: +eslint-plugin-prettier@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz#33e4e228bdb06142d03c560ce04ec23f6c767dd7" dependencies: @@ -1989,7 +1964,50 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^4.2.0, eslint@^4.5.0: +eslint@^4.0.0: + version "4.19.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^1.0.1" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + +eslint@^4.2.0: version "4.18.2" resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.18.2.tgz#0f81267ad1012e7d2051e186a9004cc2267b8d45" dependencies: @@ -2031,7 +2049,7 @@ eslint@^4.2.0, eslint@^4.5.0: table "4.0.2" text-table "~0.2.0" -espree@^3.5.2: +espree@^3.5.2, espree@^3.5.4: version "3.5.4" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: @@ -2890,10 +2908,6 @@ is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" -is-arrayish@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" - is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -3903,6 +3917,10 @@ lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -4688,30 +4706,26 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier-eslint@^6.4.2: - version "6.4.3" - resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-6.4.3.tgz#8335b7a8bd50d01879a9d505bc0b9208caa9ecfc" +prettier-eslint@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-8.8.1.tgz#38505163274742f2a0b31653c39e40f37ebd07da" dependencies: + babel-runtime "^6.26.0" common-tags "^1.4.0" dlv "^1.1.0" - eslint "^4.5.0" + eslint "^4.0.0" indent-string "^3.2.0" lodash.merge "^4.6.0" loglevel-colored-level-prefix "^1.0.0" - prettier "^1.6.0" - pretty-format "^20.0.3" + prettier "^1.7.0" + pretty-format "^22.0.3" require-relative "^0.8.7" + typescript "^2.5.1" + typescript-eslint-parser "^11.0.0" -prettier@^1.5.3, prettier@^1.6.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75" - -pretty-format@^20.0.3: - version "20.0.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14" - dependencies: - ansi-regex "^2.1.1" - ansi-styles "^3.0.0" +prettier@^1.12.1, prettier@^1.7.0: + version "1.12.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" pretty-format@^21.2.1: version "21.2.1" @@ -4720,6 +4734,13 @@ pretty-format@^21.2.1: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +pretty-format@^22.0.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + pretty-format@^22.4.0: version "22.4.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.0.tgz#237b1f7e1c50ed03bc65c03ccc29d7c8bb7beb94" @@ -4840,7 +4861,7 @@ react-lifecycles-compat@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-1.0.2.tgz#551d8b1d156346e5fcf30ffac9b32ce3f78b8850" -react-lifecycles-compat@^3.0.2: +react-lifecycles-compat@^3: version "3.0.2" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz#7279047275bd727a912e25f734c0559527e84eff" @@ -4860,16 +4881,6 @@ react-native-drawer-layout@1.3.2: dependencies: react-native-dismiss-keyboard "1.0.0" -react-native-paper@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-1.4.1.tgz#81df909c14d9ae78274662df97c3b35224aa39a9" - dependencies: - color "^2.0.1" - create-react-context "^0.2.2" - deepmerge "^2.1.0" - hoist-non-react-statics "^2.5.0" - react-lifecycles-compat "^3.0.2" - react-native-safe-area-view@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.7.0.tgz#38f5ab9368d6ef9e5d18ab64212938af3ec39421" @@ -4955,20 +4966,19 @@ react-native@^0.52.0: xmldoc "^0.4.0" yargs "^9.0.0" -react-navigation-deprecated-tab-navigator@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/react-navigation-deprecated-tab-navigator/-/react-navigation-deprecated-tab-navigator-1.1.0.tgz#58945c1c4c7c21b54954e814e8721e98423df75d" +react-navigation-deprecated-tab-navigator@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-navigation-deprecated-tab-navigator/-/react-navigation-deprecated-tab-navigator-1.2.0.tgz#e0d969c196dcd3a4a440770a7bd97fa058eb4aaf" dependencies: react-native-tab-view "^0.0.74" -react-navigation-tabs@0.1.0-alpha.7: - version "0.1.0-alpha.7" - resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.1.0-alpha.7.tgz#bcea5b031a3db3b4230be63f17fed721bf78e198" +react-navigation-tabs@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-0.2.0.tgz#d25be64e01a728c893a93cfeee6b176ab74d67de" dependencies: hoist-non-react-statics "^2.5.0" prop-types "^15.6.0" react-lifecycles-compat "^1.0.2" - react-native-paper "^1.4.0" react-native-safe-area-view "^0.7.0" react-native-tab-view "~0.0.77" @@ -5106,6 +5116,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexpp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + regexpu-core@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" @@ -5397,6 +5411,10 @@ sax@~1.1.1: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +semver@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + send@0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de" @@ -5512,12 +5530,6 @@ simple-plist@^0.2.1: bplist-parser "0.1.1" plist "2.0.1" -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - dependencies: - is-arrayish "^0.3.1" - slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -5975,6 +5987,17 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typescript-eslint-parser@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/typescript-eslint-parser/-/typescript-eslint-parser-11.0.0.tgz#37dba6a0130dd307504aa4b4b21b0d3dc7d4e9f2" + dependencies: + lodash.unescape "4.0.1" + semver "5.4.1" + +typescript@^2.5.1: + version "2.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.3.tgz#5d817f9b6f31bb871835f4edf0089f21abe6c170" + ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"