Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement scrollWidth/scrollHeight #39328

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -666,24 +666,23 @@ void YogaLayoutableShadowNode::layoutTree(
}

static EdgeInsets calculateOverflowInset(
Rect containerFrame,
Rect contentFrame) {
auto size = containerFrame.size;
Rect contentFrame,
Rect contentBounds) {
auto size = contentFrame.size;
auto overflowInset = EdgeInsets{};
overflowInset.left = std::min(contentFrame.getMinX(), Float{0.0});
overflowInset.top = std::min(contentFrame.getMinY(), Float{0.0});
overflowInset.left = std::min(contentBounds.getMinX(), Float{0.0});
overflowInset.top = std::min(contentBounds.getMinY(), Float{0.0});
overflowInset.right =
-std::max(contentFrame.getMaxX() - size.width, Float{0.0});
-std::max(contentBounds.getMaxX() - size.width, Float{0.0});
overflowInset.bottom =
-std::max(contentFrame.getMaxY() - size.height, Float{0.0});
-std::max(contentBounds.getMaxY() - size.height, Float{0.0});
return overflowInset;
}

void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) {
// Reading data from a dirtied node does not make sense.
react_native_assert(!yogaNode_.isDirty());

auto contentFrame = Rect{};
for (auto childYogaNode : yogaNode_.getChildren()) {
auto& childNode = shadowNodeFromContext(childYogaNode);

Expand Down Expand Up @@ -725,6 +724,33 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) {
childNode.layout(layoutContext);
}
}
}

if (yogaNode_.getStyle().overflow() == YGOverflowVisible) {
// Note that the parent node's overflow layout is NOT affected by its
// transform matrix. That transform matrix is applied on the parent node as
// well as all of its child nodes, which won't cause changes on the
// overflowInset values. A special note on the scale transform -- the scaled
// layout may look like it's causing overflowInset changes, but it's purely
// cosmetic and will be handled by pixel density conversion logic later when
// render the view. The actual overflowInset value is not changed as if the
// transform is not happening here.
auto contentBounds = getContentBounds();
layoutMetrics_.overflowInset =
calculateOverflowInset(layoutMetrics_.frame, contentBounds);
} else {
layoutMetrics_.overflowInset = {};
}
}

Rect YogaLayoutableShadowNode::getContentBounds() const {
auto contentBounds = Rect{};

for (auto childYogaNode : yogaNode_.getChildren()) {
auto& childNode = shadowNodeFromContext(childYogaNode);

// Verifying that the Yoga node belongs to the ShadowNode.
react_native_assert(&childNode.yogaNode_ == childYogaNode);

auto layoutMetricsWithOverflowInset = childNode.getLayoutMetrics();
if (layoutMetricsWithOverflowInset.displayType != DisplayType::None) {
Expand All @@ -733,43 +759,30 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) {
? viewChildNode->getConcreteProps().hitSlop
: EdgeInsets{};

// The contentFrame should always union with existing child node layout +
// The contentBounds should always union with existing child node layout +
// overflowInset. The transform may in a deferred animation and not
// applied yet.
contentFrame.unionInPlace(insetBy(
contentBounds.unionInPlace(insetBy(
layoutMetricsWithOverflowInset.frame,
layoutMetricsWithOverflowInset.overflowInset));
contentFrame.unionInPlace(
contentBounds.unionInPlace(
outsetBy(layoutMetricsWithOverflowInset.frame, hitSlop));

auto childTransform = childNode.getTransform();
if (childTransform != Transform::Identity()) {
// The child node's transform matrix will affect the parent node's
// contentFrame. We need to union with child node's after transform
// contentBounds. We need to union with child node's after transform
// layout here.
contentFrame.unionInPlace(insetBy(
contentBounds.unionInPlace(insetBy(
layoutMetricsWithOverflowInset.frame * childTransform,
layoutMetricsWithOverflowInset.overflowInset * childTransform));
contentFrame.unionInPlace(outsetBy(
contentBounds.unionInPlace(outsetBy(
layoutMetricsWithOverflowInset.frame * childTransform, hitSlop));
}
}
}

if (yogaNode_.getStyle().overflow() == YGOverflowVisible) {
// Note that the parent node's overflow layout is NOT affected by its
// transform matrix. That transform matrix is applied on the parent node as
// well as all of its child nodes, which won't cause changes on the
// overflowInset values. A special note on the scale transform -- the scaled
// layout may look like it's causing overflowInset changes, but it's purely
// cosmetic and will be handled by pixel density conversion logic later when
// render the view. The actual overflowInset value is not changed as if the
// transform is not happening here.
layoutMetrics_.overflowInset =
calculateOverflowInset(layoutMetrics_.frame, contentFrame);
} else {
layoutMetrics_.overflowInset = {};
}
return contentBounds;
}

#pragma mark - Yoga Connectors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode {

void layout(LayoutContext layoutContext) override;

Rect getContentBounds() const;

protected:
/*
* Yoga config associated (only) with this particular 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