mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-27 01:34:17 +08:00
[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:
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -19,7 +19,9 @@ export default class TouchableWrapper extends React.Component {
|
||||
|
||||
_handlePress = () => {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
setTimeout(() => {
|
||||
this._input.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user