diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index aa39d91a35ca..40251331f94b 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -95,10 +95,14 @@ type PressEvent = {| shiftKey: boolean, |}; +const hasPointerEvents = + typeof window !== 'undefined' && window.PointerEvent !== undefined; + const isMac = typeof window !== 'undefined' && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false; + const DEFAULT_PRESS_RETENTION_OFFSET = { bottom: 20, top: 20, @@ -106,37 +110,32 @@ const DEFAULT_PRESS_RETENTION_OFFSET = { right: 20, }; -const targetEventTypes = [ - 'keydown_active', - // We need to preventDefault on pointerdown for mouse/pen events - // that are in hit target area but not the element area. - 'pointerdown_active', - 'click_active', -]; -const rootEventTypes = [ - 'click', - 'keyup', - 'pointerup', - 'pointermove', - 'scroll', - 'pointercancel', - // We listen to this here so stopPropagation can - // block other mouseup events used internally - 'mouseup_active', - 'touchend', -]; - -// 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', 'mousedown'); - rootEventTypes.push( - 'mousemove', - 'touchmove', - 'touchcancel', - // Used as a 'cancel' signal for mouse interactions - 'dragstart', - ); -} +const targetEventTypes = hasPointerEvents + ? [ + 'keydown_active', + // We need to preventDefault on pointerdown for mouse/pen events + // that are in hit target area but not the element area. + 'pointerdown_active', + 'click_active', + ] + : ['keydown_active', 'touchstart', 'mousedown', 'click_active']; + +const rootEventTypes = hasPointerEvents + ? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll'] + : [ + 'click', + 'keyup', + 'scroll', + 'mousemove', + 'touchmove', + 'touchcancel', + // Used as a 'cancel' signal for mouse interactions + 'dragstart', + // We listen to this here so stopPropagation can + // block other mouseup events used internally + 'mouseup_active', + 'touchend', + ]; function isFunction(obj): boolean { return typeof obj === 'function'; @@ -539,7 +538,7 @@ const pressResponderImpl = { } state.shouldPreventClick = false; - if (isPointerEvent || isTouchEvent) { + if (isTouchEvent) { state.ignoreEmulatedMouseEvents = true; } else if (isKeyboardEvent) { // Ignore unrelated key events @@ -676,6 +675,7 @@ const pressResponderImpl = { if (state.isPressWithinResponderRegion) { if (isPressed) { const onPressMove = props.onPressMove; + if (isFunction(onPressMove)) { dispatchEvent( event, @@ -777,6 +777,7 @@ const pressResponderImpl = { ); } } + if (state.isPressWithinResponderRegion && button !== 1) { dispatchEvent( event, diff --git a/packages/react-events/src/dom/__tests__/Focus-test.internal.js b/packages/react-events/src/dom/__tests__/Focus-test.internal.js index a432f64091cf..316bc9f19dcd 100644 --- a/packages/react-events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Focus-test.internal.js @@ -15,8 +15,8 @@ import { keydown, setPointerEvent, platform, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerDown, + dispatchPointerUp, } from '../test-utils'; let React; @@ -138,8 +138,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is called with the correct pointerType: mouse', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'mouse'}); - dispatchPointerPressRelease(target, {pointerType: 'mouse'}); + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'mouse'}), @@ -148,8 +148,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is called with the correct pointerType: touch', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'touch'}); - dispatchPointerPressRelease(target, {pointerType: 'touch'}); + dispatchPointerDown(target, {pointerType: 'touch'}); + dispatchPointerUp(target, {pointerType: 'touch'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch'}), @@ -159,8 +159,8 @@ describe.each(table)('Focus responder', hasPointerEvents => { if (hasPointerEvents) { it('is called with the correct pointerType: pen', () => { const target = ref.current; - dispatchPointerPressDown(target, {pointerType: 'pen'}); - dispatchPointerPressRelease(target, {pointerType: 'pen'}); + dispatchPointerDown(target, {pointerType: 'pen'}); + dispatchPointerUp(target, {pointerType: 'pen'}); expect(onFocus).toHaveBeenCalledTimes(1); expect(onFocus).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'pen'}), @@ -278,7 +278,7 @@ describe.each(table)('Focus responder', hasPointerEvents => { expect(onFocusVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - dispatchPointerPressDown(target); + dispatchPointerDown(target); expect(onFocusVisibleChange).toHaveBeenCalledTimes(2); expect(onFocusVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again @@ -288,9 +288,9 @@ describe.each(table)('Focus responder', hasPointerEvents => { it('is not called after "focus" and "blur" events without keyboard', () => { const target = ref.current; - dispatchPointerPressDown(target); - dispatchPointerPressRelease(target); - dispatchPointerPressDown(container); + dispatchPointerDown(target); + dispatchPointerUp(target); + dispatchPointerDown(container); target.dispatchEvent(blur({relatedTarget: container})); expect(onFocusVisibleChange).toHaveBeenCalledTimes(0); }); diff --git a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js index e2340a21f669..ff54b6f3838e 100644 --- a/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-events/src/dom/__tests__/FocusWithin-test.internal.js @@ -14,8 +14,8 @@ import { focus, keydown, setPointerEvent, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerDown, + dispatchPointerUp, } from '../test-utils'; let React; @@ -203,7 +203,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the next target, focus should no longer be visible - dispatchPointerPressDown(innerTarget2); + dispatchPointerDown(innerTarget2); innerTarget1.dispatchEvent(blur({relatedTarget: innerTarget2})); innerTarget2.dispatchEvent(focus()); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2); @@ -215,7 +215,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true); // then use pointer on the target, focus should no longer be visible - dispatchPointerPressDown(innerTarget1); + dispatchPointerDown(innerTarget1); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4); expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false); // onFocusVisibleChange should not be called again @@ -225,8 +225,8 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { it('is not called after "focus" and "blur" events without keyboard', () => { const innerTarget = innerRef.current; - dispatchPointerPressDown(innerTarget); - dispatchPointerPressRelease(innerTarget); + dispatchPointerDown(innerTarget); + dispatchPointerUp(innerTarget); innerTarget.dispatchEvent(blur({relatedTarget: container})); expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0); }); diff --git a/packages/react-events/src/dom/__tests__/Hover-test.internal.js b/packages/react-events/src/dom/__tests__/Hover-test.internal.js index eb9eab90c302..2198e7455115 100644 --- a/packages/react-events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Hover-test.internal.js @@ -213,7 +213,8 @@ describe.each(table)('Hover responder', hasPointerEvents => { const target = ref.current; dispatchPointerHoverEnter(target); - dispatchPointerHoverMove(target, {from: {x: 0, y: 0}, to: {x: 1, y: 1}}); + dispatchPointerHoverMove(target, {x: 0, y: 0}); + dispatchPointerHoverMove(target, {x: 1, y: 1}); expect(onHoverMove).toHaveBeenCalledTimes(2); expect(onHoverMove).toHaveBeenCalledWith( expect.objectContaining({type: 'hovermove'}), @@ -317,10 +318,8 @@ describe.each(table)('Hover responder', hasPointerEvents => { const target = ref.current; dispatchPointerHoverEnter(target, {x: 10, y: 10}); - dispatchPointerHoverMove(target, { - from: {x: 10, y: 10}, - to: {x: 20, y: 20}, - }); + dispatchPointerHoverMove(target, {x: 10, y: 10}); + dispatchPointerHoverMove(target, {x: 20, y: 20}); dispatchPointerHoverExit(target, {x: 20, y: 20}); expect(eventLog).toEqual([ diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index de87adde0dc5..a6a42a5d011a 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -9,6 +9,21 @@ 'use strict'; +import { + click, + dispatchPointerCancel, + dispatchPointerDown, + dispatchPointerUp, + dispatchPointerHoverMove, + dispatchPointerMove, + keydown, + keyup, + scroll, + pointerdown, + pointerup, + setPointerEvent, +} from '../test-utils'; + let React; let ReactFeatureFlags; let ReactDOM; @@ -16,39 +31,9 @@ let PressResponder; let usePressResponder; let Scheduler; -const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; -}; - -function createTouchEvent(type, id, data) { - return createEvent(type, { - changedTouches: [ - { - ...data, - identifier: id, - }, - ], - targetTouches: [ - { - ...data, - identifier: id, - }, - ], - }); -} - -const createKeyboardEvent = (type, data) => { - return createEvent(type, data); -}; - -function init() { +function initializeModules(hasPointerEvents) { + jest.resetModules(); + setPointerEvent(hasPointerEvents); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableFlareAPI = true; React = require('react'); @@ -58,12 +43,23 @@ function init() { Scheduler = require('scheduler'); } -describe('Event responder: Press', () => { +function removePressMoveStrings(eventString) { + if (eventString === 'onPressMove') { + return false; + } + return true; +} + +const forcePointerEvents = true; +const environmentTable = [[forcePointerEvents], [!forcePointerEvents]]; + +const pointerTypesTable = [['mouse'], ['touch']]; + +describe.each(environmentTable)('Press responder', hasPointerEvents => { let container; beforeEach(() => { - jest.resetModules(); - init(); + initializeModules(hasPointerEvents); container = document.createElement('div'); document.body.appendChild(container); }); @@ -92,11 +88,13 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('prevents custom events being dispatched', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointerup')); + it('does not call callbacks', () => { + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target); expect(onPressStart).not.toBeCalled(); expect(onPress).not.toBeCalled(); expect(onPressEnd).not.toBeCalled(); @@ -116,27 +114,22 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerdown" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'pressstart'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer down: %s', + pointerType => { + dispatchPointerDown(ref.current, {pointerType}); + expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressStart).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressstart'}), + ); + }, + ); - it('is called after auxillary-button "pointerdown" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {button: 1, pointerType: 'mouse'}), - ); + it('is called after auxillary-button pointer down', () => { + dispatchPointerDown(ref.current, {button: 1, pointerType: 'mouse'}); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -148,64 +141,38 @@ describe('Event responder: Press', () => { }); it('is not called after "pointermove" following auxillary-button press', () => { - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent( - createEvent('pointerdown', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - button: 1, - pointerType: 'mouse', - clientX: 110, - clientY: 110, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - button: 1, - pointerType: 'mouse', - clientX: 50, - clientY: 50, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - }); - - it('ignores browser emulated events', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('touchstart')); - ref.current.dispatchEvent(createEvent('mousedown')); + dispatchPointerDown(target, { + button: 1, + pointerType: 'mouse', + }); + dispatchPointerUp(target, { + button: 1, + pointerType: 'mouse', + }); + dispatchPointerHoverMove(target, {x: 110, y: 110}); + dispatchPointerHoverMove(target, {x: 50, y: 50}); expect(onPressStart).toHaveBeenCalledTimes(1); }); it('ignores any events not caused by primary/auxillary-click or touch/pen contact', () => { - ref.current.dispatchEvent(createEvent('pointerdown', {button: 5})); - ref.current.dispatchEvent(createEvent('mousedown', {button: 2})); + const target = ref.current; + dispatchPointerDown(target, {button: 2}); + dispatchPointerDown(target, {button: 5}); expect(onPressStart).toHaveBeenCalledTimes(0); }); it('is called once after "keydown" events for Enter', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keydown({key: 'Enter'})); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), @@ -213,14 +180,11 @@ describe('Event responder: Press', () => { }); it('is called once after "keydown" events for Spacebar', () => { + const target = ref.current; const preventDefault = jest.fn(); - ref.current.dispatchEvent( - createKeyboardEvent('keydown', {key: ' ', preventDefault}), - ); + target.dispatchEvent(keydown({key: ' ', preventDefault})); expect(preventDefault).toBeCalled(); - ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); + target.dispatchEvent(keydown({key: ' ', preventDefault})); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( expect.objectContaining({ @@ -231,34 +195,9 @@ describe('Event responder: Press', () => { }); it('is not called after "keydown" for other keys', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'a'})); + ref.current.dispatchEvent(keydown({key: 'a'})); expect(onPressStart).not.toBeCalled(); }); - - // No PointerEvent fallbacks - it('is called after "mousedown" event', () => { - ref.current.dispatchEvent( - createEvent('mousedown', { - button: 0, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressstart'}), - ); - }); - - it('is called after "touchstart" event', () => { - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - expect(onPressStart).toHaveBeenCalledTimes(1); - expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressstart'}), - ); - }); }); describe('onPressEnd', () => { @@ -274,36 +213,26 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent(createEvent('pointerup', {pointerType: 'pen'})); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'pressend'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerUp(target, {pointerType}); + expect(onPressEnd).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressend'}), + ); + }, + ); - it('is called after auxillary-button "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {button: 1, pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {button: 1, pointerType: 'mouse'}), - ); + it('is called after auxillary-button pointer up', () => { + const target = ref.current; + dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); + dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({ @@ -314,37 +243,12 @@ describe('Event responder: Press', () => { ); }); - it('ignores browser emulated events', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent(createEvent('mousedown')); - ref.current.dispatchEvent(createEvent('mouseup')); - ref.current.dispatchEvent(createEvent('click')); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressend'}), - ); - }); - it('is called after "keyup" event for Enter', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); // click occurs before keyup - ref.current.dispatchEvent(createKeyboardEvent('click')); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + target.dispatchEvent(click()); + target.dispatchEvent(keyup({key: 'Enter'})); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -352,8 +256,9 @@ describe('Event responder: Press', () => { }); it('is called after "keyup" event for Spacebar', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '})); + const target = ref.current; + target.dispatchEvent(keydown({key: ' '})); + target.dispatchEvent(keyup({key: ' '})); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}), @@ -361,15 +266,17 @@ describe('Event responder: Press', () => { }); it('is not called after "keyup" event for other keys', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'a'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'a'})); expect(onPressEnd).not.toBeCalled(); }); it('is called with keyboard modifiers', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent( - createKeyboardEvent('keyup', { + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent( + keyup({ key: 'Enter', metaKey: true, ctrlKey: true, @@ -388,41 +295,6 @@ describe('Event responder: Press', () => { }), ); }); - - // No PointerEvent fallbacks - it('is called after "mouseup" event', () => { - ref.current.dispatchEvent( - createEvent('mousedown', { - button: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('mouseup', { - button: 0, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressend'}), - ); - }); - it('is called after "touchend" event', () => { - document.elementFromPoint = () => ref.current; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - expect(onPressEnd).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'touch', type: 'pressend'}), - ); - }); }); describe('onPressChange', () => { @@ -438,22 +310,27 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerdown" and "pointerup" events', () => { - ref.current.dispatchEvent(createEvent('pointerdown')); - expect(onPressChange).toHaveBeenCalledTimes(1); - expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createEvent('pointerup')); - expect(onPressChange).toHaveBeenCalledTimes(2); - expect(onPressChange).toHaveBeenCalledWith(false); - }); + it.each(pointerTypesTable)( + 'is called after pointer down and up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + expect(onPressChange).toHaveBeenCalledTimes(1); + expect(onPressChange).toHaveBeenCalledWith(true); + dispatchPointerUp(target, {pointerType}); + expect(onPressChange).toHaveBeenCalledTimes(2); + expect(onPressChange).toHaveBeenCalledWith(false); + }, + ); it('is called after valid "keydown" and "keyup" events', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(keydown({key: 'Enter'})); expect(onPressChange).toHaveBeenCalledTimes(1); expect(onPressChange).toHaveBeenCalledWith(true); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + ref.current.dispatchEvent(keyup({key: 'Enter'})); expect(onPressChange).toHaveBeenCalledTimes(2); expect(onPressChange).toHaveBeenCalledWith(false); }); @@ -478,58 +355,33 @@ describe('Event responder: Press', () => { bottom: 100, right: 100, }); + document.elementFromPoint = () => ref.current; }); - it('is called after "pointerup" event', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'pen'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - target: ref.current, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - pointerType: 'pen', - clientX: 0, - clientY: 0, - }), - ); - expect(onPress).toHaveBeenCalledTimes(1); - expect(onPress).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'pen', type: 'press'}), - ); - }); + it.each(pointerTypesTable)( + 'is called after pointer up: %s', + pointerType => { + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerUp(target, {pointerType, x: 10, y: 10}); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'press'}), + ); + }, + ); it('is not called after auxillary-button press', () => { - const Component = () => { - const listener = usePressResponder({ - onPress, - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.dispatchEvent(createEvent('pointerdown', {button: 1})); - ref.current.dispatchEvent( - createEvent('pointerup', {button: 1, clientX: 10, clientY: 10}), - ); + const target = ref.current; + dispatchPointerDown(target, {button: 1, pointerType: 'mouse'}); + dispatchPointerUp(target, {button: 1, pointerType: 'mouse'}); expect(onPress).not.toHaveBeenCalled(); }); it('is called after valid "keyup" event', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'keyboard', type: 'press'}), @@ -545,26 +397,21 @@ describe('Event responder: Press', () => { return ; }; ReactDOM.render(, container); - inputRef.current.dispatchEvent( - createKeyboardEvent('keydown', {key: 'Enter'}), - ); - inputRef.current.dispatchEvent( - createKeyboardEvent('keyup', {key: 'Enter'}), - ); - inputRef.current.dispatchEvent( - createKeyboardEvent('keydown', {key: ' '}), - ); - inputRef.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '})); + const target = inputRef.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(keyup({key: 'Enter'})); + target.dispatchEvent(keydown({key: ' '})); + target.dispatchEvent(keyup({key: ' '})); expect(onPress).not.toBeCalled(); }); it('is called with modifier keys', () => { - ref.current.dispatchEvent( - createEvent('pointerdown', {metaKey: true, pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {metaKey: true, pointerType: 'mouse'}), - ); + const target = ref.current; + dispatchPointerDown(target, {metaKey: true, pointerType: 'mouse'}); + dispatchPointerUp(target, { + metaKey: true, + pointerType: 'mouse', + }); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({ pointerType: 'mouse', @@ -596,28 +443,19 @@ describe('Event responder: Press', () => { bottom: 0, top: 0, }); - buttonRef.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse'}), - ); - buttonRef.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'mouse'}), - ); + const target = buttonRef.current; + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); expect(onPress).toBeCalled(); }); - - // No PointerEvent fallbacks - // TODO: jsdom missing APIs - // it('is called after "touchend" event', () => { - // ref.current.dispatchEvent(createEvent('touchstart')); - // ref.current.dispatchEvent(createEvent('touchend')); - // expect(onPress).toHaveBeenCalledTimes(1); - // }); }); describe('onPressMove', () => { - it('is called after "pointermove"', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); + let onPressMove, ref; + + beforeEach(() => { + onPressMove = jest.fn(); + ref = React.createRef(); const Component = () => { const listener = usePressResponder({ onPressMove, @@ -625,102 +463,90 @@ describe('Event responder: Press', () => { return
; }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse'}), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - clientX: 10, - clientY: 10, - }), - ); - expect(onPressMove).toHaveBeenCalledTimes(1); - expect(onPressMove).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'mouse', type: 'pressmove'}), - ); + document.elementFromPoint = () => ref.current; }); - it('is not called if "pointermove" occurs during keyboard press', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); - const Component = () => { - const listener = usePressResponder({ - onPressMove, + it.each(pointerTypesTable)( + 'is called after pointer move: %s', + pointerType => { + const target = ref.current; + target.getBoundingClientRect = () => ({ + top: 0, + left: 0, + bottom: 100, + right: 100, }); - return
; - }; - ReactDOM.render(, container); + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, { + pointerType, + x: 10, + y: 10, + }); + dispatchPointerMove(target, { + pointerType, + x: 20, + y: 20, + }); + expect(onPressMove).toHaveBeenCalledTimes(2); + expect(onPressMove).toHaveBeenCalledWith( + expect.objectContaining({pointerType, type: 'pressmove'}), + ); + }, + ); - ref.current.getBoundingClientRect = () => ({ + it('is not called if pointer move occurs during keyboard press', () => { + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - clientX: 10, - clientY: 10, - }), - ); + target.dispatchEvent(keydown({key: 'Enter'})); + dispatchPointerMove(target, { + button: -1, + pointerType: 'mouse', + x: 10, + y: 10, + }); expect(onPressMove).not.toBeCalled(); }); + }); - it('ignores browser emulated events', () => { - const onPressMove = jest.fn(); - const ref = React.createRef(); + describe.each(pointerTypesTable)('press with movement: %s', pointerType => { + let events, ref, outerRef; + + beforeEach(() => { + events = []; + ref = React.createRef(); + outerRef = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; const Component = () => { const listener = usePressResponder({ - onPressMove, + onPress: createEventHandler('onPress'), + onPressChange: createEventHandler('onPressChange'), + onPressMove: createEventHandler('onPressMove'), + onPressStart: createEventHandler('onPressStart'), + onPressEnd: createEventHandler('onPressEnd'), }); - return
; + return ( +
+
+
+ ); }; ReactDOM.render(, container); - - ref.current.getBoundingClientRect = () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'touch'}), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'touch', - clientX: 10, - clientY: 10, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - target: ref.current, - clientX: 10, - clientY: 10, - }), - ); - ref.current.dispatchEvent(createEvent('mousemove')); - expect(onPressMove).toHaveBeenCalledTimes(1); + document.elementFromPoint = () => ref.current; }); - }); - describe('press with movement (pointer events)', () => { const rectMock = { width: 100, height: 100, @@ -732,12 +558,12 @@ describe('Event responder: Press', () => { const pressRectOffset = 20; const getBoundingClientRectMock = () => rectMock; const coordinatesInside = { - clientX: rectMock.left - pressRectOffset, - clientY: rectMock.top - pressRectOffset, + x: rectMock.left - pressRectOffset, + y: rectMock.top - pressRectOffset, }; const coordinatesOutside = { - clientX: rectMock.left - pressRectOffset - 1, - clientY: rectMock.top - pressRectOffset - 1, + x: rectMock.left - pressRectOffset - 1, + y: rectMock.top - pressRectOffset - 1, }; describe('within bounds of hit rect', () => { @@ -749,32 +575,12 @@ describe('Event responder: Press', () => { * └──────────────────┘ */ it('"onPress*" events are called immediately', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); jest.runAllTimers(); - expect(events).toEqual([ 'onPressStart', 'onPressChange', @@ -786,12 +592,41 @@ describe('Event responder: Press', () => { }); it('"onPress*" events are correctly called with target change', () => { - let events = []; - const outerRef = React.createRef(); - const innerRef = React.createRef(); + const target = ref.current; + const outer = outerRef.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + // TODO: this sequence may differ in the future between PointerEvent and mouse fallback when + // use 'setPointerCapture'. + if (pointerType === 'touch') { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(outer, {pointerType, ...coordinatesOutside}); + } + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); + + expect(events.filter(removePressMoveStrings)).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); + }); + + it('press retention offset can be configured', () => { + let localEvents = []; + const localRef = React.createRef(); const createEventHandler = msg => () => { - events.push(msg); + localEvents.push(msg); }; + const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40}; const Component = () => { const listener = usePressResponder({ @@ -800,72 +635,22 @@ describe('Event responder: Press', () => { onPressMove: createEventHandler('onPressMove'), onPressStart: createEventHandler('onPressStart'), onPressEnd: createEventHandler('onPressEnd'), + pressRetentionOffset, }); - return ( -
-
-
- ); + return
; }; ReactDOM.render(, container); - innerRef.current.getBoundingClientRect = getBoundingClientRectMock; - innerRef.current.dispatchEvent(createEvent('pointerdown')); - outerRef.current.dispatchEvent( - createEvent('pointermove', coordinatesOutside), - ); - innerRef.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - innerRef.current.dispatchEvent( - createEvent('pointerup', coordinatesInside), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressEnd', - 'onPressChange', - 'onPressStart', - 'onPressChange', - '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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - pressRetentionOffset, - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', { - clientX: rectMock.left - pressRetentionOffset.left, - clientY: rectMock.top - pressRetentionOffset.top, - }), - ); - ref.current.dispatchEvent(createEvent('pointerup', coordinatesInside)); - expect(events).toEqual([ + const target = localRef.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, { + pointerType, + x: rectMock.left, + y: rectMock.top, + }); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); + expect(localEvents).toEqual([ 'onPressStart', 'onPressChange', 'onPressMove', @@ -876,26 +661,11 @@ describe('Event responder: Press', () => { }); it('responder region accounts for decrease in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); // emulate smaller dimensions change on activation - ref.current.getBoundingClientRect = () => ({ + target.getBoundingClientRect = () => ({ width: 80, height: 80, top: 60, @@ -904,36 +674,28 @@ describe('Event responder: Press', () => { bottom: 490, }); const coordinates = { - clientX: rectMock.left, - clientY: rectMock.top, + x: rectMock.left, + y: rectMock.top, }; // move to an area within the pre-activation region - ref.current.dispatchEvent(createEvent('pointermove', coordinates)); - ref.current.dispatchEvent(createEvent('pointerup', coordinates)); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); + dispatchPointerMove(target, {pointerType, ...coordinates}); + dispatchPointerUp(target, {pointerType, ...coordinates}); + expect(events).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressMove', + 'onPressEnd', + 'onPressChange', + 'onPress', + ]); }); it('responder region accounts for increase in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); // emulate larger dimensions change on activation - ref.current.getBoundingClientRect = () => ({ + target.getBoundingClientRect = () => ({ width: 200, height: 200, top: 0, @@ -942,397 +704,12 @@ describe('Event responder: Press', () => { bottom: 550, }); const coordinates = { - clientX: rectMock.left - 50, - clientY: rectMock.top - 50, + x: rectMock.left - 50, + y: rectMock.top - 50, }; // move to an area within the post-activation region - ref.current.dispatchEvent(createEvent('pointermove', coordinates)); - ref.current.dispatchEvent(createEvent('pointerup', 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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointermove', coordinatesInside), - ); - container.dispatchEvent(createEvent('pointermove', coordinatesOutside)); - container.dispatchEvent(createEvent('pointerup', coordinatesOutside)); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - ]); - }); - }); - - it('"onPress" is not called on release with mouse', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'mouse', - }), - ); - container.dispatchEvent( - createEvent('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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesOutside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: ref.current, - }), - ); - container.dispatchEvent( - createEvent('pointermove', { - ...coordinatesInside, - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - container.dispatchEvent( - 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: 150, - bottom: 150, - }; - const pressRectOffset = 20; - const getBoundingClientRectMock = () => rectMock; - const coordinatesInside = { - clientX: rectMock.left - pressRectOffset, - clientY: rectMock.top - pressRectOffset, - }; - const coordinatesOutside = { - clientX: rectMock.left - pressRectOffset - 1, - clientY: rectMock.top - pressRectOffset - 1, - }; - - describe('within bounds of hit rect', () => { - /** ┌──────────────────┐ - * │ ┌────────────┐ │ - * │ │ VisualRect │ │ - * │ └────────────┘ │ - * │ HitRect X │ <= Move to X and release - * └──────────────────┘ - */ - it('"onPress*" events are called immediately', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - '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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - pressRetentionOffset, - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - clientX: rectMock.left - pressRetentionOffset.left, - clientY: rectMock.top - pressRetentionOffset.top, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); + dispatchPointerMove(target, {pointerType, ...coordinates}); + dispatchPointerUp(target, {pointerType, ...coordinates}); expect(events).toEqual([ 'onPressStart', 'onPressChange', @@ -1342,112 +719,6 @@ describe('Event responder: Press', () => { 'onPress', ]); }); - - it('responder region accounts for decrease in element dimensions', () => { - let events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - // emulate smaller dimensions change on activation - ref.current.getBoundingClientRect = () => ({ - width: 80, - height: 80, - top: 60, - left: 60, - right: 140, - bottom: 140, - }); - const coordinates = { - clientX: rectMock.left, - clientY: rectMock.top, - }; - // move to an area within the pre-activation region - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinates, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinates, - target: ref.current, - }), - ); - 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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - // emulate larger dimensions change on activation - ref.current.getBoundingClientRect = () => ({ - width: 200, - height: 200, - top: 0, - left: 0, - right: 200, - bottom: 200, - }); - const coordinates = { - clientX: rectMock.left - 50, - clientY: rectMock.top - 50, - }; - // move to an area within the post-activation region - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinates, - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinates, - target: ref.current, - }), - ); - expect(events).toEqual(['onPressStart', 'onPressEnd', 'onPress']); - }); }); describe('beyond bounds of hit rect', () => { @@ -1459,117 +730,40 @@ describe('Event responder: Press', () => { * └──────────────────┘ * 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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => container; - container.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: container, - }), - ); - container.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesOutside, - target: container, - }), - ); - jest.runAllTimers(); - - expect(events).toEqual([ - 'onPressStart', - 'onPressChange', - 'onPressMove', - 'onPressEnd', - 'onPressChange', - ]); - }); - }); - - 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 Component = () => { - const listener = usePressResponder({ - onPress: createEventHandler('onPress'), - onPressChange: createEventHandler('onPressChange'), - onPressMove: createEventHandler('onPressMove'), - onPressStart: createEventHandler('onPressStart'), - onPressEnd: createEventHandler('onPressEnd'), - }); - return
; - }; - ReactDOM.render(, container); - - document.elementFromPoint = () => ref.current; - ref.current.getBoundingClientRect = getBoundingClientRectMock; - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => container; - container.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesOutside, - target: container, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchmove', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - document.elementFromPoint = () => ref.current; - ref.current.dispatchEvent( - createTouchEvent('touchend', 0, { - ...coordinatesInside, - target: ref.current, - }), - ); - jest.runAllTimers(); + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + if (pointerType === 'mouse') { + // TODO: use setPointerCapture so this is only true for fallback mouse events. + dispatchPointerMove(container, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } + dispatchPointerUp(container, {pointerType, ...coordinatesOutside}); + expect(events.filter(removePressMoveStrings)).toEqual([ + 'onPressStart', + 'onPressChange', + 'onPressEnd', + 'onPressChange', + ]); + }); + }); + + it('"onPress" is called on re-entry to hit rect', () => { + const target = ref.current; + target.getBoundingClientRect = getBoundingClientRectMock; + dispatchPointerDown(target, {pointerType}); + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + if (pointerType === 'mouse') { + // TODO: use setPointerCapture so this is only true for fallback mouse events. + dispatchPointerMove(container, {pointerType, ...coordinatesOutside}); + } else { + dispatchPointerMove(target, {pointerType, ...coordinatesOutside}); + } + dispatchPointerMove(target, {pointerType, ...coordinatesInside}); + dispatchPointerUp(target, {pointerType, ...coordinatesInside}); expect(events).toEqual([ 'onPressStart', @@ -1587,71 +781,71 @@ describe('Event responder: Press', () => { }); describe('nested responders', () => { - it('dispatch events in the correct order', () => { - const events = []; - const ref = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; + if (hasPointerEvents) { + it('dispatch events in the correct order', () => { + const events = []; + const ref = React.createRef(); + const createEventHandler = msg => () => { + events.push(msg); + }; - const Inner = () => { - const listener = usePressResponder({ - onPress: createEventHandler('inner: onPress'), - onPressChange: createEventHandler('inner: onPressChange'), - onPressMove: createEventHandler('inner: onPressMove'), - onPressStart: createEventHandler('inner: onPressStart'), - onPressEnd: createEventHandler('inner: onPressEnd'), - stopPropagation: false, - }); - return ( -
- ); - }; + const Inner = () => { + const listener = usePressResponder({ + onPress: createEventHandler('inner: onPress'), + onPressChange: createEventHandler('inner: onPressChange'), + onPressMove: createEventHandler('inner: onPressMove'), + onPressStart: createEventHandler('inner: onPressStart'), + onPressEnd: createEventHandler('inner: onPressEnd'), + stopPropagation: false, + }); + return ( +
+ ); + }; - const Outer = () => { - const listener = usePressResponder({ - onPress: createEventHandler('outer: onPress'), - onPressChange: createEventHandler('outer: onPressChange'), - onPressMove: createEventHandler('outer: onPressMove'), - onPressStart: createEventHandler('outer: onPressStart'), - onPressEnd: createEventHandler('outer: onPressEnd'), - }); - return ( -
- -
- ); - }; - ReactDOM.render(, container); + const Outer = () => { + const listener = usePressResponder({ + onPress: createEventHandler('outer: onPress'), + onPressChange: createEventHandler('outer: onPressChange'), + onPressMove: createEventHandler('outer: onPressMove'), + onPressStart: createEventHandler('outer: onPressStart'), + onPressEnd: createEventHandler('outer: onPressEnd'), + }); + return ( +
+ +
+ ); + }; + ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, + const target = ref.current; + target.getBoundingClientRect = () => ({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }); + dispatchPointerDown(target); + dispatchPointerUp(target); + expect(events).toEqual([ + 'inner: onPressStart', + 'inner: onPressChange', + 'pointerdown', + 'inner: onPressEnd', + 'inner: onPressChange', + 'inner: onPress', + 'pointerup', + ]); }); - - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', {clientX: 10, clientY: 10}), - ); - expect(events).toEqual([ - 'inner: onPressStart', - 'inner: onPressChange', - 'pointerdown', - 'inner: onPressEnd', - 'inner: onPressChange', - 'inner: onPress', - 'pointerup', - ]); - }); + } describe('correctly not propagate', () => { it('for onPress', () => { @@ -1677,17 +871,15 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 0, left: 0, bottom: 100, right: 100, }); - - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', {clientX: 10, clientY: 10}), - ); + dispatchPointerDown(target); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(1); }); @@ -1717,10 +909,11 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(0); - ref.current.dispatchEvent(createEvent('pointerup')); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); }); @@ -1748,16 +941,17 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); expect(fn).toHaveBeenCalledTimes(1); - ref.current.dispatchEvent(createEvent('pointerup')); + dispatchPointerUp(target); expect(fn).toHaveBeenCalledTimes(2); }); }); }); describe('link components', () => { - it('prevents native behaviour for pointer events by default', () => { + it('prevents native behavior by default', () => { const onPress = jest.fn(); const preventDefault = jest.fn(); const ref = React.createRef(); @@ -1770,14 +964,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1797,9 +986,10 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); - ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(click({preventDefault})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1823,14 +1013,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - buttonRef.current.dispatchEvent(createEvent('pointerdown')); - buttonRef.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - buttonRef.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = buttonRef.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); }); @@ -1851,14 +1036,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: true}), @@ -1879,19 +1059,9 @@ describe('Event responder: Press', () => { ReactDOM.render(, container); ['metaKey', 'ctrlKey', 'shiftKey'].forEach(modifierKey => { - ref.current.dispatchEvent( - createEvent('pointerdown', {[modifierKey]: true}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - [modifierKey]: true, - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent( - createEvent('click', {[modifierKey]: true, preventDefault}), - ); + const target = ref.current; + dispatchPointerDown(target, {[modifierKey]: true}); + dispatchPointerUp(target, {[modifierKey]: true, preventDefault}); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1913,14 +1083,9 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent( - createEvent('pointerup', { - clientX: 0, - clientY: 0, - }), - ); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerUp(target, {preventDefault}); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1941,9 +1106,10 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); - ref.current.dispatchEvent(createEvent('click', {preventDefault})); - ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + const target = ref.current; + target.dispatchEvent(keydown({key: 'Enter'})); + target.dispatchEvent(click({preventDefault})); + target.dispatchEvent(keyup({key: 'Enter'})); expect(preventDefault).not.toBeCalled(); expect(onPress).toHaveBeenCalledWith( expect.objectContaining({defaultPrevented: false}), @@ -1952,7 +1118,7 @@ describe('Event responder: Press', () => { }); describe('responder cancellation', () => { - it('ends on "pointercancel", "touchcancel", "scroll", and "dragstart"', () => { + it.each(pointerTypesTable)('ends on pointer cancel', pointerType => { const onPressEnd = jest.fn(); const ref = React.createRef(); @@ -1964,64 +1130,14 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - // Should cancel for non-mouse events - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'touch', - }), - ); - ref.current.dispatchEvent(createEvent('scroll')); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Should not cancel for mouse events - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent(createEvent('scroll')); - expect(onPressEnd).toHaveBeenCalledTimes(0); - - // When pointer events are supported - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - }), - ); - ref.current.dispatchEvent( - createEvent('pointercancel', { - pointerType: 'mouse', - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Touch fallback - ref.current.dispatchEvent( - createTouchEvent('touchstart', 0, { - target: ref.current, - }), - ); - ref.current.dispatchEvent( - createTouchEvent('touchcancel', 0, { - target: ref.current, - }), - ); - expect(onPressEnd).toHaveBeenCalledTimes(1); - - onPressEnd.mockReset(); - - // Mouse fallback - ref.current.dispatchEvent(createEvent('mousedown')); - ref.current.dispatchEvent(createEvent('dragstart')); + const target = ref.current; + dispatchPointerDown(target, {pointerType}); + dispatchPointerCancel(target, {pointerType}); expect(onPressEnd).toHaveBeenCalledTimes(1); }); }); - it('does end on "scroll" to document', () => { + it('does end on "scroll" to document (not mouse)', () => { const onPressEnd = jest.fn(); const ref = React.createRef(); @@ -2033,12 +1149,13 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - document.dispatchEvent(createEvent('scroll')); + const target = ref.current; + dispatchPointerDown(target, {pointerType: 'touch'}); + document.dispatchEvent(scroll()); expect(onPressEnd).toHaveBeenCalledTimes(1); }); - it('does end on "scroll" to a parent container', () => { + it('does end on "scroll" to a parent container (not mouse)', () => { const onPressEnd = jest.fn(); const ref = React.createRef(); const containerRef = React.createRef(); @@ -2055,8 +1172,8 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - containerRef.current.dispatchEvent(createEvent('scroll')); + dispatchPointerDown(ref.current, {pointerType: 'touch'}); + containerRef.current.dispatchEvent(scroll()); expect(onPressEnd).toHaveBeenCalledTimes(1); }); @@ -2078,8 +1195,8 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - outsideRef.current.dispatchEvent(createEvent('scroll')); + dispatchPointerDown(ref.current); + outsideRef.current.dispatchEvent(scroll()); expect(onPressEnd).not.toBeCalled(); }); @@ -2095,10 +1212,11 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.dispatchEvent(createEvent('pointerdown')); - ref.current.dispatchEvent(createEvent('pointermove')); - ref.current.dispatchEvent(createEvent('pointerup')); - ref.current.dispatchEvent(createEvent('pointerdown')); + const target = ref.current; + dispatchPointerDown(target); + dispatchPointerMove(target); + dispatchPointerUp(target); + dispatchPointerDown(target); }); it('should correctly pass through event properties', () => { @@ -2133,57 +1251,49 @@ describe('Event responder: Press', () => { }; ReactDOM.render(, container); - ref.current.getBoundingClientRect = () => ({ + const target = ref.current; + target.getBoundingClientRect = () => ({ top: 10, left: 10, bottom: 110, right: 110, }); - - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - pageX: 15, - pageY: 16, - screenX: 20, - screenY: 21, - clientX: 30, - clientY: 31, - }), - ); - ref.current.dispatchEvent( - createEvent('pointermove', { - pointerType: 'mouse', - pageX: 16, - pageY: 17, - screenX: 21, - screenY: 22, - clientX: 31, - clientY: 32, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerup', { - pointerType: 'mouse', - pageX: 17, - pageY: 18, - screenX: 22, - screenY: 23, - clientX: 32, - clientY: 33, - }), - ); - ref.current.dispatchEvent( - createEvent('pointerdown', { - pointerType: 'mouse', - pageX: 18, - pageY: 19, - screenX: 23, - screenY: 24, - clientX: 33, - clientY: 34, - }), - ); + dispatchPointerDown(target, { + pointerType: 'mouse', + pageX: 15, + pageY: 16, + screenX: 20, + screenY: 21, + clientX: 30, + clientY: 31, + }); + dispatchPointerMove(target, { + pointerType: 'mouse', + pageX: 16, + pageY: 17, + screenX: 21, + screenY: 22, + clientX: 31, + clientY: 32, + }); + dispatchPointerUp(target, { + pointerType: 'mouse', + pageX: 17, + pageY: 18, + screenX: 22, + screenY: 23, + clientX: 32, + clientY: 33, + }); + dispatchPointerDown(target, { + pointerType: 'mouse', + pageX: 18, + pageY: 19, + screenX: 23, + screenY: 24, + clientX: 33, + clientY: 34, + }); expect(typeof timeStamps[0] === 'number').toBe(true); expect(eventLog).toEqual([ { @@ -2249,175 +1359,85 @@ describe('Event responder: Press', () => { ]); }); - function dispatchEventWithTimeStamp(elem, name, timeStamp) { - const event = createEvent(name, { - clientX: 0, - clientY: 0, - }); - Object.defineProperty(event, 'timeStamp', { - value: timeStamp, - }); - elem.dispatchEvent(event); - } - - it('should properly only flush sync once when the event systems are mixed', () => { - const ref = React.createRef(); - let renderCounts = 0; - - function MyComponent() { - const [, updateCounter] = React.useState(0); - renderCounts++; - - function handlePress() { - updateCounter(count => count + 1); - } - - const listener = usePressResponder({ - onPress: handlePress, - }); - - return ( -
- -
- ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - dispatchEventWithTimeStamp(ref.current, 'click', 100); - - if (__DEV__) { - expect(renderCounts).toBe(2); - } else { - expect(renderCounts).toBe(1); - } - Scheduler.unstable_flushAll(); - if (__DEV__) { - expect(renderCounts).toBe(4); - } else { - expect(renderCounts).toBe(2); - } - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - // Ensure the timeStamp logic works - dispatchEventWithTimeStamp(ref.current, 'click', 101); - - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } + if (hasPointerEvents) { + it('should properly only flush sync once when the event systems are mixed', () => { + const ref = React.createRef(); + let renderCounts = 0; - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); + function MyComponent() { + const [, updateCounter] = React.useState(0); + renderCounts++; - it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { - const ref = React.createRef(); - let renderCounts = 0; + function handlePress() { + updateCounter(count => count + 1); + } - function MyComponent() { - const [, updateCounter] = React.useState(0); - renderCounts++; + const listener = usePressResponder({ + onPress: handlePress, + }); - function handlePress() { - updateCounter(count => count + 1); + return ( +
+ +
+ ); } - const listener = usePressResponder({ - onPress: handlePress, - }); - - return ( -
- -
- ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - dispatchEventWithTimeStamp(ref.current, 'click', 100); - - if (__DEV__) { - expect(renderCounts).toBe(4); - } else { - expect(renderCounts).toBe(2); - } - Scheduler.unstable_flushAll(); - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } + const newContainer = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(newContainer); + document.body.appendChild(newContainer); + root.render(); + Scheduler.unstable_flushAll(); - dispatchEventWithTimeStamp(ref.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(ref.current, 'pointerup', 100); - // Ensure the timeStamp logic works - dispatchEventWithTimeStamp(ref.current, 'click', 101); + const target = ref.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); - if (__DEV__) { - expect(renderCounts).toBe(8); - } else { - expect(renderCounts).toBe(4); - } + if (__DEV__) { + expect(renderCounts).toBe(2); + } else { + expect(renderCounts).toBe(1); + } + Scheduler.unstable_flushAll(); + if (__DEV__) { + expect(renderCounts).toBe(4); + } else { + expect(renderCounts).toBe(2); + } - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + // Ensure the timeStamp logic works + target.dispatchEvent(click({timeStamp: 101})); - it( - 'should only flush before outermost discrete event handler when mixing ' + - 'event systems', - async () => { - const {useState} = React; + if (__DEV__) { + expect(renderCounts).toBe(6); + } else { + expect(renderCounts).toBe(3); + } - const button = React.createRef(); + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); - const ops = []; + it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { + const ref = React.createRef(); + let renderCounts = 0; function MyComponent() { - const [pressesCount, updatePressesCount] = useState(0); - const [clicksCount, updateClicksCount] = useState(0); + const [, updateCounter] = React.useState(0); + renderCounts++; function handlePress() { - // This dispatches a synchronous, discrete event in the legacy event - // system. However, because it's nested inside the new event system, - // its updates should not flush until the end of the outer handler. - button.current.click(); - // Text context should not have changed - ops.push(newContainer.textContent); - updatePressesCount(pressesCount + 1); + updateCounter(count => count + 1); } const listener = usePressResponder({ @@ -2427,76 +1447,131 @@ describe('Event responder: Press', () => { return (
); } const newContainer = document.createElement('div'); - document.body.appendChild(newContainer); const root = ReactDOM.unstable_createRoot(newContainer); - + document.body.appendChild(newContainer); root.render(); Scheduler.unstable_flushAll(); - expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0'); - dispatchEventWithTimeStamp(button.current, 'pointerdown', 100); - dispatchEventWithTimeStamp(button.current, 'pointerup', 100); - dispatchEventWithTimeStamp(button.current, 'click', 100); + const target = ref.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); + + if (__DEV__) { + expect(renderCounts).toBe(4); + } else { + expect(renderCounts).toBe(2); + } Scheduler.unstable_flushAll(); - expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1'); + if (__DEV__) { + expect(renderCounts).toBe(6); + } else { + expect(renderCounts).toBe(3); + } - expect(ops).toEqual(['Presses: 0, Clicks: 0']); - }, - ); + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + // Ensure the timeStamp logic works + target.dispatchEvent(click({timeStamp: 101})); - it('should work correctly with stopPropagation set to true', () => { - const ref = React.createRef(); - const pointerDownEvent = jest.fn(); + if (__DEV__) { + expect(renderCounts).toBe(8); + } else { + expect(renderCounts).toBe(4); + } - const Component = () => { - const listener = usePressResponder({stopPropagation: true}); + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); - return
; - }; + it( + 'should only flush before outermost discrete event handler when mixing ' + + 'event systems', + async () => { + const {useState} = React; - container.addEventListener('pointerdown', pointerDownEvent); - ReactDOM.render(, container); + const button = React.createRef(); - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 0}), - ); - container.removeEventListener('pointerdown', pointerDownEvent); - expect(pointerDownEvent).toHaveBeenCalledTimes(0); - }); + const ops = []; - it('has the correct press target when used with event hook', () => { - const ref = React.createRef(); - const onPress = jest.fn(); - const Component = () => { - const listener = usePressResponder({onPress}); + function MyComponent() { + const [pressesCount, updatePressesCount] = useState(0); + const [clicksCount, updateClicksCount] = useState(0); - return ( -
- -
- ); - }; - ReactDOM.render(, container); + function handlePress() { + // This dispatches a synchronous, discrete event in the legacy event + // system. However, because it's nested inside the new event system, + // its updates should not flush until the end of the outer handler. + button.current.click(); + // Text context should not have changed + ops.push(newContainer.textContent); + updatePressesCount(pressesCount + 1); + } - ref.current.dispatchEvent( - createEvent('pointerdown', {pointerType: 'mouse', button: 0}), - ); - ref.current.dispatchEvent( - createEvent('pointerup', {pointerType: 'mouse', button: 0}), - ); - expect(onPress).toHaveBeenCalledTimes(1); - expect(onPress).toHaveBeenCalledWith( - expect.objectContaining({target: ref.current}), + const listener = usePressResponder({ + onPress: handlePress, + }); + + return ( +
+ +
+ ); + } + + const newContainer = document.createElement('div'); + document.body.appendChild(newContainer); + const root = ReactDOM.unstable_createRoot(newContainer); + + root.render(); + Scheduler.unstable_flushAll(); + expect(newContainer.textContent).toEqual('Presses: 0, Clicks: 0'); + + const target = button.current; + target.dispatchEvent(pointerdown({timeStamp: 100})); + target.dispatchEvent(pointerup({timeStamp: 100})); + target.dispatchEvent(click({timeStamp: 100})); + + Scheduler.unstable_flushAll(); + expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1'); + + expect(ops).toEqual(['Presses: 0, Clicks: 0']); + }, ); - }); + + it('should work correctly with stopPropagation set to true', () => { + const ref = React.createRef(); + const pointerDownEvent = jest.fn(); + + const Component = () => { + const listener = usePressResponder({stopPropagation: true}); + return
; + }; + + container.addEventListener('pointerdown', pointerDownEvent); + ReactDOM.render(, container); + dispatchPointerDown(ref.current); + container.removeEventListener('pointerdown', pointerDownEvent); + expect(pointerDownEvent).toHaveBeenCalledTimes(0); + }); + } }); diff --git a/packages/react-events/src/dom/test-utils.js b/packages/react-events/src/dom/test-utils.js index 826ac219ce58..9597af078194 100644 --- a/packages/react-events/src/dom/test-utils.js +++ b/packages/react-events/src/dom/test-utils.js @@ -15,7 +15,7 @@ * Change environment support for PointerEvent. */ -function hasPointerEvent(bool) { +function hasPointerEvent() { return global != null && global.PointerEvent != null; } @@ -57,23 +57,38 @@ const platform = { * Mock native events */ -function createEvent(type, data) { +function createEvent(type, data = {}) { const event = document.createEvent('CustomEvent'); event.initCustomEvent(type, true, true); + event.clientX = data.x || 0; + event.clientY = data.y || 0; + event.x = data.x || 0; + event.y = data.y || 0; if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; + Object.keys(data).forEach(key => { + const value = data[key]; + Object.defineProperty(event, key, {value}); }); } return event; } -function createTouchEvent(type, data, id) { +function createTouchEvent(type, data = {}, id) { return createEvent(type, { changedTouches: [ { + identifier: id, + clientX: data.x || 0, + clientY: data.y || 0, ...data, + }, + ], + targetTouches: [ + { identifier: id, + clientX: 0 || data.x, + clientY: 0 || data.y, + ...data, }, ], }); @@ -112,17 +127,45 @@ function gotpointercapture(data) { } function keydown(data) { - return createKeyboardEvent('keydown', data); + return createEvent('keydown', data); } function keyup(data) { - return createKeyboardEvent('keyup', data); + return createEvent('keyup', data); } function lostpointercapture(data) { return createEvent('lostpointercapture', data); } +function mousedown(data) { + return createEvent('mousedown', data); +} + +function mouseenter(data) { + return createEvent('mouseenter', data); +} + +function mouseleave(data) { + return createEvent('mouseleave', data); +} + +function mousemove(data) { + return createEvent('mousemove', data); +} + +function mouseout(data) { + return createEvent('mouseout', data); +} + +function mouseover(data) { + return createEvent('mouseover', data); +} + +function mouseup(data) { + return createEvent('mouseup', data); +} + function pointercancel(data) { return createEvent('pointercancel', data); } @@ -155,32 +198,8 @@ function pointerup(data) { return createEvent('pointerup', data); } -function mousedown(data) { - return createEvent('mousedown', data); -} - -function mouseenter(data) { - return createEvent('mouseenter', data); -} - -function mouseleave(data) { - return createEvent('mouseleave', data); -} - -function mousemove(data) { - return createEvent('mousemove', data); -} - -function mouseout(data) { - return createEvent('mouseout', data); -} - -function mouseover(data) { - return createEvent('mouseover', data); -} - -function mouseup(data) { - return createEvent('mouseup', data); +function scroll(data) { + return createEvent('scroll', data); } function touchcancel(data, id) { @@ -264,15 +283,15 @@ function dispatchPointerHoverEnter(target, {relatedTarget, x, y} = {}) { dispatch(pointerenter({pointerType, ...event})); } dispatch(mouseover(event)); - dispatch(mouseover(event)); + dispatch(mouseenter(event)); } -function dispatchPointerHoverMove(target, {from, to} = {}) { +function dispatchPointerHoverMove(target, {x, y} = {}) { const dispatch = arg => target.dispatchEvent(arg); const button = -1; const pointerId = 1; const pointerType = 'mouse'; - function dispatchMove({x, y}) { + function dispatchMove() { const event = { button, clientX: x, @@ -285,8 +304,7 @@ function dispatchPointerHoverMove(target, {from, to} = {}) { } dispatch(mousemove(event)); } - dispatchMove({x: from.x, y: from.y}); - dispatchMove({x: to.x, y: to.y}); + dispatchMove(); } function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) { @@ -309,77 +327,135 @@ function dispatchPointerHoverExit(target, {relatedTarget, x, y} = {}) { dispatch(mouseleave(event)); } -function dispatchPointerCancel(target, options) { +function dispatchPointerCancel(target, {pointerType = 'mouse', ...rest} = {}) { const dispatchEvent = arg => target.dispatchEvent(arg); - dispatchEvent(pointercancel({pointerType: 'mouse'})); - dispatchEvent(dragstart({pointerType: 'mouse'})); + if (hasPointerEvent()) { + dispatchEvent(pointercancel({pointerType, ...rest})); + } else { + if (pointerType === 'mouse') { + dispatchEvent(dragstart({...rest})); + } else { + dispatchEvent(touchcancel({...rest})); + } + } } -function dispatchPointerPressDown( +function dispatchPointerDown( target, - {button = 0, pointerType = 'mouse'} = {}, + {button = 0, pointerType = 'mouse', ...rest} = {}, ) { const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (pointerType !== 'mouse') { + const pointerEvent = {button, pointerId, pointerType, ...rest}; + const mouseEvent = {button, ...rest}; + const touch = {...rest}; + + if (pointerType === 'mouse') { if (hasPointerEvent()) { - dispatch(pointerover({button, pointerId, pointerType})); - dispatch(pointerenter({button, pointerId, pointerType})); - dispatch(pointerdown({button, pointerId, pointerType})); + dispatch(pointerover(pointerEvent)); + dispatch(pointerenter(pointerEvent)); } - dispatch(touchstart(null, pointerId)); + dispatch(mouseover(mouseEvent)); + dispatch(mouseenter(mouseEvent)); if (hasPointerEvent()) { - dispatch(gotpointercapture({button, pointerId, pointerType})); + dispatch(pointerdown(pointerEvent)); + } + dispatch(mousedown(mouseEvent)); + if (document.activeElement !== target) { + dispatch(focus()); } } else { if (hasPointerEvent()) { - dispatch(pointerdown({button, pointerId, pointerType})); + dispatch(pointerover(pointerEvent)); + dispatch(pointerenter(pointerEvent)); + dispatch(pointerdown(pointerEvent)); } - dispatch(mousedown({button})); - if (document.activeElement !== target) { - dispatch(focus({button})); + dispatch(touchstart(touch, pointerId)); + if (hasPointerEvent()) { + dispatch(gotpointercapture(pointerEvent)); } } } -function dispatchPointerPressRelease( +function dispatchPointerUp( target, - {button = 0, pointerType = 'mouse'} = {}, + {button = 0, pointerType = 'mouse', ...rest} = {}, ) { const dispatch = arg => target.dispatchEvent(arg); const pointerId = 1; - if (pointerType !== 'mouse') { + const pointerEvent = {button, pointerId, pointerType, ...rest}; + const mouseEvent = {button, ...rest}; + const touch = {...rest}; + + if (pointerType === 'mouse') { if (hasPointerEvent()) { - dispatch(pointerup({button, pointerId, pointerType})); - dispatch(lostpointercapture({button, pointerId, pointerType})); - dispatch(pointerout({button, pointerId, pointerType})); - dispatch(pointerleave({button, pointerId, pointerType})); - } - dispatch(touchend(null, pointerId)); - dispatch(mouseover({button})); - dispatch(mousemove({button})); - dispatch(mousedown({button})); - if (document.activeElement !== target) { - dispatch(focus({button})); + dispatch(pointerup(pointerEvent)); } - dispatch(mouseup({button})); - dispatch(click({button})); + dispatch(mouseup(mouseEvent)); + dispatch(click(mouseEvent)); } else { if (hasPointerEvent()) { - dispatch(pointerup({button, pointerId, pointerType})); + dispatch(pointerup(pointerEvent)); + dispatch(lostpointercapture(pointerEvent)); + dispatch(pointerout(pointerEvent)); + dispatch(pointerleave(pointerEvent)); } - dispatch(mouseup({button})); - dispatch(click({button})); + dispatch(touchend(touch, pointerId)); + dispatch(mouseover(mouseEvent)); + dispatch(mousemove(mouseEvent)); + dispatch(mousedown(mouseEvent)); + if (document.activeElement !== target) { + dispatch(focus()); + } + dispatch(mouseup(mouseEvent)); + dispatch(click(mouseEvent)); + } +} + +function dispatchPointerMove( + target, + {button = 0, pointerType = 'mouse', ...rest} = {}, +) { + const dispatch = arg => target.dispatchEvent(arg); + const pointerId = 1; + const pointerEvent = { + button, + pointerId, + pointerType, + ...rest, + }; + const mouseEvent = { + button, + ...rest, + }; + const touch = { + ...rest, + }; + + if (hasPointerEvent()) { + dispatch(pointermove(pointerEvent)); + } + if (pointerType === 'mouse') { + dispatch(mousemove(mouseEvent)); + } + if (pointerType === 'touch') { + dispatch(touchmove(touch, pointerId)); } } function dispatchTouchTap(target) { - dispatchPointerPressDown(target, {pointerType: 'touch'}); - dispatchPointerPressRelease(target, {pointerType: 'touch'}); + dispatchPointerDown(target, {pointerType: 'touch'}); + dispatchPointerUp(target, {pointerType: 'touch'}); +} + +function dispatchMouseTap(target) { + dispatchPointerDown(target, {pointerType: 'mouse'}); + dispatchPointerUp(target, {pointerType: 'mouse'}); } module.exports = { blur, + click, focus, createEvent, dispatchLongPressContextMenu, @@ -389,11 +465,16 @@ module.exports = { dispatchPointerHoverEnter, dispatchPointerHoverExit, dispatchPointerHoverMove, - dispatchPointerPressDown, - dispatchPointerPressRelease, + dispatchPointerMove, + dispatchPointerDown, + dispatchPointerUp, dispatchTouchTap, + dispatchMouseTap, keydown, keyup, + scroll, + pointerdown, + pointerup, platform, hasPointerEvent, setPointerEvent,