@samitouri / QOS-React-2 / commits / f6efb224b0

[react-interactions] Tap cancels on second pointerdown (#16936)

This patch causes onTapCancel to be called whenever a second pointer interacts with the responder target.

Nicolas Gallagher committed Oct 1, 2019 at 15:13 UTC f6efb224b091a741fad1bc7916b5936ff91f85fe
4 files changed +143 -59
packages/react-interactions/events/src/dom/Tap.js
+28 -18
@@ -344,7 +344,7 @@ function isActivePointer(
344 const touch = getTouchById(nativeEvent, activePointerId);
345 return touch != null;
346 } else {
347 - // accept all events that don't have ids
347 + // accept all events that don't have pointer ids
348 return true;
349 }
350 }
@@ -496,26 +496,29 @@ const responderImpl = {
496 case 'pointerdown':
497 case 'mousedown':
498 case 'touchstart': {
499 - if (hasPointerEvents) {
500 - const pointerId = nativeEvent.pointerId;
501 - state.activePointerId = pointerId;
502 - // Make mouse and touch pointers consistent.
503 - // Flow bug: https://github.com/facebook/flow/issues/8055
504 - // $FlowExpectedError
505 - eventTarget.releasePointerCapture(pointerId);
506 - } else {
507 - if (eventType === 'touchstart') {
508 - const targetTouches = nativeEvent.targetTouches;
509 - if (targetTouches.length > 0) {
510 - state.activePointerId = targetTouches[0].identifier;
511 - }
512 - }
513 - if (eventType === 'mousedown' && state.ignoreEmulatedEvents) {
514 - return;
515 - }
499 + if (eventType === 'mousedown' && state.ignoreEmulatedEvents) {
500 + return;
501 }
502
503 if (!state.isActive) {
504 + if (hasPointerEvents) {
505 + const pointerId = nativeEvent.pointerId;
506 + state.activePointerId = pointerId;
507 + // Make mouse and touch pointers consistent.
508 + // Flow bug: https://github.com/facebook/flow/issues/8055
509 + // $FlowExpectedError
510 + eventTarget.releasePointerCapture(pointerId);
511 + } else {
512 + if (eventType === 'touchstart') {
513 + const targetTouches = nativeEvent.targetTouches;
514 + if (targetTouches.length === 1) {
515 + state.activePointerId = targetTouches[0].identifier;
516 + } else {
517 + return;
518 + }
519 + }
520 + }
521 +
522 const activate = shouldActivate(event);
523 const activateAuxiliary = isAuxiliary(nativeEvent.buttons, event);
524
@@ -547,6 +550,13 @@ const responderImpl = {
550 state.initialPosition.y = gestureState.y;
551 dispatchStart(context, props, state);
552 }
553 + } else if (
554 + !isActivePointer(event, state) ||
555 + (eventType === 'touchstart' && nativeEvent.targetTouches.length > 1)
556 + ) {
557 + // Cancel the gesture if a second pointer becomes active on the target.
558 + state.isActive = false;
559 + dispatchCancel(context, props, state);
560 }
561 break;
562 }
packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
+73 -11
@@ -212,7 +212,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
212 const buttons = buttonsType.auxiliary;
213 const target = createEventTarget(ref.current);
214 target.pointerdown({buttons, pointerType});
215 - target.pointerup({pointerType});
215 + target.pointerup({buttons, pointerType});
216 expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
217 });
218
@@ -221,7 +221,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
221 const buttons = buttonsType.primary;
222 const target = createEventTarget(ref.current);
223 target.pointerdown({buttons, pointerType});
224 - target.pointerup({metaKey: true, pointerType});
224 + target.pointerup({buttons, metaKey: true, pointerType});
225 expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
226 });
227 });
@@ -284,7 +284,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
284 );
285 });
286
287 - test('second pointer down', () => {
287 + test('second pointer on target', () => {
288 const pointerType = 'touch';
289 const target = createEventTarget(ref.current);
290 const buttons = buttonsType.primary;
@@ -294,10 +294,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
294 target.pointerdown({buttons, pointerId: 2, pointerType});
295 } else {
296 // TouchEvents
297 - target.pointerdown([
298 - {pointerId: 1, pointerType},
299 - {pointerId: 2, pointerType},
300 - ]);
297 + target.pointerdown([{pointerId: 1}, {pointerId: 2}]);
298 }
299 expect(onTapStart).toHaveBeenCalledTimes(1);
300 });
@@ -349,8 +346,10 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
346
347 testWithPointerType('pointer up', pointerType => {
348 const target = createEventTarget(ref.current);
352 - target.pointerdown({buttons: buttonsType.primary, pointerType});
349 + const buttons = buttonsType.primary;
350 + target.pointerdown({buttons, pointerType});
351 target.pointerup({
352 + buttons,
353 pageX: 10,
354 pageY: 10,
355 pointerType,
@@ -420,18 +419,40 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
419 expect(onTapEnd).not.toBeCalled();
420 });
421
422 + if (hasPointerEvents) {
423 + test('second pointer up off target', () => {
424 + const pointerType = 'touch';
425 + const target = createEventTarget(ref.current);
426 + const offTarget = createEventTarget(container);
427 + const buttons = buttonsType.primary;
428 +
429 + target.pointerdown({buttons, pointerId: 1, pointerType});
430 + offTarget.pointerdown({buttons, pointerId: 2, pointerType});
431 + offTarget.pointerup({
432 + buttons,
433 + pageX: 10,
434 + pageY: 10,
435 + pointerId: 2,
436 + pointerType,
437 + x: 10,
438 + y: 10,
439 + });
440 + expect(onTapEnd).toHaveBeenCalledTimes(0);
441 + });
442 + }
443 +
444 test('ignored buttons and modifiers', () => {
445 const target = createEventTarget(ref.current);
446 const primary = buttonsType.primary;
447 // right-click
448 target.pointerdown({buttons: buttonsType.secondary});
428 - target.pointerup();
449 + target.pointerup({buttons: buttonsType.secondary});
450 // middle-click
451 target.pointerdown({buttons: buttonsType.auxiliary});
431 - target.pointerup();
452 + target.pointerup({buttons: buttonsType.auxiliary});
453 // pen eraser
454 target.pointerdown({buttons: buttonsType.eraser});
434 - target.pointerup();
455 + target.pointerup({buttons: buttonsType.eraser});
456 // alt-click
457 target.pointerdown({buttons: primary});
458 target.pointerup({altKey: true});
@@ -533,6 +554,21 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
554 // No extra 'onTapUpdate' calls when the pointer is outside the target
555 expect(onTapUpdate).toHaveBeenCalledTimes(1);
556 });
557 +
558 + if (hasPointerEvents) {
559 + test('second pointer off target', () => {
560 + const pointerType = 'touch';
561 + const target = createEventTarget(ref.current);
562 + const offTarget = createEventTarget(container);
563 + const buttons = buttonsType.primary;
564 + target.pointerdown({buttons, pointerId: 1, pointerType});
565 + offTarget.pointerdown({buttons, pointerId: 2, pointerType});
566 + target.pointermove({pointerId: 1, pointerType, x: 10, y: 10});
567 + expect(onTapUpdate).toHaveBeenCalledTimes(1);
568 + offTarget.pointermove({pointerId: 2, pointerType, x: 10, y: 10});
569 + expect(onTapUpdate).toHaveBeenCalledTimes(1);
570 + });
571 + }
572 });
573
574 describe('onTapChange', () => {
@@ -652,6 +688,32 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
688 expect(onTapUpdate).not.toBeCalled();
689 });
690
691 + test('second pointer on target', () => {
692 + const pointerType = 'touch';
693 + const target = createEventTarget(ref.current);
694 + const buttons = buttonsType.primary;
695 + target.pointerdown({buttons, pointerId: 1, pointerType});
696 + if (hasPointerEvents) {
697 + target.pointerdown({buttons, pointerId: 2, pointerType});
698 + } else {
699 + // TouchEvents
700 + target.pointerdown([{pointerId: 1}, {pointerId: 2}]);
701 + }
702 + expect(onTapCancel).toHaveBeenCalledTimes(1);
703 + });
704 +
705 + if (hasPointerEvents) {
706 + test('second pointer off target', () => {
707 + const pointerType = 'touch';
708 + const target = createEventTarget(ref.current);
709 + const offTarget = createEventTarget(container);
710 + const buttons = buttonsType.primary;
711 + target.pointerdown({buttons, pointerId: 1, pointerType});
712 + offTarget.pointerdown({buttons, pointerId: 2, pointerType});
713 + expect(onTapCancel).toHaveBeenCalledTimes(0);
714 + });
715 + }
716 +
717 testWithPointerType('pointer move outside target', pointerType => {
718 const downTarget = createEventTarget(ref.current);
719 const upTarget = createEventTarget(container);
packages/react-interactions/events/src/dom/testing-library/domEventSequences.js
+36 -29
@@ -18,6 +18,8 @@ function getPointerType(payload) {
18 let pointerType = 'mouse';
19 if (payload != null && payload.pointerType != null) {
20 pointerType = payload.pointerType;
21 + } else if (Array.isArray(payload)) {
22 + pointerType = 'touch';
23 }
24 return pointerType;
25 }
@@ -77,31 +79,36 @@ export function pointercancel(target, payload) {
79 export function pointerdown(target, defaultPayload) {
80 const dispatch = arg => target.dispatchEvent(arg);
81 const pointerType = getPointerType(defaultPayload);
80 - const payload = {buttons: buttonsType.primary, ...defaultPayload};
82
82 - if (pointerType === 'mouse') {
83 - if (hasPointerEvent()) {
84 - dispatch(domEvents.pointerover(payload));
85 - dispatch(domEvents.pointerenter(payload));
86 - }
87 - dispatch(domEvents.mouseover(payload));
88 - dispatch(domEvents.mouseenter(payload));
89 - if (hasPointerEvent()) {
90 - dispatch(domEvents.pointerdown(payload));
91 - }
92 - dispatch(domEvents.mousedown(payload));
93 - if (document.activeElement !== target) {
94 - dispatch(domEvents.focus());
95 - }
83 + if (Array.isArray(defaultPayload)) {
84 + // Arrays are for multi-touch only
85 + dispatch(domEvents.touchstart(defaultPayload));
86 } else {
97 - if (hasPointerEvent()) {
98 - dispatch(domEvents.pointerover(payload));
99 - dispatch(domEvents.pointerenter(payload));
100 - dispatch(domEvents.pointerdown(payload));
101 - }
102 - dispatch(domEvents.touchstart(payload));
103 - if (hasPointerEvent()) {
104 - dispatch(domEvents.gotpointercapture(payload));
87 + const payload = {buttons: buttonsType.primary, ...defaultPayload};
88 + if (pointerType === 'mouse') {
89 + if (hasPointerEvent()) {
90 + dispatch(domEvents.pointerover(payload));
91 + dispatch(domEvents.pointerenter(payload));
92 + }
93 + dispatch(domEvents.mouseover(payload));
94 + dispatch(domEvents.mouseenter(payload));
95 + if (hasPointerEvent()) {
96 + dispatch(domEvents.pointerdown(payload));
97 + }
98 + dispatch(domEvents.mousedown(payload));
99 + if (document.activeElement !== target) {
100 + dispatch(domEvents.focus());
101 + }
102 + } else {
103 + if (hasPointerEvent()) {
104 + dispatch(domEvents.pointerover(payload));
105 + dispatch(domEvents.pointerenter(payload));
106 + dispatch(domEvents.pointerdown(payload));
107 + }
108 + dispatch(domEvents.touchstart(payload));
109 + if (hasPointerEvent()) {
110 + dispatch(domEvents.gotpointercapture(payload));
111 + }
112 }
113 }
114 }
@@ -153,13 +160,14 @@ export function pointermove(target, payload) {
160 }
161 }
162
156 -export function pointerup(target, defaultPayload = {}) {
163 +export function pointerup(target, payload) {
164 const dispatch = arg => target.dispatchEvent(arg);
158 - const pointerType = getPointerType(defaultPayload);
159 - // eslint-disable-next-line no-unused-vars
160 - const {buttons, ...payload} = defaultPayload;
165 + const pointerType = getPointerType(payload);
166
162 - if (pointerType === 'mouse') {
167 + if (Array.isArray(payload)) {
168 + // Arrays are for multi-touch only
169 + dispatch(domEvents.touchend(payload));
170 + } else if (pointerType === 'mouse') {
171 if (hasPointerEvent()) {
172 dispatch(domEvents.pointerup(payload));
173 }
@@ -175,7 +183,6 @@ export function pointerup(target, defaultPayload = {}) {
183 dispatch(domEvents.touchend(payload));
184 dispatch(domEvents.mouseover(payload));
185 dispatch(domEvents.mousemove(payload));
178 - // NOTE: the value of 'buttons' for 'mousedown' must not be 0
186 dispatch(domEvents.mousedown(payload));
187 if (document.activeElement !== target) {
188 dispatch(domEvents.focus());
packages/react-interactions/events/src/dom/testing-library/domEvents.js
+6 -1
@@ -422,9 +422,14 @@ export function pointerup(payload) {
422 */
423
424 export function mousedown(payload) {
425 + // The value of 'buttons' for 'mousedown' must not be 0
426 + const buttons =
427 + payload == null || payload.buttons === 0
428 + ? buttonsType.primary
429 + : payload.buttons;
430 return createMouseEvent('mousedown', {
426 - buttons: buttonsType.primary,
431 ...payload,
432 + buttons,
433 });
434 }
435