From eec2df2992eb4794826676707f654513c5c5035f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 26 Feb 2026 12:21:09 +0000 Subject: [PATCH] [RN] Fix timeStamp property of SyntheticEvent in React Native --- .../__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,