diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 6eaaec450533b..1af5ed5dc6dec 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -994,4 +994,68 @@ describe('DOMEventResponderSystem', () => { dispatchClickEvent(ref.current); expect(log).toEqual([{counter: 1}]); }); + + it('should correctly pass through event properties', () => { + const timeStamps = []; + const ref = React.createRef(); + const eventLog = []; + const logEvent = event => { + const propertiesWeCareAbout = { + counter: event.counter, + target: event.target, + timeStamp: event.timeStamp, + type: event.type, + }; + timeStamps.push(event.timeStamp); + eventLog.push(propertiesWeCareAbout); + }; + let counter = 0; + + const TestResponder = createEventResponder({ + targetEventTypes: ['click'], + onEvent(event, context, props) { + const obj = { + counter, + timeStamp: context.getTimeStamp(), + target: event.responderTarget, + type: 'click-test', + }; + context.dispatchEvent(obj, props.onClick, DiscreteEvent); + }, + }); + + const Component = () => { + const listener = React.unstable_useResponder(TestResponder, { + onClick: logEvent, + }); + return + + ); + } + + const newContainer = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(newContainer); + document.body.appendChild(newContainer); + root.render(); + Scheduler.unstable_flushAll(); + + const target = createEventTarget(ref.current); + target.pointerdown({timeStamp: 100}); + target.pointerup({timeStamp: 100}); + target.click({timeStamp: 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); + } + + target.pointerdown({timeStamp: 100}); + target.pointerup({timeStamp: 100}); + // Ensure the timeStamp logic works + target.click({timeStamp: 101}); + + if (__DEV__) { + expect(renderCounts).toBe(6); + } else { + expect(renderCounts).toBe(3); + } + + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); + + it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { + const usePress = require('react-events/press').usePress; + const ref = React.createRef(); + let renderCounts = 0; + + function MyComponent() { + const [, updateCounter] = React.useState(0); + renderCounts++; + + function handlePress() { + updateCounter(count => count + 1); + } + + const listener = usePress({ + onPress: handlePress, + }); + + return ( +
+ +
+ ); + } + + const newContainer = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(newContainer); + document.body.appendChild(newContainer); + root.render(); + Scheduler.unstable_flushAll(); + + const target = createEventTarget(ref.current); + target.pointerdown({timeStamp: 100}); + target.pointerup({timeStamp: 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); + } + + target.pointerdown({timeStamp: 100}); + // Ensure the timeStamp logic works + target.pointerup({timeStamp: 101}); + + if (__DEV__) { + expect(renderCounts).toBe(8); + } else { + expect(renderCounts).toBe(4); + } + + Scheduler.unstable_flushAll(); + document.body.removeChild(newContainer); + }); + + it( + 'should only flush before outermost discrete event handler when mixing ' + + 'event systems', + async () => { + const {useState} = React; + const usePress = require('react-events/press').usePress; + + const button = React.createRef(); + + const ops = []; + + function MyComponent() { + const [pressesCount, updatePressesCount] = useState(0); + const [clicksCount, updateClicksCount] = useState(0); + + 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. + const target = createEventTarget(button.current); + target.click(); + // Text context should not have changed + ops.push(newContainer.textContent); + updatePressesCount(pressesCount + 1); + } + + const listener = usePress({ + 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 = createEventTarget(button.current); + target.pointerdown({timeStamp: 100}); + target.pointerup({timeStamp: 100}); + + Scheduler.unstable_flushAll(); + expect(newContainer.textContent).toEqual('Presses: 1, Clicks: 1'); + + expect(ops).toEqual(['Presses: 0, Clicks: 0']); + }, + ); + + describe('mixing the Input and Press repsonders', () => { + it('is async for non-input events', () => { + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.enableUserBlockingEvents = true; + const usePress = require('react-events/press').usePress; + const useInput = require('react-events/input').useInput; + const root = ReactDOM.unstable_createRoot(container); + let input; + + let ops = []; + + function Component({innerRef, onChange, controlledValue, pressListener}) { + const inputListener = useInput({ + onChange, + }); + return ( + + ); + } + + function PressWrapper({innerRef, onPress, onChange, controlledValue}) { + const pressListener = usePress({ + onPress, + }); + return ( + (input = el)} + controlledValue={controlledValue} + pressListener={pressListener} + /> + ); + } + + class ControlledInput extends React.Component { + state = {value: 'initial'}; + onChange = event => this.setState({value: event.target.value}); + reset = () => { + this.setState({value: ''}); + }; + render() { + ops.push(`render: ${this.state.value}`); + const controlledValue = + this.state.value === 'changed' ? 'changed [!]' : this.state.value; + return ( + (input = el)} + controlledValue={controlledValue} + /> + ); + } + } + + // Initial mount. Test that this is async. + root.render(); + // Should not have flushed yet. + expect(ops).toEqual([]); + expect(input).toBe(undefined); + // Flush callbacks. + Scheduler.unstable_flushAll(); + expect(ops).toEqual(['render: initial']); + expect(input.value).toBe('initial'); + + ops = []; + + // Trigger a click event + input.dispatchEvent( + new MouseEvent('mousedown', {bubbles: true, cancelable: true}), + ); + input.dispatchEvent( + new MouseEvent('mouseup', {bubbles: true, cancelable: true}), + ); + // Nothing should have changed + expect(ops).toEqual([]); + expect(input.value).toBe('initial'); + + // Flush callbacks. + Scheduler.unstable_flushAll(); + // Now the click update has flushed. + expect(ops).toEqual(['render: ']); + expect(input.value).toBe(''); + }); + }); +}); 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 5e155f5ad839f..acb30e5290fc1 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -1120,334 +1120,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => { target.pointerdown(); }); - it('should correctly pass through event properties', () => { - const timeStamps = []; - const ref = React.createRef(); - const eventLog = []; - const logEvent = event => { - const propertiesWeCareAbout = { - pageX: event.pageX, - pageY: event.pageY, - screenX: event.screenX, - screenY: event.screenY, - clientX: event.clientX, - clientY: event.clientY, - pointerType: event.pointerType, - target: event.target, - timeStamp: event.timeStamp, - type: event.type, - }; - timeStamps.push(event.timeStamp); - eventLog.push(propertiesWeCareAbout); - }; - - const Component = () => { - const listener = usePress({ - onPressStart: logEvent, - onPressEnd: logEvent, - onPressMove: logEvent, - onPress: logEvent, - }); - return - - ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - const target = ref.current; - target.dispatchEvent(pointerdown({timeStamp: 100})); - target.dispatchEvent(pointerup({timeStamp: 100})); - target.dispatchEvent(click({timeStamp: 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); - } - - target.dispatchEvent(pointerdown({timeStamp: 100})); - target.dispatchEvent(pointerup({timeStamp: 100})); - // Ensure the timeStamp logic works - target.dispatchEvent(click({timeStamp: 101})); - - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } - - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); - - it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => { - const ref = React.createRef(); - let renderCounts = 0; - - function MyComponent() { - const [, updateCounter] = React.useState(0); - renderCounts++; - - function handlePress() { - updateCounter(count => count + 1); - } - - const listener = usePress({ - onPress: handlePress, - }); - - return ( -
- -
- ); - } - - const newContainer = document.createElement('div'); - const root = ReactDOM.unstable_createRoot(newContainer); - document.body.appendChild(newContainer); - root.render(); - Scheduler.unstable_flushAll(); - - 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(); - if (__DEV__) { - expect(renderCounts).toBe(6); - } else { - expect(renderCounts).toBe(3); - } - - target.dispatchEvent(pointerdown({timeStamp: 100})); - target.dispatchEvent(pointerup({timeStamp: 100})); - // Ensure the timeStamp logic works - target.dispatchEvent(click({timeStamp: 101})); - - if (__DEV__) { - expect(renderCounts).toBe(8); - } else { - expect(renderCounts).toBe(4); - } - - Scheduler.unstable_flushAll(); - document.body.removeChild(newContainer); - }); - - it( - 'should only flush before outermost discrete event handler when mixing ' + - 'event systems', - async () => { - const {useState} = React; - - const button = React.createRef(); - - const ops = []; - - function MyComponent() { - const [pressesCount, updatePressesCount] = useState(0); - const [clicksCount, updateClicksCount] = useState(0); - - 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); - } - - const listener = usePress({ - 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(); diff --git a/packages/react-events/src/dom/testing-library/domEvents.js b/packages/react-events/src/dom/testing-library/domEvents.js index e7b09c92c5ac9..a832757bff08c 100644 --- a/packages/react-events/src/dom/testing-library/domEvents.js +++ b/packages/react-events/src/dom/testing-library/domEvents.js @@ -37,6 +37,9 @@ function createEvent(type, data = {}) { if (data != null) { Object.keys(data).forEach(key => { const value = data[key]; + if (key === 'timeStamp' && !value) { + return; + } Object.defineProperty(event, key, {value}); }); } @@ -83,6 +86,7 @@ function createPointerEvent( tangentialPressure = 0, tiltX = 0, tiltY = 0, + timeStamp, twist = 0, width, x = 0, @@ -122,6 +126,7 @@ function createPointerEvent( tangentialPressure, tiltX, tiltY, + timeStamp, twist, width: isMouse ? 1 : width != null ? width : defaultPointerSize, }); @@ -171,6 +176,7 @@ function createMouseEvent( screenX, screenY, shiftKey = false, + timeStamp, x = 0, y = 0, } = {}, @@ -198,6 +204,7 @@ function createMouseEvent( screenX: screenX === 0 ? screenX : x, screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize, shiftKey, + timeStamp, }); } @@ -209,6 +216,8 @@ function createTouchEvent(type, payload) { let metaKey = false; let preventDefault = emptyFunction; let shiftKey = false; + let timeStamp; + if (firstTouch != null) { if (firstTouch.altKey != null) { altKey = firstTouch.altKey; @@ -225,6 +234,9 @@ function createTouchEvent(type, payload) { if (firstTouch.shiftKey != null) { shiftKey = firstTouch.shiftKey; } + if (firstTouch.timeStamp != null) { + timeStamp = firstTouch.timeStamp; + } } const touches = touchesPayload.map( @@ -268,6 +280,7 @@ function createTouchEvent(type, payload) { firesTouchEvents: true, }, targetTouches: activeTouches, + timeStamp, touches: activeTouches, }); }