From b889a0629a2b4d7c0be784b923af2b33168de455 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Fri, 27 Sep 2019 16:54:07 -0700 Subject: [PATCH] [react-interactions] Tap cancels on second pointerdown This patch causes onTapCancel to be called whenever a second pointer interacts with the responder target. --- .../react-interactions/events/src/dom/Tap.js | 44 ++++++++++++------- .../src/dom/__tests__/Tap-test.internal.js | 17 +++++++ .../dom/testing-library/domEventSequences.js | 21 +++++++-- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/react-interactions/events/src/dom/Tap.js b/packages/react-interactions/events/src/dom/Tap.js index db6df9a70c932..38e11bf349eee 100644 --- a/packages/react-interactions/events/src/dom/Tap.js +++ b/packages/react-interactions/events/src/dom/Tap.js @@ -496,26 +496,29 @@ const responderImpl = { case 'pointerdown': case 'mousedown': case 'touchstart': { - if (hasPointerEvents) { - const pointerId = nativeEvent.pointerId; - state.activePointerId = pointerId; - // Make mouse and touch pointers consistent. - // Flow bug: https://github.com/facebook/flow/issues/8055 - // $FlowExpectedError - eventTarget.releasePointerCapture(pointerId); - } else { - if (eventType === 'touchstart') { - const targetTouches = nativeEvent.targetTouches; - if (targetTouches.length > 0) { - state.activePointerId = targetTouches[0].identifier; - } - } - if (eventType === 'mousedown' && state.ignoreEmulatedEvents) { - return; - } + if (eventType === 'mousedown' && state.ignoreEmulatedEvents) { + return; } if (!state.isActive) { + if (hasPointerEvents) { + const pointerId = nativeEvent.pointerId; + state.activePointerId = pointerId; + // Make mouse and touch pointers consistent. + // Flow bug: https://github.com/facebook/flow/issues/8055 + // $FlowExpectedError + eventTarget.releasePointerCapture(pointerId); + } else { + if (eventType === 'touchstart') { + const targetTouches = nativeEvent.targetTouches; + if (targetTouches.length === 1) { + state.activePointerId = targetTouches[0].identifier; + } else { + return; + } + } + } + const activate = shouldActivate(event); const activateAuxiliary = isAuxiliary(nativeEvent.buttons, event); @@ -547,6 +550,13 @@ const responderImpl = { state.initialPosition.y = gestureState.y; dispatchStart(context, props, state); } + } else if ( + !isActivePointer(event, state) || + (eventType === 'touchstart' && nativeEvent.targetTouches.length > 1) + ) { + // Cancel the gesture if a second pointer becomes active on the target. + state.isActive = false; + dispatchCancel(context, props, state); } break; } diff --git a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js index 3cfa5e55b0736..3b30443daf392 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js @@ -652,6 +652,23 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { expect(onTapUpdate).not.toBeCalled(); }); + test('second pointer down', () => { + const pointerType = 'touch'; + const target = createEventTarget(ref.current); + const buttons = buttonsType.primary; + target.pointerdown({buttons, pointerId: 1, pointerType}); + if (hasPointerEvents) { + target.pointerdown({buttons, pointerId: 2, pointerType}); + } else { + // TouchEvents + target.pointerdown([ + {pointerId: 1, pointerType}, + {pointerId: 2, pointerType}, + ]); + } + expect(onTapCancel).toHaveBeenCalledTimes(1); + }); + testWithPointerType('pointer move outside target', pointerType => { const downTarget = createEventTarget(ref.current); const upTarget = createEventTarget(container); diff --git a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js b/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js index 344d9700181cf..be5a58cfe9dbc 100644 --- a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js +++ b/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js @@ -18,6 +18,12 @@ function getPointerType(payload) { let pointerType = 'mouse'; if (payload != null && payload.pointerType != null) { pointerType = payload.pointerType; + } else if ( + Array.isArray(payload) && + payload[0] != null && + payload[0].pointerType != null + ) { + pointerType = payload[0].pointerType; } return pointerType; } @@ -77,7 +83,12 @@ export function pointercancel(target, payload) { export function pointerdown(target, defaultPayload) { const dispatch = arg => target.dispatchEvent(arg); const pointerType = getPointerType(defaultPayload); - const payload = {buttons: buttonsType.primary, ...defaultPayload}; + + // Payload could be an array for TouchEvents + const payload = + hasPointerEvent() || pointerType === 'mouse' + ? {buttons: buttonsType.primary, ...defaultPayload} + : defaultPayload; if (pointerType === 'mouse') { if (hasPointerEvent()) { @@ -156,8 +167,12 @@ export function pointermove(target, payload) { export function pointerup(target, defaultPayload = {}) { const dispatch = arg => target.dispatchEvent(arg); const pointerType = getPointerType(defaultPayload); - // eslint-disable-next-line no-unused-vars - const {buttons, ...payload} = defaultPayload; + + // Payload could be an array for TouchEvents + const payload = defaultPayload; + if (defaultPayload.buttons) { + delete payload.buttons; + } if (pointerType === 'mouse') { if (hasPointerEvent()) {