Repository: facebook/react
Pull request: [RN] Fix timeStamp property of SyntheticEvent in React Native

## 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.

Changed files:
filename: packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
patch: @@ -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(
+        <>
+          <View
+            ref={ref1}
+            id="default"
+            onTouchEnd={event => {
+              expect(event.timeStamp).toBe(performanceNowValue);
+            }}
+          />
+          <View
+            ref={ref2}
+            id="explicitTimeStampCamelCase"
+            onTouchEnd={event => {
+              expect(event.timeStamp).toBe(explicitTimeStampCamelCase);
+            }}
+          />
+          <View
+            ref={ref2}
+            id="explicitTimeStampLowerCase"
+            onTouchEnd={event => {
+              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},

filename: packages/react-native-renderer/src/legacy-events/SyntheticEvent.js
patch: @@ -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,