From 130a951e2e346a1554fa3965bfa79ab8d489ad86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 13 Apr 2023 03:07:47 -0700 Subject: [PATCH] Implement compatibility methods in ReactNativeElement and use it behind a flag Summary: This implements all compatibility methods from `ReactFabricHostComponent` into `ReactNativeElement` and uses the feature flag `enableAccessToHostTreeInFabric` to choose between them at runtime. This flag is disabled everywhere so this won't have an effect for now. Both these classes implement the NativeMethods interface, so they're interchangeable as refs in existing product code. Changelog: [internal] bypass-github-export-checks Reviewed By: yungsters Differential Revision: D44304435 fbshipit-source-id: 43b5525b943e385287d7b00107147884b859c1fd --- .../Libraries/DOM/Nodes/ReactNativeElement.js | 98 +++++- .../Libraries/DOM/Nodes/ReadOnlyElement.js | 2 +- .../Libraries/DOM/Nodes/ReadOnlyNode.js | 31 +- .../ReactFabricHostComponent.js | 23 +- .../ReactFabricPublicInstance.js | 34 ++- .../ReactFabricPublicInstance-test.js | 284 +++++++++--------- .../warnForStyleProps.js | 32 ++ 7 files changed, 321 insertions(+), 183 deletions(-) create mode 100644 packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps.js diff --git a/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js index 1f9e9b77b0a..99dbff9956b 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js @@ -5,22 +5,55 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict + * @flow strict-local */ // flowlint unsafe-getters-setters:off import type { HostComponent, + INativeMethods, + InternalInstanceHandle, MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, + ViewConfig, } from '../../Renderer/shims/ReactNativeTypes'; import type {ElementRef} from 'react'; +import TextInputState from '../../Components/TextInput/TextInputState'; +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import {create as createAttributePayload} from '../../ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; +import warnForStyleProps from '../../ReactNative/ReactFabricPublicInstance/warnForStyleProps'; import ReadOnlyElement from './ReadOnlyElement'; +import ReadOnlyNode from './ReadOnlyNode'; +import {getShadowNode} from './ReadOnlyNode'; +import nullthrows from 'nullthrows'; + +const noop = () => {}; + +export default class ReactNativeElement + extends ReadOnlyElement + implements INativeMethods +{ + // These need to be accessible from `ReactFabricPublicInstanceUtils`. + __nativeTag: number; + __internalInstanceHandle: InternalInstanceHandle; + + _viewConfig: ViewConfig; + + constructor( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: InternalInstanceHandle, + ) { + super(internalInstanceHandle); + + this.__nativeTag = tag; + this.__internalInstanceHandle = internalInstanceHandle; + this._viewConfig = viewConfig; + } -export default class ReactNativeElement extends ReadOnlyElement { get offsetHeight(): number { throw new TypeError('Unimplemented'); } @@ -46,30 +79,71 @@ export default class ReactNativeElement extends ReadOnlyElement { */ blur(): void { - throw new TypeError('Unimplemented'); + // $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this. + TextInputState.blurTextInput(this); } - focus(): void { - throw new TypeError('Unimplemented'); + focus() { + // $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this. + TextInputState.focusTextInput(this); } - measure(callback: MeasureOnSuccessCallback): void { - throw new TypeError('Unimplemented'); + measure(callback: MeasureOnSuccessCallback) { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).measure(node, callback); + } } - measureInWindow(callback: MeasureInWindowOnSuccessCallback): void { - throw new TypeError('Unimplemented'); + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).measureInWindow(node, callback); + } } measureLayout( relativeToNativeNode: number | ElementRef>, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void /* currently unused */, - ): void { - throw new TypeError('Unimplemented'); + ) { + if (!(relativeToNativeNode instanceof ReadOnlyNode)) { + if (__DEV__) { + console.error( + 'Warning: ref.measureLayout must be called with a ref to a native component.', + ); + } + + return; + } + + const toStateNode = getShadowNode(this); + const fromStateNode = getShadowNode(relativeToNativeNode); + + if (toStateNode != null && fromStateNode != null) { + nullthrows(getFabricUIManager()).measureLayout( + toStateNode, + fromStateNode, + onFail != null ? onFail : noop, + onSuccess != null ? onSuccess : noop, + ); + } } setNativeProps(nativeProps: {...}): void { - throw new TypeError('Unimplemented'); + if (__DEV__) { + warnForStyleProps(nativeProps, this._viewConfig.validAttributes); + } + + const updatePayload = createAttributePayload( + nativeProps, + this._viewConfig.validAttributes, + ); + + const node = getShadowNode(this); + + if (node != null && updatePayload != null) { + nullthrows(getFabricUIManager()).setNativeProps(node, updatePayload); + } } } diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 1307ba57c8b..8d2de166fe3 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict + * @flow strict-local */ // flowlint unsafe-getters-setters:off diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js index d6cc6b878f4..fc8dc2df8cb 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js @@ -5,15 +5,25 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict + * @flow strict-local */ // flowlint unsafe-getters-setters:off +import type { + InternalInstanceHandle, + Node as ShadowNode, +} from '../../Renderer/shims/ReactNativeTypes'; import type NodeList from '../OldStyleCollections/NodeList'; import type ReadOnlyElement from './ReadOnlyElement'; +import ReactFabric from '../../Renderer/shims/ReactFabric'; + export default class ReadOnlyNode { + constructor(internalInstanceHandle: InternalInstanceHandle) { + setInstanceHandle(this, internalInstanceHandle); + } + get childNodes(): NodeList { throw new TypeError('Unimplemented'); } @@ -165,3 +175,22 @@ export default class ReadOnlyNode { */ static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number = 32; } + +const INSTANCE_HANDLE_KEY = Symbol('internalInstanceHandle'); + +function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle { + // $FlowExpectedError[prop-missing] + return node[INSTANCE_HANDLE_KEY]; +} + +function setInstanceHandle( + node: ReadOnlyNode, + instanceHandle: InternalInstanceHandle, +): void { + // $FlowExpectedError[prop-missing] + node[INSTANCE_HANDLE_KEY] = instanceHandle; +} + +export function getShadowNode(node: ReadOnlyNode): ?ShadowNode { + return ReactFabric.getNodeFromInternalInstanceHandle(getInstanceHandle(node)); +} diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js index b423fad97c7..f71d6fa9bf7 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js @@ -9,7 +9,6 @@ */ import type { - AttributeConfiguration, HostComponent, INativeMethods, InternalInstanceHandle, @@ -24,6 +23,7 @@ import TextInputState from '../../Components/TextInput/TextInputState'; import {getNodeFromInternalInstanceHandle} from '../../Renderer/shims/ReactFabric'; import {getFabricUIManager} from '../FabricUIManager'; import {create} from './ReactNativeAttributePayload'; +import warnForStyleProps from './warnForStyleProps'; import nullthrows from 'nullthrows'; const { @@ -149,24 +149,3 @@ export default class ReactFabricHostComponent implements INativeMethods { } } } - -function warnForStyleProps( - props: {...}, - validAttributes: AttributeConfiguration, -): void { - if (__DEV__) { - for (const key in validAttributes.style) { - if (!(validAttributes[key] || props[key] === undefined)) { - console.error( - 'You are setting the style `{ %s' + - ': ... }` as a prop. You ' + - 'should nest it in a style object. ' + - 'E.g. `{ style: { %s' + - ': ... } }`', - key, - key, - ); - } - } - } -} diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js index a2c6581912d..f8722df1d60 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js @@ -8,15 +8,22 @@ * @flow strict-local */ +import type ReactNativeElement from '../../DOM/Nodes/ReactNativeElement'; import typeof ReactFabricType from '../../Renderer/shims/ReactFabric'; import type { InternalInstanceHandle, + Node, ViewConfig, } from '../../Renderer/shims/ReactNativeTypes'; -import type ReactFabricHostComponentType from './ReactFabricHostComponent'; +import type ReactFabricHostComponent from './ReactFabricHostComponent'; + +import ReactNativeFeatureFlags from '../ReactNativeFeatureFlags'; // Lazy loaded to avoid evaluating the module when using the legacy renderer. -let ReactFabricHostComponent: Class; +let PublicInstanceClass: + | Class + | Class; + // Lazy loaded to avoid evaluating the module when using the legacy renderer. let ReactFabric: ReactFabricType; @@ -24,11 +31,19 @@ export function createPublicInstance( tag: number, viewConfig: ViewConfig, internalInstanceHandle: InternalInstanceHandle, -): ReactFabricHostComponentType { - if (ReactFabricHostComponent == null) { - ReactFabricHostComponent = require('./ReactFabricHostComponent').default; +): ReactFabricHostComponent | ReactNativeElement { + if (PublicInstanceClass == null) { + // We don't use inline requires in react-native, so this forces lazy loading + // the right module to avoid eagerly loading both. + if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) { + PublicInstanceClass = + require('../../DOM/Nodes/ReactNativeElement').default; + } else { + PublicInstanceClass = require('./ReactFabricHostComponent').default; + } } - return new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle); + + return new PublicInstanceClass(tag, viewConfig, internalInstanceHandle); } export function createPublicTextInstance(internalInstanceHandle: mixed): {} { @@ -39,14 +54,14 @@ export function createPublicTextInstance(internalInstanceHandle: mixed): {} { } export function getNativeTagFromPublicInstance( - publicInstance: ReactFabricHostComponentType, + publicInstance: ReactFabricHostComponent | ReactNativeElement, ): number { return publicInstance.__nativeTag; } export function getNodeFromPublicInstance( - publicInstance: ReactFabricHostComponentType, -): mixed { + publicInstance: ReactFabricHostComponent | ReactNativeElement, +): ?Node { // Avoid loading ReactFabric if using an instance from the legacy renderer. if (publicInstance.__internalInstanceHandle == null) { return null; @@ -55,7 +70,6 @@ export function getNodeFromPublicInstance( if (ReactFabric == null) { ReactFabric = require('../../Renderer/shims/ReactFabric'); } - return ReactFabric.getNodeFromInternalInstanceHandle( publicInstance.__internalInstanceHandle, ); diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js index e2987243b82..73e54a8fdd6 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js @@ -15,13 +15,13 @@ import * as React from 'react'; import {act} from 'react-test-renderer'; const TextInputState = require('../../../Components/TextInput/TextInputState'); -const FabricUIManager = require('../../../ReactNative/FabricUIManager'); const ReactFabric = require('../../../Renderer/shims/ReactFabric'); const ReactNativeViewConfigRegistry = require('../../../Renderer/shims/ReactNativeViewConfigRegistry'); +const FabricUIManager = require('../../FabricUIManager'); const nullthrows = require('nullthrows'); -jest.mock('../../../ReactNative/FabricUIManager', () => - require('../../../ReactNative/__mocks__/FabricUIManager'), +jest.mock('../../FabricUIManager', () => + require('../../__mocks__/FabricUIManager'), ); /** @@ -107,161 +107,171 @@ async function mockRenderKeys( return result; } -describe('ReactFabricPublicInstance', () => { - beforeEach(() => { - jest.resetModules(); - // Installs the global `nativeFabricUIManager` pointing to the mock. - require('../../../ReactNative/__mocks__/FabricUIManager'); - jest.spyOn(TextInputState, 'blurTextInput'); - jest.spyOn(TextInputState, 'focusTextInput'); - }); +[ + {enableAccessToHostTreeInFabric: false}, + {enableAccessToHostTreeInFabric: true}, +].forEach(flags => { + describe(`ReactFabricPublicInstance (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric = ${String( + flags.enableAccessToHostTreeInFabric, + )})'`, () => { + beforeEach(() => { + jest.resetModules(); + // Installs the global `nativeFabricUIManager` pointing to the mock. + require('../../../ReactNative/__mocks__/FabricUIManager'); + jest.spyOn(TextInputState, 'blurTextInput'); + jest.spyOn(TextInputState, 'focusTextInput'); - describe('blur', () => { - test('blur() invokes TextInputState', async () => { - const result = await mockRenderKeys([['foo']]); - const fooRef = nullthrows(result?.[0]?.[0]); - - fooRef.blur(); - - expect(mockOf(TextInputState.blurTextInput).mock.calls).toEqual([ - [fooRef], - ]); - }); - }); - - describe('focus', () => { - test('focus() invokes TextInputState', async () => { - const result = await mockRenderKeys([['foo']]); - const fooRef = nullthrows(result?.[0]?.[0]); - - fooRef.focus(); - - expect(mockOf(TextInputState.focusTextInput).mock.calls).toEqual([ - [fooRef], - ]); - }); - }); - - describe('measure', () => { - test('component.measure(...) invokes callback', async () => { - const result = await mockRenderKeys([['foo']]); - const fooRef = nullthrows(result?.[0]?.[0]); - - const callback = jest.fn(); - fooRef.measure(callback); - - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measure, - ).toHaveBeenCalledTimes(1); - expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 0, 0]]); + require('../../../ReactNative/ReactNativeFeatureFlags').enableAccessToHostTreeInFabric = + () => flags.enableAccessToHostTreeInFabric; }); - test('unmounted.measure(...) does nothing', async () => { - const result = await mockRenderKeys([['foo'], null]); - const fooRef = nullthrows(result?.[0]?.[0]); - const callback = jest.fn(); - fooRef.measure(callback); + describe('blur', () => { + test('blur() invokes TextInputState', async () => { + const result = await mockRenderKeys([['foo']]); + const fooRef = nullthrows(result?.[0]?.[0]); - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measure, - ).not.toHaveBeenCalled(); - expect(callback).not.toHaveBeenCalled(); - }); - }); + fooRef.blur(); - describe('measureInWindow', () => { - test('component.measureInWindow(...) invokes callback', async () => { - const result = await mockRenderKeys([['foo']]); - const fooRef = nullthrows(result?.[0]?.[0]); - - const callback = jest.fn(); - fooRef.measureInWindow(callback); - - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow, - ).toHaveBeenCalledTimes(1); - expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]); + expect(mockOf(TextInputState.blurTextInput).mock.calls).toEqual([ + [fooRef], + ]); + }); }); - test('unmounted.measureInWindow(...) does nothing', async () => { - const result = await mockRenderKeys([['foo'], null]); - const fooRef = nullthrows(result?.[0]?.[0]); + describe('focus', () => { + test('focus() invokes TextInputState', async () => { + const result = await mockRenderKeys([['foo']]); + const fooRef = nullthrows(result?.[0]?.[0]); - const callback = jest.fn(); - fooRef.measureInWindow(callback); + fooRef.focus(); - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow, - ).not.toHaveBeenCalled(); - expect(callback).not.toHaveBeenCalled(); - }); - }); - - describe('measureLayout', () => { - test('component.measureLayout(component, ...) invokes callback', async () => { - const result = await mockRenderKeys([['foo', 'bar']]); - const fooRef = nullthrows(result?.[0]?.[0]); - const barRef = nullthrows(result?.[0]?.[1]); - - const successCallback = jest.fn(); - const failureCallback = jest.fn(); - fooRef.measureLayout(barRef, successCallback, failureCallback); - - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, - ).toHaveBeenCalledTimes(1); - expect(successCallback.mock.calls).toEqual([[1, 1, 100, 100]]); + expect(mockOf(TextInputState.focusTextInput).mock.calls).toEqual([ + [fooRef], + ]); + }); }); - test('unmounted.measureLayout(component, ...) does nothing', async () => { - const result = await mockRenderKeys([ - ['foo', 'bar'], - ['foo', null], - ]); - const fooRef = nullthrows(result?.[0]?.[0]); - const barRef = nullthrows(result?.[0]?.[1]); + describe('measure', () => { + test('component.measure(...) invokes callback', async () => { + const result = await mockRenderKeys([['foo']]); + const fooRef = nullthrows(result?.[0]?.[0]); - const successCallback = jest.fn(); - const failureCallback = jest.fn(); - fooRef.measureLayout(barRef, successCallback, failureCallback); + const callback = jest.fn(); + fooRef.measure(callback); - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, - ).not.toHaveBeenCalled(); - expect(successCallback).not.toHaveBeenCalled(); + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measure, + ).toHaveBeenCalledTimes(1); + expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 0, 0]]); + }); + + test('unmounted.measure(...) does nothing', async () => { + const result = await mockRenderKeys([['foo'], null]); + const fooRef = nullthrows(result?.[0]?.[0]); + const callback = jest.fn(); + fooRef.measure(callback); + + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measure, + ).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + }); }); - test('component.measureLayout(unmounted, ...) does nothing', async () => { - const result = await mockRenderKeys([ - ['foo', 'bar'], - [null, 'bar'], - ]); - const fooRef = nullthrows(result?.[0]?.[0]); - const barRef = nullthrows(result?.[0]?.[1]); + describe('measureInWindow', () => { + test('component.measureInWindow(...) invokes callback', async () => { + const result = await mockRenderKeys([['foo']]); + const fooRef = nullthrows(result?.[0]?.[0]); - const successCallback = jest.fn(); - const failureCallback = jest.fn(); - fooRef.measureLayout(barRef, successCallback, failureCallback); + const callback = jest.fn(); + fooRef.measureInWindow(callback); - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, - ).not.toHaveBeenCalled(); - expect(successCallback).not.toHaveBeenCalled(); + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow, + ).toHaveBeenCalledTimes(1); + expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]); + }); + + test('unmounted.measureInWindow(...) does nothing', async () => { + const result = await mockRenderKeys([['foo'], null]); + const fooRef = nullthrows(result?.[0]?.[0]); + + const callback = jest.fn(); + fooRef.measureInWindow(callback); + + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureInWindow, + ).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + }); }); - test('unmounted.measureLayout(unmounted, ...) does nothing', async () => { - const result = await mockRenderKeys([['foo', 'bar'], null]); - const fooRef = nullthrows(result?.[0]?.[0]); - const barRef = nullthrows(result?.[0]?.[1]); + describe('measureLayout', () => { + test('component.measureLayout(component, ...) invokes callback', async () => { + const result = await mockRenderKeys([['foo', 'bar']]); + const fooRef = nullthrows(result?.[0]?.[0]); + const barRef = nullthrows(result?.[0]?.[1]); - const successCallback = jest.fn(); - const failureCallback = jest.fn(); - fooRef.measureLayout(barRef, successCallback, failureCallback); + const successCallback = jest.fn(); + const failureCallback = jest.fn(); + fooRef.measureLayout(barRef, successCallback, failureCallback); - expect( - nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, - ).not.toHaveBeenCalled(); - expect(successCallback).not.toHaveBeenCalled(); + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, + ).toHaveBeenCalledTimes(1); + expect(successCallback.mock.calls).toEqual([[1, 1, 100, 100]]); + }); + + test('unmounted.measureLayout(component, ...) does nothing', async () => { + const result = await mockRenderKeys([ + ['foo', 'bar'], + ['foo', null], + ]); + const fooRef = nullthrows(result?.[0]?.[0]); + const barRef = nullthrows(result?.[0]?.[1]); + + const successCallback = jest.fn(); + const failureCallback = jest.fn(); + fooRef.measureLayout(barRef, successCallback, failureCallback); + + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, + ).not.toHaveBeenCalled(); + expect(successCallback).not.toHaveBeenCalled(); + }); + + test('component.measureLayout(unmounted, ...) does nothing', async () => { + const result = await mockRenderKeys([ + ['foo', 'bar'], + [null, 'bar'], + ]); + const fooRef = nullthrows(result?.[0]?.[0]); + const barRef = nullthrows(result?.[0]?.[1]); + + const successCallback = jest.fn(); + const failureCallback = jest.fn(); + fooRef.measureLayout(barRef, successCallback, failureCallback); + + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, + ).not.toHaveBeenCalled(); + expect(successCallback).not.toHaveBeenCalled(); + }); + + test('unmounted.measureLayout(unmounted, ...) does nothing', async () => { + const result = await mockRenderKeys([['foo', 'bar'], null]); + const fooRef = nullthrows(result?.[0]?.[0]); + const barRef = nullthrows(result?.[0]?.[1]); + + const successCallback = jest.fn(); + const failureCallback = jest.fn(); + fooRef.measureLayout(barRef, successCallback, failureCallback); + + expect( + nullthrows(FabricUIManager.getFabricUIManager()).measureLayout, + ).not.toHaveBeenCalled(); + expect(successCallback).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps.js new file mode 100644 index 00000000000..25713ab4454 --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import type {AttributeConfiguration} from '../../Renderer/shims/ReactNativeTypes'; + +export default function warnForStyleProps( + props: {...}, + validAttributes: AttributeConfiguration, +): void { + if (__DEV__) { + for (const key in validAttributes.style) { + if (!(validAttributes[key] || props[key] === undefined)) { + console.error( + 'You are setting the style `{ %s' + + ': ... }` as a prop. You ' + + 'should nest it in a style object. ' + + 'E.g. `{ style: { %s' + + ': ... } }`', + key, + key, + ); + } + } + } +}