[change] ResponderEventPlugin filters browser emulated mouse events

Browsers dispatch mouse events after touch events:
https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent

There have been several attempts to avoid this behaviour affecting the
ResponderEvent system. The previous approach of cancelling the event in
the `onResponderRelease` event handler can end up cancelling other
events that are expected, e.g., `focus`.

Instead, this patch changes the `ResponderEventPlugin.extractEvents`
function to filter the mouse events that occur a short time after a
touch event. (It's assumed that people will not be clicking a mouse
within a few hundred ms of performing a touch.) This allows the
ResponderEvent system to function as expected and leaves other callbacks
to fire as they would be expected to in React DOM, i.e., both
`onTouchStart` and `onMouseDown` will be called following a touch start.

Fix #835
Fix #888
Fix #932
Close #938
Ref #802
This commit is contained in:
Nicolas Gallagher
2018-05-08 11:57:26 -07:00
parent e8f2c98786
commit edc99e79eb
5 changed files with 69 additions and 15 deletions

View File

@@ -46,17 +46,6 @@ const adjustProps = domProps => {
if (isEventHandler) {
if (isButtonRole && isDisabled) {
domProps[propName] = undefined;
} else if (propName === 'onResponderRelease') {
// Browsers fire mouse events after touch events. This causes the
// 'onResponderRelease' handler to be called twice for Touchables.
// Auto-fix this issue by calling 'preventDefault' to cancel the mouse
// events.
domProps[propName] = e => {
if (e.cancelable && !e.isDefaultPrevented()) {
e.preventDefault();
}
return prop(e);
};
} else {
// TODO: move this out of the render path
domProps[propName] = e => {

View File

@@ -39,14 +39,30 @@ ResponderEventPlugin.eventTypes.selectionChangeShouldSetResponder.dependencies =
ResponderEventPlugin.eventTypes.scrollShouldSetResponder.dependencies = [topScroll];
ResponderEventPlugin.eventTypes.startShouldSetResponder.dependencies = startDependencies;
let lastActiveTouchTimestamp = null;
const originalExtractEvents = ResponderEventPlugin.extractEvents;
ResponderEventPlugin.extractEvents = (topLevelType, targetInst, nativeEvent, nativeEventTarget) => {
const hasActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches > 0;
const eventType = nativeEvent.type;
let shouldSkipMouseAfterTouch = false;
if (eventType.indexOf('touch') > -1) {
lastActiveTouchTimestamp = Date.now();
} else if (lastActiveTouchTimestamp && eventType.indexOf('mouse') > -1) {
const now = Date.now();
shouldSkipMouseAfterTouch = now - lastActiveTouchTimestamp < 250;
}
if (
// Filter out mousemove and mouseup events when a touch hasn't started yet
((topLevelType === topMouseMove || topLevelType === topMouseUp) && !hasActiveTouches) ||
((eventType === 'mousemove' || eventType === 'mouseup') && !hasActiveTouches) ||
// Filter out events from wheel/middle and right click.
(nativeEvent.button === 1 || nativeEvent.button === 2)
(nativeEvent.button === 1 || nativeEvent.button === 2) ||
// Filter out mouse events that browsers dispatch immediately after touch events end
// Prevents the REP from calling handlers twice for touch interactions.
// See #802 and #932.
shouldSkipMouseAfterTouch
) {
return;
}

View File

@@ -12,6 +12,7 @@ import PropOnValueChange from './examples/PropOnValueChange';
import PropThumbColor from './examples/PropThumbColor';
import PropTrackColor from './examples/PropTrackColor';
import PropValue from './examples/PropValue';
import TouchableWrapper from './examples/TouchableWrapper';
import React from 'react';
import UIExplorer, {
AppText,
@@ -127,12 +128,19 @@ const SwitchScreen = () => (
<Section title="More examples">
<DocItem
description="Custom sizes can be created using styles"
description="Custom sizes can be created using styles."
example={{
code: '<Switch style={{ height: 30 }} />',
render: () => <CustomSize />
}}
/>
<DocItem
description="Wrapped in a Touchable."
example={{
render: () => <TouchableWrapper />
}}
/>
</Section>
</UIExplorer>
);

View File

@@ -0,0 +1,39 @@
/* eslint-disable react/jsx-no-bind */
/**
* @flow
*/
import React from 'react';
import { Switch, TouchableHighlight, View } from 'react-native';
class TouchableWrapperExample extends React.PureComponent {
state = {
on: false
};
render() {
const { on } = this.state;
return (
<View>
<TouchableHighlight onPress={() => {}} style={style} underlayColor="#eee">
<Switch onValueChange={this._handleChange} value={on} />
</TouchableHighlight>
</View>
);
}
_handleChange = value => {
this.setState({ on: value });
};
}
const style = {
alignSelf: 'flex-start',
borderWidth: 1,
borderColor: '#ddd',
paddingHorizontal: 50,
paddingVertical: 20
};
export default TouchableWrapperExample;

View File

@@ -19,7 +19,9 @@ export default class TouchableWrapper extends React.Component {
_handlePress = () => {
if (this._input) {
this._input.focus();
setTimeout(() => {
this._input.focus();
}, 0);
}
};