Skip to content

Commit

Permalink
[change] ResponderEventPlugin filters browser emulated mouse events
Browse files Browse the repository at this point in the history
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
  • Loading branch information
necolas committed May 18, 2018
1 parent e8f2c98 commit edc99e7
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 15 deletions.
11 changes: 0 additions & 11 deletions packages/react-native-web/src/exports/createElement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 9 additions & 1 deletion website/storybook/1-components/Switch/SwitchScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
);
Expand Down
39 changes: 39 additions & 0 deletions website/storybook/1-components/Switch/examples/TouchableWrapper.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export default class TouchableWrapper extends React.Component {

_handlePress = () => {
if (this._input) {
this._input.focus();
setTimeout(() => {
this._input.focus();
}, 0);
}
};

Expand Down

0 comments on commit edc99e7

Please sign in to comment.