Skip to content

Commit

Permalink
Fix and actually understand event emitter (react-navigation#3367)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericvicenti authored and sourcecode911 committed Mar 9, 2020
1 parent db8317c commit 6065317
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 122 deletions.
101 changes: 68 additions & 33 deletions src/__tests__/getChildEventSubscriber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ test('grandchildren transitions', () => {
});
expect(childWillBlurHandler.mock.calls.length).toBe(1);
expect(childDidBlurHandler.mock.calls.length).toBe(0);
expect(childActionHandler.mock.calls.length).toBe(2);
expect(childActionHandler.mock.calls.length).toBe(1);
emitGrandParentAction({
type: 'action',
state: blurred2State,
Expand All @@ -213,67 +213,102 @@ test('grandchildren transitions', () => {
});
expect(childWillBlurHandler.mock.calls.length).toBe(1);
expect(childDidBlurHandler.mock.calls.length).toBe(1);
expect(childActionHandler.mock.calls.length).toBe(3);
expect(childActionHandler.mock.calls.length).toBe(1);
});

test('pass through focus', () => {
const parentSubscriber = jest.fn();
const emitParentAction = payload => {
parentSubscriber.mock.calls.forEach(subs => {
test('grandchildren pass through transitions', () => {
const grandParentSubscriber = jest.fn();
const emitGrandParentAction = payload => {
grandParentSubscriber.mock.calls.forEach(subs => {
if (subs[0] === payload.type) {
subs[1](payload);
}
});
};
const subscriptionRemove = () => {};
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
const parentSubscriber = getChildEventSubscriber(
grandParentSubscriber,
'parent'
);
const childEventSubscriber = getChildEventSubscriber(
parentSubscriber,
'testKey'
'key1'
);
const testRoute = {
key: 'foo',
routeName: 'FooRoute',
routes: [{ key: 'key0' }, { key: 'testKey' }],
index: 1,
isTransitioning: false,
};
const makeFakeState = (childIndex, childIsTransitioning) => ({
index: childIndex,
isTransitioning: childIsTransitioning,
routes: [
{ key: 'nothing' },
{
key: 'parent',
index: 1,
isTransitioning: false,
routes: [{ key: 'key0' }, { key: 'key1' }, { key: 'key2' }],
},
].slice(0, childIndex + 1),
});
const blurredState = makeFakeState(0, false);
const transitionState = makeFakeState(1, true);
const focusState = makeFakeState(1, false);
const transition2State = makeFakeState(0, true);
const blurred2State = makeFakeState(0, 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);
emitParentAction({
type: 'willFocus',
state: testRoute,
lastState: testRoute,
emitGrandParentAction({
type: 'action',
state: transitionState,
lastState: blurredState,
action: { type: 'FooAction' },
});
expect(childActionHandler.mock.calls.length).toBe(0);
expect(childWillFocusHandler.mock.calls.length).toBe(1);
emitParentAction({
type: 'didFocus',
state: testRoute,
lastState: testRoute,
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);
emitParentAction({
type: 'willBlur',
state: testRoute,
lastState: testRoute,
action: { type: 'FooAction' },
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);
emitParentAction({
type: 'didBlur',
state: testRoute,
lastState: testRoute,
action: { type: 'FooAction' },
expect(childDidBlurHandler.mock.calls.length).toBe(0);
expect(childActionHandler.mock.calls.length).toBe(1);
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(1);
});

test('child focus with transition', () => {
Expand Down
148 changes: 59 additions & 89 deletions src/getChildEventSubscriber.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,21 @@ export default function getChildEventSubscriber(addListener, key) {
}
};

const emit = payload => {
const subscribers = getChildSubscribers(payload.type);
const emit = (type, payload) => {
const payloadWithType = { ...payload, type };
const subscribers = getChildSubscribers(type);
subscribers &&
subscribers.forEach(subs => {
subs(payload);
subs(payloadWithType);
});
};

let isParentFocused = true;
let isChildFocused = false;
// lastEmittedEvent keeps track of focus state for one route. First we assume
// we are blurred. If we are focused on initialization, the first 'action'
// event will cause onFocus+willFocus events because we had previously been
// considered blurred
let lastEmittedEvent = 'didBlur';

const cleanup = () => {
upstreamSubscribers.forEach(subs => subs && subs.remove());
};
Expand All @@ -55,110 +60,75 @@ export default function getChildEventSubscriber(addListener, key) {
const { state, lastState, action } = payload;
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 isChildFocused = focusKey === key;
const lastRoute =
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}`,
context: `${key}:${action.type}_${payload.context || 'Root'}`,
state: newRoute,
lastState: lastRoute,
action,
type: eventName,
};

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':
isParentFocused = true;
break;
case 'didBlur':
isParentFocused = false;
break;
}
if (isFocused && eventName === 'willFocus') {
emit(childPayload);
}
if (isFocused && !isTransitioning && eventName === 'didFocus') {
emit(childPayload);
isChildFocused = true;
}
if (isFocused && eventName === 'willBlur') {
emit(childPayload);

const previouslyLastEmittedEvent = lastEmittedEvent;

if (lastEmittedEvent === 'didBlur') {
// The child is currently blurred. Look for willFocus conditions
if (eventName === 'willFocus' && isChildFocused) {
emit((lastEmittedEvent = 'willFocus'), childPayload);
} else if (eventName === 'action' && isChildFocused) {
emit((lastEmittedEvent = 'willFocus'), childPayload);
}
if (isFocused && !isTransitioning && eventName === 'didBlur') {
emit(childPayload);
}
if (lastEmittedEvent === 'willFocus') {
// We are currently mid-focus. Look for didFocus conditions.
// If state.isTransitioning is false, this child event happens immediately after willFocus
if (eventName === 'didFocus' && isChildFocused && !isTransitioning) {
emit((lastEmittedEvent = 'didFocus'), childPayload);
} else if (
eventName === 'action' &&
isChildFocused &&
!isTransitioning
) {
emit((lastEmittedEvent = 'didFocus'), childPayload);
}
return;
}

// now we're exclusively handling the "action" event
if (!isParentFocused) {
return;
if (lastEmittedEvent === 'didFocus') {
// The child is currently focused. Look for blurring events
if (!isChildFocused) {
// The child is no longer focused within this navigation state
emit((lastEmittedEvent = 'willBlur'), childPayload);
} else if (eventName === 'willBlur') {
// The parent is getting a willBlur event
emit((lastEmittedEvent = 'willBlur'), childPayload);
} else if (
eventName === 'action' &&
previouslyLastEmittedEvent === 'didFocus'
) {
// While focused, pass action events to children for grandchildren focus
emit('action', childPayload);
}
}

if (isChildFocused && newRoute) {
// fire this action event to pass navigation events to children subscribers
emit(childPayload);
}
if (isFocused && didStartTransitioning && !isChildFocused) {
emit({
...childPayload,
type: 'willFocus',
});
}
if (isFocused && didFinishTransitioning && !isChildFocused) {
emit({
...childPayload,
type: 'didFocus',
});
isChildFocused = true;
}
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 && isChildFocused) {
emit({
...childPayload,
type: 'didBlur',
});
isChildFocused = false;
}
if (!isFocused && isChildFocused && !didStartTransitioning) {
emit({
...childPayload,
type: 'willBlur',
});
emit({
...childPayload,
type: 'didBlur',
});
isChildFocused = false;
if (lastEmittedEvent === 'willBlur') {
// The child is mid-blur. Wait for transition to end
if (eventName === 'action' && !isChildFocused && !isTransitioning) {
// The child is done blurring because transitioning is over, or isTransitioning
// never began and didBlur fires immediately after willBlur
emit((lastEmittedEvent = 'didBlur'), childPayload);
} else if (eventName === 'didBlur') {
// Pass through the parent didBlur event if it happens
emit((lastEmittedEvent = 'didBlur'), childPayload);
}
}
})
);
Expand Down

0 comments on commit 6065317

Please sign in to comment.