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); } };