[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, gestureState: GestureState,
) => boolean; ) => boolean;
type InteractionState = {handle: ?number, shouldCancelClick: boolean, timeout: ?TimeoutID};
type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed; type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed;
type PanResponderConfig = $ReadOnly<{| type PanResponderConfig = $ReadOnly<{|
@@ -384,9 +386,12 @@ const PanResponder = {
* are the responder. * are the responder.
*/ */
create(config: PanResponderConfig) { create(config: PanResponderConfig) {
const interactionState = { const interactionState: InteractionState = {
handle: (null: ?number), handle: null,
shouldCancelClick: false,
timeout: null,
}; };
const gestureState: GestureState = { const gestureState: GestureState = {
// Useful for debugging // Useful for debugging
stateID: Math.random(), stateID: Math.random(),
@@ -427,15 +432,6 @@ const PanResponder = {
onMoveShouldSetResponderCapture(event: PressEvent): boolean { onMoveShouldSetResponderCapture(event: PressEvent): boolean {
const touchHistory = event.touchHistory; 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); PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
return config.onMoveShouldSetPanResponderCapture return config.onMoveShouldSetPanResponderCapture
? config.onMoveShouldSetPanResponderCapture(event, gestureState) ? config.onMoveShouldSetPanResponderCapture(event, gestureState)
@@ -446,6 +442,10 @@ const PanResponder = {
if (!interactionState.handle) { if (!interactionState.handle) {
interactionState.handle = InteractionManager.createInteractionHandle(); interactionState.handle = InteractionManager.createInteractionHandle();
} }
if (interactionState.timeout) {
clearInteractionTimeout(interactionState);
}
interactionState.shouldCancelClick = true;
gestureState.x0 = currentCentroidX(event.touchHistory); gestureState.x0 = currentCentroidX(event.touchHistory);
gestureState.y0 = currentCentroidY(event.touchHistory); gestureState.y0 = currentCentroidY(event.touchHistory);
gestureState.dx = 0; gestureState.dx = 0;
@@ -475,6 +475,7 @@ const PanResponder = {
event, event,
gestureState, gestureState,
); );
setInteractionTimeout(interactionState);
PanResponder._initializeGestureState(gestureState); PanResponder._initializeGestureState(gestureState);
}, },
@@ -488,14 +489,6 @@ const PanResponder = {
onResponderMove(event: PressEvent): void { onResponderMove(event: PressEvent): void {
const touchHistory = event.touchHistory; 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 // Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event. // already processed multi-touch geometry during the first event.
PanResponder._updateGestureStateOnMove(gestureState, touchHistory); PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
@@ -522,6 +515,7 @@ const PanResponder = {
event, event,
gestureState, gestureState,
); );
setInteractionTimeout(interactionState);
PanResponder._initializeGestureState(gestureState); PanResponder._initializeGestureState(gestureState);
}, },
@@ -530,7 +524,19 @@ const PanResponder = {
? true ? true
: config.onPanResponderTerminationRequest(event, gestureState); : 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 { return {
panHandlers, panHandlers,
getInteractionHandle(): ?number { getInteractionHandle(): ?number {
@@ -541,7 +547,7 @@ const PanResponder = {
}; };
function clearInteractionHandle( function clearInteractionHandle(
interactionState: {handle: ?number}, interactionState: InteractionState,
callback: ?(ActiveCallback | PassiveCallback), callback: ?(ActiveCallback | PassiveCallback),
event: PressEvent, event: PressEvent,
gestureState: GestureState, 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< export type PanResponderInstance = $Call<
$PropertyType<typeof PanResponder, 'create'>, $PropertyType<typeof PanResponder, 'create'>,
PanResponderConfig, PanResponderConfig,