diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 588be56433..3385fbebfd 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -190,14 +190,13 @@ function dispatchLongPressChangeEvent( function activate(event, context, props, state) { const nativeEvent: any = event.nativeEvent; - const pageX = nativeEvent.pageX; - const pageY = nativeEvent.pageY; + const {x, y} = getEventPageCoords(nativeEvent); const wasActivePressed = state.isActivePressed; state.isActivePressed = true; - if (pageX != null && pageY != null) { + if (x !== null && y !== null) { state.activationPosition = { - pageX: nativeEvent.pageX, - pageY: nativeEvent.pageY, + pageX: x, + pageY: y, }; } @@ -402,12 +401,39 @@ function calculateResponderRegion(target: Element, props: PressProps) { }; } +function isTouchEvent(nativeEvent: Event): boolean { + return Array.isArray((nativeEvent: any).changedTouches); +} + +function getTouchFromPressEvent(nativeEvent: TouchEvent): Touch { + const {changedTouches, touches} = nativeEvent; + return changedTouches.length > 0 + ? changedTouches[0] + : touches.length > 0 + ? touches[0] + : (nativeEvent: any); +} + +function getEventPageCoords( + nativeEvent: Event, +): {x: null | number, y: null | number} { + let eventObject = (nativeEvent: any); + if (isTouchEvent(eventObject)) { + eventObject = getTouchFromPressEvent(eventObject); + } + const pageX = eventObject.pageX; + const pageY = eventObject.pageY; + return { + x: pageX != null ? pageX : null, + y: pageY != null ? pageY : null, + }; +} + function isPressWithinResponderRegion( nativeEvent: $PropertyType, state: PressState, ): boolean { const {responderRegionOnActivation, responderRegionOnDeactivation} = state; - const event = (nativeEvent: any); let left, top, right, bottom; if (responderRegionOnActivation != null) { @@ -423,16 +449,16 @@ function isPressWithinResponderRegion( bottom = Math.max(bottom, responderRegionOnDeactivation.bottom); } } + const {x, y} = getEventPageCoords(((nativeEvent: any): Event)); return ( left != null && right != null && top != null && bottom != null && - (event.pageX >= left && - event.pageX <= right && - event.pageY >= top && - event.pageY <= bottom) + x !== null && + y !== null && + (x >= left && x <= right && y >= top && y <= bottom) ); } diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index 40b9ba81e8..17cd25d278 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -16,7 +16,7 @@ let Press; const DEFAULT_LONG_PRESS_DELAY = 500; -const createPointerEvent = (type, data) => { +const createEvent = (type, data) => { const event = document.createEvent('CustomEvent'); event.initCustomEvent(type, true, true); if (data != null) { @@ -77,8 +77,8 @@ describe('Event responder: Press', () => { }); it('prevents custom events being dispatched', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onPressStart).not.toBeCalled(); expect(onPress).not.toBeCalled(); expect(onPressEnd).not.toBeCalled(); @@ -101,7 +101,7 @@ describe('Event responder: Press', () => { it('is called after "pointerdown" event', () => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'pen'}), + createEvent('pointerdown', {pointerType: 'pen'}), ); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( @@ -110,16 +110,16 @@ describe('Event responder: Press', () => { }); it('ignores browser emulated events', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('touchstart')); - ref.current.dispatchEvent(createPointerEvent('mousedown')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('mousedown')); expect(onPressStart).toHaveBeenCalledTimes(1); }); it('ignores any events not caused by left-click or touch/pen contact', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown', {button: 1})); - ref.current.dispatchEvent(createPointerEvent('pointerdown', {button: 5})); - ref.current.dispatchEvent(createPointerEvent('mousedown', {button: 2})); + ref.current.dispatchEvent(createEvent('pointerdown', {button: 1})); + ref.current.dispatchEvent(createEvent('pointerdown', {button: 5})); + ref.current.dispatchEvent(createEvent('mousedown', {button: 2})); expect(onPressStart).toHaveBeenCalledTimes(0); }); @@ -151,14 +151,14 @@ describe('Event responder: Press', () => { // No PointerEvent fallbacks it('is called after "mousedown" event', () => { - ref.current.dispatchEvent(createPointerEvent('mousedown')); + ref.current.dispatchEvent(createEvent('mousedown')); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}), ); }); it('is called after "touchstart" event', () => { - ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchstart')); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch', type: 'pressstart'}), @@ -174,7 +174,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(1999); expect(onPressStart).not.toBeCalled(); jest.advanceTimersByTime(1); @@ -196,11 +196,11 @@ describe('Event responder: Press', () => { right: 500, }); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(499); expect(onPressStart).toHaveBeenCalledTimes(0); ref.current.dispatchEvent( - createPointerEvent('pointerup', { + createEvent('pointerup', { pageX: 55, pageY: 55, }), @@ -218,7 +218,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(onPressStart).toHaveBeenCalledTimes(1); }); }); @@ -235,9 +235,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(onPressStart).toHaveBeenCalledTimes(2); }); }); @@ -259,9 +259,9 @@ describe('Event responder: Press', () => { it('is called after "pointerup" event', () => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'pen'}), + createEvent('pointerdown', {pointerType: 'pen'}), ); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'pen', type: 'pressend'}), @@ -270,13 +270,13 @@ describe('Event responder: Press', () => { it('ignores browser emulated events', () => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'touch'}), + createEvent('pointerdown', {pointerType: 'touch'}), ); - ref.current.dispatchEvent(createPointerEvent('touchstart')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); - ref.current.dispatchEvent(createPointerEvent('touchend')); - ref.current.dispatchEvent(createPointerEvent('mousedown')); - ref.current.dispatchEvent(createPointerEvent('mouseup')); + ref.current.dispatchEvent(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent(createEvent('touchend')); + ref.current.dispatchEvent(createEvent('mousedown')); + ref.current.dispatchEvent(createEvent('mouseup')); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch', type: 'pressend'}), @@ -309,16 +309,16 @@ describe('Event responder: Press', () => { // No PointerEvent fallbacks it('is called after "mouseup" event', () => { - ref.current.dispatchEvent(createPointerEvent('mousedown')); - ref.current.dispatchEvent(createPointerEvent('mouseup')); + ref.current.dispatchEvent(createEvent('mousedown')); + ref.current.dispatchEvent(createEvent('mouseup')); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse', type: 'pressend'}), ); }); it('is called after "touchend" event', () => { - ref.current.dispatchEvent(createPointerEvent('touchstart')); - ref.current.dispatchEvent(createPointerEvent('touchend')); + ref.current.dispatchEvent(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchend')); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch', type: 'pressend'}), @@ -334,8 +334,8 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.advanceTimersByTime(1999); expect(onPressEnd).not.toBeCalled(); jest.advanceTimersByTime(1); @@ -350,13 +350,13 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.advanceTimersByTime(499); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(1); expect(onPressEnd).not.toBeCalled(); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.runAllTimers(); expect(onPressEnd).toHaveBeenCalledTimes(1); }); @@ -370,8 +370,8 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onPressEnd).toHaveBeenCalledTimes(1); }); }); @@ -391,10 +391,10 @@ describe('Event responder: Press', () => { }); it('is called after "pointerdown" and "pointerup" events', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); @@ -416,7 +416,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(499); expect(onPressChange).not.toBeCalled(); jest.advanceTimersByTime(1); @@ -439,10 +439,10 @@ describe('Event responder: Press', () => { right: 500, }); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(100); ref.current.dispatchEvent( - createPointerEvent('pointerup', { + createEvent('pointerup', { pageX: 55, pageY: 55, }), @@ -461,10 +461,10 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.advanceTimersByTime(499); expect(onPressChange).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(1); @@ -474,18 +474,18 @@ describe('Event responder: Press', () => { // No PointerEvent fallbacks it('is called after "mousedown" and "mouseup" events', () => { - ref.current.dispatchEvent(createPointerEvent('mousedown')); + ref.current.dispatchEvent(createEvent('mousedown')); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('mouseup')); + ref.current.dispatchEvent(createEvent('mouseup')); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); it('is called after "touchstart" and "touchend" events', () => { - ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchstart')); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('touchend')); + ref.current.dispatchEvent(createEvent('touchend')); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); @@ -513,10 +513,10 @@ describe('Event responder: Press', () => { it('is called after "pointerup" event', () => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'pen'}), + createEvent('pointerdown', {pointerType: 'pen'}), ); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( @@ -541,9 +541,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); expect(onPress).toHaveBeenCalledTimes(1); }); @@ -551,8 +551,8 @@ describe('Event responder: Press', () => { // No PointerEvent fallbacks // TODO: jsdom missing APIs // it('is called after "touchend" event', () => { - // ref.current.dispatchEvent(createPointerEvent('touchstart')); - // ref.current.dispatchEvent(createPointerEvent('touchend')); + // ref.current.dispatchEvent(createEvent('touchstart')); + // ref.current.dispatchEvent(createEvent('touchend')); // expect(onPress).toHaveBeenCalledTimes(1); // }); }); @@ -573,7 +573,7 @@ describe('Event responder: Press', () => { it('is called if "pointerdown" lasts default delay', () => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'pen'}), + createEvent('pointerdown', {pointerType: 'pen'}), ); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); expect(onLongPress).not.toBeCalled(); @@ -585,9 +585,9 @@ describe('Event responder: Press', () => { }); it('is not called if "pointerup" is dispatched before delay', () => { - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.advanceTimersByTime(1); expect(onLongPress).not.toBeCalled(); }); @@ -619,10 +619,10 @@ describe('Event responder: Press', () => { right: 100, }); ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pageX: 10, pageY: 10}), + createEvent('pointerdown', {pageX: 10, pageY: 10}), ); ref.current.dispatchEvent( - createPointerEvent('pointermove', {pageX: 50, pageY: 50}), + createEvent('pointermove', {pageX: 50, pageY: 50}), ); jest.runAllTimers(); expect(onLongPress).not.toBeCalled(); @@ -637,7 +637,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(1999); expect(onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); @@ -652,7 +652,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(9); expect(onLongPress).not.toBeCalled(); jest.advanceTimersByTime(1); @@ -668,7 +668,7 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime( delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1, ); @@ -690,11 +690,11 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); expect(onLongPressChange).toHaveBeenCalledTimes(1); expect(onLongPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onLongPressChange).toHaveBeenCalledTimes(2); expect(onLongPressChange).toHaveBeenCalledWith(false); }); @@ -709,11 +709,11 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); expect(onLongPressChange).toHaveBeenCalledTimes(1); expect(onLongPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); jest.advanceTimersByTime(499); expect(onLongPressChange).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(1); @@ -739,10 +739,10 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); // NOTE: onPressChange behavior should not be affected - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(onPressChange).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(onPress).not.toBeCalled(); expect(onPressChange).toHaveBeenCalledTimes(2); }); @@ -766,10 +766,10 @@ describe('Event responder: Press', () => { right: 100, }); ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'touch'}), + createEvent('pointerdown', {pointerType: 'touch'}), ); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { pointerType: 'touch', pageX: 10, pageY: 10, @@ -799,7 +799,7 @@ describe('Event responder: Press', () => { }); ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { pointerType: 'mouse', pageX: 10, pageY: 10, @@ -825,23 +825,23 @@ describe('Event responder: Press', () => { right: 100, }); ref.current.dispatchEvent( - createPointerEvent('pointerdown', {pointerType: 'touch'}), + createEvent('pointerdown', {pointerType: 'touch'}), ); - ref.current.dispatchEvent(createPointerEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchstart')); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { pointerType: 'touch', pageX: 10, pageY: 10, }), ); - ref.current.dispatchEvent(createPointerEvent('touchmove')); - ref.current.dispatchEvent(createPointerEvent('mousemove')); + ref.current.dispatchEvent(createEvent('touchmove')); + ref.current.dispatchEvent(createEvent('mousemove')); expect(onPressMove).toHaveBeenCalledTimes(1); }); }); - describe('press with movement', () => { + describe('press with movement (pointer events)', () => { const rectMock = { width: 100, height: 100, @@ -890,13 +890,11 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinatesInside), - ); - ref.current.dispatchEvent( - createPointerEvent('pointerup', coordinatesInside), + createEvent('pointermove', coordinatesInside), ); + ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); jest.runAllTimers(); expect(events).toEqual([ @@ -931,9 +929,9 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinatesInside), + createEvent('pointermove', coordinatesInside), ); jest.advanceTimersByTime(499); expect(events).toEqual(['onPressMove']); @@ -943,9 +941,7 @@ describe('Event responder: Press', () => { expect(events).toEqual(['onPressStart', 'onPressChange']); events = []; - ref.current.dispatchEvent( - createPointerEvent('pointerup', coordinatesInside), - ); + ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); expect(events).toEqual(['onPressEnd', 'onPressChange', 'onPress']); }); @@ -971,16 +967,14 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { pageX: rectMock.left - pressRetentionOffset.left, pageY: rectMock.top - pressRetentionOffset.top, }), ); - ref.current.dispatchEvent( - createPointerEvent('pointerup', coordinatesInside), - ); + ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); expect(events).toEqual([ 'onPressStart', 'onPressChange', @@ -1009,7 +1003,7 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); // emulate smaller dimensions change on activation ref.current.getBoundingClientRect = () => ({ width: 80, @@ -1024,10 +1018,8 @@ describe('Event responder: Press', () => { pageY: rectMock.top, }; // move to an area within the pre-activation region - ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinates), - ); - ref.current.dispatchEvent(createPointerEvent('pointerup', coordinates)); + ref.current.dispatchEvent(createEvent('pointermove', coordinates)); + ref.current.dispatchEvent(createEvent('pointerup', coordinates)); expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); }); @@ -1049,7 +1041,7 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); // emulate larger dimensions change on activation ref.current.getBoundingClientRect = () => ({ width: 200, @@ -1064,10 +1056,8 @@ describe('Event responder: Press', () => { pageY: rectMock.top - 50, }; // move to an area within the post-activation region - ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinates), - ); - ref.current.dispatchEvent(createPointerEvent('pointerup', coordinates)); + ref.current.dispatchEvent(createEvent('pointermove', coordinates)); + ref.current.dispatchEvent(createEvent('pointerup', coordinates)); expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); }); }); @@ -1103,16 +1093,12 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinatesInside), - ); - container.dispatchEvent( - createPointerEvent('pointermove', coordinatesOutside), - ); - container.dispatchEvent( - createPointerEvent('pointerup', coordinatesOutside), + createEvent('pointermove', coordinatesInside), ); + container.dispatchEvent(createEvent('pointermove', coordinatesOutside)); + container.dispatchEvent(createEvent('pointerup', coordinatesOutside)); jest.runAllTimers(); expect(events).toEqual([ @@ -1148,19 +1134,15 @@ describe('Event responder: Press', () => { ReactDOM.render(element, container); ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointermove', coordinatesInside), - ); - container.dispatchEvent( - createPointerEvent('pointermove', coordinatesOutside), + createEvent('pointermove', coordinatesInside), ); + container.dispatchEvent(createEvent('pointermove', coordinatesOutside)); jest.runAllTimers(); expect(events).toEqual(['onPressMove']); events = []; - container.dispatchEvent( - createPointerEvent('pointerup', coordinatesOutside), - ); + container.dispatchEvent(createEvent('pointerup', coordinatesOutside)); jest.runAllTimers(); expect(events).toEqual([]); }); @@ -1188,24 +1170,24 @@ describe('Event responder: Press', () => { ref.current.getBoundingClientRect = getBoundingClientRectMock; ref.current.dispatchEvent( - createPointerEvent('pointerdown', { + createEvent('pointerdown', { pointerType: 'mouse', }), ); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesInside, pointerType: 'mouse', }), ); container.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesOutside, pointerType: 'mouse', }), ); container.dispatchEvent( - createPointerEvent('pointerup', { + createEvent('pointerup', { ...coordinatesOutside, pointerType: 'mouse', }), @@ -1243,30 +1225,30 @@ describe('Event responder: Press', () => { ref.current.getBoundingClientRect = getBoundingClientRectMock; ref.current.dispatchEvent( - createPointerEvent('pointerdown', { + createEvent('pointerdown', { pointerType: 'mouse', }), ); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesInside, pointerType: 'mouse', }), ); container.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesOutside, pointerType: 'mouse', }), ); container.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesInside, pointerType: 'mouse', }), ); container.dispatchEvent( - createPointerEvent('pointerup', { + createEvent('pointerup', { ...coordinatesInside, pointerType: 'mouse', }), @@ -1309,30 +1291,404 @@ describe('Event responder: Press', () => { ref.current.getBoundingClientRect = getBoundingClientRectMock; ref.current.dispatchEvent( - createPointerEvent('pointerdown', { + createEvent('pointerdown', { pointerType: 'touch', }), ); ref.current.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesInside, pointerType: 'touch', }), ); container.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesOutside, pointerType: 'touch', }), ); container.dispatchEvent( - createPointerEvent('pointermove', { + createEvent('pointermove', { ...coordinatesInside, pointerType: 'touch', }), ); container.dispatchEvent( - createPointerEvent('pointerup', { + createEvent('pointerup', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + }); + + describe('press with movement (touch events fallback)', () => { + const rectMock = { + width: 100, + height: 100, + top: 50, + left: 50, + right: 500, + bottom: 500, + }; + const pressRectOffset = 20; + const getBoundingClientRectMock = () => rectMock; + const coordinatesInside = { + changedTouches: [ + { + pageX: rectMock.left - pressRectOffset, + pageY: rectMock.top - pressRectOffset, + }, + ], + }; + const coordinatesOutside = { + changedTouches: [ + { + pageX: rectMock.left - pressRectOffset - 1, + pageY: rectMock.top - pressRectOffset - 1, + }, + ], + }; + + describe('within bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect X │ <= Move to X and release + * └──────────────────┘ + */ + it('no delay and "onPress*" events are called immediately', () => { + 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(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchmove', coordinatesInside)); + ref.current.dispatchEvent(createEvent('touchend', coordinatesInside)); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('delay and "onPressMove" is called before "onPress*" events', () => { + 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(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchmove', coordinatesInside)); + jest.advanceTimersByTime(499); + expect(events).toEqual(['onPressMove']); + events = []; + + jest.advanceTimersByTime(1); + expect(events).toEqual(['onPressStart', 'onPressChange']); + events = []; + + ref.current.dispatchEvent(createEvent('touchend', coordinatesInside)); + expect(events).toEqual(['onPressEnd', 'onPressChange', 'onPress']); + }); + + it('press retention offset can be configured', () => { + let events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; + const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; + + const element = ( + +
+ + ); + + ReactDOM.render(element, container); + ref.current.getBoundingClientRect = getBoundingClientRectMock; + ref.current.dispatchEvent(createEvent('touchstart')); + ref.current.dispatchEvent( + createEvent('touchmove', { + pageX: rectMock.left - pressRetentionOffset.left, + pageY: rectMock.top - pressRetentionOffset.top, + }), + ); + ref.current.dispatchEvent(createEvent('touchend', coordinatesInside)); + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('responder region accounts for decrease in element dimensions', () => { + 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(createEvent('touchstart')); + // emulate smaller dimensions change on activation + ref.current.getBoundingClientRect = () => ({ + width: 80, + height: 80, + top: 60, + left: 60, + right: 490, + bottom: 490, + }); + const coordinates = { + pageX: rectMock.left, + pageY: rectMock.top, + }; + // move to an area within the pre-activation region + ref.current.dispatchEvent(createEvent('touchmove', coordinates)); + ref.current.dispatchEvent(createEvent('touchend', coordinates)); + expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); + }); + + it('responder region accounts for increase in element dimensions', () => { + 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(createEvent('touchstart')); + // emulate larger dimensions change on activation + ref.current.getBoundingClientRect = () => ({ + width: 200, + height: 200, + top: 0, + left: 0, + right: 550, + bottom: 550, + }); + const coordinates = { + pageX: rectMock.left - 50, + pageY: rectMock.top - 50, + }; + // move to an area within the post-activation region + ref.current.dispatchEvent(createEvent('touchmove', coordinates)); + ref.current.dispatchEvent(createEvent('touchend', coordinates)); + expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); + }); + }); + + describe('beyond bounds of hit rect', () => { + /** ┌──────────────────┐ + * │ ┌────────────┐ │ + * │ │ VisualRect │ │ + * │ └────────────┘ │ + * │ HitRect │ + * └──────────────────┘ + * X <= Move to X and release + */ + + it('"onPress" is not called on release', () => { + 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(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchmove', coordinatesInside)); + container.dispatchEvent(createEvent('touchmove', coordinatesOutside)); + container.dispatchEvent(createEvent('touchend', coordinatesOutside)); + jest.runAllTimers(); + + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + ]); + }); + + it('"onPress*" events are not called after delay expires', () => { + 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(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchmove', coordinatesInside)); + container.dispatchEvent(createEvent('touchmove', coordinatesOutside)); + jest.runAllTimers(); + expect(events).toEqual(['onPressMove']); + events = []; + container.dispatchEvent(createEvent('touchend', coordinatesOutside)); + jest.runAllTimers(); + expect(events).toEqual([]); + }); + }); + + 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( + createEvent('touchstart', { + pointerType: 'touch', + }), + ); + ref.current.dispatchEvent( + createEvent('touchmove', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createEvent('touchmove', { + ...coordinatesOutside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createEvent('touchmove', { + ...coordinatesInside, + pointerType: 'touch', + }), + ); + container.dispatchEvent( + createEvent('touchend', { ...coordinatesInside, pointerType: 'touch', }), @@ -1386,13 +1742,13 @@ describe('Event responder: Press', () => { // 1 events = []; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); jest.runAllTimers(); @@ -1408,11 +1764,11 @@ describe('Event responder: Press', () => { // 2 events = []; - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(250); jest.advanceTimersByTime(500); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); jest.runAllTimers(); @@ -1468,9 +1824,9 @@ describe('Event responder: Press', () => { right: 0, }); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); expect(events).toEqual([ 'inner: onPressStart', @@ -1502,9 +1858,9 @@ describe('Event responder: Press', () => { right: 0, }); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); ref.current.dispatchEvent( - createPointerEvent('pointerup', {pageX: 10, pageY: 10}), + createEvent('pointerup', {pageX: 10, pageY: 10}), ); expect(fn).toHaveBeenCalledTimes(1); }); @@ -1521,9 +1877,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(fn).toHaveBeenCalledTimes(1); }); @@ -1540,10 +1896,10 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(0); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); }); @@ -1560,9 +1916,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); expect(fn).toHaveBeenCalledTimes(1); - ref.current.dispatchEvent(createPointerEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerup')); expect(fn).toHaveBeenCalledTimes(2); }); }); @@ -1580,9 +1936,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); - ref.current.dispatchEvent(createPointerEvent('click', {preventDefault})); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).toBeCalled(); }); @@ -1599,13 +1955,13 @@ describe('Event responder: Press', () => { ['metaKey', 'ctrlKey', 'shiftKey'].forEach(modifierKey => { ref.current.dispatchEvent( - createPointerEvent('pointerdown', {[modifierKey]: true}), + createEvent('pointerdown', {[modifierKey]: true}), ); ref.current.dispatchEvent( - createPointerEvent('pointerup', {[modifierKey]: true}), + createEvent('pointerup', {[modifierKey]: true}), ); ref.current.dispatchEvent( - createPointerEvent('click', {[modifierKey]: true, preventDefault}), + createEvent('click', {[modifierKey]: true, preventDefault}), ); expect(preventDefault).not.toBeCalled(); }); @@ -1622,9 +1978,9 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); - ref.current.dispatchEvent(createPointerEvent('click', {preventDefault})); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent(createEvent('click', {preventDefault})); expect(preventDefault).not.toBeCalled(); }); }); @@ -1641,8 +1997,8 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('scroll')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('scroll')); expect(onPressEnd).toHaveBeenCalledTimes(1); jest.runAllTimers(); expect(onLongPress).not.toBeCalled(); @@ -1651,8 +2007,8 @@ describe('Event responder: Press', () => { onPressEnd.mockReset(); // When pointer events are supported - ref.current.dispatchEvent(createPointerEvent('pointerdown')); - ref.current.dispatchEvent(createPointerEvent('pointercancel')); + ref.current.dispatchEvent(createEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointercancel')); expect(onPressEnd).toHaveBeenCalledTimes(1); jest.runAllTimers(); expect(onLongPress).not.toBeCalled(); @@ -1661,8 +2017,8 @@ describe('Event responder: Press', () => { onPressEnd.mockReset(); // Touch fallback - ref.current.dispatchEvent(createPointerEvent('touchstart')); - ref.current.dispatchEvent(createPointerEvent('touchcancel')); + ref.current.dispatchEvent(createEvent('touchstart')); + ref.current.dispatchEvent(createEvent('touchcancel')); expect(onPressEnd).toHaveBeenCalledTimes(1); jest.runAllTimers(); expect(onLongPress).not.toBeCalled(); @@ -1671,8 +2027,8 @@ describe('Event responder: Press', () => { onPressEnd.mockReset(); // Mouse fallback - ref.current.dispatchEvent(createPointerEvent('mousedown')); - ref.current.dispatchEvent(createPointerEvent('dragstart')); + ref.current.dispatchEvent(createEvent('mousedown')); + ref.current.dispatchEvent(createEvent('dragstart')); expect(onPressEnd).toHaveBeenCalledTimes(1); jest.runAllTimers(); expect(onLongPress).not.toBeCalled(); @@ -1692,11 +2048,11 @@ describe('Event responder: Press', () => { ); ReactDOM.render(element, container); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointerdown')); jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY); - ref.current.dispatchEvent(createPointerEvent('pointermove')); - ref.current.dispatchEvent(createPointerEvent('pointerup')); - ref.current.dispatchEvent(createPointerEvent('pointerdown')); + ref.current.dispatchEvent(createEvent('pointermove')); + ref.current.dispatchEvent(createEvent('pointerup')); + ref.current.dispatchEvent(createEvent('pointerdown')); ReactDOM.render(element, container); }); });