Skip to content

Commit

Permalink
Implement scrollWidth/scrollHeight (#39328)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #39328

This adds a new method in Fabric to get the scroll size for an element, and uses it to implement `scrollWidth` and `scrollHeight` as defined in react-native-community/discussions-and-proposals#607

Scroll size determine how much of the content of a node would move if the node was scrollable. If the content does not overflow the padding box of the node, then this is the same as the `client{Width,Height}` (the size of the node without its borders). If the content would overflow the node, then it would be the size of the content that would be scrollable (in other words, what would "move" when you scrolled).

If the element isn't displayed or it has display: inline, it return 0 in both cases.

These APIs provide rounded integers.

NOTE: The current implementation of `ScrollView` has several known bugs and inconsistencies across platforms (Android vs. iOS) and architectures (Paper vs. Fabric) (e.g.: content showing on top of the border on Android, `overflow: visible` only working on Android but not on iOS, etc.). The data that this API reports is the one that aligns with the Web (with a few limitations), and we'll need to fix the implementation to align with this.

NOTE: transforms are not considered correctly for the sake of this API, but also not applied correctly in any of the native platforms. On Web, the scrollable area is the overflow of all the children **with transforms applied** which isn't honored in RN. We''ll fix the data reported by this API when we also fix the user perceived behavior.

Changelog: [internal]

Reviewed By: sammy-SC

Differential Revision: D49058368

fbshipit-source-id: 39a10bf7bddec9afc54f46cc02284d601b6962f3
  • Loading branch information
rubennorte authored and facebook-github-bot committed Sep 8, 2023
1 parent f2473a1 commit f275603
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 2 deletions.
22 changes: 20 additions & 2 deletions packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -141,6 +144,7 @@ const CACHED_PROPERTIES = [
'getBoundingClientRect',
'getOffset',
'getScrollPosition',
'getScrollSize',
'getInnerSize',
'getBorderSize',
'getTagName',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = */ false});

if (layoutMetrics == EmptyLayoutMetrics ||
layoutMetrics.displayType == DisplayType::Inline) {
return jsi::Value::undefined();
}

auto layoutableShadowNode =
traitCast<YogaLayoutableShadowNode const*>(
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,39 @@ inline static void getTextContentInShadowNode(
getTextContentInShadowNode(*childNode.get(), result);
}
}

inline static Rect getScrollableContentBounds(
Rect contentBounds,
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(
LayoutMetrics layoutMetrics,
Rect contentBounds) {
auto scrollableContentBounds =
getScrollableContentBounds(contentBounds, layoutMetrics);
return scrollableContentBounds.size;
}
} // namespace facebook::react

0 comments on commit f275603

Please sign in to comment.