mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-26 09:14:22 +08:00
Better handling of focus/blur events out of order
This commit is contained in:
@@ -11,6 +11,7 @@ import { ListSection, Divider } from 'react-native-paper';
|
||||
|
||||
import SimpleStack from './src/SimpleStack';
|
||||
import SimpleTabs from './src/SimpleTabs';
|
||||
import EventsStack from './src/EventsStack';
|
||||
|
||||
// Comment/uncomment the following two lines to toggle react-native-screens
|
||||
// import { useScreens } from 'react-native-screens';
|
||||
@@ -23,6 +24,7 @@ I18nManager.forceRTL(false);
|
||||
const data = [
|
||||
{ component: SimpleStack, title: 'Simple Stack', routeName: 'SimpleStack' },
|
||||
{ component: SimpleTabs, title: 'Simple Tabs', routeName: 'SimpleTabs' },
|
||||
{ component: EventsStack, title: 'Events', routeName: 'EventsStack' },
|
||||
];
|
||||
|
||||
// Cache images
|
||||
|
||||
136
packages/core/example/src/EventsStack.js
Normal file
136
packages/core/example/src/EventsStack.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { Button, ScrollView, View, Text } from 'react-native';
|
||||
import { withNavigation } from '@react-navigation/core';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
const getColorOfEvent = evt => {
|
||||
switch (evt) {
|
||||
case 'willFocus':
|
||||
return 'purple';
|
||||
case 'didFocus':
|
||||
return 'blue';
|
||||
case 'willBlur':
|
||||
return 'brown';
|
||||
default:
|
||||
return 'black';
|
||||
}
|
||||
};
|
||||
class FocusTagWithNav extends React.Component {
|
||||
state = { mode: 'didBlur' };
|
||||
componentDidMount() {
|
||||
this.props.navigation.addListener('willFocus', () => {
|
||||
this.setMode('willFocus');
|
||||
});
|
||||
this.props.navigation.addListener('willBlur', () => {
|
||||
this.setMode('willBlur');
|
||||
});
|
||||
this.props.navigation.addListener('didFocus', () => {
|
||||
this.setMode('didFocus');
|
||||
});
|
||||
this.props.navigation.addListener('didBlur', () => {
|
||||
this.setMode('didBlur');
|
||||
});
|
||||
}
|
||||
setMode = mode => {
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({ mode });
|
||||
}
|
||||
};
|
||||
componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
render() {
|
||||
const key = this.props.navigation.state.key;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
padding: 20,
|
||||
backgroundColor: getColorOfEvent(this.state.mode),
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: 'white' }}>
|
||||
{key} {String(this.state.mode)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FocusTag = withNavigation(FocusTagWithNav);
|
||||
|
||||
class SampleScreen extends React.Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: 'Lorem Ipsum',
|
||||
headerRight: navigation.getParam('nextPage') ? (
|
||||
<Button
|
||||
title="Next"
|
||||
onPress={() => navigation.navigate(navigation.getParam('nextPage'))}
|
||||
/>
|
||||
) : null,
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.addListener('refocus', () => {
|
||||
if (this.props.navigation.isFocused()) {
|
||||
this.scrollView.scrollTo({ x: 0, y: 0 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScrollView
|
||||
ref={view => {
|
||||
this.scrollView = view;
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<FocusTag />
|
||||
<Text
|
||||
onPress={() => {
|
||||
this.props.navigation.push('PageTwo');
|
||||
}}
|
||||
>
|
||||
Push
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => {
|
||||
const { push, goBack } = this.props.navigation;
|
||||
push('PageTwo');
|
||||
setTimeout(() => {
|
||||
goBack(null);
|
||||
}, 150);
|
||||
}}
|
||||
>
|
||||
Push and Pop Quickly
|
||||
</Text>
|
||||
<Text
|
||||
onPress={() => {
|
||||
this.props.navigation.navigate('Home');
|
||||
}}
|
||||
>
|
||||
Back to Examples
|
||||
</Text>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SimpleStack = createStackNavigator(
|
||||
{
|
||||
PageOne: {
|
||||
screen: SampleScreen,
|
||||
},
|
||||
PageTwo: {
|
||||
screen: SampleScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'PageOne',
|
||||
}
|
||||
);
|
||||
|
||||
export default SimpleStack;
|
||||
@@ -458,3 +458,86 @@ test('child focus with immediate transition', () => {
|
||||
expect(childWillBlurHandler.mock.calls.length).toBe(1);
|
||||
expect(childDidBlurHandler.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
const setupEventTest = (subscriptionKey, initialLastFocusEvent) => {
|
||||
const parentSubscriber = jest.fn();
|
||||
const emitEvent = payload => {
|
||||
parentSubscriber.mock.calls.forEach(subs => {
|
||||
if (subs[0] === payload.type) {
|
||||
subs[1](payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
const subscriptionRemove = () => {};
|
||||
parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
|
||||
const evtProvider = getChildEventSubscriber(
|
||||
parentSubscriber,
|
||||
subscriptionKey,
|
||||
initialLastFocusEvent
|
||||
);
|
||||
const handlers = {};
|
||||
evtProvider.addListener('action', (handlers.action = jest.fn()));
|
||||
evtProvider.addListener('willFocus', (handlers.willFocus = jest.fn()));
|
||||
evtProvider.addListener('didFocus', (handlers.didFocus = jest.fn()));
|
||||
evtProvider.addListener('willBlur', (handlers.willBlur = jest.fn()));
|
||||
evtProvider.addListener('didBlur', (handlers.didBlur = jest.fn()));
|
||||
return { emitEvent, handlers, evtProvider };
|
||||
};
|
||||
|
||||
test('immediate back with uncompleted transition will focus first screen again', () => {
|
||||
const { handlers, emitEvent } = setupEventTest('key0', 'didFocus');
|
||||
emitEvent({
|
||||
type: 'action',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [{ key: 'key0' }, { key: 'key1' }],
|
||||
isTransitioning: true,
|
||||
},
|
||||
lastState: {
|
||||
index: 0,
|
||||
routes: [{ key: 'key0' }],
|
||||
isTransitioning: false,
|
||||
},
|
||||
action: { type: 'Any action, does not matter here' },
|
||||
});
|
||||
expect(handlers.willFocus.mock.calls.length).toBe(0);
|
||||
expect(handlers.didFocus.mock.calls.length).toBe(0);
|
||||
expect(handlers.willBlur.mock.calls.length).toBe(1);
|
||||
expect(handlers.didBlur.mock.calls.length).toBe(0);
|
||||
emitEvent({
|
||||
type: 'action',
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [{ key: 'key0' }],
|
||||
isTransitioning: true,
|
||||
},
|
||||
lastState: {
|
||||
index: 1,
|
||||
routes: [{ key: 'key0' }, { key: 'key1' }],
|
||||
isTransitioning: true,
|
||||
},
|
||||
action: { type: 'Any action, does not matter here' },
|
||||
});
|
||||
expect(handlers.willFocus.mock.calls.length).toBe(1);
|
||||
expect(handlers.didFocus.mock.calls.length).toBe(0);
|
||||
expect(handlers.willBlur.mock.calls.length).toBe(1);
|
||||
expect(handlers.didBlur.mock.calls.length).toBe(0);
|
||||
emitEvent({
|
||||
type: 'action',
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [{ key: 'key0' }],
|
||||
isTransitioning: false,
|
||||
},
|
||||
lastState: {
|
||||
index: 0,
|
||||
routes: [{ key: 'key0' }],
|
||||
isTransitioning: true,
|
||||
},
|
||||
action: { type: 'Any action, does not matter here' },
|
||||
});
|
||||
expect(handlers.willFocus.mock.calls.length).toBe(1);
|
||||
expect(handlers.didFocus.mock.calls.length).toBe(1);
|
||||
expect(handlers.willBlur.mock.calls.length).toBe(1);
|
||||
expect(handlers.didBlur.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* Based on the 'action' events that get fired for this navigation state, this utility will fire
|
||||
* focus and blur events for this child
|
||||
*/
|
||||
export default function getChildEventSubscriber(addListener, key) {
|
||||
export default function getChildEventSubscriber(
|
||||
addListener,
|
||||
key,
|
||||
initialLastFocusEvent = 'didBlur'
|
||||
) {
|
||||
const actionSubscribers = new Set();
|
||||
const willFocusSubscribers = new Set();
|
||||
const didFocusSubscribers = new Set();
|
||||
@@ -53,11 +57,11 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
});
|
||||
};
|
||||
|
||||
// lastEmittedEvent keeps track of focus state for one route. First we assume
|
||||
// lastFocusEvent 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';
|
||||
let lastFocusEvent = initialLastFocusEvent;
|
||||
|
||||
const upstreamEvents = [
|
||||
'willFocus',
|
||||
@@ -96,60 +100,72 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
};
|
||||
const isTransitioning = !!state && state.isTransitioning;
|
||||
|
||||
const previouslyLastEmittedEvent = lastEmittedEvent;
|
||||
const previouslylastFocusEvent = lastFocusEvent;
|
||||
|
||||
if (lastEmittedEvent === 'didBlur') {
|
||||
if (lastFocusEvent === 'didBlur') {
|
||||
// The child is currently blurred. Look for willFocus conditions
|
||||
if (eventName === 'willFocus' && isChildFocused) {
|
||||
emit((lastEmittedEvent = 'willFocus'), childPayload);
|
||||
emit((lastFocusEvent = 'willFocus'), childPayload);
|
||||
} else if (eventName === 'action' && isChildFocused) {
|
||||
emit((lastEmittedEvent = 'willFocus'), childPayload);
|
||||
emit((lastFocusEvent = 'willFocus'), childPayload);
|
||||
}
|
||||
}
|
||||
if (lastEmittedEvent === 'willFocus') {
|
||||
if (lastFocusEvent === '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);
|
||||
emit((lastFocusEvent = 'didFocus'), childPayload);
|
||||
} else if (
|
||||
eventName === 'action' &&
|
||||
isChildFocused &&
|
||||
!isTransitioning
|
||||
) {
|
||||
emit((lastEmittedEvent = 'didFocus'), childPayload);
|
||||
emit((lastFocusEvent = 'didFocus'), childPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEmittedEvent === 'didFocus') {
|
||||
if (lastFocusEvent === '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);
|
||||
emit((lastFocusEvent = 'willBlur'), childPayload);
|
||||
} else if (eventName === 'willBlur') {
|
||||
// The parent is getting a willBlur event
|
||||
emit((lastEmittedEvent = 'willBlur'), childPayload);
|
||||
emit((lastFocusEvent = 'willBlur'), childPayload);
|
||||
} else if (
|
||||
eventName === 'action' &&
|
||||
previouslyLastEmittedEvent === 'didFocus'
|
||||
previouslylastFocusEvent === 'didFocus'
|
||||
) {
|
||||
// While focused, pass action events to children for grandchildren focus
|
||||
emit('action', childPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEmittedEvent === 'willBlur') {
|
||||
if (lastFocusEvent === '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);
|
||||
emit((lastFocusEvent = 'didBlur'), childPayload);
|
||||
} else if (eventName === 'didBlur') {
|
||||
// Pass through the parent didBlur event if it happens
|
||||
emit((lastEmittedEvent = 'didBlur'), childPayload);
|
||||
emit((lastFocusEvent = 'didBlur'), childPayload);
|
||||
} else if (
|
||||
eventName === 'action' &&
|
||||
isChildFocused &&
|
||||
!isTransitioning
|
||||
) {
|
||||
emit((lastFocusEvent = 'didFocus'), childPayload);
|
||||
} else if (
|
||||
eventName === 'action' &&
|
||||
isChildFocused &&
|
||||
isTransitioning
|
||||
) {
|
||||
emit((lastFocusEvent = 'willFocus'), childPayload);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEmittedEvent === 'didBlur' && !newRoute) {
|
||||
if (lastFocusEvent === 'didBlur' && !newRoute) {
|
||||
removeAll();
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user