diff --git a/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js index 491f8d1019290a..2900fe769f7fe7 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js @@ -25,7 +25,7 @@ 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 ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement'; import ReadOnlyNode from './ReadOnlyNode'; import { getPublicInstanceFromInternalInstanceHandle, @@ -58,7 +58,9 @@ export default class ReactNativeElement } get offsetHeight(): number { - return Math.round(this.getBoundingClientRect().height); + return Math.round( + getBoundingClientRect(this, {includeTransform: false}).height, + ); } get offsetLeft(): number { @@ -110,7 +112,9 @@ export default class ReactNativeElement } get offsetWidth(): number { - return Math.round(this.getBoundingClientRect().width); + return Math.round( + getBoundingClientRect(this, {includeTransform: false}).width, + ); } /** diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index d81e8b8c6ad6b0..03cf37949b3b2c 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -211,20 +211,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { } 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); + return getBoundingClientRect(this, {includeTransform: true}); } /** @@ -262,3 +249,29 @@ function getChildElements(node: ReadOnlyNode): $ReadOnlyArray { childNode => childNode instanceof ReadOnlyElement, ); } + +/** + * The public API for `getBoundingClientRect` always includes transform, + * so we use this internal version to get the data without transform to + * implement methods like `offsetWidth` and `offsetHeight`. + */ +export function getBoundingClientRect( + node: ReadOnlyElement, + {includeTransform}: {includeTransform: boolean}, +): DOMRect { + const shadowNode = getShadowNode(node); + + if (shadowNode != null) { + const rect = nullthrows(getFabricUIManager()).getBoundingClientRect( + shadowNode, + includeTransform, + ); + + 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); +} diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 0c31dc97d61858..99c8c438d49ea5 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -75,6 +75,7 @@ export interface Spec { +getTextContent: (node: Node) => string; +getBoundingClientRect: ( node: Node, + includeTransform: boolean, ) => ?[ /* x: */ number, /* y: */ number, diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js index f71d6fa9bf726d..7ddad94e8ecd22 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js @@ -124,7 +124,7 @@ export default class ReactFabricHostComponent implements INativeMethods { this.__internalInstanceHandle, ); if (node != null) { - const rect = fabricGetBoundingClientRect(node); + const rect = fabricGetBoundingClientRect(node, true); if (rect) { return new DOMRect(rect[0], rect[1], rect[2], rect[3]); diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index 5a7442c5f5071d..83727b345374b2 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -295,6 +295,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = { getBoundingClientRect: jest.fn( ( node: Node, + includeTransform: boolean, ): ?[ /* x:*/ number, /* y:*/ number, diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 98ba8c901ad791..c00411b6c58d78 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -841,14 +841,20 @@ jsi::Value UIManagerBinding::get( // This is similar to `measureInWindow`, except it's explicitly synchronous // (returns the result instead of passing it to a callback). - // getBoundingClientRect(shadowNode: ShadowNode): + // It allows indicating whether to include transforms so it can also be used + // to implement methods like + // [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) + // and + // [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight). + + // getBoundingClientRect(shadowNode: ShadowNode, includeTransform: boolean): // [ // /* x: */ number, // /* y: */ number, // /* width: */ number, // /* height: */ number // ] - auto paramCount = 1; + auto paramCount = 2; return jsi::Function::createFromHostFunction( runtime, name, @@ -860,10 +866,12 @@ jsi::Value UIManagerBinding::get( size_t count) -> jsi::Value { validateArgumentCount(runtime, methodName, paramCount, count); + bool includeTransform = arguments[1].getBool(); + auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNodeFromValue(runtime, arguments[0]), nullptr, - {/* .includeTransform = */ true, + {/* .includeTransform = */ includeTransform, /* .includeViewportOffset = */ true}); if (layoutMetrics == EmptyLayoutMetrics) { @@ -1101,7 +1109,7 @@ jsi::Value UIManagerBinding::get( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); + *shadowNode, nullptr, {/* .includeTransform = */ false}); if (layoutMetrics == EmptyLayoutMetrics) { return jsi::Value::undefined(); @@ -1313,7 +1321,7 @@ jsi::Value UIManagerBinding::get( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); + *shadowNode, nullptr, {/* .includeTransform = */ false}); if (layoutMetrics == EmptyLayoutMetrics || layoutMetrics.displayType == DisplayType::Inline) { @@ -1367,7 +1375,7 @@ jsi::Value UIManagerBinding::get( // If the node is not displayed (itself or any of its ancestors has // "display: none"), this returns an empty layout metrics object. auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); + *shadowNode, nullptr, {/* .includeTransform = */ false}); if (layoutMetrics == EmptyLayoutMetrics || layoutMetrics.displayType == DisplayType::Inline) {