diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 163deca45fd..b6ecccb0c84 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -91,19 +91,41 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get scrollHeight(): number { - throw new TypeError('Unimplemented'); + throw new Error('Unimplemented'); } get scrollLeft(): number { - throw new TypeError('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const scrollPosition = nullthrows(getFabricUIManager()).getScrollPosition( + node, + ); + if (scrollPosition != null) { + return scrollPosition[0]; + } + } + + return 0; } get scrollTop(): number { - throw new TypeError('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + const scrollPosition = nullthrows(getFabricUIManager()).getScrollPosition( + node, + ); + if (scrollPosition != null) { + return scrollPosition[1]; + } + } + + return 0; } get scrollWidth(): number { - throw new TypeError('Unimplemented'); + throw new Error('Unimplemented'); } get tagName(): string { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index e0c062fb854..68372d8b44e 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -74,10 +74,10 @@ export type Spec = {| +getBoundingClientRect: ( node: Node, ) => ?[ - /* x:*/ number, - /* y:*/ number, - /* width:*/ number, - /* height:*/ number, + /* x: */ number, + /* y: */ number, + /* width: */ number, + /* height: */ number, ], +getOffset: ( node: Node, @@ -86,6 +86,9 @@ export type Spec = {| /* offsetTop: */ number, /* offsetLeft: */ number, ], + +getScrollPosition: ( + node: Node, + ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number], |}; // This is exposed as a getter because apps using the legacy renderer AND diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index 34cf9f82871..0e9a67dccb0 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -431,6 +431,33 @@ const FabricUIManagerMock: FabricUIManager = { ]; }, ), + getScrollPosition: 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: ?{ + scrollLeft: number, + scrollTop: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__scrollForTests; + + if (scrollForTests == null) { + return null; + } + + const {scrollLeft, scrollTop} = scrollForTests; + return [scrollLeft, scrollTop]; + }, + ), }; global.nativeFabricUIManager = FabricUIManagerMock; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 54485334feb..96cae3169e0 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1013,6 +1013,71 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "getScrollPosition") { + // This is a method to access scroll information for React Native nodes, to + // implement these methods: + // * `Element.prototype.scrollLeft`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft. + // * `Element.prototype.scrollTop`: see + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop. + + // 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 position. + + // getScrollPosition(shadowNode: ShadowNode): + // ?[ + // /* scrollLeft: */ number, + // /* scrollTop: */ number, + // ] + return jsi::Function::createFromHostFunction( + runtime, + name, + 1, + [uiManager]( + jsi::Runtime &runtime, + jsi::Value const & /*thisValue*/, + jsi::Value const *arguments, + size_t /*count*/) noexcept -> jsi::Value { + 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", it returns an empty layout metrics object. + auto layoutMetrics = uiManager->getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ true}); + if (layoutMetrics == EmptyLayoutMetrics) { + return jsi::Value::undefined(); + } + + auto layoutableShadowNode = traitCast( + newestCloneOfShadowNode.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return jsi::Value::undefined(); + } + + auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); + + return jsi::Array::createWithElements( + runtime, + jsi::Value{ + runtime, + scrollPosition.x == 0 ? 0 : (double)-scrollPosition.x}, + jsi::Value{ + runtime, + scrollPosition.y == 0 ? 0 : (double)-scrollPosition.y}); + }); + } + return jsi::Value::undefined(); }