diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 3ae69319491088..18b55a88f0026c 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -46,11 +46,29 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get clientLeft(): number { - throw new TypeError('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const borderSize = nullthrows(getFabricUIManager()).getBorderSize(node); + if (borderSize != null) { + return borderSize[3]; + } + } + + return 0; } get clientTop(): number { - throw new TypeError('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const borderSize = nullthrows(getFabricUIManager()).getBorderSize(node); + if (borderSize != null) { + return borderSize[0]; + } + } + + return 0; } get clientWidth(): number { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index ceddf4cccde087..d31a5859d3de64 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -92,6 +92,14 @@ export interface Spec { node: Node, ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number]; +getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number]; + +getBorderSize: ( + node: Node, + ) => ?[ + /* topWidth: */ number, + /* rightWidth: */ number, + /* bottomWidth: */ number, + /* leftWidth: */ number, + ]; +getTagName: (node: Node) => string; /** @@ -134,6 +142,7 @@ const CACHED_PROPERTIES = [ 'getOffset', 'getScrollPosition', 'getInnerSize', + 'getBorderSize', 'getTagName', 'hasPointerCapture', 'setPointerCapture', diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index 1991ca827ed685..d9f9c122cf0db3 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -552,6 +552,48 @@ const FabricUIManagerMock: IFabricUIManagerMock = { }, ), + getBorderSize: jest.fn( + ( + node: Node, + ): ?[ + /* topWidth: */ number, + /* rightWidth: */ number, + /* bottomWidth: */ number, + /* leftWidth: */ number, + ] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const borderSizeForTests: ?{ + topWidth?: number, + rightWidth?: number, + bottomWidth?: number, + leftWidth?: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__borderSizeForTests; + + if (borderSizeForTests == null) { + return null; + } + + const { + topWidth = 0, + rightWidth = 0, + bottomWidth = 0, + leftWidth = 0, + } = borderSizeForTests; + return [topWidth, rightWidth, bottomWidth, leftWidth]; + }, + ), + getTagName: jest.fn((node: Node): string => { ensureHostNode(node); return 'RN:' + fromNode(node).viewName; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index e67aabc714d454..36dcb3e5f826a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1261,6 +1261,60 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "getBorderSize") { + // This is a method to access the border size of a shadow node, to implement + // these methods: + // * `Element.prototype.clientLeft`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientLeft. + // * `Element.prototype.clientTop`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientTop. + + // It uses the version of the shadow node that is present in the current + // revision of the shadow tree. If the node is not present, it is not + // displayed (because any of its ancestors or itself have 'display: none'), + // or it has an inline display, it returns undefined. + // Otherwise, it returns its border size. + + // getBorderSize(shadowNode: ShadowNode): + // ?[ + // /* topWidth: */ number, + // /* rightWidth: */ number, + // /* bottomWidth: */ number, + // /* leftWidth: */ number, + // ] + auto paramCount = 1; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + + // 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}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return jsi::Value::undefined(); + } + + return jsi::Array::createWithElements( + runtime, + jsi::Value{runtime, std::round(layoutMetrics.borderWidth.top)}, + jsi::Value{runtime, std::round(layoutMetrics.borderWidth.right)}, + jsi::Value{runtime, std::round(layoutMetrics.borderWidth.bottom)}, + jsi::Value{runtime, std::round(layoutMetrics.borderWidth.left)}); + }); + } + if (methodName == "getTagName") { // This is a method to access the normalized tag name of a shadow node, to // implement `Element.prototype.tagName` (see