diff --git a/packages/react-native-web/src/exports/createElement/index.js b/packages/react-native-web/src/exports/createElement/index.js
index 1501fb2a9..3fa5a8f86 100644
--- a/packages/react-native-web/src/exports/createElement/index.js
+++ b/packages/react-native-web/src/exports/createElement/index.js
@@ -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 => {
diff --git a/packages/react-native-web/src/modules/injectResponderEventPlugin/index.js b/packages/react-native-web/src/modules/injectResponderEventPlugin/index.js
index 592836257..049f09dc9 100644
--- a/packages/react-native-web/src/modules/injectResponderEventPlugin/index.js
+++ b/packages/react-native-web/src/modules/injectResponderEventPlugin/index.js
@@ -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;
}
diff --git a/website/storybook/1-components/Switch/SwitchScreen.js b/website/storybook/1-components/Switch/SwitchScreen.js
index d0bd47873..588026ab9 100644
--- a/website/storybook/1-components/Switch/SwitchScreen.js
+++ b/website/storybook/1-components/Switch/SwitchScreen.js
@@ -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 = () => (
',
render: () =>
}}
/>
+
+
+ }}
+ />
);
diff --git a/website/storybook/1-components/Switch/examples/TouchableWrapper.js b/website/storybook/1-components/Switch/examples/TouchableWrapper.js
new file mode 100644
index 000000000..90a20777a
--- /dev/null
+++ b/website/storybook/1-components/Switch/examples/TouchableWrapper.js
@@ -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 (
+
+ {}} style={style} underlayColor="#eee">
+
+
+
+ );
+ }
+
+ _handleChange = value => {
+ this.setState({ on: value });
+ };
+}
+
+const style = {
+ alignSelf: 'flex-start',
+ borderWidth: 1,
+ borderColor: '#ddd',
+ paddingHorizontal: 50,
+ paddingVertical: 20
+};
+
+export default TouchableWrapperExample;
diff --git a/website/storybook/1-components/TextInput/examples/TouchableWrapper.js b/website/storybook/1-components/TextInput/examples/TouchableWrapper.js
index 3c9d69e46..d5bb70603 100644
--- a/website/storybook/1-components/TextInput/examples/TouchableWrapper.js
+++ b/website/storybook/1-components/TextInput/examples/TouchableWrapper.js
@@ -19,7 +19,9 @@ export default class TouchableWrapper extends React.Component {
_handlePress = () => {
if (this._input) {
- this._input.focus();
+ setTimeout(() => {
+ this._input.focus();
+ }, 0);
}
};