diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 30140ffcf35..33b01190e97 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -12,8 +12,11 @@ import type HTMLCollection from '../OldStyleCollections/HTMLCollection'; +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import DOMRect from '../Geometry/DOMRect'; import {createHTMLCollection} from '../OldStyleCollections/HTMLCollection'; -import ReadOnlyNode, {getChildNodes} from './ReadOnlyNode'; +import ReadOnlyNode, {getChildNodes, getShadowNode} from './ReadOnlyNode'; +import nullthrows from 'nullthrows'; export default class ReadOnlyElement extends ReadOnlyNode { get childElementCount(): number { @@ -124,6 +127,23 @@ export default class ReadOnlyElement extends ReadOnlyNode { throw new TypeError('Unimplemented'); } + getBoundingClientRect(): DOMRect { + const shadowNode = getShadowNode(this); + + if (shadowNode != null) { + const rect = nullthrows(getFabricUIManager()).getBoundingClientRect( + shadowNode, + ); + + if (rect) { + return new DOMRect(rect[0], rect[1], rect[2], rect[3]); + } + } + + // Empty rect if any of the above failed + return new DOMRect(0, 0, 0, 0); + } + getClientRects(): DOMRectList { throw new TypeError('Unimplemented'); } diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js index 1dcf9cc8399..40cc988b2a2 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js @@ -76,7 +76,7 @@ export default class ReadOnlyNode { */ get nodeName(): string { throw new TypeError( - 'Unexpected access to `nodeName` in abstract class `ReadOnlyNode`', + '`nodeName` is abstract and must be implemented in a subclass of `ReadOnlyNode`', ); } diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 29bdfa06faa..ac1d9c98648 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -58,7 +58,7 @@ export type Spec = {| +findShadowNodeByTag_DEPRECATED: (reactTag: number) => ?Node, +getBoundingClientRect: ( node: Node, - ) => [ + ) => ?[ /* x:*/ number, /* y:*/ number, /* width:*/ number, diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index f1aeb3cf03a..bf722637a8f 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -113,6 +113,17 @@ function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> { return null; } +function getNodeInCurrentTree(node: Node): ?Node { + const ancestors = getAncestorsInCurrentTree(node); + if (ancestors == null) { + return null; + } + + const [parent, position] = ancestors[ancestors.length - 1]; + const nodeInCurrentTree = fromNode(parent).children[position]; + return nodeInCurrentTree; +} + const FabricUIManagerMock: FabricUIManager = { createNode: jest.fn( ( @@ -215,7 +226,7 @@ const FabricUIManagerMock: FabricUIManager = { getBoundingClientRect: jest.fn( ( node: Node, - ): [ + ): ?[ /* x:*/ number, /* y:*/ number, /* width:*/ number, @@ -223,7 +234,28 @@ const FabricUIManagerMock: FabricUIManager = { ] => { ensureHostNode(node); - return [10, 10, 100, 100]; + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const boundingClientRectForTests: ?{ + x: number, + y: number, + width: number, + height: number, + } = + // $FlowExpectedError[prop-missing] + currentProps.__boundingClientRectForTests; + + if (boundingClientRectForTests == null) { + return null; + } + + const {x, y, width, height} = boundingClientRectForTests; + return [x, y, width, height]; }, ), setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}), @@ -242,13 +274,12 @@ const FabricUIManagerMock: FabricUIManager = { }), getChildNodes: jest.fn( (node: Node): $ReadOnlyArray => { - const ancestors = getAncestorsInCurrentTree(node); - if (ancestors == null) { + const nodeInCurrentTree = getNodeInCurrentTree(node); + + if (nodeInCurrentTree == null) { return []; } - const [parent, position] = ancestors[ancestors.length - 1]; - const nodeInCurrentTree = fromNode(parent).children[position]; return fromNode(nodeInCurrentTree).children.map( child => fromNode(child).instanceHandle, ); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 7de51a69aa3..aefe552fb20 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -728,10 +728,20 @@ jsi::Value UIManagerBinding::get( */ if (methodName == "getBoundingClientRect") { + // This is a React Native implementation of + // `Element.prototype.getBoundingClientRect` (see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). + // This is similar to `measureInWindow`, except it's explicitly synchronous // (returns the result instead of passing it to a callback). - // The behavior is similar to `Element.prototype.getBoundingClientRect` from - // Web. + + // getBoundingClientRect(shadowNode: ShadowNode): + // [ + // /* x: */ number, + // /* y: */ number, + // /* width: */ number, + // /* height: */ number + // ] return jsi::Function::createFromHostFunction( runtime, name,