Skip to content

Commit 909559a

Browse files
rubennortefacebook-github-bot
authored andcommitted
Implement scrollWidth/scrollHeight (facebook#39328)
Summary: Pull Request resolved: facebook#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: 03f1875e66dbdaafb51291888f5fdf551692543e
1 parent c3e6ac2 commit 909559a

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
lines changed

packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,16 @@ export default class ReadOnlyElement extends ReadOnlyNode {
135135
}
136136

137137
get scrollHeight(): number {
138-
throw new Error('Unimplemented');
138+
const node = getShadowNode(this);
139+
140+
if (node != null) {
141+
const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node);
142+
if (scrollSize != null) {
143+
return scrollSize[1];
144+
}
145+
}
146+
147+
return 0;
139148
}
140149

141150
get scrollLeft(): number {
@@ -169,7 +178,16 @@ export default class ReadOnlyElement extends ReadOnlyNode {
169178
}
170179

171180
get scrollWidth(): number {
172-
throw new Error('Unimplemented');
181+
const node = getShadowNode(this);
182+
183+
if (node != null) {
184+
const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node);
185+
if (scrollSize != null) {
186+
return scrollSize[0];
187+
}
188+
}
189+
190+
return 0;
173191
}
174192

175193
get tagName(): string {

packages/react-native/Libraries/ReactNative/FabricUIManager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ export interface Spec {
9191
+getScrollPosition: (
9292
node: Node,
9393
) => ?[/* scrollLeft: */ number, /* scrollTop: */ number];
94+
+getScrollSize: (
95+
node: Node,
96+
) => ?[/* scrollWidth: */ number, /* scrollHeight: */ number];
9497
+getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number];
9598
+getBorderSize: (
9699
node: Node,
@@ -141,6 +144,7 @@ const CACHED_PROPERTIES = [
141144
'getBoundingClientRect',
142145
'getOffset',
143146
'getScrollPosition',
147+
'getScrollSize',
144148
'getInnerSize',
145149
'getBorderSize',
146150
'getTagName',

packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,34 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
524524
},
525525
),
526526

527+
getScrollSize: jest.fn(
528+
(node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => {
529+
ensureHostNode(node);
530+
531+
const nodeInCurrentTree = getNodeInCurrentTree(node);
532+
const currentProps =
533+
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
534+
if (currentProps == null) {
535+
return null;
536+
}
537+
538+
const scrollForTests: ?{
539+
scrollWidth: number,
540+
scrollHeight: number,
541+
...
542+
} =
543+
// $FlowExpectedError[prop-missing]
544+
currentProps.__scrollForTests;
545+
546+
if (scrollForTests == null) {
547+
return null;
548+
}
549+
550+
const {scrollWidth, scrollHeight} = scrollForTests;
551+
return [scrollWidth, scrollHeight];
552+
},
553+
),
554+
527555
getInnerSize: jest.fn(
528556
(node: Node): ?[/* width: */ number, /* height: */ number] => {
529557
ensureHostNode(node);

packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,74 @@ jsi::Value UIManagerBinding::get(
12091209
});
12101210
}
12111211

1212+
if (methodName == "getScrollSize") {
1213+
// This is a method to access the scroll information of a shadow node, to
1214+
// implement these methods:
1215+
// * `Element.prototype.scrollWidth`: see
1216+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth.
1217+
// * `Element.prototype.scrollHeight`: see
1218+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight.
1219+
1220+
// It uses the version of the shadow node that is present in the current
1221+
// revision of the shadow tree. If the node is not present or is not
1222+
// displayed (because any of its ancestors or itself have 'display: none'),
1223+
// it returns undefined. Otherwise, it returns the scroll size.
1224+
1225+
// getScrollSize(shadowNode: ShadowNode):
1226+
// ?[
1227+
// /* scrollWidth: */ number,
1228+
// /* scrollHeight: */ number,
1229+
// ]
1230+
auto paramCount = 1;
1231+
return jsi::Function::createFromHostFunction(
1232+
runtime,
1233+
name,
1234+
paramCount,
1235+
[uiManager, methodName, paramCount](
1236+
jsi::Runtime& runtime,
1237+
const jsi::Value& /*thisValue*/,
1238+
const jsi::Value* arguments,
1239+
size_t count) -> jsi::Value {
1240+
validateArgumentCount(runtime, methodName, paramCount, count);
1241+
1242+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
1243+
1244+
auto newestCloneOfShadowNode =
1245+
uiManager->getNewestCloneOfShadowNode(*shadowNode);
1246+
// The node is no longer part of an active shadow tree, or it is the
1247+
// root node
1248+
if (newestCloneOfShadowNode == nullptr) {
1249+
return jsi::Value::undefined();
1250+
}
1251+
1252+
// If the node is not displayed (itself or any of its ancestors has
1253+
// "display: none"), this returns an empty layout metrics object.
1254+
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
1255+
*shadowNode, nullptr, {/* .includeTransform = */ false});
1256+
1257+
if (layoutMetrics == EmptyLayoutMetrics ||
1258+
layoutMetrics.displayType == DisplayType::Inline) {
1259+
return jsi::Value::undefined();
1260+
}
1261+
1262+
auto layoutableShadowNode =
1263+
traitCast<YogaLayoutableShadowNode const*>(
1264+
newestCloneOfShadowNode.get());
1265+
// This should never happen
1266+
if (layoutableShadowNode == nullptr) {
1267+
return jsi::Value::undefined();
1268+
}
1269+
1270+
Size scrollSize = getScrollSize(
1271+
layoutMetrics, layoutableShadowNode->getContentBounds());
1272+
1273+
return jsi::Array::createWithElements(
1274+
runtime,
1275+
jsi::Value{runtime, std::round(scrollSize.width)},
1276+
jsi::Value{runtime, std::round(scrollSize.height)});
1277+
});
1278+
}
1279+
12121280
if (methodName == "getInnerSize") {
12131281
// This is a method to access the inner size of a shadow node, to implement
12141282
// these methods:

packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,39 @@ inline static void getTextContentInShadowNode(
221221
getTextContentInShadowNode(*childNode.get(), result);
222222
}
223223
}
224+
225+
inline static Rect getScrollableContentBounds(
226+
Rect contentBounds,
227+
LayoutMetrics layoutMetrics) {
228+
auto paddingFrame = layoutMetrics.getPaddingFrame();
229+
230+
auto paddingBottom =
231+
layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom;
232+
auto paddingLeft =
233+
layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left;
234+
auto paddingRight =
235+
layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right;
236+
237+
auto minY = paddingFrame.getMinY();
238+
auto maxY =
239+
std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom);
240+
241+
auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft
242+
? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft)
243+
: paddingFrame.getMinX();
244+
auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft
245+
? paddingFrame.getMaxX()
246+
: std::max(
247+
paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight);
248+
249+
return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}};
250+
}
251+
252+
inline static Size getScrollSize(
253+
LayoutMetrics layoutMetrics,
254+
Rect contentBounds) {
255+
auto scrollableContentBounds =
256+
getScrollableContentBounds(contentBounds, layoutMetrics);
257+
return scrollableContentBounds.size;
258+
}
224259
} // namespace facebook::react

0 commit comments

Comments
 (0)