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
This commit is contained in:
Rubén Norte
2023-06-12 11:02:22 -07:00
committed by Facebook GitHub Bot
parent 7d1f7f3f5f
commit 08948810cb
4 changed files with 125 additions and 8 deletions
@@ -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 {
@@ -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
@@ -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;
@@ -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<LayoutableShadowNode const *>(
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();
}