[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