From 08948810cbac8dc85b11abc462a3ea2750ff8693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 12 Jun 2023 11:02:22 -0700 Subject: [PATCH] Implement scrollTop and scrollLeft (#37754) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37754 This adds a new method in Fabric to get the scroll position for an element, and uses it to implement the following methods as defined in as defined in https://github.com/react-native-community/discussions-and-proposals/pull/607 : * `scrollTop`: indicates the number of pixels (in device independent pixels) from the content that have moved in the vertical axis in the scroll container. * `scrollLeft`: indicates the number of pixels (in device independent pixels) from the content that have moved in the horizontal axis in the scroll container. These API can provide decimal values. Changelog: [internal] Reviewed By: javache Differential Revision: D44757811 fbshipit-source-id: e1b58db8d9f61e823b62d54620a9a0deaae1566b --- .../Libraries/DOM/Nodes/ReadOnlyElement.js | 30 +++++++-- .../Libraries/ReactNative/FabricUIManager.js | 11 ++-- .../ReactNative/__mocks__/FabricUIManager.js | 27 ++++++++ .../renderer/uimanager/UIManagerBinding.cpp | 65 +++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) 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(); }