[fix] Pan interactions should cancel 'click' events on the target

If a pan interaction has taken place, it is not expected that 'click' events
occur on the target element when the pointer is released (as was occuring with
mouse pointers). This patch cancels any 'click' that occurs within the pan
target's subtree, within 250ms of the pan gesture ending.

Fix #1788
This commit is contained in:
Nicolas Gallagher
2020-10-29 14:14:21 -07:00
parent 03897d32be
commit d2e6c29e25
@@ -188,6 +188,8 @@ type ActiveCallback = (
gestureState: GestureState,
) => boolean;
type InteractionState = {handle: ?number, shouldCancelClick: boolean, timeout: ?TimeoutID};
type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed;
type PanResponderConfig = $ReadOnly<{|
@@ -384,9 +386,12 @@ const PanResponder = {
* are the responder.
*/
create(config: PanResponderConfig) {
const interactionState = {
handle: (null: ?number),
const interactionState: InteractionState = {
handle: null,
shouldCancelClick: false,
timeout: null,
};
const gestureState: GestureState = {
// Useful for debugging
stateID: Math.random(),
@@ -427,15 +432,6 @@ const PanResponder = {
onMoveShouldSetResponderCapture(event: PressEvent): boolean {
const touchHistory = event.touchHistory;
// Responder system incorrectly dispatches should* to current responder
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
if (
gestureState._accountsForMovesUpTo ===
touchHistory.mostRecentTimeStamp
) {
return false;
}
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
return config.onMoveShouldSetPanResponderCapture
? config.onMoveShouldSetPanResponderCapture(event, gestureState)
@@ -446,6 +442,10 @@ const PanResponder = {
if (!interactionState.handle) {
interactionState.handle = InteractionManager.createInteractionHandle();
}
if (interactionState.timeout) {
clearInteractionTimeout(interactionState);
}
interactionState.shouldCancelClick = true;
gestureState.x0 = currentCentroidX(event.touchHistory);
gestureState.y0 = currentCentroidY(event.touchHistory);
gestureState.dx = 0;
@@ -475,6 +475,7 @@ const PanResponder = {
event,
gestureState,
);
setInteractionTimeout(interactionState);
PanResponder._initializeGestureState(gestureState);
},
@@ -488,14 +489,6 @@ const PanResponder = {
onResponderMove(event: PressEvent): void {
const touchHistory = event.touchHistory;
// Guard against the dispatch of two touch moves when there are two
// simultaneously changed touches.
if (
gestureState._accountsForMovesUpTo ===
touchHistory.mostRecentTimeStamp
) {
return;
}
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
@@ -522,6 +515,7 @@ const PanResponder = {
event,
gestureState,
);
setInteractionTimeout(interactionState);
PanResponder._initializeGestureState(gestureState);
},
@@ -530,7 +524,19 @@ const PanResponder = {
? true
: config.onPanResponderTerminationRequest(event, gestureState);
},
// We do not want to trigger 'click' activated gestures or native behaviors
// on any pan target that is under a mouse cursor when it is released.
// Browsers will natively cancel 'click' events on a target if a non-mouse
// active pointer moves.
onClickCapture: (event: any): void => {
if (interactionState.shouldCancelClick === true) {
event.stopPropagation();
event.preventDefault();
}
},
};
return {
panHandlers,
getInteractionHandle(): ?number {
@@ -541,7 +547,7 @@ const PanResponder = {
};
function clearInteractionHandle(
interactionState: {handle: ?number},
interactionState: InteractionState,
callback: ?(ActiveCallback | PassiveCallback),
event: PressEvent,
gestureState: GestureState,
@@ -555,6 +561,16 @@ function clearInteractionHandle(
}
}
function clearInteractionTimeout(interactionState: InteractionState) {
clearTimeout(interactionState.timeout);
}
function setInteractionTimeout(interactionState: InteractionState) {
interactionState.timeout = setTimeout(() => {
interactionState.shouldCancelClick = false;
}, 250);
}
export type PanResponderInstance = $Call<
$PropertyType<typeof PanResponder, 'create'>,
PanResponderConfig,