Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2ce862c2 | ||
|
|
d778479e4a | ||
|
|
352dae50e1 | ||
|
|
61385cae59 | ||
|
|
aa3c13891e | ||
|
|
9696d7220d | ||
|
|
2b83b44816 | ||
|
|
ec749023ed | ||
|
|
adc9389eb3 | ||
|
|
54d143fee2 | ||
|
|
d50e74d0c7 | ||
|
|
22926c5230 | ||
|
|
f6c47a6c66 | ||
|
|
046a9f8930 | ||
|
|
72f17538c2 | ||
|
|
550001b053 | ||
|
|
d168ab26f9 | ||
|
|
99916328a1 | ||
|
|
08e1b53f2e | ||
|
|
2243528e97 | ||
|
|
931271198b | ||
|
|
1876706bad | ||
|
|
e97d6b26a8 |
@@ -1,6 +1,6 @@
|
||||
# React Navigation
|
||||
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-community/react-navigation) [](https://reactnavigation.org/docs/contributing.html)
|
||||
[](https://badge.fury.io/js/react-navigation) [](https://codecov.io/gh/react-navigation/react-navigation) [](https://circleci.com/gh/react-navigation/react-navigation/tree/master) [](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.
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
56
flow/react-navigation.js
vendored
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('NavigationContainer', () => {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
6
src/react-navigation.js
vendored
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 659 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 812 B |