Skip to content

Commit 41036c8

Browse files
rubennortefacebook-github-bot
authored andcommitted
Implement clientWidth/clientHeight in ReadOnlyElement (facebook#39305)
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] Reviewed By: NickGerleman Differential Revision: D49008698 fbshipit-source-id: 46304d16a70acf1f2e5da7dd74cded0dc41e9710
1 parent 5de7161 commit 41036c8

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed

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

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

3535
get clientHeight(): number {
36-
throw new TypeError('Unimplemented');
36+
const node = getShadowNode(this);
37+
38+
if (node != null) {
39+
const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node);
40+
if (innerSize != null) {
41+
return innerSize[1];
42+
}
43+
}
44+
45+
return 0;
3746
}
3847

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

4756
get clientWidth(): number {
48-
throw new TypeError('Unimplemented');
57+
const node = getShadowNode(this);
58+
59+
if (node != null) {
60+
const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node);
61+
if (innerSize != null) {
62+
return innerSize[0];
63+
}
64+
}
65+
66+
return 0;
4967
}
5068

5169
get firstElementChild(): ReadOnlyElement | null {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export interface Spec {
9191
+getScrollPosition: (
9292
node: Node,
9393
) => ?[/* scrollLeft: */ number, /* scrollTop: */ number];
94+
+getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number];
9495
+getTagName: (node: Node) => string;
9596

9697
/**
@@ -132,6 +133,7 @@ const CACHED_PROPERTIES = [
132133
'getBoundingClientRect',
133134
'getOffset',
134135
'getScrollPosition',
136+
'getInnerSize',
135137
'getTagName',
136138
'hasPointerCapture',
137139
'setPointerCapture',

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,15 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
199199
});
200200
},
201201
),
202+
202203
cloneNode: jest.fn((node: Node): Node => {
203204
return toNode({...fromNode(node)});
204205
}),
206+
205207
cloneNodeWithNewChildren: jest.fn((node: Node): Node => {
206208
return toNode({...fromNode(node), children: []});
207209
}),
210+
208211
cloneNodeWithNewProps: jest.fn((node: Node, newProps: NodeProps): Node => {
209212
return toNode({
210213
...fromNode(node),
@@ -214,6 +217,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
214217
},
215218
});
216219
}),
220+
217221
cloneNodeWithNewChildrenAndProps: jest.fn(
218222
(node: Node, newProps: NodeProps): Node => {
219223
return toNode({
@@ -226,35 +230,42 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
226230
});
227231
},
228232
),
233+
229234
createChildSet: jest.fn((rootTag: RootTag): NodeSet => {
230235
return [];
231236
}),
237+
232238
appendChild: jest.fn((parentNode: Node, child: Node): Node => {
233239
// Although the signature returns a Node, React expects this to be mutating.
234240
fromNode(parentNode).children.push(child);
235241
return parentNode;
236242
}),
243+
237244
appendChildToSet: jest.fn((childSet: NodeSet, child: Node): void => {
238245
childSet.push(child);
239246
}),
247+
240248
completeRoot: jest.fn((rootTag: RootTag, childSet: NodeSet): void => {
241249
commitHooks.forEach(hook =>
242250
hook.shadowTreeWillCommit(rootTag, roots.get(rootTag), childSet),
243251
);
244252
roots.set(rootTag, childSet);
245253
}),
254+
246255
measure: jest.fn((node: Node, callback: MeasureOnSuccessCallback): void => {
247256
ensureHostNode(node);
248257

249258
callback(10, 10, 100, 100, 0, 0);
250259
}),
260+
251261
measureInWindow: jest.fn(
252262
(node: Node, callback: MeasureInWindowOnSuccessCallback): void => {
253263
ensureHostNode(node);
254264

255265
callback(10, 10, 100, 100);
256266
},
257267
),
268+
258269
measureLayout: jest.fn(
259270
(
260271
node: Node,
@@ -268,15 +279,19 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
268279
onSuccess(1, 1, 100, 100);
269280
},
270281
),
282+
271283
configureNextLayoutAnimation: jest.fn(
272284
(
273285
config: LayoutAnimationConfig,
274286
callback: () => void, // check what is returned here
275287
errorCallback: () => void,
276288
): void => {},
277289
),
290+
278291
sendAccessibilityEvent: jest.fn((node: Node, eventType: string): void => {}),
292+
279293
findShadowNodeByTag_DEPRECATED: jest.fn((reactTag: number): ?Node => {}),
294+
280295
getBoundingClientRect: jest.fn(
281296
(
282297
node: Node,
@@ -312,13 +327,19 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
312327
return [x, y, width, height];
313328
},
314329
),
330+
315331
hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false),
332+
316333
setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}),
334+
317335
releasePointerCapture: jest.fn((node: Node, pointerId: number): void => {}),
336+
318337
setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}),
338+
319339
dispatchCommand: jest.fn(
320340
(node: Node, commandName: string, args: Array<mixed>): void => {},
321341
),
342+
322343
getParentNode: jest.fn((node: Node): ?InternalInstanceHandle => {
323344
const ancestors = getAncestorsInCurrentTree(node);
324345
if (ancestors == null || ancestors.length - 2 < 0) {
@@ -329,6 +350,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
329350
const parentInCurrentTree = fromNode(parentOfParent).children[position];
330351
return fromNode(parentInCurrentTree).instanceHandle;
331352
}),
353+
332354
getChildNodes: jest.fn(
333355
(node: Node): $ReadOnlyArray<InternalInstanceHandle> => {
334356
const nodeInCurrentTree = getNodeInCurrentTree(node);
@@ -342,9 +364,11 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
342364
);
343365
},
344366
),
367+
345368
isConnected: jest.fn((node: Node): boolean => {
346369
return getNodeInCurrentTree(node) != null;
347370
}),
371+
348372
getTextContent: jest.fn((node: Node): string => {
349373
const nodeInCurrentTree = getNodeInCurrentTree(node);
350374

@@ -366,6 +390,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
366390
}
367391
return result;
368392
}),
393+
369394
compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => {
370395
/* eslint-disable no-bitwise */
371396
const ReadOnlyNode = require('../../DOM/Nodes/ReadOnlyNode').default;
@@ -419,6 +444,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
419444

420445
return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING;
421446
}),
447+
422448
getOffset: jest.fn(
423449
(
424450
node: Node,
@@ -469,6 +495,7 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
469495
];
470496
},
471497
),
498+
472499
getScrollPosition: jest.fn(
473500
(node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => {
474501
ensureHostNode(node);
@@ -497,6 +524,34 @@ const FabricUIManagerMock: IFabricUIManagerMock = {
497524
},
498525
),
499526

527+
getInnerSize: jest.fn(
528+
(node: Node): ?[/* width: */ number, /* height: */ 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 innerSizeForTests: ?{
539+
width: number,
540+
height: number,
541+
...
542+
} =
543+
// $FlowExpectedError[prop-missing]
544+
currentProps.__innerSizeForTests;
545+
546+
if (innerSizeForTests == null) {
547+
return null;
548+
}
549+
550+
const {width, height} = innerSizeForTests;
551+
return [width, height];
552+
},
553+
),
554+
500555
getTagName: jest.fn((node: Node): string => {
501556
ensureHostNode(node);
502557
return 'RN:' + fromNode(node).viewName;

packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ struct LayoutMetrics {
5353
frame.size.height - contentInsets.top - contentInsets.bottom}};
5454
}
5555

56+
// Origin: the outer border of the node.
57+
// Size: includes content and padding (but no borders).
58+
Rect getPaddingFrame() const {
59+
return Rect{
60+
Point{borderWidth.left, borderWidth.top},
61+
Size{
62+
frame.size.width - borderWidth.left - borderWidth.right,
63+
frame.size.height - borderWidth.top - borderWidth.bottom}};
64+
}
65+
5666
bool operator==(const LayoutMetrics& rhs) const {
5767
return std::tie(
5868
this->frame,

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,7 @@ jsi::Value UIManagerBinding::get(
10511051
}
10521052

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

11011101
// If the node is not displayed (itself or any of its ancestors has
1102-
// "display: none", it returns an empty layout metrics object.
1102+
// "display: none"), this returns an empty layout metrics object.
11031103
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
11041104
*shadowNode, nullptr, {/* .includeTransform = */ true});
1105+
11051106
if (layoutMetrics == EmptyLayoutMetrics) {
11061107
return jsi::Value::undefined();
11071108
}
@@ -1140,7 +1141,7 @@ jsi::Value UIManagerBinding::get(
11401141
}
11411142

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

11821183
// If the node is not displayed (itself or any of its ancestors has
1183-
// "display: none", it returns an empty layout metrics object.
1184+
// "display: none"), this returns an empty layout metrics object.
11841185
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
11851186
*shadowNode, nullptr, {/* .includeTransform = */ true});
1187+
11861188
if (layoutMetrics == EmptyLayoutMetrics) {
11871189
return jsi::Value::undefined();
11881190
}
@@ -1207,6 +1209,58 @@ jsi::Value UIManagerBinding::get(
12071209
});
12081210
}
12091211

1212+
if (methodName == "getInnerSize") {
1213+
// This is a method to access the inner size of a shadow node, to implement
1214+
// these methods:
1215+
// * `Element.prototype.clientWidth`: see
1216+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth.
1217+
// * `Element.prototype.clientHeight`: see
1218+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight.
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, it is not
1222+
// displayed (because any of its ancestors or itself have 'display: none'),
1223+
// or it has an inline display, it returns undefined.
1224+
// Otherwise, it returns its inner size.
1225+
1226+
// getInnerSize(shadowNode: ShadowNode):
1227+
// ?[
1228+
// /* width: */ number,
1229+
// /* height: */ number,
1230+
// ]
1231+
auto paramCount = 1;
1232+
return jsi::Function::createFromHostFunction(
1233+
runtime,
1234+
name,
1235+
paramCount,
1236+
[uiManager, methodName, paramCount](
1237+
jsi::Runtime& runtime,
1238+
const jsi::Value& /*thisValue*/,
1239+
const jsi::Value* arguments,
1240+
size_t count) -> jsi::Value {
1241+
validateArgumentCount(runtime, methodName, paramCount, count);
1242+
1243+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
1244+
1245+
// If the node is not displayed (itself or any of its ancestors has
1246+
// "display: none"), this returns an empty layout metrics object.
1247+
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
1248+
*shadowNode, nullptr, {/* .includeTransform = */ true});
1249+
1250+
if (layoutMetrics == EmptyLayoutMetrics ||
1251+
layoutMetrics.displayType == DisplayType::Inline) {
1252+
return jsi::Value::undefined();
1253+
}
1254+
1255+
auto paddingFrame = layoutMetrics.getPaddingFrame();
1256+
1257+
return jsi::Array::createWithElements(
1258+
runtime,
1259+
jsi::Value{runtime, std::round(paddingFrame.size.width)},
1260+
jsi::Value{runtime, std::round(paddingFrame.size.height)});
1261+
});
1262+
}
1263+
12101264
if (methodName == "getTagName") {
12111265
// This is a method to access the normalized tag name of a shadow node, to
12121266
// implement `Element.prototype.tagName` (see

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
#include <react/debug/react_native_assert.h>
1414
#include <react/renderer/components/text/RawTextShadowNode.h>
1515
#include <react/renderer/core/EventHandler.h>
16+
#include <react/renderer/core/LayoutMetrics.h>
1617
#include <react/renderer/core/ShadowNode.h>
1718
#include <react/renderer/core/TraitCast.h>
19+
#include <react/renderer/graphics/Rect.h>
1820
#include <react/utils/CoreFeatures.h>
1921

2022
namespace facebook::react {

0 commit comments

Comments
 (0)