Navigation Event Subscriptions (#3345)

* Add fbemitter, keep flow passing

* Begin support for event emitter

- Adds emitter to navigation prop
- Emits top level onAction event
- stub getChildEventSubscriber for child events

* Support navigationState.isNavigating

* Focus and blur events starting to work

- Navigation completion action wired up
- Event chaining logic built in getChildEventSubscriber
- Renamed onAction evt to ‘action’

* Wrap up events progress and testing

* Rename to isTransitioning and COMPLETE_TRANSITION

* rm accidental dependency

* Suppoert event payload type
This commit is contained in:
Eric Vicenti
2018-01-25 00:13:28 -08:00
parent 750e354280
commit 966ecb53ba
28 changed files with 734 additions and 180 deletions

View File

@@ -1,81 +0,0 @@
[ignore]
; We fork some components by platform
.*/*[.]android.js
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
.*/Libraries/polyfills/.*
.*/react-navigation/node_modules/.*
; Additional create-react-native-app ignores
; Ignore duplicate module providers
.*/node_modules/fbemitter/lib/*
; Ignore misbehaving dev-dependencies
.*/node_modules/xdl/build/*
.*/node_modules/reqwest/tests/*
; Ignore missing expo-sdk dependencies (temporarily)
; https://github.com/expo/expo/issues/162
.*/node_modules/expo/src/*
; Ignore react-native-fbads dependency of the expo sdk
.*/node_modules/react-native-fbads/*
.*/react-navigation/lib-rn/.*
.*/react-navigation/lib/.*
.*/react-navigation/examples/ReduxExample/.*
.*/react-navigation/website/.*
; This package is required by Expo and causes Flow errors
.*/node_modules/react-native-gesture-handler/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
[options]
module.system=haste
emoji=true
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.56.0

View File

@@ -7,38 +7,101 @@ import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
title="Go to a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Go to a photos screen"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
class MyNavScreen extends React.Component {
render() {
const { navigation, banner } = this.props;
return (
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
title="Go to a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Go to a photos screen"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>
);
}
}
const MyHomeScreen = ({ navigation }) => (
<MyNavScreen banner="Home Screen" navigation={navigation} />
);
MyHomeScreen.navigationOptions = {
title: 'Welcome',
};
class MyHomeScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus HomeScreen', a);
};
_onDF = a => {
console.log('_didFocus HomeScreen', a);
};
_onWB = a => {
console.log('_willBlur HomeScreen', a);
};
_onDB = a => {
console.log('_didBlur HomeScreen', a);
};
const MyPhotosScreen = ({ navigation }) => (
<MyNavScreen
banner={`${navigation.state.params.name}'s Photos`}
navigation={navigation}
/>
);
MyPhotosScreen.navigationOptions = {
title: 'Photos',
};
render() {
const { navigation } = this.props;
return <MyNavScreen banner="Home Screen" navigation={navigation} />;
}
}
class MyPhotosScreen extends React.Component {
static navigationOptions = {
title: 'Photos',
};
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_willFocus PhotosScreen', a);
};
_onDF = a => {
console.log('_didFocus PhotosScreen', a);
};
_onWB = a => {
console.log('_willBlur PhotosScreen', a);
};
_onDB = a => {
console.log('_didBlur PhotosScreen', a);
};
render() {
const { navigation } = this.props;
return (
<MyNavScreen
banner={`${navigation.state.params.name}'s Photos`}
navigation={navigation}
/>
);
}
}
const MyProfileScreen = ({ navigation }) => (
<MyNavScreen

View File

@@ -114,4 +114,35 @@ const SimpleTabs = TabNavigator(
}
);
export default SimpleTabs;
class SimpleTabsContainer extends React.Component {
static router = SimpleTabs.router;
componentDidMount() {
this._s0 = this.props.navigation.addListener('willFocus', this._onWF);
this._s1 = this.props.navigation.addListener('didFocus', this._onDF);
this._s2 = this.props.navigation.addListener('willBlur', this._onWB);
this._s3 = this.props.navigation.addListener('didBlur', this._onDB);
}
componentWillUnmount() {
this._s0.remove();
this._s1.remove();
this._s2.remove();
this._s3.remove();
}
_onWF = a => {
console.log('_onWillFocus tabsExample ', a);
};
_onDF = a => {
console.log('_onDidFocus tabsExample ', a);
};
_onWB = a => {
console.log('_onWillBlur tabsExample ', a);
};
_onDB = a => {
console.log('_onDidBlur tabsExample ', a);
};
render() {
return <SimpleTabs navigation={this.props.navigation} />;
}
}
export default SimpleTabsContainer;

View File

@@ -2399,7 +2399,7 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
once "^1.3.0"
path-is-absolute "^1.0.0"
global@^4.3.0:
global@^4.3.0, global@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
dependencies:
@@ -4690,15 +4690,8 @@ react-native@^0.51.0:
yargs "^9.0.0"
"react-navigation@link:../..":
version "1.0.0-beta.27"
dependencies:
babel-plugin-transform-define "^1.3.0"
clamp "^1.0.1"
hoist-non-react-statics "^2.2.0"
path-to-regexp "^1.7.0"
prop-types "^15.5.10"
react-native-drawer-layout-polyfill "^1.3.2"
react-native-tab-view "^0.0.74"
version "0.0.0"
uid ""
react-proxy@^1.1.7:
version "1.1.8"

View File

@@ -16,6 +16,7 @@
"test": "npm run lint && npm run jest",
"codecov": "codecov",
"jest": "jest",
"test-update-snapshot": "jest --updateSnapshot",
"lint": "eslint .",
"format": "eslint --fix .",
"precommit": "lint-staged"
@@ -56,7 +57,7 @@
"eslint-plugin-prettier": "^2.1.2",
"eslint-plugin-react": "^7.1.0",
"husky": "^0.14.3",
"jest": "^20.0.4",
"jest": "^22.1.3",
"lint-staged": "^4.2.1",
"prettier": "^1.5.3",
"prettier-eslint": "^6.4.2",

View File

@@ -0,0 +1,4 @@
module.exports = {
trailingComma: 'es5',
singleQuote: true,
};

View File

@@ -4,6 +4,7 @@ const NAVIGATE = 'Navigation/NAVIGATE';
const RESET = 'Navigation/RESET';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
const createAction = (type, fn) => {
fn.toString = () => type;
@@ -57,6 +58,10 @@ const uri = createAction(URI, payload => ({
uri: payload.uri,
}));
const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
type: COMPLETE_TRANSITION,
}));
const mapDeprecatedNavigateAction = action => {
if (action.type === 'Navigate') {
const payload = {
@@ -118,6 +123,7 @@ export default {
RESET,
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
// Action creators
back,
@@ -126,6 +132,7 @@ export default {
reset,
setParams,
uri,
completeTransition,
// TODO: Remove once old actions are deprecated
mapDeprecatedActionAndWarn,

View File

@@ -5,7 +5,11 @@ const routeName = 'Anything';
describe('StateUtils', () => {
// Getters
it('gets route', () => {
const state = { index: 0, routes: [{ key: 'a', routeName }] };
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.get(state, 'a')).toEqual({
key: 'a',
routeName,
@@ -17,6 +21,7 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.indexOf(state, 'a')).toBe(0);
expect(NavigationStateUtils.indexOf(state, 'b')).toBe(1);
@@ -27,6 +32,7 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.has(state, 'b')).toBe(true);
expect(NavigationStateUtils.has(state, 'c')).toBe(false);
@@ -34,9 +40,14 @@ describe('StateUtils', () => {
// Push
it('pushes a route', () => {
const state = { index: 0, routes: [{ key: 'a', routeName }] };
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
isTransitioning: false,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
};
expect(NavigationStateUtils.push(state, { key: 'b', routeName })).toEqual(
@@ -45,7 +56,11 @@ describe('StateUtils', () => {
});
it('does not push duplicated route', () => {
const state = { index: 0, routes: [{ key: 'a', routeName }] };
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(() =>
NavigationStateUtils.push(state, { key: 'a', routeName })
).toThrow();
@@ -56,13 +71,22 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
const newState = { index: 0, routes: [{ key: 'a', routeName }] };
expect(NavigationStateUtils.pop(state)).toEqual(newState);
});
it('does not pop route if not applicable', () => {
const state = { index: 0, routes: [{ key: 'a', routeName }] };
const state = {
index: 0,
routes: [{ key: 'a', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.pop(state)).toBe(state);
});
@@ -71,10 +95,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.jumpToIndex(state, 0)).toBe(state);
expect(NavigationStateUtils.jumpToIndex(state, 1)).toEqual(newState);
@@ -84,6 +110,7 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
});
@@ -92,10 +119,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.jumpTo(state, 'a')).toBe(state);
expect(NavigationStateUtils.jumpTo(state, 'b')).toEqual(newState);
@@ -105,6 +134,7 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
});
@@ -113,10 +143,12 @@ describe('StateUtils', () => {
const state = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.back(state)).toEqual(newState);
expect(NavigationStateUtils.back(newState)).toBe(newState);
@@ -126,10 +158,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(NavigationStateUtils.forward(state)).toEqual(newState);
expect(NavigationStateUtils.forward(newState)).toBe(newState);
@@ -140,10 +174,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAt(state, 'b', { key: 'c', routeName })
@@ -154,10 +190,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, { key: 'c', routeName })
@@ -168,6 +206,7 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
@@ -179,10 +218,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 1,
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.reset(state, [
@@ -200,10 +241,12 @@ describe('StateUtils', () => {
const state = {
index: 0,
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
isTransitioning: false,
};
const newState = {
index: 0,
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
isTransitioning: false,
};
expect(
NavigationStateUtils.reset(

View File

@@ -1,6 +1,10 @@
import NavigationActions from '../NavigationActions';
import addNavigationHelpers from '../addNavigationHelpers';
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
});
describe('addNavigationHelpers', () => {
it('handles Back action', () => {
const mockedDispatch = jest
@@ -10,6 +14,7 @@ describe('addNavigationHelpers', () => {
addNavigationHelpers({
state: { key: 'A', routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).goBack('A')
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
@@ -27,6 +32,7 @@ describe('addNavigationHelpers', () => {
addNavigationHelpers({
state: { routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).goBack()
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({ type: NavigationActions.BACK });
@@ -41,6 +47,7 @@ describe('addNavigationHelpers', () => {
addNavigationHelpers({
state: { routeName: 'Home' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).navigate('Profile', { name: 'Matt' })
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({
@@ -59,6 +66,7 @@ describe('addNavigationHelpers', () => {
addNavigationHelpers({
state: { key: 'B', routeName: 'Settings' },
dispatch: mockedDispatch,
addListener: dummyEventSubscriber,
}).setParams({ notificationsEnabled: 'yes' })
).toEqual(true);
expect(mockedDispatch).toBeCalledWith({

View File

@@ -17,6 +17,8 @@ export default function createNavigationContainer(Component) {
static router = Component.router;
static navigationOptions = null;
_actionEventSubscribers = new Set();
constructor(props) {
super(props);
@@ -150,13 +152,27 @@ export default function createNavigationContainer(Component) {
const oldNav = this._nav;
invariant(oldNav, 'should be set in constructor if stateful');
const nav = Component.router.getStateForAction(action, oldNav);
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
// $FlowFixMe - Payload should probably understand generic state type
subscriber({
type: 'action',
action,
state: nav,
lastState: oldNav,
})
);
};
if (nav && nav !== oldNav) {
// 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.setState({ nav }, () => {
this._onNavigationStateChange(oldNav, nav, action);
dispatchActionEvents();
});
return true;
} else {
dispatchActionEvents();
}
return false;
};
@@ -170,6 +186,17 @@ export default function createNavigationContainer(Component) {
this._navigation = addNavigationHelpers({
dispatch: this.dispatch,
state: nav,
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
this._actionEventSubscribers.add(handler);
return {
remove: () => {
this._actionEventSubscribers.delete(handler);
},
};
},
});
}
navigation = this._navigation;

View File

@@ -0,0 +1,154 @@
/**
* @flow
*/
import type {
NavigationEventSubscriber,
NavigationAction,
NavigationState,
NavigationEventPayload,
} from './TypeDefinition';
/*
* This is used to extract one children's worth of events from a stream of navigation action events
*
* Based on the 'action' events that get fired for this navigation state, this utility will fire
* focus and blur events for this child
*/
export default function getChildEventSubscriber(
addListener: NavigationEventSubscriber,
key: string
): NavigationEventSubscriber {
const actionSubscribers = new Set();
const willFocusSubscribers = new Set();
const didFocusSubscribers = new Set();
const willBlurSubscribers = new Set();
const didBlurSubscribers = new Set();
const getChildSubscribers = evtName => {
switch (evtName) {
case 'action':
return actionSubscribers;
case 'willFocus':
return willFocusSubscribers;
case 'didFocus':
return didFocusSubscribers;
case 'willBlur':
return willBlurSubscribers;
case 'didBlur':
return didBlurSubscribers;
default:
return null;
}
};
const emit = payload => {
const subscribers = getChildSubscribers(payload.type);
subscribers &&
subscribers.forEach(subs => {
// $FlowFixMe - Payload should probably understand generic state type
subs(payload);
});
};
let isSelfFocused = false;
const cleanup = () => {
upstreamSubscribers.forEach(subs => subs && subs.remove());
};
const upstreamEvents = [
'willFocus',
'didFocus',
'willBlur',
'didBlur',
'action',
];
const upstreamSubscribers = upstreamEvents.map(eventName =>
addListener(eventName, (payload: NavigationEventPayload) => {
const { state, lastState, action } = payload;
const lastFocusKey = lastState && lastState.routes[lastState.index].key;
const focusKey = state && state.routes[state.index].key;
const isFocused = focusKey === key;
const wasFocused = lastFocusKey === key;
const lastRoute =
lastState && lastState.routes.find(route => route.key === key);
const newRoute = state && state.routes.find(route => route.key === key);
const childPayload = {
state: newRoute,
lastState: lastRoute,
action,
type: eventName,
};
const didNavigate =
(lastState && lastState.isTransitioning) !==
(state && state.isTransitioning);
const isTransitioning = !!state && state.isTransitioning;
const wasTransitioning = !!lastState && lastState.isTransitioning;
const didStartTransitioning = !wasTransitioning && isTransitioning;
const didFinishTransitioning = wasTransitioning && !isTransitioning;
if (eventName !== 'action') {
switch (eventName) {
case 'didFocus':
isSelfFocused = true;
break;
case 'didBlur':
isSelfFocused = false;
break;
}
emit(childPayload);
return;
}
// now we're exclusively handling the "action" event
if (newRoute) {
// fire this event to pass navigation events to children subscribers
emit(childPayload);
}
if (isFocused && didStartTransitioning && !isSelfFocused) {
emit({
...childPayload,
type: 'willFocus',
});
}
if (isFocused && didFinishTransitioning && !isSelfFocused) {
emit({
...childPayload,
type: 'didFocus',
});
isSelfFocused = true;
}
if (!isFocused && didStartTransitioning && isSelfFocused) {
emit({
...childPayload,
type: 'willBlur',
});
}
if (!isFocused && didFinishTransitioning && isSelfFocused) {
emit({
...childPayload,
type: 'didBlur',
});
isSelfFocused = false;
}
})
);
return (eventName, eventHandler) => {
const subscribers = getChildSubscribers(eventName);
if (!subscribers) {
throw new Error(`Invalid event name "${eventName}"`);
}
subscribers.add(eventHandler);
const remove = () => {
subscribers.delete(eventHandler);
};
return { remove };
};
}

View File

@@ -4,6 +4,7 @@ import createNavigator from './createNavigator';
import CardStackTransitioner from '../views/CardStack/CardStackTransitioner';
import StackRouter from '../routers/StackRouter';
import NavigatorTypes from './NavigatorTypes';
import NavigationActions from '../NavigationActions';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)
@@ -46,7 +47,11 @@ export default (routeConfigMap, stackConfig = {}) => {
cardStyle={cardStyle}
transitionConfig={transitionConfig}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
onTransitionEnd={(lastTransition, transition) => {
const { state, dispatch } = props.navigation;
dispatch(NavigationActions.completeTransition());
onTransitionEnd && onTransitionEnd();
}}
/>
));

View File

@@ -93,12 +93,14 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"routes": Array [
Object {
"key": "Init-id-0-1",
@@ -324,12 +326,14 @@ exports[`StackNavigator renders successfully 1`] = `
mode="float"
navigation={
Object {
"addListener": [Function],
"dispatch": [Function],
"goBack": [Function],
"navigate": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
"isTransitioning": false,
"routes": Array [
Object {
"key": "Init-id-0-0",

View File

@@ -87,6 +87,7 @@ export default (routeConfigs, stackConfig = {}) => {
childRouters[action.routeName] !== undefined
) {
return {
isTransitioning: false,
index: 0,
routes: [
{
@@ -120,6 +121,7 @@ export default (routeConfigs, stackConfig = {}) => {
};
// eslint-disable-next-line no-param-reassign
state = {
isTransitioning: false,
index: 0,
routes: [route],
};
@@ -171,7 +173,20 @@ export default (routeConfigs, stackConfig = {}) => {
routeName: action.routeName,
};
}
return StateUtils.push(state, route);
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
}
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
// Handle navigation to other child routers that are not yet pushed
@@ -257,19 +272,17 @@ export default (routeConfigs, stackConfig = {}) => {
if (action.type === NavigationActions.BACK) {
const key = action.key;
let backRouteIndex = null;
let backRouteIndex = state.index;
if (key) {
const backRoute = state.routes.find(route => route.key === key);
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex == null) {
return StateUtils.pop(state);
}
if (backRouteIndex > 0) {
return {
...state,
routes: state.routes.slice(0, backRouteIndex),
index: backRouteIndex - 1,
isTransitioning: action.immediate !== true,
};
}
}

View File

@@ -55,6 +55,7 @@ export default (routeConfigs, config = {}) => {
state = {
routes,
index: initialRouteIndex,
isTransitioning: false,
};
// console.log(`${order.join('-')}: Initial state`, {state});
}

View File

@@ -13,6 +13,10 @@ const ROUTERS = {
StackRouter,
};
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
});
Object.keys(ROUTERS).forEach(routerName => {
const Router = ROUTERS[routerName];
@@ -49,19 +53,31 @@ Object.keys(ROUTERS).forEach(routerName => {
];
expect(
router.getScreenOptions(
addNavigationHelpers({ state: routes[0], dispatch: () => false }),
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual(undefined);
expect(
router.getScreenOptions(
addNavigationHelpers({ state: routes[1], dispatch: () => false }),
addNavigationHelpers({
state: routes[1],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('BarTitle');
expect(
router.getScreenOptions(
addNavigationHelpers({ state: routes[2], dispatch: () => false }),
addNavigationHelpers({
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('Baz-123');
@@ -114,6 +130,7 @@ test('Handles deep action', () => {
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-2',
@@ -126,6 +143,7 @@ test('Handles deep action', () => {
{
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
immediate: true,
action: { type: NavigationActions.NAVIGATE, routeName: 'Zoo' },
},
state1
@@ -152,6 +170,7 @@ test('Supports lazily-evaluated getScreen', () => {
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
const state2 = TestRouter.getStateForAction({
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-4');
@@ -159,7 +178,11 @@ test('Supports lazily-evaluated getScreen', () => {
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Zap' },
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Zap',
},
state2
);
expect(state2).toEqual(state3);

View File

@@ -83,6 +83,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -93,6 +94,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -116,6 +118,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 0,
isTransitioning: false,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -126,6 +129,7 @@ describe('StackRouter', () => {
expect(
router.getComponentForState({
index: 1,
isTransitioning: false,
routes: [
{ key: 'a', routeName: 'foo' },
{ key: 'b', routeName: 'bar' },
@@ -324,6 +328,7 @@ describe('StackRouter', () => {
const initState = TestRouter.getStateForAction(NavigationActions.init());
expect(initState).toEqual({
index: 0,
isTransitioning: false,
routes: [{ key: 'Init-id-0-0', routeName: 'foo' }],
});
const pushedState = TestRouter.getStateForAction(
@@ -349,6 +354,7 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-4',
@@ -361,6 +367,7 @@ describe('StackRouter', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: { name: 'Zoom' },
immediate: true,
},
state
);
@@ -369,11 +376,12 @@ describe('StackRouter', () => {
expect(state2 && state2.routes[1].params).toEqual({ name: 'Zoom' });
expect(state2 && state2.routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
{ type: NavigationActions.BACK, immediate: true },
state2
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-4',
@@ -383,6 +391,38 @@ describe('StackRouter', () => {
});
});
test('Handles push transition logic with completion action', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
const router = StackRouter({
Foo: {
screen: FooScreen,
},
Bar: {
screen: BarScreen,
},
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: { name: 'Zoom' },
},
state
);
expect(state2 && state2.index).toEqual(1);
expect(state2 && state2.isTransitioning).toEqual(true);
const state3 = router.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.isTransitioning).toEqual(false);
});
test('Handle basic stack logic for components with router', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -402,9 +442,10 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-6',
key: 'Init-id-0-8',
routeName: 'Foo',
},
],
@@ -414,6 +455,7 @@ describe('StackRouter', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: { name: 'Zoom' },
immediate: true,
},
state
);
@@ -422,14 +464,15 @@ describe('StackRouter', () => {
expect(state2 && state2.routes[1].params).toEqual({ name: 'Zoom' });
expect(state2 && state2.routes.length).toEqual(2);
const state3 = router.getStateForAction(
{ type: NavigationActions.BACK },
{ type: NavigationActions.BACK, immediate: true },
state2
);
expect(state3).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-6',
key: 'Init-id-0-8',
routeName: 'Foo',
},
],
@@ -452,6 +495,7 @@ describe('StackRouter', () => {
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
immediate: true,
params: { name: 'Zoom' },
},
state
@@ -460,6 +504,7 @@ describe('StackRouter', () => {
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
immediate: true,
params: { name: 'Foo' },
},
state2
@@ -470,7 +515,11 @@ describe('StackRouter', () => {
);
expect(state3).toEqual(state4);
const state5 = router.getStateForAction(
{ type: NavigationActions.BACK, key: state3 && state3.routes[1].key },
{
type: NavigationActions.BACK,
key: state3 && state3.routes[1].key,
immediate: true,
},
state4
);
expect(state5).toEqual(state);
@@ -493,9 +542,10 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-12',
key: 'Init-id-0-14',
routeName: 'Bar',
},
],
@@ -515,9 +565,10 @@ describe('StackRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
key: 'Init-id-0-13',
key: state && state.routes[0].key,
routeName: 'Bar',
params: { foo: 'bar' },
},
@@ -542,6 +593,7 @@ describe('StackRouter', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
params: { bar: '42' },
immediate: true,
},
state
);
@@ -566,14 +618,17 @@ describe('StackRouter', () => {
}
);
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
params: { name: 'Qux' },
key: 'Init-id-0-16',
},
state
);
const key = state && state.routes[0].key;
const state2 =
key &&
router.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
params: { name: 'Qux' },
key,
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].params).toEqual({ name: 'Qux' });
});
@@ -598,14 +653,14 @@ describe('StackRouter', () => {
{
type: NavigationActions.SET_PARAMS,
params: { name: 'foobar' },
key: 'Init-id-0-17',
key: 'Init-id-0-19',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.routes[0].routes[0].routes).toEqual([
{
key: 'Init-id-0-17',
key: 'Init-id-0-19',
routeName: 'Quux',
params: { name: 'foobar' },
},
@@ -630,8 +685,13 @@ describe('StackRouter', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
params: { bar: '42' },
immediate: true,
},
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
immediate: true,
},
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
],
index: 1,
},
@@ -665,7 +725,13 @@ describe('StackRouter', () => {
const state2 = router.getStateForAction(
{
type: NavigationActions.RESET,
actions: [{ type: NavigationActions.NAVIGATE, routeName: 'Foo' }],
actions: [
{
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
immediate: true,
},
],
index: 0,
},
state
@@ -699,7 +765,12 @@ describe('StackRouter', () => {
{
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
action: { type: NavigationActions.NAVIGATE, routeName: 'baz' },
immediate: true,
action: {
type: NavigationActions.NAVIGATE,
routeName: 'baz',
immediate: true,
},
},
state
);
@@ -707,7 +778,13 @@ describe('StackRouter', () => {
{
type: NavigationActions.RESET,
key: 'Init',
actions: [{ type: NavigationActions.NAVIGATE, routeName: 'Foo' }],
actions: [
{
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
immediate: true,
},
],
index: 0,
},
state2
@@ -716,7 +793,13 @@ describe('StackRouter', () => {
{
type: NavigationActions.RESET,
key: null,
actions: [{ type: NavigationActions.NAVIGATE, routeName: 'Bar' }],
actions: [
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
immediate: true,
},
],
index: 0,
},
state3
@@ -738,6 +821,7 @@ describe('StackRouter', () => {
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Bar',
params: { foo: '42' },
},
@@ -767,6 +851,7 @@ describe('StackRouter', () => {
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
immediate: true,
routeName: 'Bar',
params: { foo: '42' },
},
@@ -836,6 +921,7 @@ describe('StackRouter', () => {
const state = {
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
@@ -954,3 +1040,51 @@ describe('StackRouter', () => {
});
});
});
test('Handles deep navigate completion action', () => {
const LeafScreen = () => <div />;
const FooScreen = () => <div />;
FooScreen.router = StackRouter({
Boo: { path: 'boo', screen: LeafScreen },
Baz: { path: 'baz/:bazId', screen: LeafScreen },
});
const router = StackRouter({
Foo: {
screen: FooScreen,
},
Bar: {
screen: LeafScreen,
},
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
expect(state && state.index).toEqual(0);
expect(state && state.routes[0].routeName).toEqual('Foo');
const key = state && state.routes[0].key;
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Baz',
},
state
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state2 && state2.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
},
state2
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state3 && state3.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});

View File

@@ -27,6 +27,7 @@ describe('TabRouter', () => {
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
],
isTransitioning: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
@@ -39,6 +40,7 @@ describe('TabRouter', () => {
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
],
isTransitioning: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
@@ -64,6 +66,7 @@ describe('TabRouter', () => {
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
],
isTransitioning: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
@@ -76,6 +79,7 @@ describe('TabRouter', () => {
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
],
isTransitioning: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
@@ -99,6 +103,7 @@ describe('TabRouter', () => {
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
],
isTransitioning: false,
});
});
@@ -170,6 +175,7 @@ describe('TabRouter', () => {
const state = router.getStateForAction(navAction);
expect(state).toEqual({
index: 1,
isTransitioning: false,
routes: [
{
key: 'Foo',
@@ -177,6 +183,7 @@ describe('TabRouter', () => {
},
{
index: 1,
isTransitioning: false,
key: 'Baz',
routeName: 'Baz',
routes: [
@@ -216,12 +223,14 @@ describe('TabRouter', () => {
let state = router.getStateForAction(navAction);
expect(state).toEqual({
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{
index: 0,
key: 'Baz',
routeName: 'Baz',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Bar', routeName: 'Bar' },
@@ -241,6 +250,7 @@ describe('TabRouter', () => {
);
expect(state && state.routes[1]).toEqual({
index: 0,
isTransitioning: false,
key: 'Baz',
routeName: 'Baz',
routes: [
@@ -267,12 +277,14 @@ describe('TabRouter', () => {
});
expect(state).toEqual({
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{
index: 1,
key: 'Baz',
routeName: 'Baz',
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
@@ -287,12 +299,14 @@ describe('TabRouter', () => {
);
expect(state2).toEqual({
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{
index: 0,
key: 'Baz',
routeName: 'Baz',
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
@@ -331,16 +345,19 @@ describe('TabRouter', () => {
const state = router.getStateForAction(INIT_ACTION);
expect(state).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz' },
@@ -350,6 +367,7 @@ describe('TabRouter', () => {
index: 0,
key: 'Bar',
routeName: 'Bar',
isTransitioning: false,
routes: [
{ key: 'Zoo', routeName: 'Zoo' },
{ key: 'Zap', routeName: 'Zap' },
@@ -366,16 +384,19 @@ describe('TabRouter', () => {
);
expect(state2).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz' },
@@ -385,6 +406,7 @@ describe('TabRouter', () => {
index: 1,
key: 'Bar',
routeName: 'Bar',
isTransitioning: false,
routes: [
{ key: 'Zoo', routeName: 'Zoo' },
{ key: 'Zap', routeName: 'Zap' },
@@ -411,16 +433,19 @@ describe('TabRouter', () => {
});
expect(state4).toEqual({
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{
index: 0,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz' },
@@ -430,6 +455,7 @@ describe('TabRouter', () => {
index: 1,
key: 'Bar',
routeName: 'Bar',
isTransitioning: false,
routes: [
{ key: 'Zoo', routeName: 'Zoo' },
{ key: 'Zap', routeName: 'Zap' },
@@ -467,6 +493,7 @@ describe('TabRouter', () => {
const state = router.getStateForAction({ type: NavigationActions.INIT });
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar' },
@@ -476,6 +503,7 @@ describe('TabRouter', () => {
const state2 = router.getStateForAction(expectedAction, state);
const expectedState2 = {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo' },
{ key: 'Bar', routeName: 'Bar', params },
@@ -530,11 +558,13 @@ describe('TabRouter', () => {
const state = {
index: 0,
isTransitioning: false,
routes: [
{
index: 1,
key: 'Foo',
routeName: 'Foo',
isTransitioning: false,
routes: [
{ key: 'Boo', routeName: 'Boo' },
{ key: 'Baz', routeName: 'Baz' },
@@ -583,6 +613,7 @@ describe('TabRouter', () => {
expect(state0).toEqual({
index: 0,
isTransitioning: false,
routes: [{ key: 'a', routeName: 'a' }, { key: 'b', routeName: 'b' }],
});
@@ -595,6 +626,7 @@ describe('TabRouter', () => {
expect(state1).toEqual({
index: 1,
isTransitioning: false,
routes: [
{ key: 'a', routeName: 'a' },
{ key: 'b', routeName: 'b', params },
@@ -620,11 +652,13 @@ describe('TabRouter', () => {
const screenApreState = {
index: 0,
key: 'Init',
isTransitioning: false,
routeName: 'Foo',
routes: [{ key: 'Init', routeName: 'Bar' }],
};
const preState = {
index: 0,
isTransitioning: false,
routes: [screenApreState],
};

View File

@@ -2,6 +2,10 @@ import { Component } from 'react';
import createConfigGetter from '../createConfigGetter';
import addNavigationHelpers from '../../addNavigationHelpers';
const dummyEventSubscriber = (name: string, handler: (*) => void) => ({
remove: () => {},
});
test('should get config for screen', () => {
/* eslint-disable react/no-multi-comp */
@@ -63,49 +67,81 @@ test('should get config for screen', () => {
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[0], dispatch: () => false }),
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('Welcome anonymous');
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[1], dispatch: () => false }),
addNavigationHelpers({
state: routes[1],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('Welcome jane');
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[0], dispatch: () => false }),
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).gesturesEnabled
).toEqual(true);
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[2], dispatch: () => false }),
addNavigationHelpers({
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('Settings!!!');
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[2], dispatch: () => false }),
addNavigationHelpers({
state: routes[2],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).gesturesEnabled
).toEqual(false);
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[3], dispatch: () => false }),
addNavigationHelpers({
state: routes[3],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).title
).toEqual('10 new notifications');
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[3], dispatch: () => false }),
addNavigationHelpers({
state: routes[3],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).gesturesEnabled
).toEqual(true);
expect(
getScreenOptions(
addNavigationHelpers({ state: routes[4], dispatch: () => false }),
addNavigationHelpers({
state: routes[4],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
).gesturesEnabled
).toEqual(false);
@@ -128,7 +164,11 @@ test('should throw if the route does not exist', () => {
expect(() =>
getScreenOptions(
addNavigationHelpers({ state: routes[0], dispatch: () => false }),
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
}),
{}
)
).toThrowError(
@@ -147,7 +187,11 @@ test('should throw if the screen is not defined under the route config', () => {
expect(() =>
getScreenOptions(
addNavigationHelpers({ state: routes[0], dispatch: () => false })
addNavigationHelpers({
state: routes[0],
dispatch: () => false,
addListener: dummyEventSubscriber,
})
)
).toThrowError('Route Home must define a screen or a getScreen.');
});

View File

@@ -3,6 +3,7 @@ import invariant from '../utils/invariant';
import getScreenForRouteName from './getScreenForRouteName';
import addNavigationHelpers from '../addNavigationHelpers';
import validateScreenOptions from './validateScreenOptions';
import getChildEventSubscriber from '../getChildEventSubscriber';
function applyConfig(configurer, navigationOptions, configProps) {
if (typeof configurer === 'function') {
@@ -51,6 +52,10 @@ export default (routeConfigs, navigatorScreenConfig) => (
const childNavigation = addNavigationHelpers({
state: childRoute,
dispatch,
addListener: getChildEventSubscriber(
navigation.addListener,
childRoute.key
),
});
outputConfig = router.getScreenOptions(childNavigation, screenProps);
}

View File

@@ -15,6 +15,7 @@ import Card from './Card';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import getChildEventSubscriber from '../../getChildEventSubscriber';
import SceneView from '../SceneView';
import TransitionConfigs from './TransitionConfigs';
@@ -99,6 +100,10 @@ class CardStack extends React.Component {
const screenNavigation = addNavigationHelpers({
dispatch: navigation.dispatch,
state: scene.route,
addListener: getChildEventSubscriber(
navigation.addListener,
scene.route.key
),
});
screenDetails = {
state: scene.route,

View File

@@ -4,6 +4,7 @@ import DrawerLayout from 'react-native-drawer-layout-polyfill';
import addNavigationHelpers from '../../addNavigationHelpers';
import DrawerSidebar from './DrawerSidebar';
import getChildEventSubscriber from '../../getChildEventSubscriber';
/**
* Component that renders the drawer.
@@ -81,6 +82,10 @@ export default class DrawerView extends React.PureComponent {
this._screenNavigationProp = addNavigationHelpers({
dispatch: navigation.dispatch,
state: navigationState,
addListener: getChildEventSubscriber(
navigation.addListener,
navigationState.key
),
});
};
@@ -120,13 +125,8 @@ export default class DrawerView extends React.PureComponent {
this.props.drawerCloseRoute
);
const screenNavigation = addNavigationHelpers({
state: this._screenNavigationProp.state,
dispatch: this._screenNavigationProp.dispatch,
});
const config = this.props.router.getScreenOptions(
screenNavigation,
this._screenNavigationProp,
this.props.screenProps
);

View File

@@ -120,6 +120,14 @@ class Transitioner extends React.Component {
]
: [];
// When there are no animations happening, avoid calling onTransitionStart/End.
// This is important because the stack navigator fires the completion prop when
// the transition is ended.
if (!animations.length) {
this.setState(nextState);
return;
}
// update scenes and play the transition
this._isTransitionRunning = true;
this.setState(nextState, async () => {

View File

@@ -7,6 +7,7 @@ function testTransition(states) {
const routes = states.map(keys => ({
index: 0,
routes: keys.map(key => ({ key, routeName: '' })),
isTransitioning: false,
}));
let scenes = [];
@@ -89,11 +90,13 @@ describe('ScenesReducer', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 1,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
@@ -106,11 +109,13 @@ describe('ScenesReducer', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
@@ -122,11 +127,13 @@ describe('ScenesReducer', () => {
const state1 = {
index: 0,
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
isTransitioning: false,
};
const state2 = {
index: 0,
routes: [{ key: '2', routeName: '' }, { key: '1', routeName: '' }],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
@@ -141,6 +148,7 @@ describe('ScenesReducer', () => {
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
@@ -149,6 +157,7 @@ describe('ScenesReducer', () => {
{ key: '1', x: 3, routeName: '' },
{ key: '2', x: 4, routeName: '' },
],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);
@@ -163,6 +172,7 @@ describe('ScenesReducer', () => {
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const state2 = {
@@ -171,6 +181,7 @@ describe('ScenesReducer', () => {
{ key: '1', x: 1, routeName: '' },
{ key: '2', x: 2, routeName: '' },
],
isTransitioning: false,
};
const scenes1 = ScenesReducer([], state1, null);

View File

@@ -6,6 +6,10 @@ import TabRouter from '../../routers/TabRouter';
import TabView from '../TabView/TabView';
import TabBarBottom from '../TabView/TabBarBottom';
const dummyEventSubscriber = (name, handler) => ({
remove: () => {},
});
describe('TabBarBottom', () => {
it('renders successfully', () => {
const navigation = {
@@ -13,6 +17,7 @@ describe('TabBarBottom', () => {
index: 0,
routes: [{ key: 's1', routeName: 's1' }],
},
addListener: dummyEventSubscriber,
};
const router = TabRouter({ s1: { screen: View } });

View File

@@ -219,6 +219,7 @@ exports[`TabBarBottom renders successfully 1`] = `
<View
navigation={
Object {
"addListener": [Function],
"dispatch": undefined,
"goBack": [Function],
"navigate": [Function],

View File

@@ -1,5 +1,6 @@
import React from 'react';
import addNavigationHelpers from './addNavigationHelpers';
import getChildEventSubscriber from './getChildEventSubscriber';
/**
* HOC which caches the child navigation items.
@@ -30,6 +31,10 @@ export default function withCachedChildNavigation(Comp) {
this._childNavigationProps[route.key] = addNavigationHelpers({
dispatch: navigation.dispatch,
state: route,
addListener: getChildEventSubscriber(
navigation.addListener,
route.key
),
});
});
};

View File

@@ -2295,6 +2295,12 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
fbemitter@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865"
dependencies:
fbjs "^0.8.4"
fbjs-scripts@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/fbjs-scripts/-/fbjs-scripts-0.8.1.tgz#c1c6efbecb7f008478468976b783880c2f669765"
@@ -2308,7 +2314,7 @@ fbjs-scripts@^0.8.1:
semver "^5.1.0"
through2 "^2.0.0"
fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.9:
fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9:
version "0.8.16"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
dependencies: