Skip to content

Commit f6efb22

Browse files
authored
[react-interactions] Tap cancels on second pointerdown (#16936)
This patch causes onTapCancel to be called whenever a second pointer interacts with the responder target.
1 parent 3445772 commit f6efb22

File tree

4 files changed

+143
-59
lines changed

4 files changed

+143
-59
lines changed

packages/react-interactions/events/src/dom/Tap.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ function isActivePointer(
344344
const touch = getTouchById(nativeEvent, activePointerId);
345345
return touch != null;
346346
} else {
347-
// accept all events that don't have ids
347+
// accept all events that don't have pointer ids
348348
return true;
349349
}
350350
}
@@ -496,26 +496,29 @@ const responderImpl = {
496496
case 'pointerdown':
497497
case 'mousedown':
498498
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;
516501
}
517502

518503
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+
519522
const activate = shouldActivate(event);
520523
const activateAuxiliary = isAuxiliary(nativeEvent.buttons, event);
521524

@@ -547,6 +550,13 @@ const responderImpl = {
547550
state.initialPosition.y = gestureState.y;
548551
dispatchStart(context, props, state);
549552
}
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);
550560
}
551561
break;
552562
}

packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
212212
const buttons = buttonsType.auxiliary;
213213
const target = createEventTarget(ref.current);
214214
target.pointerdown({buttons, pointerType});
215-
target.pointerup({pointerType});
215+
target.pointerup({buttons, pointerType});
216216
expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
217217
});
218218

@@ -221,7 +221,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
221221
const buttons = buttonsType.primary;
222222
const target = createEventTarget(ref.current);
223223
target.pointerdown({buttons, pointerType});
224-
target.pointerup({metaKey: true, pointerType});
224+
target.pointerup({buttons, metaKey: true, pointerType});
225225
expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
226226
});
227227
});
@@ -284,7 +284,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
284284
);
285285
});
286286

287-
test('second pointer down', () => {
287+
test('second pointer on target', () => {
288288
const pointerType = 'touch';
289289
const target = createEventTarget(ref.current);
290290
const buttons = buttonsType.primary;
@@ -294,10 +294,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
294294
target.pointerdown({buttons, pointerId: 2, pointerType});
295295
} else {
296296
// TouchEvents
297-
target.pointerdown([
298-
{pointerId: 1, pointerType},
299-
{pointerId: 2, pointerType},
300-
]);
297+
target.pointerdown([{pointerId: 1}, {pointerId: 2}]);
301298
}
302299
expect(onTapStart).toHaveBeenCalledTimes(1);
303300
});
@@ -349,8 +346,10 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
349346

350347
testWithPointerType('pointer up', pointerType => {
351348
const target = createEventTarget(ref.current);
352-
target.pointerdown({buttons: buttonsType.primary, pointerType});
349+
const buttons = buttonsType.primary;
350+
target.pointerdown({buttons, pointerType});
353351
target.pointerup({
352+
buttons,
354353
pageX: 10,
355354
pageY: 10,
356355
pointerType,
@@ -420,18 +419,40 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
420419
expect(onTapEnd).not.toBeCalled();
421420
});
422421

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+
423444
test('ignored buttons and modifiers', () => {
424445
const target = createEventTarget(ref.current);
425446
const primary = buttonsType.primary;
426447
// right-click
427448
target.pointerdown({buttons: buttonsType.secondary});
428-
target.pointerup();
449+
target.pointerup({buttons: buttonsType.secondary});
429450
// middle-click
430451
target.pointerdown({buttons: buttonsType.auxiliary});
431-
target.pointerup();
452+
target.pointerup({buttons: buttonsType.auxiliary});
432453
// pen eraser
433454
target.pointerdown({buttons: buttonsType.eraser});
434-
target.pointerup();
455+
target.pointerup({buttons: buttonsType.eraser});
435456
// alt-click
436457
target.pointerdown({buttons: primary});
437458
target.pointerup({altKey: true});
@@ -533,6 +554,21 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
533554
// No extra 'onTapUpdate' calls when the pointer is outside the target
534555
expect(onTapUpdate).toHaveBeenCalledTimes(1);
535556
});
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+
}
536572
});
537573

538574
describe('onTapChange', () => {
@@ -652,6 +688,32 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
652688
expect(onTapUpdate).not.toBeCalled();
653689
});
654690

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+
655717
testWithPointerType('pointer move outside target', pointerType => {
656718
const downTarget = createEventTarget(ref.current);
657719
const upTarget = createEventTarget(container);

packages/react-interactions/events/src/dom/testing-library/domEventSequences.js

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ function getPointerType(payload) {
1818
let pointerType = 'mouse';
1919
if (payload != null && payload.pointerType != null) {
2020
pointerType = payload.pointerType;
21+
} else if (Array.isArray(payload)) {
22+
pointerType = 'touch';
2123
}
2224
return pointerType;
2325
}
@@ -77,31 +79,36 @@ export function pointercancel(target, payload) {
7779
export function pointerdown(target, defaultPayload) {
7880
const dispatch = arg => target.dispatchEvent(arg);
7981
const pointerType = getPointerType(defaultPayload);
80-
const payload = {buttons: buttonsType.primary, ...defaultPayload};
8182

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));
9686
} 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+
}
105112
}
106113
}
107114
}
@@ -153,13 +160,14 @@ export function pointermove(target, payload) {
153160
}
154161
}
155162

156-
export function pointerup(target, defaultPayload = {}) {
163+
export function pointerup(target, payload) {
157164
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);
161166

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') {
163171
if (hasPointerEvent()) {
164172
dispatch(domEvents.pointerup(payload));
165173
}
@@ -175,7 +183,6 @@ export function pointerup(target, defaultPayload = {}) {
175183
dispatch(domEvents.touchend(payload));
176184
dispatch(domEvents.mouseover(payload));
177185
dispatch(domEvents.mousemove(payload));
178-
// NOTE: the value of 'buttons' for 'mousedown' must not be 0
179186
dispatch(domEvents.mousedown(payload));
180187
if (document.activeElement !== target) {
181188
dispatch(domEvents.focus());

packages/react-interactions/events/src/dom/testing-library/domEvents.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,14 @@ export function pointerup(payload) {
422422
*/
423423

424424
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;
425430
return createMouseEvent('mousedown', {
426-
buttons: buttonsType.primary,
427431
...payload,
432+
buttons,
428433
});
429434
}
430435

0 commit comments

Comments
 (0)