From 315aef102f501e9566756e30ccd34c2a6e5c5b37 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Fri, 3 May 2019 14:30:46 -0700 Subject: [PATCH] React Events: fix cancel events for Press --- packages/react-events/docs/Press.md | 14 +++-- packages/react-events/src/Press.js | 61 +++++++++++-------- .../src/__tests__/Press-test.internal.js | 50 +++++++++++++++ 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/packages/react-events/docs/Press.md b/packages/react-events/docs/Press.md index de40a1f12a23..cc7881342ca3 100644 --- a/packages/react-events/docs/Press.md +++ b/packages/react-events/docs/Press.md @@ -100,10 +100,10 @@ Called when the element changes press state (i.e., after `onPressStart` and ### onPressEnd: (e: PressEvent) => void -Called once the element is no longer pressed (because it was released, or moved -beyond the hit bounds). If the press starts again before the `delayPressEnd` -threshold is exceeded then the delay is reset to prevent `onPressEnd` being -called during a press. +Called once the element is no longer pressed (because the press was released, +cancelled, or moved beyond the hit bounds). If the press starts again before the +`delayPressEnd` threshold is exceeded then the delay is reset to prevent +`onPressEnd` being called during a press. ### onPressMove: (e: PressEvent) => void @@ -120,8 +120,10 @@ Called once the element is pressed down. If the press is released before the ### pressRetentionOffset: PressOffset Defines how far the pointer (while held down) may move outside the bounds of the -element before it is deactivated. Ensure you pass in a constant to reduce memory -allocations. Default is `20` for each offset. +element before it is deactivated. Once deactivated, the pointer (still held +down) can be moved back within the bounds of the element to reactivate it. +Ensure you pass in a constant to reduce memory allocations. Default is `20` for +each offset. ### preventDefault: boolean = true diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 6e708f2c6ea4..dc7f10c8650e 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -108,18 +108,26 @@ const targetEventTypes = [ // We need to preventDefault on pointerdown for mouse/pen events // that are in hit target area but not the element area. {name: 'pointerdown', passive: false}, +]; +const rootEventTypes = [ + 'keyup', + 'pointerup', + 'pointermove', + 'scroll', 'pointercancel', ]; -const rootEventTypes = ['keyup', 'pointerup', 'pointermove', 'scroll']; // If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events. if (typeof window !== 'undefined' && window.PointerEvent === undefined) { - targetEventTypes.push('touchstart', 'touchcancel', 'mousedown'); + targetEventTypes.push('touchstart', 'mousedown'); rootEventTypes.push( {name: 'mouseup', passive: false}, + 'mousemove', 'touchmove', 'touchend', - 'mousemove', + 'touchcancel', + // Used as a 'cancel' signal for mouse interactions + 'dragstart', ); } @@ -319,6 +327,28 @@ function dispatchPressEndEvents( } } +function dispatchCancel( + event: ReactResponderEvent, + context: ReactResponderContext, + props: PressProps, + state: PressState, +): void { + const nativeEvent: any = event.nativeEvent; + const type = event.type; + + if (state.isPressed) { + if (type === 'contextmenu' && props.preventDefault !== false) { + nativeEvent.preventDefault(); + } else { + state.ignoreEmulatedMouseEvents = false; + removeRootEventTypes(context, state); + dispatchPressEndEvents(event, context, props, state); + } + } else if (state.allowPressReentry) { + removeRootEventTypes(context, state); + } +} + function isAnchorTagElement(eventTarget: EventTarget): boolean { return (eventTarget: any).nodeName === 'A'; } @@ -415,28 +445,6 @@ function unmountResponder( } } -function dispatchCancel( - event: ReactResponderEvent, - context: ReactResponderContext, - props: PressProps, - state: PressState, -): void { - const nativeEvent: any = event.nativeEvent; - const type = event.type; - - if (state.isPressed) { - if (type === 'contextmenu' && props.preventDefault !== false) { - nativeEvent.preventDefault(); - } else { - state.ignoreEmulatedMouseEvents = false; - removeRootEventTypes(context, state); - dispatchPressEndEvents(event, context, props, state); - } - } else if (state.allowPressReentry) { - removeRootEventTypes(context, state); - } -} - function addRootEventTypes( context: ReactResponderContext, state: PressState, @@ -710,7 +718,8 @@ const PressResponder = { // CANCEL case 'pointercancel': case 'scroll': - case 'touchcancel': { + case 'touchcancel': + case 'dragstart': { dispatchCancel(event, context, props, state); } } diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index 599c507f439d..40b9ba81e84d 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -1629,6 +1629,56 @@ describe('Event responder: Press', () => { }); }); + describe('responder cancellation', () => { + it('ends on "pointercancel", "touchcancel", "scroll", and "dragstart"', () => { + const onLongPress = jest.fn(); + const onPressEnd = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('scroll')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + expect(onLongPress).not.toBeCalled(); + + onLongPress.mockReset(); + onPressEnd.mockReset(); + + // When pointer events are supported + ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createPointerEvent('pointercancel')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + expect(onLongPress).not.toBeCalled(); + + onLongPress.mockReset(); + onPressEnd.mockReset(); + + // Touch fallback + ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createPointerEvent('touchcancel')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + expect(onLongPress).not.toBeCalled(); + + onLongPress.mockReset(); + onPressEnd.mockReset(); + + // Mouse fallback + ref.current.dispatchEvent(createPointerEvent('mousedown')); + ref.current.dispatchEvent(createPointerEvent('dragstart')); + expect(onPressEnd).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + expect(onLongPress).not.toBeCalled(); + }); + }); + it('expect displayName to show up for event component', () => { expect(Press.displayName).toBe('Press'); });