diff --git a/examples/NavigationPlayground/js/SimpleTabs.js b/examples/NavigationPlayground/js/SimpleTabs.js index fb1fa00593..c925963450 100644 --- a/examples/NavigationPlayground/js/SimpleTabs.js +++ b/examples/NavigationPlayground/js/SimpleTabs.js @@ -45,35 +45,69 @@ MyHomeScreen.navigationOptions = { ), }; -const MyPeopleScreen = ({ navigation }) => ( - -); - -MyPeopleScreen.navigationOptions = { - tabBarLabel: 'People', - tabBarIcon: ({ tintColor, focused }) => ( - - ), -}; - -const MyChatScreen = ({ navigation }) => ( - -); +class MyPeopleScreen extends React.Component { + static navigationOptions = { + tabBarLabel: 'People', + tabBarIcon: ({ tintColor, focused }) => ( + + ), + }; + componentDidMount() { + this._s0 = this.props.navigation.addListener('willFocus', this._onEvent); + this._s1 = this.props.navigation.addListener('didFocus', this._onEvent); + this._s2 = this.props.navigation.addListener('willBlur', this._onEvent); + this._s3 = this.props.navigation.addListener('didBlur', this._onEvent); + } + componentWillUnmount() { + this._s0.remove(); + this._s1.remove(); + this._s2.remove(); + this._s3.remove(); + } + _onEvent = a => { + console.log('EVENT ON PEOPLE TAB', a.type, a); + }; + render() { + const { navigation } = this.props; + return ; + } +} -MyChatScreen.navigationOptions = { - tabBarLabel: 'Chat', - tabBarIcon: ({ tintColor, focused }) => ( - - ), -}; +class MyChatScreen extends React.Component { + static navigationOptions = { + tabBarLabel: 'Chat', + tabBarIcon: ({ tintColor, focused }) => ( + + ), + }; + componentDidMount() { + this._s0 = this.props.navigation.addListener('willFocus', this._onEvent); + this._s1 = this.props.navigation.addListener('didFocus', this._onEvent); + this._s2 = this.props.navigation.addListener('willBlur', this._onEvent); + this._s3 = this.props.navigation.addListener('didBlur', this._onEvent); + } + componentWillUnmount() { + this._s0.remove(); + this._s1.remove(); + this._s2.remove(); + this._s3.remove(); + } + _onEvent = a => { + console.log('EVENT ON CHAT TAB', a.type, a); + }; + render() { + const { navigation } = this.props; + return ; + } +} const MySettingsScreen = ({ navigation }) => ( @@ -128,10 +162,10 @@ class SimpleTabsContainer extends React.Component { _s3: EventListener; 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); + this._s0 = this.props.navigation.addListener('willFocus', this._onAction); + this._s1 = this.props.navigation.addListener('didFocus', this._onAction); + this._s2 = this.props.navigation.addListener('willBlur', this._onAction); + this._s3 = this.props.navigation.addListener('didBlur', this._onAction); } componentWillUnmount() { this._s0.remove(); @@ -139,17 +173,8 @@ class SimpleTabsContainer extends React.Component { 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); + _onAction = a => { + console.log('TABS EVENT', a.type, a); }; render() { return ; diff --git a/examples/ReduxExample/app.json b/examples/ReduxExample/app.json index 60df3da525..cf41253aea 100644 --- a/examples/ReduxExample/app.json +++ b/examples/ReduxExample/app.json @@ -12,13 +12,10 @@ "icon": "./assets/icons/react-navigation.png", "hideExponentText": false }, - "sdkVersion": "23.0.0", + "sdkVersion": "24.0.0", "entryPoint": "./node_modules/react-native-scripts/build/bin/crna-entry.js", "packagerOpts": { - "assetExts": [ - "ttf", - "mp4" - ] + "assetExts": ["ttf", "mp4"] }, "ios": { "supportsTablet": true diff --git a/examples/ReduxExample/package.json b/examples/ReduxExample/package.json index 6a50356d8f..5ad815afca 100644 --- a/examples/ReduxExample/package.json +++ b/examples/ReduxExample/package.json @@ -21,10 +21,10 @@ ] }, "dependencies": { - "expo": "^23.0.0", + "expo": "^24.0.2", "prop-types": "^15.5.10", "react": "16.0.0", - "react-native": "^0.50.3", + "react-native": "^0.51.0", "react-redux": "^5.0.6", "redux": "^3.7.2", "react-navigation": "link:../.." diff --git a/examples/ReduxExample/src/navigators/AppNavigator.js b/examples/ReduxExample/src/navigators/AppNavigator.js index 3572ef6623..e089289a54 100644 --- a/examples/ReduxExample/src/navigators/AppNavigator.js +++ b/examples/ReduxExample/src/navigators/AppNavigator.js @@ -13,17 +13,51 @@ export const AppNavigator = StackNavigator({ Profile: { screen: ProfileScreen }, }); -const AppWithNavigationState = ({ dispatch, nav }) => ( - -); +class AppWithNavigationState extends React.Component { + static propTypes = { + dispatch: PropTypes.func.isRequired, + nav: PropTypes.object.isRequired, + }; -AppWithNavigationState.propTypes = { - dispatch: PropTypes.func.isRequired, - nav: PropTypes.object.isRequired, -}; + _actionEventSubscribers = new Set(); + + _addListener = (eventName, handler) => { + eventName === 'action' && this._actionEventSubscribers.add(handler); + return { + remove: () => { + this._actionEventSubscribers.delete(handler); + }, + }; + }; + + componentDidUpdate(lastProps) { + const lastState = lastProps.nav; + this._actionEventSubscribers.forEach(subscriber => { + subscriber({ + lastState: lastProps.nav, + state: this.props.nav, + action: this.props.lastAction, + }); + }); + } + + render() { + const { dispatch, nav } = this.props; + return ( + + ); + } +} const mapStateToProps = state => ({ nav: state.nav, + lastAction: state.lastAction, }); export default connect(mapStateToProps)(AppWithNavigationState); diff --git a/examples/ReduxExample/src/reducers/index.js b/examples/ReduxExample/src/reducers/index.js index 1ece9bcd01..2a8ef8c0ac 100644 --- a/examples/ReduxExample/src/reducers/index.js +++ b/examples/ReduxExample/src/reducers/index.js @@ -36,6 +36,10 @@ function nav(state = initialNavState, action) { return nextState || state; } +function lastAction(state = null, action) { + return action; +} + const initialAuthState = { isLoggedIn: false }; function auth(state = initialAuthState, action) { @@ -50,6 +54,7 @@ function auth(state = initialAuthState, action) { } const AppReducer = combineReducers({ + lastAction, nav, auth, }); diff --git a/examples/ReduxExample/yarn.lock b/examples/ReduxExample/yarn.lock index 0e80ea5501..0fb4114e5e 100644 --- a/examples/ReduxExample/yarn.lock +++ b/examples/ReduxExample/yarn.lock @@ -57,7 +57,7 @@ dependencies: cross-spawn "^5.1.0" -"@expo/vector-icons@^6.2.0": +"@expo/vector-icons@^6.2.2": version "6.2.2" resolved "https://registry.yarnpkg.com/@expo/vector-icons/-/vector-icons-6.2.2.tgz#441edb58a52c0f4e5b4aba1e6f8da1e87cea7e11" dependencies: @@ -801,7 +801,7 @@ babel-plugin-transform-es3-property-literals@^6.8.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-exponentiation-operator@^6.24.1: +babel-plugin-transform-exponentiation-operator@^6.24.1, babel-plugin-transform-exponentiation-operator@^6.5.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" dependencies: @@ -1994,11 +1994,11 @@ expect@^21.2.1: jest-message-util "^21.2.1" jest-regex-util "^21.2.0" -expo@^23.0.0: - version "23.0.6" - resolved "https://registry.yarnpkg.com/expo/-/expo-23.0.6.tgz#8cb2c3992b385eb5866cc91f25f159420258d19a" +expo@^24.0.2: + version "24.0.2" + resolved "https://registry.yarnpkg.com/expo/-/expo-24.0.2.tgz#3ff9784afd9efbb8eb739289aa53290ddf31a5a5" dependencies: - "@expo/vector-icons" "^6.2.0" + "@expo/vector-icons" "^6.2.2" babel-preset-expo "^4.0.0" fbemitter "^2.1.1" invariant "^2.2.2" @@ -3768,7 +3768,7 @@ methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -metro-bundler@^0.20.1: +metro-bundler@^0.20.0: version "0.20.3" resolved "https://registry.yarnpkg.com/metro-bundler/-/metro-bundler-0.20.3.tgz#0ded01b64e8963117017b106f75b83cfc34f3656" dependencies: @@ -4717,9 +4717,9 @@ react-native-vector-icons@4.4.2: prop-types "^15.5.10" yargs "^8.0.2" -react-native@^0.50.3: - version "0.50.4" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.50.4.tgz#194f5da4939087b3acee712a503475f4942dca7e" +react-native@^0.51.0: + version "0.51.0" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.51.0.tgz#fe25934b3030fd323f3ca1a70f034133465955ed" dependencies: absolute-path "^0.0.0" art "^0.10.0" @@ -4727,6 +4727,7 @@ react-native@^0.50.3: babel-plugin-syntax-trailing-function-commas "^6.20.0" babel-plugin-transform-async-to-generator "6.16.0" babel-plugin-transform-class-properties "^6.18.0" + babel-plugin-transform-exponentiation-operator "^6.5.0" babel-plugin-transform-flow-strip-types "^6.21.0" babel-plugin-transform-object-rest-spread "^6.20.2" babel-register "^6.24.1" @@ -4747,7 +4748,7 @@ react-native@^0.50.3: graceful-fs "^4.1.3" inquirer "^3.0.6" lodash "^4.16.6" - metro-bundler "^0.20.1" + metro-bundler "^0.20.0" mime "^1.3.4" minimist "^1.2.0" mkdirp "^0.5.1" @@ -4763,7 +4764,7 @@ react-native@^0.50.3: react-clone-referenced-element "^1.0.1" react-devtools-core "^2.5.0" react-timer-mixin "^0.13.2" - regenerator-runtime "^0.9.5" + regenerator-runtime "^0.11.0" rimraf "^2.5.4" semver "^5.0.3" shell-quote "1.6.1" @@ -4775,15 +4776,8 @@ react-native@^0.50.3: 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" @@ -4924,10 +4918,6 @@ regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" -regenerator-runtime@^0.9.5: - version "0.9.6" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" - regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" diff --git a/src/__tests__/getChildEventSubscriber-test.js b/src/__tests__/getChildEventSubscriber-test.js new file mode 100644 index 0000000000..3e25271121 --- /dev/null +++ b/src/__tests__/getChildEventSubscriber-test.js @@ -0,0 +1,437 @@ +import getChildEventSubscriber from '../getChildEventSubscriber'; + +test('child action events only flow when focused', () => { + const parentSubscriber = jest.fn(); + const emitParentAction = payload => { + parentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'key1' + ); + const testState = { + key: 'foo', + routeName: 'FooRoute', + routes: [{ key: 'key0' }, { key: 'key1' }], + index: 0, + isTransitioning: false, + }; + const focusedTestState = { + ...testState, + index: 1, + }; + const childActionHandler = jest.fn(); + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + childEventSubscriber('action', childActionHandler); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + emitParentAction({ + type: 'action', + state: focusedTestState, + lastState: testState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(0); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'action', + state: focusedTestState, + lastState: focusedTestState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(1); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); +}); + +test('grandchildren subscription', () => { + const grandParentSubscriber = jest.fn(); + const emitGrandParentAction = payload => { + grandParentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const parentSubscriber = getChildEventSubscriber( + grandParentSubscriber, + 'parent' + ); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'key1' + ); + const parentBlurState = { + key: 'foo', + routeName: 'FooRoute', + routes: [ + { key: 'aunt' }, + { + key: 'parent', + routes: [{ key: 'key0' }, { key: 'key1' }], + index: 1, + isTransitioning: false, + }, + ], + index: 0, + isTransitioning: false, + }; + const parentTransitionState = { + ...parentBlurState, + index: 1, + isTransitioning: true, + }; + const parentFocusState = { + ...parentTransitionState, + isTransitioning: false, + }; + const childActionHandler = jest.fn(); + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + childEventSubscriber('action', childActionHandler); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + emitGrandParentAction({ + type: 'action', + state: parentTransitionState, + lastState: parentBlurState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(0); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(0); + emitGrandParentAction({ + type: 'action', + state: parentFocusState, + lastState: parentTransitionState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(0); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); +}); + +test('grandchildren transitions', () => { + const grandParentSubscriber = jest.fn(); + const emitGrandParentAction = payload => { + grandParentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const parentSubscriber = getChildEventSubscriber( + grandParentSubscriber, + 'parent' + ); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'key1' + ); + const makeFakeState = (childIndex, childIsTransitioning) => ({ + index: 1, + isTransitioning: false, + routes: [ + { key: 'nothing' }, + { + key: 'parent', + index: childIndex, + isTransitioning: childIsTransitioning, + routes: [{ key: 'key0' }, { key: 'key1' }, { key: 'key2' }], + }, + ], + }); + const blurredState = makeFakeState(0, false); + const transitionState = makeFakeState(1, true); + const focusState = makeFakeState(1, false); + const transition2State = makeFakeState(2, true); + const blurred2State = makeFakeState(2, false); + + const childActionHandler = jest.fn(); + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + const childWillBlurHandler = jest.fn(); + const childDidBlurHandler = jest.fn(); + childEventSubscriber('action', childActionHandler); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + childEventSubscriber('willBlur', childWillBlurHandler); + childEventSubscriber('didBlur', childDidBlurHandler); + emitGrandParentAction({ + type: 'action', + state: transitionState, + lastState: blurredState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(0); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(0); + emitGrandParentAction({ + type: 'action', + state: focusState, + lastState: transitionState, + action: { type: 'FooAction' }, + }); + expect(childActionHandler.mock.calls.length).toBe(0); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + emitGrandParentAction({ + type: 'action', + state: focusState, + lastState: focusState, + action: { type: 'TestAction' }, + }); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + expect(childActionHandler.mock.calls.length).toBe(1); + emitGrandParentAction({ + type: 'action', + state: transition2State, + lastState: focusState, + action: { type: 'CauseWillBlurAction' }, + }); + expect(childWillBlurHandler.mock.calls.length).toBe(1); + expect(childDidBlurHandler.mock.calls.length).toBe(0); + expect(childActionHandler.mock.calls.length).toBe(2); + emitGrandParentAction({ + type: 'action', + state: blurred2State, + lastState: transition2State, + action: { type: 'CauseDidBlurAction' }, + }); + expect(childWillBlurHandler.mock.calls.length).toBe(1); + expect(childDidBlurHandler.mock.calls.length).toBe(1); + expect(childActionHandler.mock.calls.length).toBe(3); +}); + +test('pass through focus', () => { + const parentSubscriber = jest.fn(); + const emitParentAction = payload => { + parentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'testKey' + ); + const testRoute = { + key: 'foo', + routeName: 'FooRoute', + routes: [{ key: 'key0' }, { key: 'testKey' }], + index: 1, + isTransitioning: false, + }; + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + const childWillBlurHandler = jest.fn(); + const childDidBlurHandler = jest.fn(); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + childEventSubscriber('willBlur', childWillBlurHandler); + childEventSubscriber('didBlur', childDidBlurHandler); + emitParentAction({ + type: 'willFocus', + state: testRoute, + lastState: testRoute, + action: { type: 'FooAction' }, + }); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'didFocus', + state: testRoute, + lastState: testRoute, + action: { type: 'FooAction' }, + }); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'willBlur', + state: testRoute, + lastState: testRoute, + action: { type: 'FooAction' }, + }); + expect(childWillBlurHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'didBlur', + state: testRoute, + lastState: testRoute, + action: { type: 'FooAction' }, + }); + expect(childDidBlurHandler.mock.calls.length).toBe(1); +}); + +test('child focus with transition', () => { + const parentSubscriber = jest.fn(); + const emitParentAction = payload => { + parentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'key1' + ); + const randomAction = { type: 'FooAction' }; + const testState = { + key: 'foo', + routeName: 'FooRoute', + routes: [{ key: 'key0' }, { key: 'key1' }], + index: 0, + isTransitioning: false, + }; + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + const childWillBlurHandler = jest.fn(); + const childDidBlurHandler = jest.fn(); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + childEventSubscriber('willBlur', childWillBlurHandler); + childEventSubscriber('didBlur', childDidBlurHandler); + emitParentAction({ + type: 'didFocus', + action: randomAction, + lastState: testState, + state: testState, + }); + emitParentAction({ + type: 'action', + action: randomAction, + lastState: testState, + state: { + ...testState, + index: 1, + isTransitioning: true, + }, + }); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'action', + action: randomAction, + lastState: { + ...testState, + index: 1, + isTransitioning: true, + }, + state: { + ...testState, + index: 1, + isTransitioning: false, + }, + }); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'action', + action: randomAction, + lastState: { + ...testState, + index: 1, + isTransitioning: false, + }, + state: { + ...testState, + index: 0, + isTransitioning: true, + }, + }); + expect(childWillBlurHandler.mock.calls.length).toBe(1); + emitParentAction({ + type: 'action', + action: randomAction, + lastState: { + ...testState, + index: 0, + isTransitioning: true, + }, + state: { + ...testState, + index: 0, + isTransitioning: false, + }, + }); + expect(childDidBlurHandler.mock.calls.length).toBe(1); +}); + +test('child focus with immediate transition', () => { + const parentSubscriber = jest.fn(); + const emitParentAction = payload => { + parentSubscriber.mock.calls.forEach(subs => { + if (subs[0] === payload.type) { + subs[1](payload); + } + }); + }; + const subscriptionRemove = () => {}; + parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove }); + const childEventSubscriber = getChildEventSubscriber( + parentSubscriber, + 'key1' + ); + const randomAction = { type: 'FooAction' }; + const testState = { + key: 'foo', + routeName: 'FooRoute', + routes: [{ key: 'key0' }, { key: 'key1' }], + index: 0, + isTransitioning: false, + }; + const childWillFocusHandler = jest.fn(); + const childDidFocusHandler = jest.fn(); + const childWillBlurHandler = jest.fn(); + const childDidBlurHandler = jest.fn(); + childEventSubscriber('willFocus', childWillFocusHandler); + childEventSubscriber('didFocus', childDidFocusHandler); + childEventSubscriber('willBlur', childWillBlurHandler); + childEventSubscriber('didBlur', childDidBlurHandler); + emitParentAction({ + type: 'didFocus', + action: randomAction, + lastState: testState, + state: testState, + }); + emitParentAction({ + type: 'action', + action: randomAction, + lastState: testState, + state: { + ...testState, + index: 1, + }, + }); + expect(childWillFocusHandler.mock.calls.length).toBe(1); + expect(childDidFocusHandler.mock.calls.length).toBe(1); + + emitParentAction({ + type: 'action', + action: randomAction, + lastState: { + ...testState, + index: 1, + }, + state: { + ...testState, + index: 0, + }, + }); + expect(childWillBlurHandler.mock.calls.length).toBe(1); + expect(childDidBlurHandler.mock.calls.length).toBe(1); +}); diff --git a/src/getChildEventSubscriber.js b/src/getChildEventSubscriber.js index fbac4dcd45..e4f784eb32 100644 --- a/src/getChildEventSubscriber.js +++ b/src/getChildEventSubscriber.js @@ -32,13 +32,12 @@ export default function getChildEventSubscriber(addListener, key) { const subscribers = getChildSubscribers(payload.type); subscribers && subscribers.forEach(subs => { - // $FlowFixMe - Payload should probably understand generic state type subs(payload); }); }; - let isSelfFocused = false; - + let isParentFocused = true; + let isChildFocused = false; const cleanup = () => { upstreamSubscribers.forEach(subs => subs && subs.remove()); }; @@ -54,74 +53,112 @@ export default function getChildEventSubscriber(addListener, key) { const upstreamSubscribers = upstreamEvents.map(eventName => addListener(eventName, payload => { const { state, lastState, action } = payload; - const lastFocusKey = lastState && lastState.routes[lastState.index].key; - const focusKey = state && state.routes[state.index].key; + const lastRoutes = lastState && lastState.routes; + const routes = state && state.routes; + const lastFocusKey = + lastState && lastState.routes && lastState.routes[lastState.index].key; + const focusKey = routes && 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); + lastRoutes && lastRoutes.find(route => route.key === key); + const newRoute = routes && routes.find(route => route.key === key); + const eventContext = payload.context || 'Root'; const childPayload = { + context: `${key}:${action.type}_${eventContext}`, 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; - + const wasChildFocused = isChildFocused; if (eventName !== 'action') { switch (eventName) { case 'didFocus': - isSelfFocused = true; + isParentFocused = true; break; case 'didBlur': - isSelfFocused = false; + isParentFocused = false; break; } - emit(childPayload); + if (isFocused && eventName === 'willFocus') { + emit(childPayload); + } + if (isFocused && !isTransitioning && eventName === 'didFocus') { + emit(childPayload); + isChildFocused = true; + } + if (isFocused && eventName === 'willBlur') { + emit(childPayload); + } + if (isFocused && !isTransitioning && eventName === 'didBlur') { + emit(childPayload); + } return; } // now we're exclusively handling the "action" event + if (!isParentFocused) { + return; + } - if (newRoute) { - // fire this event to pass navigation events to children subscribers + if (isChildFocused && newRoute) { + // fire this action event to pass navigation events to children subscribers emit(childPayload); } - if (isFocused && didStartTransitioning && !isSelfFocused) { + if (isFocused && didStartTransitioning && !isChildFocused) { emit({ ...childPayload, type: 'willFocus', }); } - if (isFocused && didFinishTransitioning && !isSelfFocused) { + if (isFocused && didFinishTransitioning && !isChildFocused) { + emit({ + ...childPayload, + type: 'didFocus', + }); + isChildFocused = true; + } + if (isFocused && !isChildFocused && !didStartTransitioning) { + emit({ + ...childPayload, + type: 'willFocus', + }); emit({ ...childPayload, type: 'didFocus', }); - isSelfFocused = true; + isChildFocused = true; } - if (!isFocused && didStartTransitioning && isSelfFocused) { + if (!isFocused && didStartTransitioning && isChildFocused) { emit({ ...childPayload, type: 'willBlur', }); } - if (!isFocused && didFinishTransitioning && isSelfFocused) { + if (!isFocused && didFinishTransitioning && isChildFocused) { + emit({ + ...childPayload, + type: 'didBlur', + }); + isChildFocused = false; + } + if (!isFocused && isChildFocused && !didStartTransitioning) { + emit({ + ...childPayload, + type: 'willBlur', + }); emit({ ...childPayload, type: 'didBlur', }); - isSelfFocused = false; + isChildFocused = false; } }) );