Skip to content

Commit edc99e7

Browse files
committed
[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
1 parent e8f2c98 commit edc99e7

File tree

5 files changed

+69
-15
lines changed

5 files changed

+69
-15
lines changed

packages/react-native-web/src/exports/createElement/index.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,6 @@ const adjustProps = domProps => {
4646
if (isEventHandler) {
4747
if (isButtonRole && isDisabled) {
4848
domProps[propName] = undefined;
49-
} else if (propName === 'onResponderRelease') {
50-
// Browsers fire mouse events after touch events. This causes the
51-
// 'onResponderRelease' handler to be called twice for Touchables.
52-
// Auto-fix this issue by calling 'preventDefault' to cancel the mouse
53-
// events.
54-
domProps[propName] = e => {
55-
if (e.cancelable && !e.isDefaultPrevented()) {
56-
e.preventDefault();
57-
}
58-
return prop(e);
59-
};
6049
} else {
6150
// TODO: move this out of the render path
6251
domProps[propName] = e => {

packages/react-native-web/src/modules/injectResponderEventPlugin/index.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,30 @@ ResponderEventPlugin.eventTypes.selectionChangeShouldSetResponder.dependencies =
3939
ResponderEventPlugin.eventTypes.scrollShouldSetResponder.dependencies = [topScroll];
4040
ResponderEventPlugin.eventTypes.startShouldSetResponder.dependencies = startDependencies;
4141

42+
let lastActiveTouchTimestamp = null;
43+
4244
const originalExtractEvents = ResponderEventPlugin.extractEvents;
4345
ResponderEventPlugin.extractEvents = (topLevelType, targetInst, nativeEvent, nativeEventTarget) => {
4446
const hasActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches > 0;
47+
const eventType = nativeEvent.type;
48+
49+
let shouldSkipMouseAfterTouch = false;
50+
if (eventType.indexOf('touch') > -1) {
51+
lastActiveTouchTimestamp = Date.now();
52+
} else if (lastActiveTouchTimestamp && eventType.indexOf('mouse') > -1) {
53+
const now = Date.now();
54+
shouldSkipMouseAfterTouch = now - lastActiveTouchTimestamp < 250;
55+
}
56+
4557
if (
4658
// Filter out mousemove and mouseup events when a touch hasn't started yet
47-
((topLevelType === topMouseMove || topLevelType === topMouseUp) && !hasActiveTouches) ||
59+
((eventType === 'mousemove' || eventType === 'mouseup') && !hasActiveTouches) ||
4860
// Filter out events from wheel/middle and right click.
49-
(nativeEvent.button === 1 || nativeEvent.button === 2)
61+
(nativeEvent.button === 1 || nativeEvent.button === 2) ||
62+
// Filter out mouse events that browsers dispatch immediately after touch events end
63+
// Prevents the REP from calling handlers twice for touch interactions.
64+
// See #802 and #932.
65+
shouldSkipMouseAfterTouch
5066
) {
5167
return;
5268
}

website/storybook/1-components/Switch/SwitchScreen.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import PropOnValueChange from './examples/PropOnValueChange';
1212
import PropThumbColor from './examples/PropThumbColor';
1313
import PropTrackColor from './examples/PropTrackColor';
1414
import PropValue from './examples/PropValue';
15+
import TouchableWrapper from './examples/TouchableWrapper';
1516
import React from 'react';
1617
import UIExplorer, {
1718
AppText,
@@ -127,12 +128,19 @@ const SwitchScreen = () => (
127128

128129
<Section title="More examples">
129130
<DocItem
130-
description="Custom sizes can be created using styles"
131+
description="Custom sizes can be created using styles."
131132
example={{
132133
code: '<Switch style={{ height: 30 }} />',
133134
render: () => <CustomSize />
134135
}}
135136
/>
137+
138+
<DocItem
139+
description="Wrapped in a Touchable."
140+
example={{
141+
render: () => <TouchableWrapper />
142+
}}
143+
/>
136144
</Section>
137145
</UIExplorer>
138146
);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable react/jsx-no-bind */
2+
/**
3+
* @flow
4+
*/
5+
6+
import React from 'react';
7+
import { Switch, TouchableHighlight, View } from 'react-native';
8+
9+
class TouchableWrapperExample extends React.PureComponent {
10+
state = {
11+
on: false
12+
};
13+
14+
render() {
15+
const { on } = this.state;
16+
17+
return (
18+
<View>
19+
<TouchableHighlight onPress={() => {}} style={style} underlayColor="#eee">
20+
<Switch onValueChange={this._handleChange} value={on} />
21+
</TouchableHighlight>
22+
</View>
23+
);
24+
}
25+
26+
_handleChange = value => {
27+
this.setState({ on: value });
28+
};
29+
}
30+
31+
const style = {
32+
alignSelf: 'flex-start',
33+
borderWidth: 1,
34+
borderColor: '#ddd',
35+
paddingHorizontal: 50,
36+
paddingVertical: 20
37+
};
38+
39+
export default TouchableWrapperExample;

website/storybook/1-components/TextInput/examples/TouchableWrapper.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export default class TouchableWrapper extends React.Component {
1919

2020
_handlePress = () => {
2121
if (this._input) {
22-
this._input.focus();
22+
setTimeout(() => {
23+
this._input.focus();
24+
}, 0);
2325
}
2426
};
2527

0 commit comments

Comments
 (0)