From a48e9e3f10fed06c813399ccae8a28db7dd76683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 26 Feb 2026 15:51:07 +0000 Subject: [PATCH 1/2] [RN] Fix timeStamp property of SyntheticEvent in React Native (#35912) ## Summary This fixes the semantics of the `timeStamp` property of events in React Native. Currently, most events just assign `Date.now()` (at the time of creating the event object in JavaScript) as the `timeStamp` property. This is a divergence with Web and most native platforms, that use a monotonic timestamp for the value (on Web, the same timestamp provided by `performance.now()`). Additionally, many native events specify a timestamp in the event data object as `timestamp` and gets ignored by the logic in JS as it only looks at properties named `timeStamp` specifically (camel case). This PR fixes both issues by: 1. Using `performance.now()` instead of `Date.now()` by default (if available). 2. Checking for a `timestamp` property before falling back to the default (apart from `timeStamp`). ## How did you test this change? Added unit tests for verify the new behavior. --- .../__tests__/ReactFabric-test.internal.js | 132 ++++++++++++++++++ .../src/legacy-events/SyntheticEvent.js | 17 ++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 42cc136a7202..aa7e518e2ba7 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -1153,6 +1153,138 @@ describe('ReactFabric', () => { expect.assertions(6); }); + it('propagates timeStamps from native events and sets defaults', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: { + id: true, + }, + uiViewClassName: 'RCTView', + directEventTypes: { + topTouchStart: { + registrationName: 'onTouchStart', + }, + topTouchEnd: { + registrationName: 'onTouchEnd', + }, + }, + })); + + function getViewById(id) { + const [reactTag, , , , instanceHandle] = + nativeFabricUIManager.createNode.mock.calls.find( + args => args[3] && args[3].id === id, + ); + + return {reactTag, instanceHandle}; + } + + const ref1 = React.createRef(); + const ref2 = React.createRef(); + const ref3 = React.createRef(); + + const explicitTimeStampCamelCase = 'explicit-timestamp-camelcase'; + const explicitTimeStampLowerCase = 'explicit-timestamp-lowercase'; + const performanceNowValue = 'performance-now-timestamp'; + + jest.spyOn(performance, 'now').mockReturnValue(performanceNowValue); + + await act(() => { + ReactFabric.render( + <> + { + expect(event.timeStamp).toBe(performanceNowValue); + }} + /> + { + expect(event.timeStamp).toBe(explicitTimeStampCamelCase); + }} + /> + { + expect(event.timeStamp).toBe(explicitTimeStampLowerCase); + }} + /> + , + 1, + null, + true, + ); + }); + + const [dispatchEvent] = + nativeFabricUIManager.registerEventHandler.mock.calls[0]; + + dispatchEvent(getViewById('default').instanceHandle, 'topTouchStart', { + target: getViewById('default').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + }); + dispatchEvent(getViewById('default').instanceHandle, 'topTouchEnd', { + target: getViewById('default').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + // No timeStamp property + }); + + dispatchEvent( + getViewById('explicitTimeStampCamelCase').instanceHandle, + 'topTouchStart', + { + target: getViewById('explicitTimeStampCamelCase').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + }, + ); + + dispatchEvent( + getViewById('explicitTimeStampCamelCase').instanceHandle, + 'topTouchEnd', + { + target: getViewById('explicitTimeStampCamelCase').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + timeStamp: explicitTimeStampCamelCase, + }, + ); + + dispatchEvent( + getViewById('explicitTimeStampLowerCase').instanceHandle, + 'topTouchStart', + { + target: getViewById('explicitTimeStampLowerCase').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + }, + ); + + dispatchEvent( + getViewById('explicitTimeStampLowerCase').instanceHandle, + 'topTouchEnd', + { + target: getViewById('explicitTimeStampLowerCase').reactTag, + identifier: 17, + touches: [], + changedTouches: [], + timestamp: explicitTimeStampLowerCase, + }, + ); + + expect.assertions(3); + }); + it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', async () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js index 723daa0dc9e5..b6a4bf4e7cbb 100644 --- a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js +++ b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js @@ -11,6 +11,21 @@ import assign from 'shared/assign'; const EVENT_POOL_SIZE = 10; +let currentTimeStamp = () => { + // Lazily define the function based on the existence of performance.now() + if ( + typeof performance === 'object' && + performance !== null && + typeof performance.now === 'function' + ) { + currentTimeStamp = () => performance.now(); + } else { + currentTimeStamp = () => Date.now(); + } + + return currentTimeStamp(); +}; + /** * @interface Event * @see http://www.w3.org/TR/DOM-Level-3-Events/ @@ -26,7 +41,7 @@ const EventInterface = { bubbles: null, cancelable: null, timeStamp: function (event) { - return event.timeStamp || Date.now(); + return event.timeStamp || event.timestamp || currentTimeStamp(); }, defaultPrevented: null, isTrusted: null, From 98ce535fdb6ff559a5dd76b58c1bcaa983804957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 26 Feb 2026 15:51:23 +0000 Subject: [PATCH 2/2] [RN] Expose event as a global variable during dispatch (#35913) ## Summary This PR updates the event dispatching logic in React Native to expose the dispatched event in the global scope as done on Web (https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke) and in the new implementation of `EventTarget` in React Native (https://github.com/facebook/react-native/blob/d1b2ddc9cb4f7b4cb795fed197347173ed5c4bfb/packages/react-native/src/private/webapis/dom/events/EventTarget.js#L372). ## How did you test this change? Added unit tests --- .../src/__tests__/ReactFabric-test.internal.js | 11 ++++++++++- .../src/legacy-events/EventPluginUtils.js | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index aa7e518e2ba7..5d0c8b8b9e97 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -1099,6 +1099,8 @@ describe('ReactFabric', () => { // Check for referential equality expect(ref1.current).toBe(event.target); expect(ref1.current).toBe(event.currentTarget); + + expect(global.event).toBe(event); }} onStartShouldSetResponder={() => true} /> @@ -1110,6 +1112,8 @@ describe('ReactFabric', () => { // Check for referential equality expect(ref2.current).toBe(event.target); expect(ref2.current).toBe(event.currentTarget); + + expect(global.event).toBe(event); }} onStartShouldSetResponder={() => true} /> @@ -1123,6 +1127,9 @@ describe('ReactFabric', () => { const [dispatchEvent] = nativeFabricUIManager.registerEventHandler.mock.calls[0]; + const preexistingEvent = {}; + global.event = preexistingEvent; + dispatchEvent(getViewById('one').instanceHandle, 'topTouchStart', { target: getViewById('one').reactTag, identifier: 17, @@ -1150,7 +1157,9 @@ describe('ReactFabric', () => { changedTouches: [], }); - expect.assertions(6); + expect(global.event).toBe(preexistingEvent); + + expect.assertions(9); }); it('propagates timeStamps from native events and sets defaults', async () => { diff --git a/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js b/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js index 64a05cef33fa..2027ac2e20fc 100644 --- a/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js +++ b/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js @@ -67,6 +67,9 @@ function validateEventDispatches(event) { */ export function executeDispatch(event, listener, inst) { event.currentTarget = getNodeFromInstance(inst); + const currentEvent = global.event; + global.event = event; + try { listener(event); } catch (error) { @@ -77,6 +80,8 @@ export function executeDispatch(event, listener, inst) { // TODO: Make sure this error gets logged somehow. } } + + global.event = currentEvent; event.currentTarget = null; }