Add events support to redux example (#3368)

* Fixing redux

* Fix tab events and test event nested event subscriptions

* Add event support to redux example
This commit is contained in:
Eric Vicenti
2018-01-27 14:12:16 -08:00
committed by Brent Vatne
parent 50a3f8de93
commit dca37627a2
8 changed files with 630 additions and 105 deletions

View File

@@ -45,35 +45,69 @@ MyHomeScreen.navigationOptions = {
),
};
const MyPeopleScreen = ({ navigation }) => (
<MyNavScreen banner="People Tab" navigation={navigation} />
);
class MyPeopleScreen extends React.Component {
static navigationOptions = {
tabBarLabel: 'People',
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-people' : 'ios-people-outline'}
size={26}
style={{ color: tintColor }}
/>
),
};
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 <MyNavScreen banner="People Tab" navigation={navigation} />;
}
}
MyPeopleScreen.navigationOptions = {
tabBarLabel: 'People',
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-people' : 'ios-people-outline'}
size={26}
style={{ color: tintColor }}
/>
),
};
const MyChatScreen = ({ navigation }) => (
<MyNavScreen banner="Chat Tab" navigation={navigation} />
);
MyChatScreen.navigationOptions = {
tabBarLabel: 'Chat',
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-chatboxes' : 'ios-chatboxes-outline'}
size={26}
style={{ color: tintColor }}
/>
),
};
class MyChatScreen extends React.Component {
static navigationOptions = {
tabBarLabel: 'Chat',
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-chatboxes' : 'ios-chatboxes-outline'}
size={26}
style={{ color: tintColor }}
/>
),
};
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 <MyNavScreen banner="Chat Tab" navigation={navigation} />;
}
}
const MySettingsScreen = ({ navigation }) => (
<MyNavScreen banner="Settings Tab" navigation={navigation} />
@@ -128,10 +162,10 @@ class SimpleTabsContainer extends React.Component<SimpleTabsContainerProps> {
_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<SimpleTabsContainerProps> {
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 <SimpleTabs navigation={this.props.navigation} />;

View File

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

View File

@@ -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:../.."

View File

@@ -13,17 +13,51 @@ export const AppNavigator = StackNavigator({
Profile: { screen: ProfileScreen },
});
const AppWithNavigationState = ({ dispatch, nav }) => (
<AppNavigator navigation={addNavigationHelpers({ dispatch, state: 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 (
<AppNavigator
navigation={addNavigationHelpers({
dispatch,
state: nav,
addListener: this._addListener,
})}
/>
);
}
}
const mapStateToProps = state => ({
nav: state.nav,
lastAction: state.lastAction,
});
export default connect(mapStateToProps)(AppWithNavigationState);

View File

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

View File

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

View File

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

View File

@@ -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',
});
isSelfFocused = true;
isChildFocused = true;
}
if (!isFocused && didStartTransitioning && isSelfFocused) {
if (isFocused && !isChildFocused && !didStartTransitioning) {
emit({
...childPayload,
type: 'willFocus',
});
emit({
...childPayload,
type: 'didFocus',
});
isChildFocused = true;
}
if (!isFocused && didStartTransitioning && isChildFocused) {
emit({
...childPayload,
type: 'willBlur',
});
}
if (!isFocused && didFinishTransitioning && isSelfFocused) {
if (!isFocused && didFinishTransitioning && isChildFocused) {
emit({
...childPayload,
type: 'didBlur',
});
isSelfFocused = false;
isChildFocused = false;
}
if (!isFocused && isChildFocused && !didStartTransitioning) {
emit({
...childPayload,
type: 'willBlur',
});
emit({
...childPayload,
type: 'didBlur',
});
isChildFocused = false;
}
})
);