Skip to content

Commit 26fdb44

Browse files
rubennortefacebook-github-bot
authored andcommitted
Implement offsetParent, offsetTop and offsetLeft
Summary: This adds a new method in Fabric to get the offset for an element, and uses it to implement the following methods as defined in as defined in react-native-community/discussions-and-proposals#607 : * `offsetParent`: returns the ancestor that's used to position the element relative to. In React Native, this is always the immediate parent, except if the element has `display: none`, in which case is `null`. * `offsetTop`, `offsetLeft`: returns the position of the outer border of the element, relative to the inner border of its offset parent. This is similar to our existing `measure` function, except that `measure` uses the outer border of the parent, instead of the inner one. As with `offsetWidth` and `offsetHeight`, these methods return integers instead of double precision values. Extra: added descriptions to some fields in `LayoutMetrics` :) Changelog: [internal] bypass-github-export-checks Reviewed By: rshest Differential Revision: D44512594 fbshipit-source-id: 8f05e21f73397db7b88e6841cc117320d1229979
1 parent 6b2596d commit 26fdb44

File tree

5 files changed

+198
-4
lines changed

5 files changed

+198
-4
lines changed

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ import {create as createAttributePayload} from '../../ReactNative/ReactFabricPub
2727
import warnForStyleProps from '../../ReactNative/ReactFabricPublicInstance/warnForStyleProps';
2828
import ReadOnlyElement from './ReadOnlyElement';
2929
import ReadOnlyNode from './ReadOnlyNode';
30-
import {getShadowNode} from './ReadOnlyNode';
30+
import {
31+
getPublicInstanceFromInternalInstanceHandle,
32+
getShadowNode,
33+
} from './ReadOnlyNode';
3134
import nullthrows from 'nullthrows';
3235

3336
const noop = () => {};
@@ -59,15 +62,48 @@ export default class ReactNativeElement
5962
}
6063

6164
get offsetLeft(): number {
62-
throw new TypeError('Unimplemented');
65+
const node = getShadowNode(this);
66+
67+
if (node != null) {
68+
const offset = nullthrows(getFabricUIManager()).getOffset(node);
69+
if (offset != null) {
70+
return Math.round(offset[2]);
71+
}
72+
}
73+
74+
return 0;
6375
}
6476

6577
get offsetParent(): ReadOnlyElement | null {
66-
throw new TypeError('Unimplemented');
78+
const node = getShadowNode(this);
79+
80+
if (node != null) {
81+
const offset = nullthrows(getFabricUIManager()).getOffset(node);
82+
if (offset != null) {
83+
const offsetParentInstanceHandle = offset[0];
84+
const offsetParent = getPublicInstanceFromInternalInstanceHandle(
85+
offsetParentInstanceHandle,
86+
);
87+
// $FlowExpectedError[incompatible-type] The value returned by `getOffset` is always an instance handle for `ReadOnlyElement`.
88+
const offsetParentElement: ReadOnlyElement = offsetParent;
89+
return offsetParentElement;
90+
}
91+
}
92+
93+
return null;
6794
}
6895

6996
get offsetTop(): number {
70-
throw new TypeError('Unimplemented');
97+
const node = getShadowNode(this);
98+
99+
if (node != null) {
100+
const offset = nullthrows(getFabricUIManager()).getOffset(node);
101+
if (offset != null) {
102+
return Math.round(offset[1]);
103+
}
104+
}
105+
106+
return 0;
71107
}
72108

73109
get offsetWidth(): number {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ export type Spec = {|
7979
/* width:*/ number,
8080
/* height:*/ number,
8181
],
82+
+getOffset: (
83+
node: Node,
84+
) => ?[
85+
/* offsetParent: */ InternalInstanceHandle,
86+
/* offsetTop: */ number,
87+
/* offsetLeft: */ number,
88+
],
8289
|};
8390

8491
// This is exposed as a getter because apps using the legacy renderer AND

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ function* dfs(node: ?Node): Iterator<Node> {
136136
}
137137
}
138138

139+
function hasDisplayNone(node: Node): boolean {
140+
const props = fromNode(node).props;
141+
// Style is flattened when passed to native, so there's no style object.
142+
// $FlowFixMe[prop-missing]
143+
return props != null && props.display === 'none';
144+
}
145+
139146
const FabricUIManagerMock: FabricUIManager = {
140147
createNode: jest.fn(
141148
(
@@ -374,6 +381,56 @@ const FabricUIManagerMock: FabricUIManager = {
374381

375382
return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING;
376383
}),
384+
getOffset: jest.fn(
385+
(
386+
node: Node,
387+
): ?[
388+
/* offsetParent: */ InternalInstanceHandle,
389+
/* offsetTop: */ number,
390+
/* offsetLeft: */ number,
391+
] => {
392+
const ancestors = getAncestorsInCurrentTree(node);
393+
if (ancestors == null) {
394+
return null;
395+
}
396+
397+
const [parent, position] = ancestors[ancestors.length - 1];
398+
const nodeInCurrentTree = fromNode(parent).children[position];
399+
400+
const currentProps =
401+
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
402+
if (currentProps == null || hasDisplayNone(nodeInCurrentTree)) {
403+
return null;
404+
}
405+
406+
const offsetForTests: ?{
407+
top: number,
408+
left: number,
409+
} =
410+
// $FlowExpectedError[prop-missing]
411+
currentProps.__offsetForTests;
412+
413+
if (offsetForTests == null) {
414+
return null;
415+
}
416+
417+
let currentIndex = ancestors.length - 1;
418+
while (currentIndex >= 0 && !hasDisplayNone(ancestors[currentIndex][0])) {
419+
currentIndex--;
420+
}
421+
422+
if (currentIndex >= 0) {
423+
// The node or one of its ancestors have display: none
424+
return null;
425+
}
426+
427+
return [
428+
fromNode(parent).instanceHandle,
429+
offsetForTests.top,
430+
offsetForTests.left,
431+
];
432+
},
433+
),
377434
};
378435

379436
global.nativeFabricUIManager = FabricUIManagerMock;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@ namespace react {
2121
* Describes results of layout process for particular shadow node.
2222
*/
2323
struct LayoutMetrics {
24+
// Origin: relative to its parent content frame (unless using a method that
25+
// computes it relative to other parent or the viewport)
26+
// Size: includes border, padding and content.
2427
Rect frame;
28+
// Width of the border + padding in all directions.
2529
EdgeInsets contentInsets{0};
30+
// Width of the border in all directions.
2631
EdgeInsets borderWidth{0};
2732
DisplayType displayType{DisplayType::Flex};
2833
LayoutDirection layoutDirection{LayoutDirection::Undefined};
2934
Float pointScaleFactor{1.0};
3035
EdgeInsets overflowInset{};
3136

37+
// Origin: the outer border of the node.
38+
// Size: includes content only.
3239
Rect getContentFrame() const {
3340
return Rect{
3441
Point{contentInsets.left, contentInsets.top},

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,93 @@ jsi::Value UIManagerBinding::get(
923923
});
924924
}
925925

926+
if (methodName == "getOffset") {
927+
// This is a method to access offset information for React Native nodes, to
928+
// implement these methods:
929+
// * `HTMLElement.prototype.offsetParent`: see
930+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent.
931+
// * `HTMLElement.prototype.offsetTop`: see
932+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop.
933+
// * `HTMLElement.prototype.offsetLeft`: see
934+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft.
935+
936+
// It uses the version of the shadow node that is present in the current
937+
// revision of the shadow tree. If the node is not present or is not
938+
// displayed (because any of its ancestors or itself have 'display: none'),
939+
// it returns undefined. Otherwise, it returns its parent (as all nodes in
940+
// React Native are currently "positioned") and its offset relative to its
941+
// parent.
942+
943+
// getOffset(shadowNode: ShadowNode):
944+
// ?[
945+
// /* parent: */ InstanceHandle,
946+
// /* top: */ number,
947+
// /* left: */ number,
948+
// ]
949+
return jsi::Function::createFromHostFunction(
950+
runtime,
951+
name,
952+
1,
953+
[uiManager](
954+
jsi::Runtime &runtime,
955+
jsi::Value const & /*thisValue*/,
956+
jsi::Value const *arguments,
957+
size_t /*count*/) noexcept -> jsi::Value {
958+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
959+
960+
auto newestCloneOfShadowNode =
961+
uiManager->getNewestCloneOfShadowNode(*shadowNode);
962+
auto newestParentOfShadowNode =
963+
uiManager->getNewestParentOfShadowNode(*shadowNode);
964+
// The node is no longer part of an active shadow tree, or it is the
965+
// root node
966+
if (newestCloneOfShadowNode == nullptr ||
967+
newestParentOfShadowNode == nullptr) {
968+
return jsi::Value::undefined();
969+
}
970+
971+
// If the node is not displayed (itself or any of its ancestors has
972+
// "display: none", it returns an empty layout metrics object.
973+
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
974+
*shadowNode, nullptr, {/* .includeTransform = */ true});
975+
if (layoutMetrics == EmptyLayoutMetrics) {
976+
return jsi::Value::undefined();
977+
}
978+
979+
auto layoutableShadowNode = traitCast<LayoutableShadowNode const *>(
980+
newestCloneOfShadowNode.get());
981+
// This should never happen
982+
if (layoutableShadowNode == nullptr) {
983+
return jsi::Value::undefined();
984+
}
985+
986+
auto layoutableParentShadowNode =
987+
traitCast<LayoutableShadowNode const *>(
988+
newestParentOfShadowNode.get());
989+
// This should never happen
990+
if (layoutableParentShadowNode == nullptr) {
991+
return jsi::Value::undefined();
992+
}
993+
994+
auto originRelativeToParentOuterBorder =
995+
layoutableShadowNode->getLayoutMetrics().frame.origin;
996+
997+
// On the Web, offsets are computed from the inner border of the
998+
// parent.
999+
auto offsetTop = originRelativeToParentOuterBorder.y -
1000+
layoutableParentShadowNode->getLayoutMetrics().borderWidth.top;
1001+
auto offsetLeft = originRelativeToParentOuterBorder.x -
1002+
layoutableParentShadowNode->getLayoutMetrics().borderWidth.left;
1003+
1004+
return jsi::Array::createWithElements(
1005+
runtime,
1006+
getInstanceHandleFromShadowNode(
1007+
newestParentOfShadowNode, runtime),
1008+
jsi::Value{runtime, (double)offsetTop},
1009+
jsi::Value{runtime, (double)offsetLeft});
1010+
});
1011+
}
1012+
9261013
return jsi::Value::undefined();
9271014
}
9281015

0 commit comments

Comments
 (0)