From f6efb224b091a741fad1bc7916b5936ff91f85fe Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 1 Oct 2019 15:13:31 -0700 Subject: [PATCH] [react-interactions] Tap cancels on second pointerdown (#16936) This patch causes onTapCancel to be called whenever a second pointer interacts with the responder target. --- .../react-interactions/events/src/dom/Tap.js | 46 ++++++---- .../src/dom/__tests__/Tap-test.internal.js | 84 ++++++++++++++++--- .../dom/testing-library/domEventSequences.js | 65 +++++++------- .../src/dom/testing-library/domEvents.js | 7 +- 4 files changed, 143 insertions(+), 59 deletions(-) diff --git a/packages/react-interactions/events/src/dom/Tap.js b/packages/react-interactions/events/src/dom/Tap.js index db6df9a70c93..0e175e5db957 100644 --- a/packages/react-interactions/events/src/dom/Tap.js +++ b/packages/react-interactions/events/src/dom/Tap.js @@ -344,7 +344,7 @@ function isActivePointer( const touch = getTouchById(nativeEvent, activePointerId); return touch != null; } else { - // accept all events that don't have ids + // accept all events that don't have pointer ids return true; } } @@ -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 3cfa5e55b073..9c7d82891594 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 @@ -212,7 +212,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { const buttons = buttonsType.auxiliary; const target = createEventTarget(ref.current); target.pointerdown({buttons, pointerType}); - target.pointerup({pointerType}); + target.pointerup({buttons, pointerType}); expect(onAuxiliaryTap).toHaveBeenCalledTimes(1); }); @@ -221,7 +221,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { const buttons = buttonsType.primary; const target = createEventTarget(ref.current); target.pointerdown({buttons, pointerType}); - target.pointerup({metaKey: true, pointerType}); + target.pointerup({buttons, metaKey: true, pointerType}); expect(onAuxiliaryTap).toHaveBeenCalledTimes(1); }); }); @@ -284,7 +284,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { ); }); - test('second pointer down', () => { + test('second pointer on target', () => { const pointerType = 'touch'; const target = createEventTarget(ref.current); const buttons = buttonsType.primary; @@ -294,10 +294,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { target.pointerdown({buttons, pointerId: 2, pointerType}); } else { // TouchEvents - target.pointerdown([ - {pointerId: 1, pointerType}, - {pointerId: 2, pointerType}, - ]); + target.pointerdown([{pointerId: 1}, {pointerId: 2}]); } expect(onTapStart).toHaveBeenCalledTimes(1); }); @@ -349,8 +346,10 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { testWithPointerType('pointer up', pointerType => { const target = createEventTarget(ref.current); - target.pointerdown({buttons: buttonsType.primary, pointerType}); + const buttons = buttonsType.primary; + target.pointerdown({buttons, pointerType}); target.pointerup({ + buttons, pageX: 10, pageY: 10, pointerType, @@ -420,18 +419,40 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { expect(onTapEnd).not.toBeCalled(); }); + if (hasPointerEvents) { + test('second pointer up off target', () => { + const pointerType = 'touch'; + const target = createEventTarget(ref.current); + const offTarget = createEventTarget(container); + const buttons = buttonsType.primary; + + target.pointerdown({buttons, pointerId: 1, pointerType}); + offTarget.pointerdown({buttons, pointerId: 2, pointerType}); + offTarget.pointerup({ + buttons, + pageX: 10, + pageY: 10, + pointerId: 2, + pointerType, + x: 10, + y: 10, + }); + expect(onTapEnd).toHaveBeenCalledTimes(0); + }); + } + test('ignored buttons and modifiers', () => { const target = createEventTarget(ref.current); const primary = buttonsType.primary; // right-click target.pointerdown({buttons: buttonsType.secondary}); - target.pointerup(); + target.pointerup({buttons: buttonsType.secondary}); // middle-click target.pointerdown({buttons: buttonsType.auxiliary}); - target.pointerup(); + target.pointerup({buttons: buttonsType.auxiliary}); // pen eraser target.pointerdown({buttons: buttonsType.eraser}); - target.pointerup(); + target.pointerup({buttons: buttonsType.eraser}); // alt-click target.pointerdown({buttons: primary}); target.pointerup({altKey: true}); @@ -533,6 +554,21 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { // No extra 'onTapUpdate' calls when the pointer is outside the target expect(onTapUpdate).toHaveBeenCalledTimes(1); }); + + if (hasPointerEvents) { + test('second pointer off target', () => { + const pointerType = 'touch'; + const target = createEventTarget(ref.current); + const offTarget = createEventTarget(container); + const buttons = buttonsType.primary; + target.pointerdown({buttons, pointerId: 1, pointerType}); + offTarget.pointerdown({buttons, pointerId: 2, pointerType}); + target.pointermove({pointerId: 1, pointerType, x: 10, y: 10}); + expect(onTapUpdate).toHaveBeenCalledTimes(1); + offTarget.pointermove({pointerId: 2, pointerType, x: 10, y: 10}); + expect(onTapUpdate).toHaveBeenCalledTimes(1); + }); + } }); describe('onTapChange', () => { @@ -652,6 +688,32 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => { expect(onTapUpdate).not.toBeCalled(); }); + test('second pointer on target', () => { + 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}, {pointerId: 2}]); + } + expect(onTapCancel).toHaveBeenCalledTimes(1); + }); + + if (hasPointerEvents) { + test('second pointer off target', () => { + const pointerType = 'touch'; + const target = createEventTarget(ref.current); + const offTarget = createEventTarget(container); + const buttons = buttonsType.primary; + target.pointerdown({buttons, pointerId: 1, pointerType}); + offTarget.pointerdown({buttons, pointerId: 2, pointerType}); + expect(onTapCancel).toHaveBeenCalledTimes(0); + }); + } + 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 344d9700181c..9041a2d2cdc7 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,8 @@ function getPointerType(payload) { let pointerType = 'mouse'; if (payload != null && payload.pointerType != null) { pointerType = payload.pointerType; + } else if (Array.isArray(payload)) { + pointerType = 'touch'; } return pointerType; } @@ -77,31 +79,36 @@ 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}; - if (pointerType === 'mouse') { - if (hasPointerEvent()) { - dispatch(domEvents.pointerover(payload)); - dispatch(domEvents.pointerenter(payload)); - } - dispatch(domEvents.mouseover(payload)); - dispatch(domEvents.mouseenter(payload)); - if (hasPointerEvent()) { - dispatch(domEvents.pointerdown(payload)); - } - dispatch(domEvents.mousedown(payload)); - if (document.activeElement !== target) { - dispatch(domEvents.focus()); - } + if (Array.isArray(defaultPayload)) { + // Arrays are for multi-touch only + dispatch(domEvents.touchstart(defaultPayload)); } else { - if (hasPointerEvent()) { - dispatch(domEvents.pointerover(payload)); - dispatch(domEvents.pointerenter(payload)); - dispatch(domEvents.pointerdown(payload)); - } - dispatch(domEvents.touchstart(payload)); - if (hasPointerEvent()) { - dispatch(domEvents.gotpointercapture(payload)); + const payload = {buttons: buttonsType.primary, ...defaultPayload}; + if (pointerType === 'mouse') { + if (hasPointerEvent()) { + dispatch(domEvents.pointerover(payload)); + dispatch(domEvents.pointerenter(payload)); + } + dispatch(domEvents.mouseover(payload)); + dispatch(domEvents.mouseenter(payload)); + if (hasPointerEvent()) { + dispatch(domEvents.pointerdown(payload)); + } + dispatch(domEvents.mousedown(payload)); + if (document.activeElement !== target) { + dispatch(domEvents.focus()); + } + } else { + if (hasPointerEvent()) { + dispatch(domEvents.pointerover(payload)); + dispatch(domEvents.pointerenter(payload)); + dispatch(domEvents.pointerdown(payload)); + } + dispatch(domEvents.touchstart(payload)); + if (hasPointerEvent()) { + dispatch(domEvents.gotpointercapture(payload)); + } } } } @@ -153,13 +160,14 @@ export function pointermove(target, payload) { } } -export function pointerup(target, defaultPayload = {}) { +export function pointerup(target, payload) { const dispatch = arg => target.dispatchEvent(arg); - const pointerType = getPointerType(defaultPayload); - // eslint-disable-next-line no-unused-vars - const {buttons, ...payload} = defaultPayload; + const pointerType = getPointerType(payload); - if (pointerType === 'mouse') { + if (Array.isArray(payload)) { + // Arrays are for multi-touch only + dispatch(domEvents.touchend(payload)); + } else if (pointerType === 'mouse') { if (hasPointerEvent()) { dispatch(domEvents.pointerup(payload)); } @@ -175,7 +183,6 @@ export function pointerup(target, defaultPayload = {}) { dispatch(domEvents.touchend(payload)); dispatch(domEvents.mouseover(payload)); dispatch(domEvents.mousemove(payload)); - // NOTE: the value of 'buttons' for 'mousedown' must not be 0 dispatch(domEvents.mousedown(payload)); if (document.activeElement !== target) { dispatch(domEvents.focus()); diff --git a/packages/react-interactions/events/src/dom/testing-library/domEvents.js b/packages/react-interactions/events/src/dom/testing-library/domEvents.js index 85bc9600e37e..92391b270ddc 100644 --- a/packages/react-interactions/events/src/dom/testing-library/domEvents.js +++ b/packages/react-interactions/events/src/dom/testing-library/domEvents.js @@ -422,9 +422,14 @@ export function pointerup(payload) { */ export function mousedown(payload) { + // The value of 'buttons' for 'mousedown' must not be 0 + const buttons = + payload == null || payload.buttons === 0 + ? buttonsType.primary + : payload.buttons; return createMouseEvent('mousedown', { - buttons: buttonsType.primary, ...payload, + buttons, }); }