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

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