Skip to content

Commit

Permalink
[react-interactions] Tap cancels on second pointerdown (#16936)
Browse files Browse the repository at this point in the history
This patch causes onTapCancel to be called whenever a second pointer interacts
with the responder target.
  • Loading branch information
necolas committed Oct 1, 2019
1 parent 3445772 commit f6efb22
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 59 deletions.
46 changes: 28 additions & 18 deletions packages/react-interactions/events/src/dom/Tap.js
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down
Expand Up @@ -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);
});

Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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;
Expand All @@ -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);
});
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
}
}
}
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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());
Expand Down
Expand Up @@ -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,
});
}

Expand Down

0 comments on commit f6efb22

Please sign in to comment.