Skip to content

Commit

Permalink
Implement clientWidth/clientHeight in ReadOnlyElement (facebook#39305)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#39305

This adds a new method in Fabric to get the inner size for an element (whole size excluding borders, which would be the scrollable size of the element), and uses it to implement the following methods as defined in react-native-community/discussions-and-proposals#607 :
`clientWidth`: width of the element excluding the size of the left and right border.
`clientHeight`: height of the element excluding the size of the top and bottom border.

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

These APIs provided rounded integers.

Changelog: [internal]

Differential Revision: D49008698

fbshipit-source-id: a00851c55af21057bea228dee32be5175748b32f
  • Loading branch information
rubennorte authored and facebook-github-bot committed Sep 7, 2023
1 parent 8bff7ba commit 8b9f857
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 6 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 @@ -33,7 +33,16 @@ export default class ReadOnlyElement extends ReadOnlyNode {
}

get clientHeight(): number {
throw new TypeError('Unimplemented');
const node = getShadowNode(this);

if (node != null) {
const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node);
if (innerSize != null) {
return innerSize[1];
}
}

return 0;
}

get clientLeft(): number {
Expand All @@ -45,7 +54,16 @@ export default class ReadOnlyElement extends ReadOnlyNode {
}

get clientWidth(): number {
throw new TypeError('Unimplemented');
const node = getShadowNode(this);

if (node != null) {
const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node);
if (innerSize != null) {
return innerSize[0];
}
}

return 0;
}

get firstElementChild(): ReadOnlyElement | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface Spec {
+getScrollPosition: (
node: Node,
) => ?[/* scrollLeft: */ number, /* scrollTop: */ number];
+getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number];
+getTagName: (node: Node) => string;

/**
Expand Down Expand Up @@ -132,6 +133,7 @@ const CACHED_PROPERTIES = [
'getBoundingClientRect',
'getOffset',
'getScrollPosition',
'getInnerSize',
'getTagName',
'hasPointerCapture',
'setPointerCapture',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,15 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
});
},
),

cloneNode: jest.fn((node: Node): Node => {
return toNode({...fromNode(node)});
}),

cloneNodeWithNewChildren: jest.fn((node: Node): Node => {
return toNode({...fromNode(node), children: []});
}),

cloneNodeWithNewProps: jest.fn((node: Node, newProps: NodeProps): Node => {
return toNode({
...fromNode(node),
Expand All @@ -214,6 +217,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
},
});
}),

cloneNodeWithNewChildrenAndProps: jest.fn(
(node: Node, newProps: NodeProps): Node => {
return toNode({
Expand All @@ -226,35 +230,42 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
});
},
),

createChildSet: jest.fn((rootTag: RootTag): NodeSet => {
return [];
}),

appendChild: jest.fn((parentNode: Node, child: Node): Node => {
// Although the signature returns a Node, React expects this to be mutating.
fromNode(parentNode).children.push(child);
return parentNode;
}),

appendChildToSet: jest.fn((childSet: NodeSet, child: Node): void => {
childSet.push(child);
}),

completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {
commitHooks.forEach(hook =>
hook.shadowTreeWillCommit(rootTag, roots.get(rootTag), childSet),
);
roots.set(rootTag, childSet);
}),

measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
ensureHostNode(node);

callback(10, 10, 100, 100, 0, 0);
}),

measureInWindow: jest.fn(
(node: Node, callback: MeasureInWindowOnSuccessCallback): void => {
ensureHostNode(node);

callback(10, 10, 100, 100);
},
),

measureLayout: jest.fn(
(
node: Node,
Expand All @@ -268,15 +279,19 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
onSuccess(1, 1, 100, 100);
},
),

configureNextLayoutAnimation: jest.fn(
(
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
errorCallback: () => void,
): void => {},
),

sendAccessibilityEvent: jest.fn((node: Node, eventType: string): void => {}),

findShadowNodeByTag_DEPRECATED: jest.fn((reactTag: number): ?Node => {}),

getBoundingClientRect: jest.fn(
(
node: Node,
Expand Down Expand Up @@ -312,13 +327,19 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
return [x, y, width, height];
},
),

hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false),

setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}),

releasePointerCapture: jest.fn((node: Node, pointerId: number): void => {}),

setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),

dispatchCommand: jest.fn(
(node: Node, commandName: string, args: Array<mixed>): void => {},
),

getParentNode: jest.fn((node: Node): ?InternalInstanceHandle => {
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null || ancestors.length - 2 < 0) {
Expand All @@ -329,6 +350,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
const parentInCurrentTree = fromNode(parentOfParent).children[position];
return fromNode(parentInCurrentTree).instanceHandle;
}),

getChildNodes: jest.fn(
(node: Node): $ReadOnlyArray<InternalInstanceHandle> => {
const nodeInCurrentTree = getNodeInCurrentTree(node);
Expand All @@ -342,9 +364,11 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
);
},
),

isConnected: jest.fn((node: Node): boolean => {
return getNodeInCurrentTree(node) != null;
}),

getTextContent: jest.fn((node: Node): string => {
const nodeInCurrentTree = getNodeInCurrentTree(node);

Expand All @@ -366,6 +390,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
}
return result;
}),

compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => {
/* eslint-disable no-bitwise */
const ReadOnlyNode = require('../../DOM/Nodes/ReadOnlyNode').default;
Expand Down Expand Up @@ -419,6 +444,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {

return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING;
}),

getOffset: jest.fn(
(
node: Node,
Expand Down Expand Up @@ -469,6 +495,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
];
},
),

getScrollPosition: jest.fn(
(node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => {
ensureHostNode(node);
Expand Down Expand Up @@ -497,6 +524,34 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
},
),

getInnerSize: jest.fn(
(node: Node): ?[/* width: */ number, /* height: */ number] => {
ensureHostNode(node);

const nodeInCurrentTree = getNodeInCurrentTree(node);
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null) {
return null;
}

const innerSizeForTests: ?{
width: number,
height: number,
...
} =
// $FlowExpectedError[prop-missing]
currentProps.__innerSizeForTests;

if (innerSizeForTests == null) {
return null;
}

const {width, height} = innerSizeForTests;
return [width, height];
},
),

getTagName: jest.fn((node: Node): string => {
ensureHostNode(node);
return 'RN:' + fromNode(node).viewName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ jsi::Value UIManagerBinding::get(
}

if (methodName == "getOffset") {
// This is a method to access offset information for React Native nodes, to
// This is a method to access the offset information for a shadow node, to
// implement these methods:
// * `HTMLElement.prototype.offsetParent`: see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent.
Expand Down Expand Up @@ -1099,9 +1099,10 @@ jsi::Value UIManagerBinding::get(
}

// If the node is not displayed (itself or any of its ancestors has
// "display: none", it returns an empty layout metrics object.
// "display: none"), this returns an empty layout metrics object.
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNode, nullptr, {/* .includeTransform = */ true});

if (layoutMetrics == EmptyLayoutMetrics) {
return jsi::Value::undefined();
}
Expand Down Expand Up @@ -1140,7 +1141,7 @@ jsi::Value UIManagerBinding::get(
}

if (methodName == "getScrollPosition") {
// This is a method to access scroll information for React Native nodes, to
// This is a method to access scroll information for a shadow node, to
// implement these methods:
// * `Element.prototype.scrollLeft`: see
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft.
Expand Down Expand Up @@ -1180,9 +1181,10 @@ jsi::Value UIManagerBinding::get(
}

// If the node is not displayed (itself or any of its ancestors has
// "display: none", it returns an empty layout metrics object.
// "display: none"), this returns an empty layout metrics object.
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNode, nullptr, {/* .includeTransform = */ true});

if (layoutMetrics == EmptyLayoutMetrics) {
return jsi::Value::undefined();
}
Expand All @@ -1207,6 +1209,58 @@ jsi::Value UIManagerBinding::get(
});
}

if (methodName == "getInnerSize") {
// This is a method to access the inner size of a shadow node, to implement
// these methods:
// * `Element.prototype.clientWidth`: see
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth.
// * `Element.prototype.clientHeight`: see
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight.

// 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, it is not
// displayed (because any of its ancestors or itself have 'display: none'),
// or it has an inline display, it returns undefined.
// Otherwise, it returns its inner size.

// getInnerSize(shadowNode: ShadowNode):
// ?[
// /* width: */ number,
// /* height: */ 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]);

// 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 = */ true});

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

auto innerFrame = getInnerFrame(layoutMetrics);

return jsi::Array::createWithElements(
runtime,
jsi::Value{runtime, std::round(innerFrame.size.width)},
jsi::Value{runtime, std::round(innerFrame.size.height)});
});
}

if (methodName == "getTagName") {
// This is a method to access the normalized tag name of a shadow node, to
// implement `Element.prototype.tagName` (see
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
#include <react/debug/react_native_assert.h>
#include <react/renderer/components/text/RawTextShadowNode.h>
#include <react/renderer/core/EventHandler.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/core/TraitCast.h>
#include <react/renderer/graphics/Rect.h>
#include <react/utils/CoreFeatures.h>

namespace facebook::react {
Expand Down Expand Up @@ -219,4 +221,16 @@ inline static void getTextContentInShadowNode(
getTextContentInShadowNode(*childNode.get(), result);
}
}

// Origin: the outer border of the node.
// Size: includes content and padding (but no borders).
inline static Rect getInnerFrame(LayoutMetrics layoutMetrics) {
return Rect{
Point{layoutMetrics.borderWidth.left, layoutMetrics.borderWidth.top},
Size{
layoutMetrics.frame.size.width - layoutMetrics.borderWidth.left -
layoutMetrics.borderWidth.right,
layoutMetrics.frame.size.height - layoutMetrics.borderWidth.top -
layoutMetrics.borderWidth.bottom}};
}
} // namespace facebook::react

0 comments on commit 8b9f857

Please sign in to comment.