Compare commits

..

23 Commits
2.0.0 ... 2.0.3

Author SHA1 Message Date
Brent Vatne
1d2ce862c2 Fix Stacks over Tabs example 2018-05-25 16:07:55 -07:00
Louis Lagrange
d778479e4a Fix drawer router initial state (#4219)
* Fix drawer router initial state

* Add test

* Be concise
2018-05-25 16:01:53 -07:00
Sébastien BARBIER
352dae50e1 Apply drawerlockmode from screen options to DrawerLayout component (#4202)
Fix #4201
2018-05-25 15:50:39 -07:00
Louis Lagrange
61385cae59 Export missing Drawer components (#4221) 2018-05-25 15:49:33 -07:00
Brent Vatne
aa3c13891e Bump some versions 2018-05-25 15:26:25 -07:00
Andrei Xavier de Oliveira Calazans
9696d7220d fix(create-nav-container): pass up error on catch (#4298) 2018-05-25 15:13:18 -07:00
Louis Lagrange
2b83b44816 Remove obsolete DrawerScreen (#4222) 2018-05-25 14:45:45 -07:00
Louis Lagrange
ec749023ed [Web] Fix header height margin (#4206) 2018-05-25 14:44:46 -07:00
Sébastien Lorber
adc9389eb3 Add CircleCI Badge to README (#4318)
Displaying a badge is helpful for users but also contributors (for example to know why my upstream-rebased PR is not passing tests)
2018-05-25 14:41:27 -07:00
Sébastien Lorber
54d143fee2 Fix Codecov link -> react-community vs react-navigation (#4319) 2018-05-25 14:39:51 -07:00
Louis Lagrange
d50e74d0c7 Fix NavigatorContainer test (fixes CI) (#4327) 2018-05-24 17:43:58 +02:00
Nicolas Charpentier
22926c5230 Optimize images (#4251) 2018-05-23 16:54:23 -07:00
Ashoat Tevosyan
f6c47a6c66 [examples] Update ReduxExample (#4267)
Using two new utils:
1. `createNavigationPropConstructor`, which is now preferred over `createReduxBoundAddListener`.
2. `initializeListeners`, which is necessary to trigger the event listeners with the initial state.
2018-05-23 10:20:14 -07:00
Ashoat Tevosyan
046a9f8930 [flow] Make StackRouter action creators optional and add DrawerRouter ones (#4257)
This will force all Flow users to do runtime assertions if they want to use these action creators. Open to any better solutions.
2018-05-23 10:18:35 -07:00
Ashoat Tevosyan
72f17538c2 [flow] Fix tabBarOnPress type for react-navigation@2.0 (#4256) 2018-05-23 10:18:11 -07:00
Ashoat Tevosyan
550001b053 [flow] Fixes for v2 libdef (#4230)
* [flow] Remove "any" type from NavigationComponent

"any" cripples the typechecker, so it's best to avoid. It was introduced in #3392, but I don't think the intention was to keep it there.

* [flow] Remove `any` type from `createNavigator` return

And use objects with spread sub-types instead of unions for `React$ComponentType` type param
2018-05-23 10:17:45 -07:00
Brent Vatne
d168ab26f9 Release 2.0.2 2018-05-21 17:56:24 -07:00
Brent Vatne
99916328a1 Don't warn about multiple navigation containers on Android
This really is not ideal but it's an upstream bug on react-native Android
that we can't work around. Users may experience other unexpected behavior as a
result of this upstream bug.
2018-05-21 17:55:05 -07:00
Eric Vicenti
08e1b53f2e Update playground yarn lockfile 2018-05-12 10:48:45 -07:00
Eric Vicenti
2243528e97 Cache panResponder in StackView (#4183)
* fix swipe gesture bug

* panresponder as instance prop
2018-05-09 17:36:57 -07:00
Steven Kabbes
931271198b Remove unused code addNavigationHelpers (#4179) 2018-05-09 14:53:06 -07:00
Brent Vatne
1876706bad Release 2.0.1 2018-05-08 17:45:02 -07:00
Eric Vicenti
e97d6b26a8 Fix dispatch implementation (#4168)
Previously we had been emiting an action event with a null state when the router handles the action by returning null from getStateForAction.

- Fixes the case of null state getting emitted
- Renames a few things for clarity
- Refactors conditionals to read easier
- Comments to explain intent
2018-05-08 17:41:08 -07:00
32 changed files with 1956 additions and 2316 deletions

View File

@@ -1,6 +1,6 @@
# React Navigation
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-community/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-community/react-navigation) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)
[![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-navigation/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-navigation/react-navigation) [![CircleCI badge](https://circleci.com/gh/react-navigation/react-navigation/tree/master.svg?style=shield)](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)
React Navigation is born from the React Native community's need for an extensible yet easy-to-use navigation solution based on Javascript.

View File

@@ -11,7 +11,7 @@
"splash": {
"image": "./assets/icons/splash.png"
},
"sdkVersion": "26.0.0",
"sdkVersion": "27.0.0",
"entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"packagerOpts": {
"assetExts": [

View File

@@ -16,6 +16,8 @@ import {
SafeAreaView,
withNavigation,
} from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
@@ -48,11 +50,16 @@ const MyBackButtonWithNavigation = withNavigation(MyBackButton);
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
const { push, replace, popToTop, pop, dismiss } = navigation;
invariant(
push && replace && popToTop && pop && dismiss,
'missing action creators for StackNavigator'
);
return (
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
onPress={() => push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
@@ -60,13 +67,13 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
onPress={() => replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack()} title="Go back" />
<Button onPress={() => navigation.dismiss()} title="Dismiss" />
<Button onPress={() => dismiss()} title="Dismiss" />
<StatusBar barStyle="default" />
</SafeAreaView>
);

View File

@@ -6,6 +6,8 @@ import type { NavigationScreenProp } from 'react-navigation';
import * as React from 'react';
import { ScrollView, StatusBar } from 'react-native';
import { createStackNavigator, SafeAreaView } from 'react-navigation';
import invariant from 'invariant';
import { Button } from './commonComponents/ButtonWithMargin';
type NavScreenProps = {
@@ -19,15 +21,14 @@ class HomeScreen extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push } = navigation;
invariant(push, 'missing `push` action creator for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button onPress={() => push('Other')} title="Push another screen" />
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
onPress={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => navigation.goBack(null)} title="Go Home" />
@@ -44,18 +45,20 @@ class OtherScreen extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push, pop } = navigation;
invariant(push && pop, 'missing action creators for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('ScreenWithLongTitle')}
onPress={() => push('ScreenWithLongTitle')}
title="Push another screen"
/>
<Button
onPress={() => navigation.push('ScreenWithNoHeader')}
onPress={() => push('ScreenWithNoHeader')}
title="Push screen with no header"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -70,10 +73,12 @@ class ScreenWithLongTitle extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { pop } = navigation;
invariant(pop, 'missing `pop` action creator for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
@@ -89,14 +94,13 @@ class ScreenWithNoHeader extends React.Component<NavScreenProps> {
render() {
const { navigation } = this.props;
const { push, pop } = navigation;
invariant(push && pop, 'missing action creators for StackNavigator');
return (
<SafeAreaView style={{ paddingTop: 30 }}>
<Button
onPress={() => navigation.push('Other')}
title="Push another screen"
/>
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => push('Other')} title="Push another screen" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>

View File

@@ -19,6 +19,8 @@ import {
View,
} from 'react-native';
import { Header, createStackNavigator } from 'react-navigation';
import invariant from 'invariant';
import SampleText from './SampleText';
import { Button } from './commonComponents/ButtonWithMargin';
import { HeaderButtons } from './commonComponents/HeaderButtons';
@@ -31,11 +33,16 @@ type MyNavScreenProps = {
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
const { push, replace, popToTop, pop } = navigation;
invariant(
push && replace && popToTop && pop,
'missing action creators for StackNavigator'
);
return (
<ScrollView style={{ flex: 1 }} {...this.getHeaderInset()}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.push('Profile', { name: 'Jane' })}
onPress={() => push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
@@ -43,11 +50,11 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
title="Navigate to a photos screen"
/>
<Button
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
onPress={() => replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => popToTop()} title="Pop to top" />
<Button onPress={() => pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</ScrollView>

View File

@@ -94,6 +94,19 @@ const TabNav = createBottomTabNavigator(
}
);
TabNav.navigationOptions = ({ navigation }) => {
let { routeName } = navigation.state.routes[navigation.state.index];
let title;
if (routeName === 'SettingsTab') {
title = 'Settings';
} else if (routeName === 'MainTab') {
title = 'Home';
}
return {
title,
};
};
const StacksOverTabs = createStackNavigator({
Root: {
screen: TabNav,
@@ -107,9 +120,9 @@ const StacksOverTabs = createStackNavigator({
Profile: {
screen: MyProfileScreen,
path: '/people/:name',
navigationOptions: ({ navigation }) => {
title: `${navigation.state.params.name}'s Profile!`;
},
navigationOptions: ({ navigation }) => ({
title: `${navigation.state.params.name}'s Profile!`,
}),
},
});

View File

@@ -11,9 +11,10 @@
"test": "flow"
},
"dependencies": {
"expo": "^26.0.0",
"react": "16.3.0-alpha.1",
"react-native": "^0.54.0",
"expo": "^27.0.0",
"invariant": "^2.2.4",
"react": "16.3.1",
"react-native": "^0.55.0",
"react-native-iphone-x-helper": "^1.0.2",
"react-navigation": "link:../..",
"react-navigation-header-buttons": "^0.0.4",

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StackNavigator } from 'react-navigation';
import { initializeListeners } from 'react-navigation-redux-helpers';
import LoginScreen from '../components/LoginScreen';
import MainScreen from '../components/MainScreen';
import ProfileScreen from '../components/ProfileScreen';
import { addListener } from '../utils/redux';
import { navigationPropConstructor } from '../utils/redux';
export const AppNavigator = StackNavigator({
Login: { screen: LoginScreen },
@@ -20,17 +21,14 @@ class AppWithNavigationState extends React.Component {
nav: PropTypes.object.isRequired,
};
componentDidMount() {
initializeListeners('root', this.props.nav);
}
render() {
const { dispatch, nav } = this.props;
return (
<AppNavigator
navigation={{
dispatch,
state: nav,
addListener,
}}
/>
);
const navigation = navigationPropConstructor(dispatch, nav);
return <AppNavigator navigation={navigation} />;
}
}

View File

@@ -1,15 +1,12 @@
import {
createReactNavigationReduxMiddleware,
createReduxBoundAddListener,
createNavigationPropConstructor,
} from 'react-navigation-redux-helpers';
const middleware = createReactNavigationReduxMiddleware(
"root",
state => state.nav,
'root',
state => state.nav
);
const addListener = createReduxBoundAddListener("root");
const navigationPropConstructor = createNavigationPropConstructor('root');
export {
middleware,
addListener,
};
export { middleware, navigationPropConstructor };

View File

@@ -269,21 +269,26 @@ declare module 'react-navigation' {
declare export type NavigationComponent =
| NavigationScreenComponent<NavigationRoute, *, *>
| NavigationContainer<*, *, *>
| any;
| NavigationContainer<*, *, *>;
declare export type NavigationScreenComponent<
Route: NavigationRoute,
Options: {},
Props: {}
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, Route>,
}> &
({} | { navigationOptions: NavigationScreenConfig<Options> });
declare export type NavigationNavigator<
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<NavigationNavigatorProps<Options, State> & Props> & {
> = React$ComponentType<{
...Props,
...NavigationNavigatorProps<Options, State>,
}> & {
router: NavigationRouter<State, Options>,
navigationOptions?: ?NavigationScreenConfig<Options>,
};
@@ -426,10 +431,9 @@ declare module 'react-navigation' {
| ((options: { tintColor: ?string, focused: boolean }) => ?React$Node),
tabBarVisible?: boolean,
tabBarTestIDProps?: { testID?: string, accessibilityLabel?: string },
tabBarOnPress?: (
scene: TabScene,
jumpToIndex: (index: number) => void
) => void,
tabBarOnPress?: ({
navigation: NavigationScreenProp<NavigationRoute>,
}) => void,
|};
/**
@@ -485,8 +489,14 @@ declare module 'react-navigation' {
declare export type NavigationScreenProp<+S> = {
+state: S,
dispatch: NavigationDispatch,
addListener: (
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
getParam: (paramName: string, fallback?: any) => any,
isFocused: () => boolean,
// Shared action creators that exist for all routers
goBack: (routeKey?: ?string) => boolean,
dismiss: () => boolean,
navigate: (
routeName:
| string
@@ -500,24 +510,25 @@ declare module 'react-navigation' {
action?: NavigationNavigateAction
) => boolean,
setParams: (newParams: NavigationParams) => boolean,
getParam: (paramName: string, fallback?: any) => any,
addListener: (
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
push: (
// StackRouter action creators
pop?: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop?: (params?: { immediate?: boolean }) => boolean,
push?: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace: (
replace?: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
isFocused: () => boolean,
reset?: (actions: NavigationAction[], index: number) => boolean,
dismiss?: () => boolean,
// DrawerRouter action creators
openDrawer?: () => boolean,
closeDrawer?: () => boolean,
toggleDrawer?: () => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -534,7 +545,10 @@ declare module 'react-navigation' {
State: NavigationState,
Options: {},
Props: {}
> = React$ComponentType<NavigationContainerProps<State, Options> & Props> & {
> = React$ComponentType<{
...Props,
...NavigationContainerProps<State, Options>,
}> & {
router: NavigationRouter<State, Options>,
navigationOptions?: ?NavigationScreenConfig<Options>,
};
@@ -766,7 +780,7 @@ declare module 'react-navigation' {
view: NavigationView<O, S>,
router: NavigationRouter<S, O>,
navigatorConfig?: NavigatorConfig
): any;
): NavigationNavigator<S, O, *>;
declare export function StackNavigator(
routeConfigMap: NavigationRouteConfigMap,

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "2.0.0",
"version": "2.0.3",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -36,9 +36,9 @@
"prop-types": "^15.5.10",
"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.2.0",
"react-navigation-tabs": "0.2.0"
"react-native-safe-area-view": "^0.8.0",
"react-navigation-deprecated-tab-navigator": "1.3.0",
"react-navigation-tabs": "0.3.0"
},
"devDependencies": {
"babel-cli": "^6.24.1",

View File

@@ -208,7 +208,7 @@ describe('NavigationContainer', () => {
let spy = {};
beforeEach(() => {
spy.console = jest.spyOn(console, 'error').mockImplementation(() => {});
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {

View File

@@ -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);
});
});

View File

@@ -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()),
};
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Linking, AsyncStorage } from 'react-native';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { polyfill } from 'react-lifecycles-compat';
import { BackHandler } from './PlatformHelpers';
@@ -185,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;
}
}
@@ -199,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++;
@@ -281,6 +285,8 @@ export default function createNavigationContainer(Component) {
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
);
this.dispatch(NavigationActions.init());
} else {
throw new Error(e);
}
}
@@ -308,32 +314,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;
};

View File

@@ -5,7 +5,6 @@ import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
import DrawerRouter from '../routers/DrawerRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';

View File

@@ -79,6 +79,9 @@ module.exports = {
get TabRouter() {
return require('./routers/TabRouter').default;
},
get DrawerRouter() {
return require('./routers/DrawerRouter').default;
},
get SwitchRouter() {
return require('./routers/SwitchRouter').default;
},
@@ -121,6 +124,9 @@ module.exports = {
get DrawerItems() {
return require('./views/Drawer/DrawerNavigatorItems').default;
},
get DrawerSidebar() {
return require('./views/Drawer/DrawerSidebar').default;
},
// TabView
get TabView() {

View File

@@ -25,11 +25,14 @@ export default (routeConfigs, config = {}) => {
};
},
getStateForAction(action, lastState) {
const state = lastState || {
...switchRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
getStateForAction(action, state) {
// Set up the initial state if needed
if (!state) {
return {
...switchRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
}
const isRouterTargeted = action.key == null || action.key === state.key;

View File

@@ -45,6 +45,43 @@ describe('DrawerRouter', () => {
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
test('Handles initial route navigation', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
const router = DrawerRouter(
{
Foo: {
screen: FooScreen,
},
Bar: {
screen: BarScreen,
},
},
{ initialRouteName: 'Bar' }
);
const state = router.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
});
expect(state).toEqual({
index: 0,
isDrawerOpen: false,
isTransitioning: false,
routes: [
{
key: 'Foo',
params: undefined,
routeName: 'Foo',
},
{
key: 'Bar',
params: undefined,
routeName: 'Bar',
},
],
});
});
test('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;

View File

@@ -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(

View File

@@ -1,24 +0,0 @@
import React from 'react';
import SceneView from '../SceneView';
/**
* Component that renders the child screen of the drawer.
*/
class DrawerScreen extends React.PureComponent {
render() {
const { descriptors, navigation, screenProps } = this.props;
const { routes, index } = navigation.state;
const descriptor = descriptors[routes[index].key];
const Content = descriptor.getComponent();
return (
<SceneView
screenProps={screenProps}
component={Content}
navigation={descriptor.navigation}
/>
);
}
}
export default DrawerScreen;

View File

@@ -93,6 +93,7 @@ export default class DrawerView extends React.PureComponent {
this._drawer = c;
}}
drawerLockMode={
drawerLockMode ||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
this.props.navigationConfig.drawerLockMode
}

View File

@@ -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();
@@ -239,140 +419,7 @@ class StackViewLayout extends React.Component {
? 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 =
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 = [
@@ -471,15 +518,17 @@ class StackViewLayout extends React.Component {
if (!hasHeader && headerMode === 'float') {
const { isLandscape } = this.props;
let headerHeight;
if (Platform.OS === 'android') {
// TODO: Need to handle translucent status bar.
headerHeight = 56;
} else if (isLandscape && !Platform.isPad) {
headerHeight = 52;
} else if (IS_IPHONE_X) {
headerHeight = 88;
if (Platform.OS === 'ios') {
if (isLandscape && !Platform.isPad) {
headerHeight = 52;
} else if (IS_IPHONE_X) {
headerHeight = 88;
} else {
headerHeight = 64;
}
} else {
headerHeight = 64;
headerHeight = 56;
// TODO (Android only): Need to handle translucent status bar.
}
marginTop = -headerHeight;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 812 B

1248
yarn.lock

File diff suppressed because it is too large Load Diff