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

React Events: fix cancel events for Press (#15563)

* Fixes a bug in the cancellation logic. The cancel events are now correctly listened to on the root. * Fixes cancellation in Safari by using the dragstart event as a proxy for cancellation (i.e., the event dispatched when move-during-press occurs on an anchor tag)

Nicolas Gallagher committed May 3, 2019 at 15:33 UTC 2e5d1a8b9e2c29418a27b24306e4c8d5f8681f4f
3 files changed +93 -32
packages/react-events/docs/Press.md
+8 -6
@@ -100,10 +100,10 @@ Called when the element changes press state (i.e., after `onPressStart` and
100
101 ### onPressEnd: (e: PressEvent) => void
102
103 -Called once the element is no longer pressed (because it was released, or moved
104 -beyond the hit bounds). If the press starts again before the `delayPressEnd`
105 -threshold is exceeded then the delay is reset to prevent `onPressEnd` being
106 -called during a press.
103 +Called once the element is no longer pressed (because the press was released,
104 +cancelled, or moved beyond the hit bounds). If the press starts again before the
105 +`delayPressEnd` threshold is exceeded then the delay is reset to prevent
106 +`onPressEnd` being called during a press.
107
108 ### onPressMove: (e: PressEvent) => void
109
@@ -120,8 +120,10 @@ Called once the element is pressed down. If the press is released before the
120 ### pressRetentionOffset: PressOffset
121
122 Defines how far the pointer (while held down) may move outside the bounds of the
123 -element before it is deactivated. Ensure you pass in a constant to reduce memory
124 -allocations. Default is `20` for each offset.
123 +element before it is deactivated. Once deactivated, the pointer (still held
124 +down) can be moved back within the bounds of the element to reactivate it.
125 +Ensure you pass in a constant to reduce memory allocations. Default is `20` for
126 +each offset.
127
128 ### preventDefault: boolean = true
129
packages/react-events/src/Press.js
+35 -26
@@ -108,18 +108,26 @@ const targetEventTypes = [
108 // We need to preventDefault on pointerdown for mouse/pen events
109 // that are in hit target area but not the element area.
110 {name: 'pointerdown', passive: false},
111 +];
112 +const rootEventTypes = [
113 + 'keyup',
114 + 'pointerup',
115 + 'pointermove',
116 + 'scroll',
117 'pointercancel',
118 ];
113 -const rootEventTypes = ['keyup', 'pointerup', 'pointermove', 'scroll'];
119
120 // If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
121 if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
117 - targetEventTypes.push('touchstart', 'touchcancel', 'mousedown');
122 + targetEventTypes.push('touchstart', 'mousedown');
123 rootEventTypes.push(
124 {name: 'mouseup', passive: false},
125 + 'mousemove',
126 'touchmove',
127 'touchend',
122 - 'mousemove',
128 + 'touchcancel',
129 + // Used as a 'cancel' signal for mouse interactions
130 + 'dragstart',
131 );
132 }
133
@@ -319,6 +327,28 @@ function dispatchPressEndEvents(
327 }
328 }
329
330 +function dispatchCancel(
331 + event: ReactResponderEvent,
332 + context: ReactResponderContext,
333 + props: PressProps,
334 + state: PressState,
335 +): void {
336 + const nativeEvent: any = event.nativeEvent;
337 + const type = event.type;
338 +
339 + if (state.isPressed) {
340 + if (type === 'contextmenu' && props.preventDefault !== false) {
341 + nativeEvent.preventDefault();
342 + } else {
343 + state.ignoreEmulatedMouseEvents = false;
344 + removeRootEventTypes(context, state);
345 + dispatchPressEndEvents(event, context, props, state);
346 + }
347 + } else if (state.allowPressReentry) {
348 + removeRootEventTypes(context, state);
349 + }
350 +}
351 +
352 function isAnchorTagElement(eventTarget: EventTarget): boolean {
353 return (eventTarget: any).nodeName === 'A';
354 }
@@ -415,28 +445,6 @@ function unmountResponder(
445 }
446 }
447
418 -function dispatchCancel(
419 - event: ReactResponderEvent,
420 - context: ReactResponderContext,
421 - props: PressProps,
422 - state: PressState,
423 -): void {
424 - const nativeEvent: any = event.nativeEvent;
425 - const type = event.type;
426 -
427 - if (state.isPressed) {
428 - if (type === 'contextmenu' && props.preventDefault !== false) {
429 - nativeEvent.preventDefault();
430 - } else {
431 - state.ignoreEmulatedMouseEvents = false;
432 - removeRootEventTypes(context, state);
433 - dispatchPressEndEvents(event, context, props, state);
434 - }
435 - } else if (state.allowPressReentry) {
436 - removeRootEventTypes(context, state);
437 - }
438 -}
439 -
448 function addRootEventTypes(
449 context: ReactResponderContext,
450 state: PressState,
@@ -710,7 +718,8 @@ const PressResponder = {
718 // CANCEL
719 case 'pointercancel':
720 case 'scroll':
713 - case 'touchcancel': {
721 + case 'touchcancel':
722 + case 'dragstart': {
723 dispatchCancel(event, context, props, state);
724 }
725 }
packages/react-events/src/__tests__/Press-test.internal.js
+50
@@ -1629,6 +1629,56 @@ describe('Event responder: Press', () => {
1629 });
1630 });
1631
1632 + describe('responder cancellation', () => {
1633 + it('ends on "pointercancel", "touchcancel", "scroll", and "dragstart"', () => {
1634 + const onLongPress = jest.fn();
1635 + const onPressEnd = jest.fn();
1636 + const ref = React.createRef();
1637 + const element = (
1638 + <Press onLongPress={onLongPress} onPressEnd={onPressEnd}>
1639 + <a href="#" ref={ref} />
1640 + </Press>
1641 + );
1642 + ReactDOM.render(element, container);
1643 +
1644 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1645 + ref.current.dispatchEvent(createPointerEvent('scroll'));
1646 + expect(onPressEnd).toHaveBeenCalledTimes(1);
1647 + jest.runAllTimers();
1648 + expect(onLongPress).not.toBeCalled();
1649 +
1650 + onLongPress.mockReset();
1651 + onPressEnd.mockReset();
1652 +
1653 + // When pointer events are supported
1654 + ref.current.dispatchEvent(createPointerEvent('pointerdown'));
1655 + ref.current.dispatchEvent(createPointerEvent('pointercancel'));
1656 + expect(onPressEnd).toHaveBeenCalledTimes(1);
1657 + jest.runAllTimers();
1658 + expect(onLongPress).not.toBeCalled();
1659 +
1660 + onLongPress.mockReset();
1661 + onPressEnd.mockReset();
1662 +
1663 + // Touch fallback
1664 + ref.current.dispatchEvent(createPointerEvent('touchstart'));
1665 + ref.current.dispatchEvent(createPointerEvent('touchcancel'));
1666 + expect(onPressEnd).toHaveBeenCalledTimes(1);
1667 + jest.runAllTimers();
1668 + expect(onLongPress).not.toBeCalled();
1669 +
1670 + onLongPress.mockReset();
1671 + onPressEnd.mockReset();
1672 +
1673 + // Mouse fallback
1674 + ref.current.dispatchEvent(createPointerEvent('mousedown'));
1675 + ref.current.dispatchEvent(createPointerEvent('dragstart'));
1676 + expect(onPressEnd).toHaveBeenCalledTimes(1);
1677 + jest.runAllTimers();
1678 + expect(onLongPress).not.toBeCalled();
1679 + });
1680 + });
1681 +
1682 it('expect displayName to show up for event component', () => {
1683 expect(Press.displayName).toBe('Press');
1684 });