Skip to content
This repository was archived by the owner on Feb 25, 2020. It is now read-only.

Commit cdc4d91

Browse files
committed
Better handling of focus/blur events out of order
1 parent 1897b32 commit cdc4d91

File tree

4 files changed

+255
-18
lines changed

4 files changed

+255
-18
lines changed

example/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ListSection, Divider } from 'react-native-paper';
1111

1212
import SimpleStack from './src/SimpleStack';
1313
import SimpleTabs from './src/SimpleTabs';
14+
import EventsStack from './src/EventsStack';
1415

1516
// Comment/uncomment the following two lines to toggle react-native-screens
1617
// import { useScreens } from 'react-native-screens';
@@ -23,6 +24,7 @@ I18nManager.forceRTL(false);
2324
const data = [
2425
{ component: SimpleStack, title: 'Simple Stack', routeName: 'SimpleStack' },
2526
{ component: SimpleTabs, title: 'Simple Tabs', routeName: 'SimpleTabs' },
27+
{ component: EventsStack, title: 'Events', routeName: 'EventsStack' },
2628
];
2729

2830
// Cache images

example/src/EventsStack.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React from 'react';
2+
import { Button, ScrollView, View, Text } from 'react-native';
3+
import { withNavigation } from '@react-navigation/core';
4+
import { createStackNavigator } from 'react-navigation-stack';
5+
6+
const getColorOfEvent = evt => {
7+
switch (evt) {
8+
case 'willFocus':
9+
return 'purple';
10+
case 'didFocus':
11+
return 'blue';
12+
case 'willBlur':
13+
return 'brown';
14+
default:
15+
return 'black';
16+
}
17+
};
18+
class FocusTagWithNav extends React.Component {
19+
state = { mode: 'didBlur' };
20+
componentDidMount() {
21+
this.props.navigation.addListener('willFocus', () => {
22+
this.setMode('willFocus');
23+
});
24+
this.props.navigation.addListener('willBlur', () => {
25+
this.setMode('willBlur');
26+
});
27+
this.props.navigation.addListener('didFocus', () => {
28+
this.setMode('didFocus');
29+
});
30+
this.props.navigation.addListener('didBlur', () => {
31+
this.setMode('didBlur');
32+
});
33+
}
34+
setMode = mode => {
35+
if (!this._isUnmounted) {
36+
this.setState({ mode });
37+
}
38+
};
39+
componentWillUnmount() {
40+
this._isUnmounted = true;
41+
}
42+
render() {
43+
const key = this.props.navigation.state.key;
44+
return (
45+
<View
46+
style={{
47+
padding: 20,
48+
backgroundColor: getColorOfEvent(this.state.mode),
49+
}}
50+
>
51+
<Text style={{ color: 'white' }}>
52+
{key} {String(this.state.mode)}
53+
</Text>
54+
</View>
55+
);
56+
}
57+
}
58+
59+
const FocusTag = withNavigation(FocusTagWithNav);
60+
61+
class SampleScreen extends React.Component {
62+
static navigationOptions = ({ navigation }) => ({
63+
title: 'Lorem Ipsum',
64+
headerRight: navigation.getParam('nextPage') ? (
65+
<Button
66+
title="Next"
67+
onPress={() => navigation.navigate(navigation.getParam('nextPage'))}
68+
/>
69+
) : null,
70+
});
71+
72+
componentDidMount() {
73+
this.props.navigation.addListener('refocus', () => {
74+
if (this.props.navigation.isFocused()) {
75+
this.scrollView.scrollTo({ x: 0, y: 0 });
76+
}
77+
});
78+
}
79+
80+
render() {
81+
return (
82+
<ScrollView
83+
ref={view => {
84+
this.scrollView = view;
85+
}}
86+
style={{
87+
flex: 1,
88+
backgroundColor: '#fff',
89+
}}
90+
>
91+
<FocusTag />
92+
<Text
93+
onPress={() => {
94+
this.props.navigation.push('PageTwo');
95+
}}
96+
>
97+
Push
98+
</Text>
99+
<Text
100+
onPress={() => {
101+
const { push, goBack } = this.props.navigation;
102+
push('PageTwo');
103+
setTimeout(() => {
104+
goBack(null);
105+
}, 150);
106+
}}
107+
>
108+
Push and Pop Quickly
109+
</Text>
110+
<Text
111+
onPress={() => {
112+
this.props.navigation.navigate('Home');
113+
}}
114+
>
115+
Back to Examples
116+
</Text>
117+
</ScrollView>
118+
);
119+
}
120+
}
121+
122+
const SimpleStack = createStackNavigator(
123+
{
124+
PageOne: {
125+
screen: SampleScreen,
126+
},
127+
PageTwo: {
128+
screen: SampleScreen,
129+
},
130+
},
131+
{
132+
initialRouteName: 'PageOne',
133+
}
134+
);
135+
136+
export default SimpleStack;

src/__tests__/getChildEventSubscriber-test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,86 @@ test('child focus with immediate transition', () => {
458458
expect(childWillBlurHandler.mock.calls.length).toBe(1);
459459
expect(childDidBlurHandler.mock.calls.length).toBe(1);
460460
});
461+
462+
const setupEventTest = (subscriptionKey, initialLastFocusEvent) => {
463+
const parentSubscriber = jest.fn();
464+
const emitEvent = payload => {
465+
parentSubscriber.mock.calls.forEach(subs => {
466+
if (subs[0] === payload.type) {
467+
subs[1](payload);
468+
}
469+
});
470+
};
471+
const subscriptionRemove = () => {};
472+
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
473+
const evtProvider = getChildEventSubscriber(
474+
parentSubscriber,
475+
subscriptionKey,
476+
initialLastFocusEvent
477+
);
478+
const handlers = {};
479+
evtProvider.addListener('action', (handlers.action = jest.fn()));
480+
evtProvider.addListener('willFocus', (handlers.willFocus = jest.fn()));
481+
evtProvider.addListener('didFocus', (handlers.didFocus = jest.fn()));
482+
evtProvider.addListener('willBlur', (handlers.willBlur = jest.fn()));
483+
evtProvider.addListener('didBlur', (handlers.didBlur = jest.fn()));
484+
return { emitEvent, handlers, evtProvider };
485+
};
486+
487+
test('immediate back with uncompleted transition will focus first screen again', () => {
488+
const { handlers, emitEvent } = setupEventTest('key0', 'didFocus');
489+
emitEvent({
490+
type: 'action',
491+
state: {
492+
index: 1,
493+
routes: [{ key: 'key0' }, { key: 'key1' }],
494+
isTransitioning: true,
495+
},
496+
lastState: {
497+
index: 0,
498+
routes: [{ key: 'key0' }],
499+
isTransitioning: false,
500+
},
501+
action: { type: 'Any action, does not matter here' },
502+
});
503+
expect(handlers.willFocus.mock.calls.length).toBe(0);
504+
expect(handlers.didFocus.mock.calls.length).toBe(0);
505+
expect(handlers.willBlur.mock.calls.length).toBe(1);
506+
expect(handlers.didBlur.mock.calls.length).toBe(0);
507+
emitEvent({
508+
type: 'action',
509+
state: {
510+
index: 0,
511+
routes: [{ key: 'key0' }],
512+
isTransitioning: true,
513+
},
514+
lastState: {
515+
index: 1,
516+
routes: [{ key: 'key0' }, { key: 'key1' }],
517+
isTransitioning: true,
518+
},
519+
action: { type: 'Any action, does not matter here' },
520+
});
521+
expect(handlers.willFocus.mock.calls.length).toBe(1);
522+
expect(handlers.didFocus.mock.calls.length).toBe(0);
523+
expect(handlers.willBlur.mock.calls.length).toBe(1);
524+
expect(handlers.didBlur.mock.calls.length).toBe(0);
525+
emitEvent({
526+
type: 'action',
527+
state: {
528+
index: 0,
529+
routes: [{ key: 'key0' }],
530+
isTransitioning: false,
531+
},
532+
lastState: {
533+
index: 0,
534+
routes: [{ key: 'key0' }],
535+
isTransitioning: true,
536+
},
537+
action: { type: 'Any action, does not matter here' },
538+
});
539+
expect(handlers.willFocus.mock.calls.length).toBe(1);
540+
expect(handlers.didFocus.mock.calls.length).toBe(1);
541+
expect(handlers.willBlur.mock.calls.length).toBe(1);
542+
expect(handlers.didBlur.mock.calls.length).toBe(0);
543+
});

src/getChildEventSubscriber.js

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
* Based on the 'action' events that get fired for this navigation state, this utility will fire
55
* focus and blur events for this child
66
*/
7-
export default function getChildEventSubscriber(addListener, key) {
7+
export default function getChildEventSubscriber(
8+
addListener,
9+
key,
10+
initialLastFocusEvent = 'didBlur'
11+
) {
812
const actionSubscribers = new Set();
913
const willFocusSubscribers = new Set();
1014
const didFocusSubscribers = new Set();
@@ -53,11 +57,11 @@ export default function getChildEventSubscriber(addListener, key) {
5357
});
5458
};
5559

56-
// lastEmittedEvent keeps track of focus state for one route. First we assume
60+
// lastFocusEvent keeps track of focus state for one route. First we assume
5761
// we are blurred. If we are focused on initialization, the first 'action'
5862
// event will cause onFocus+willFocus events because we had previously been
5963
// considered blurred
60-
let lastEmittedEvent = 'didBlur';
64+
let lastFocusEvent = initialLastFocusEvent;
6165

6266
const upstreamEvents = [
6367
'willFocus',
@@ -96,60 +100,72 @@ export default function getChildEventSubscriber(addListener, key) {
96100
};
97101
const isTransitioning = !!state && state.isTransitioning;
98102

99-
const previouslyLastEmittedEvent = lastEmittedEvent;
103+
const previouslylastFocusEvent = lastFocusEvent;
100104

101-
if (lastEmittedEvent === 'didBlur') {
105+
if (lastFocusEvent === 'didBlur') {
102106
// The child is currently blurred. Look for willFocus conditions
103107
if (eventName === 'willFocus' && isChildFocused) {
104-
emit((lastEmittedEvent = 'willFocus'), childPayload);
108+
emit((lastFocusEvent = 'willFocus'), childPayload);
105109
} else if (eventName === 'action' && isChildFocused) {
106-
emit((lastEmittedEvent = 'willFocus'), childPayload);
110+
emit((lastFocusEvent = 'willFocus'), childPayload);
107111
}
108112
}
109-
if (lastEmittedEvent === 'willFocus') {
113+
if (lastFocusEvent === 'willFocus') {
110114
// We are currently mid-focus. Look for didFocus conditions.
111115
// If state.isTransitioning is false, this child event happens immediately after willFocus
112116
if (eventName === 'didFocus' && isChildFocused && !isTransitioning) {
113-
emit((lastEmittedEvent = 'didFocus'), childPayload);
117+
emit((lastFocusEvent = 'didFocus'), childPayload);
114118
} else if (
115119
eventName === 'action' &&
116120
isChildFocused &&
117121
!isTransitioning
118122
) {
119-
emit((lastEmittedEvent = 'didFocus'), childPayload);
123+
emit((lastFocusEvent = 'didFocus'), childPayload);
120124
}
121125
}
122126

123-
if (lastEmittedEvent === 'didFocus') {
127+
if (lastFocusEvent === 'didFocus') {
124128
// The child is currently focused. Look for blurring events
125129
if (!isChildFocused) {
126130
// The child is no longer focused within this navigation state
127-
emit((lastEmittedEvent = 'willBlur'), childPayload);
131+
emit((lastFocusEvent = 'willBlur'), childPayload);
128132
} else if (eventName === 'willBlur') {
129133
// The parent is getting a willBlur event
130-
emit((lastEmittedEvent = 'willBlur'), childPayload);
134+
emit((lastFocusEvent = 'willBlur'), childPayload);
131135
} else if (
132136
eventName === 'action' &&
133-
previouslyLastEmittedEvent === 'didFocus'
137+
previouslylastFocusEvent === 'didFocus'
134138
) {
135139
// While focused, pass action events to children for grandchildren focus
136140
emit('action', childPayload);
137141
}
138142
}
139143

140-
if (lastEmittedEvent === 'willBlur') {
144+
if (lastFocusEvent === 'willBlur') {
141145
// The child is mid-blur. Wait for transition to end
142146
if (eventName === 'action' && !isChildFocused && !isTransitioning) {
143147
// The child is done blurring because transitioning is over, or isTransitioning
144148
// never began and didBlur fires immediately after willBlur
145-
emit((lastEmittedEvent = 'didBlur'), childPayload);
149+
emit((lastFocusEvent = 'didBlur'), childPayload);
146150
} else if (eventName === 'didBlur') {
147151
// Pass through the parent didBlur event if it happens
148-
emit((lastEmittedEvent = 'didBlur'), childPayload);
152+
emit((lastFocusEvent = 'didBlur'), childPayload);
153+
} else if (
154+
eventName === 'action' &&
155+
isChildFocused &&
156+
!isTransitioning
157+
) {
158+
emit((lastFocusEvent = 'didFocus'), childPayload);
159+
} else if (
160+
eventName === 'action' &&
161+
isChildFocused &&
162+
isTransitioning
163+
) {
164+
emit((lastFocusEvent = 'willFocus'), childPayload);
149165
}
150166
}
151167

152-
if (lastEmittedEvent === 'didBlur' && !newRoute) {
168+
if (lastFocusEvent === 'didBlur' && !newRoute) {
153169
removeAll();
154170
}
155171
})

0 commit comments

Comments
 (0)