From d2e6c29e259856b81d0466217754c1107a44f014 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 29 Oct 2020 14:14:21 -0700 Subject: [PATCH] [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 --- .../vendor/react-native/PanResponder/index.js | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/react-native-web/src/vendor/react-native/PanResponder/index.js b/packages/react-native-web/src/vendor/react-native/PanResponder/index.js index faf59964..fd76a7d9 100644 --- a/packages/react-native-web/src/vendor/react-native/PanResponder/index.js +++ b/packages/react-native-web/src/vendor/react-native/PanResponder/index.js @@ -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, PanResponderConfig,