diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index ddefbc3cb2..d6d76e6bbf 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -68,6 +68,7 @@ type PressState = { top: number, |}>, ignoreEmulatedMouseEvents: boolean, + allowPressReentry: boolean, }; type PressEventType = @@ -301,7 +302,6 @@ function dispatchPressEndEvents( deactivate(context, props, state); } } - removeRootEventTypes(context, state); } function isAnchorTagElement(eventTarget: EventTarget): boolean { @@ -395,6 +395,7 @@ function unmountResponder( state: PressState, ): void { if (state.isPressed) { + removeRootEventTypes(context, state); dispatchPressEndEvents(context, props, state); } } @@ -411,8 +412,11 @@ function dispatchCancel( (nativeEvent: any).preventDefault(); } else { state.ignoreEmulatedMouseEvents = false; + removeRootEventTypes(context, state); dispatchPressEndEvents(context, props, state); } + } else if (state.allowPressReentry) { + removeRootEventTypes(context, state); } } @@ -432,6 +436,7 @@ function removeRootEventTypes( ): void { if (state.addedRootEvents) { state.addedRootEvents = false; + state.allowPressReentry = false; context.removeRootEventTypes(rootEventTypes); } } @@ -455,6 +460,7 @@ const PressResponder = { responderRegionOnActivation: null, responderRegionOnDeactivation: null, ignoreEmulatedMouseEvents: false, + allowPressReentry: false, }; }, stopLocalPropagation: true, @@ -467,6 +473,7 @@ const PressResponder = { const {target, type} = event; if (props.disabled) { + removeRootEventTypes(context, state); dispatchPressEndEvents(context, props, state); state.ignoreEmulatedMouseEvents = false; return; @@ -512,6 +519,7 @@ const PressResponder = { return; } + state.allowPressReentry = true; state.pointerType = pointerType; state.pressTarget = getEventCurrentTarget(event, context); state.responderRegionOnActivation = calculateResponderRegion( @@ -565,7 +573,7 @@ const PressResponder = { case 'pointermove': case 'mousemove': case 'touchmove': { - if (state.isPressed) { + if (state.isPressed || state.allowPressReentry) { // Ignore emulated events (pointermove will dispatch touch and mouse events) // Ignore pointermove events during a keyboard press. if (state.pointerType !== pointerType) { @@ -589,18 +597,24 @@ const PressResponder = { ); if (state.isPressWithinResponderRegion) { - if (props.onPressMove) { - dispatchEvent(context, state, 'pressmove', props.onPressMove, { - discrete: false, - }); + if (state.isPressed) { + if (props.onPressMove) { + dispatchEvent(context, state, 'pressmove', props.onPressMove, { + discrete: false, + }); + } + } else { + dispatchPressStartEvents(context, props, state); } } else { + if (!state.allowPressReentry) { + removeRootEventTypes(context, state); + } dispatchPressEndEvents(context, props, state); } } break; } - // END case 'pointerup': case 'keyup': @@ -635,6 +649,7 @@ const PressResponder = { } const wasLongPressed = state.isLongPressed; + removeRootEventTypes(context, state); dispatchPressEndEvents(context, props, state); if (state.pressTarget !== null && props.onPress) { @@ -652,6 +667,8 @@ const PressResponder = { } } else if (type === 'mouseup' && state.ignoreEmulatedMouseEvents) { state.ignoreEmulatedMouseEvents = false; + } else if (state.allowPressReentry) { + removeRootEventTypes(context, state); } break; } diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index 1a5d44d86d..abf190025d 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -1148,6 +1148,193 @@ describe('Event responder: Press', () => { expect(events).toEqual([]); }); }); + + it('"onPress" is not called on release with mouse', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent( + createPointerEvent('pointerdown', { + pointerType: 'mouse', + }), + ); + ref.current.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesInside, + pointerType: 'mouse', + }), + ); + container.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesOutside, + pointerType: 'mouse', + }), + ); + container.dispatchEvent( + createPointerEvent('pointerup', { + ...coordinatesOutside, + pointerType: 'mouse', + }), + ); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + ]); + }); + + it('"onPress" is called on re-entry to hit rect for mouse', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent( + createPointerEvent('pointerdown', { + pointerType: 'mouse', + }), + ); + ref.current.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesInside, + pointerType: 'mouse', + }), + ); + container.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesOutside, + pointerType: 'mouse', + }), + ); + container.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesInside, + pointerType: 'mouse', + }), + ); + container.dispatchEvent( + createPointerEvent('pointerup', { + ...coordinatesInside, + pointerType: 'mouse', + }), + ); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('"onPress" is called on re-entry to hit rect for touch', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent( + createPointerEvent('pointerdown', { + pointerType: 'touch', + }), + ); + ref.current.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesOutside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createPointerEvent('pointermove', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createPointerEvent('pointerup', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); }); describe('delayed and multiple events', () => {