diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 18b55a88f0026c..0d4143bf17647f 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -135,7 +135,16 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get scrollHeight(): number { - throw new Error('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node); + if (scrollSize != null) { + return scrollSize[1]; + } + } + + return 0; } get scrollLeft(): number { @@ -169,7 +178,16 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get scrollWidth(): number { - throw new Error('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node); + if (scrollSize != null) { + return scrollSize[0]; + } + } + + return 0; } get tagName(): string { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index d31a5859d3de64..0c31dc97d61858 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -91,6 +91,9 @@ export interface Spec { +getScrollPosition: ( node: Node, ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number]; + +getScrollSize: ( + node: Node, + ) => ?[/* scrollWidth: */ number, /* scrollHeight: */ number]; +getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number]; +getBorderSize: ( node: Node, @@ -141,6 +144,7 @@ const CACHED_PROPERTIES = [ 'getBoundingClientRect', 'getOffset', 'getScrollPosition', + 'getScrollSize', 'getInnerSize', 'getBorderSize', 'getTagName', diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index d9f9c122cf0db3..5a7442c5f5071d 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -524,6 +524,34 @@ const FabricUIManagerMock: IFabricUIManagerMock = { }, ), + getScrollSize: jest.fn( + (node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const scrollForTests: ?{ + scrollWidth: number, + scrollHeight: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__scrollForTests; + + if (scrollForTests == null) { + return null; + } + + const {scrollWidth, scrollHeight} = scrollForTests; + return [scrollWidth, scrollHeight]; + }, + ), + getInnerSize: jest.fn( (node: Node): ?[/* width: */ number, /* height: */ number] => { ensureHostNode(node); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 58d9f658790199..bbc238ae249e68 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1209,6 +1209,74 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "getScrollSize") { + // This is a method to access the scroll information of a shadow node, to + // implement these methods: + // * `Element.prototype.scrollWidth`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth. + // * `Element.prototype.scrollHeight`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight. + + // 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 or is not + // displayed (because any of its ancestors or itself have 'display: none'), + // it returns undefined. Otherwise, it returns the scroll size. + + // getScrollSize(shadowNode: ShadowNode): + // ?[ + // /* scrollWidth: */ number, + // /* scrollHeight: */ 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]); + + auto newestCloneOfShadowNode = + uiManager->getNewestCloneOfShadowNode(*shadowNode); + // The node is no longer part of an active shadow tree, or it is the + // root node + if (newestCloneOfShadowNode == nullptr) { + return jsi::Value::undefined(); + } + + // 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(); + } + + auto layoutableShadowNode = + traitCast( + newestCloneOfShadowNode.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return jsi::Value::undefined(); + } + + Size scrollSize = getScrollSize( + layoutMetrics, layoutableShadowNode->getContentBounds()); + + return jsi::Array::createWithElements( + runtime, + jsi::Value{runtime, std::round(scrollSize.width)}, + jsi::Value{runtime, std::round(scrollSize.height)}); + }); + } + if (methodName == "getInnerSize") { // This is a method to access the inner size of a shadow node, to implement // these methods: diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 23fc220c970b9c..9a4b56a15eba35 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -221,4 +221,39 @@ inline static void getTextContentInShadowNode( getTextContentInShadowNode(*childNode.get(), result); } } + +inline static Rect getScrollableContentBounds( + const Rect& contentBounds, + const LayoutMetrics& layoutMetrics) { + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + auto paddingBottom = + layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom; + auto paddingLeft = + layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left; + auto paddingRight = + layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right; + + auto minY = paddingFrame.getMinY(); + auto maxY = + std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom); + + auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft) + : paddingFrame.getMinX(); + auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? paddingFrame.getMaxX() + : std::max( + paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight); + + return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}}; +} + +inline static Size getScrollSize( + const LayoutMetrics& layoutMetrics, + const Rect& contentBounds) { + auto scrollableContentBounds = + getScrollableContentBounds(contentBounds, layoutMetrics); + return scrollableContentBounds.size; +} } // namespace facebook::react