Skip to content

Commit 43864a1

Browse files
rubennortefacebook-github-bot
authored andcommitted
Add getParentNode and getChildNodes to Fabric UIManager binding
Summary: Implements these 2 base methods to allow basic traversal of the host tree from JS to implement part of the proposal in react-native-community/discussions-and-proposals#607. Changelog: [internal] bypass-github-export-checks Reviewed By: sammy-SC Differential Revision: D44022477 fbshipit-source-id: 34fbab0fbce4d57a93b8ed1dcd0ef89e9f542237
1 parent 6be4449 commit 43864a1

File tree

8 files changed

+254
-39
lines changed

8 files changed

+254
-39
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'use strict';
1212

1313
import type {
14+
InternalInstanceHandle,
1415
LayoutAnimationConfig,
1516
MeasureInWindowOnSuccessCallback,
1617
MeasureLayoutOnSuccessCallback,
@@ -21,14 +22,13 @@ import type {RootTag} from '../Types/RootTagTypes';
2122

2223
export type NodeSet = Array<Node>;
2324
export type NodeProps = {...};
24-
export type InstanceHandle = {...};
2525
export type Spec = {|
2626
+createNode: (
2727
reactTag: number,
2828
viewName: string,
2929
rootTag: RootTag,
3030
props: NodeProps,
31-
instanceHandle: InstanceHandle,
31+
instanceHandle: InternalInstanceHandle,
3232
) => Node,
3333
+cloneNode: (node: Node) => Node,
3434
+cloneNodeWithNewChildren: (node: Node) => Node,
@@ -70,6 +70,8 @@ export type Spec = {|
7070
commandName: string,
7171
args: Array<mixed>,
7272
) => void,
73+
+getParentNode: (node: Node) => ?InternalInstanceHandle,
74+
+getChildNodes: (node: Node) => $ReadOnlyArray<InternalInstanceHandle>,
7375
|};
7476

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

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

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
import type {
13+
InternalInstanceHandle,
1314
LayoutAnimationConfig,
1415
MeasureInWindowOnSuccessCallback,
1516
MeasureLayoutOnSuccessCallback,
@@ -18,15 +19,14 @@ import type {
1819
} from '../../Renderer/shims/ReactNativeTypes';
1920
import type {RootTag} from '../../Types/RootTagTypes';
2021
import type {
21-
InstanceHandle,
2222
NodeProps,
2323
NodeSet,
2424
Spec as FabricUIManager,
2525
} from '../FabricUIManager';
2626

2727
type NodeMock = {
2828
children: NodeSet,
29-
instanceHandle: InstanceHandle,
29+
instanceHandle: InternalInstanceHandle,
3030
props: NodeProps,
3131
reactTag: number,
3232
rootTag: RootTag,
@@ -66,14 +66,61 @@ function ensureHostNode(node: Node): void {
6666
}
6767
}
6868

69+
function getAncestorsInCurrentTree(
70+
node: Node,
71+
): ?$ReadOnlyArray<[Node, number]> {
72+
const childSet = roots.get(fromNode(node).rootTag);
73+
if (childSet == null) {
74+
return null;
75+
}
76+
77+
const rootNode = toNode({
78+
reactTag: 0,
79+
rootTag: fromNode(node).rootTag,
80+
viewName: 'RootNode',
81+
// $FlowExpectedError
82+
instanceHandle: null,
83+
props: {},
84+
children: childSet,
85+
});
86+
87+
let position = 0;
88+
for (const child of childSet) {
89+
const ancestors = getAncestors(child, node);
90+
if (ancestors) {
91+
return [[rootNode, position]].concat(ancestors);
92+
}
93+
position++;
94+
}
95+
96+
return null;
97+
}
98+
99+
function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
100+
if (fromNode(root).reactTag === fromNode(node).reactTag) {
101+
return [];
102+
}
103+
104+
let position = 0;
105+
for (const child of fromNode(root).children) {
106+
const ancestors = getAncestors(child, node);
107+
if (ancestors != null) {
108+
return [[root, position]].concat(ancestors);
109+
}
110+
position++;
111+
}
112+
113+
return null;
114+
}
115+
69116
const FabricUIManagerMock: FabricUIManager = {
70117
createNode: jest.fn(
71118
(
72119
reactTag: number,
73120
viewName: string,
74121
rootTag: RootTag,
75122
props: NodeProps,
76-
instanceHandle: InstanceHandle,
123+
instanceHandle: InternalInstanceHandle,
77124
): Node => {
78125
if (allocatedTags.has(reactTag)) {
79126
throw new Error(`Created two native views with tag ${reactTag}`);
@@ -183,6 +230,30 @@ const FabricUIManagerMock: FabricUIManager = {
183230
dispatchCommand: jest.fn(
184231
(node: Node, commandName: string, args: Array<mixed>): void => {},
185232
),
233+
getParentNode: jest.fn((node: Node): ?InternalInstanceHandle => {
234+
const ancestors = getAncestorsInCurrentTree(node);
235+
if (ancestors == null || ancestors.length - 2 < 0) {
236+
return null;
237+
}
238+
239+
const [parentOfParent, position] = ancestors[ancestors.length - 2];
240+
const parentInCurrentTree = fromNode(parentOfParent).children[position];
241+
return fromNode(parentInCurrentTree).instanceHandle;
242+
}),
243+
getChildNodes: jest.fn(
244+
(node: Node): $ReadOnlyArray<InternalInstanceHandle> => {
245+
const ancestors = getAncestorsInCurrentTree(node);
246+
if (ancestors == null) {
247+
return [];
248+
}
249+
250+
const [parent, position] = ancestors[ancestors.length - 1];
251+
const nodeInCurrentTree = fromNode(parent).children[position];
252+
return fromNode(nodeInCurrentTree).children.map(
253+
child => fromNode(child).instanceHandle,
254+
);
255+
},
256+
),
186257
};
187258

188259
global.nativeFabricUIManager = FabricUIManagerMock;

packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,8 @@ void EventEmitter::setEnabled(bool enabled) const {
131131
}
132132
}
133133

134+
const SharedEventTarget &EventEmitter::getEventTarget() const {
135+
return eventTarget_;
136+
}
137+
134138
} // namespace facebook::react

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class EventEmitter {
5656
*/
5757
void setEnabled(bool enabled) const;
5858

59+
SharedEventTarget const &getEventTarget() const;
60+
5961
protected:
6062
#ifdef ANDROID
6163
// We need this temporarily due to lack of Java-counterparts for particular

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,34 @@ ShadowNode::Shared UIManager::getNewestCloneOfShadowNode(
260260
return pair->first.get().getChildren().at(pair->second);
261261
}
262262

263+
ShadowNode::Shared UIManager::getNewestParentOfShadowNode(
264+
ShadowNode const &shadowNode) const {
265+
auto ancestorShadowNode = ShadowNode::Shared{};
266+
shadowTreeRegistry_.visit(
267+
shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) {
268+
ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
269+
});
270+
271+
if (!ancestorShadowNode) {
272+
return nullptr;
273+
}
274+
275+
auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode);
276+
277+
if (ancestors.empty()) {
278+
return nullptr;
279+
}
280+
281+
if (ancestors.size() == 1) {
282+
// The parent is the shadow root
283+
return ancestorShadowNode;
284+
}
285+
286+
auto parentOfParentPair = ancestors[ancestors.size() - 2];
287+
return parentOfParentPair.first.get().getChildren().at(
288+
parentOfParentPair.second);
289+
}
290+
263291
ShadowNode::Shared UIManager::findNodeAtPoint(
264292
ShadowNode::Shared const &node,
265293
Point point) const {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class UIManager final : public ShadowTreeDelegate {
8585
ShadowNode::Shared getNewestCloneOfShadowNode(
8686
ShadowNode const &shadowNode) const;
8787

88+
ShadowNode::Shared getNewestParentOfShadowNode(
89+
ShadowNode const &shadowNode) const;
90+
8891
#pragma mark - Surface Start & Stop
8992

9093
void startSurface(

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

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -642,40 +642,6 @@ jsi::Value UIManagerBinding::get(
642642
});
643643
}
644644

645-
if (methodName == "getBoundingClientRect") {
646-
// This is similar to `measureInWindow`, except it's explicitly synchronous
647-
// (returns the result instead of passing it to a callback).
648-
// The behavior is similar to `Element.prototype.getBoundingClientRect` from
649-
// Web.
650-
return jsi::Function::createFromHostFunction(
651-
runtime,
652-
name,
653-
1,
654-
[uiManager](
655-
jsi::Runtime &runtime,
656-
jsi::Value const & /*thisValue*/,
657-
jsi::Value const *arguments,
658-
size_t /*count*/) noexcept -> jsi::Value {
659-
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
660-
*shadowNodeFromValue(runtime, arguments[0]),
661-
nullptr,
662-
{/* .includeTransform = */ true,
663-
/* .includeViewportOffset = */ true});
664-
665-
if (layoutMetrics == EmptyLayoutMetrics) {
666-
return jsi::Value::undefined();
667-
}
668-
669-
auto frame = layoutMetrics.frame;
670-
return jsi::Array::createWithElements(
671-
runtime,
672-
jsi::Value{runtime, (double)frame.origin.x},
673-
jsi::Value{runtime, (double)frame.origin.y},
674-
jsi::Value{runtime, (double)frame.size.width},
675-
jsi::Value{runtime, (double)frame.size.height});
676-
});
677-
}
678-
679645
if (methodName == "sendAccessibilityEvent") {
680646
return jsi::Function::createFromHostFunction(
681647
runtime,
@@ -757,6 +723,109 @@ jsi::Value UIManagerBinding::get(
757723
});
758724
}
759725

726+
/**
727+
* DOM traversal and layout APIs
728+
*/
729+
730+
if (methodName == "getBoundingClientRect") {
731+
// This is similar to `measureInWindow`, except it's explicitly synchronous
732+
// (returns the result instead of passing it to a callback).
733+
// The behavior is similar to `Element.prototype.getBoundingClientRect` from
734+
// Web.
735+
return jsi::Function::createFromHostFunction(
736+
runtime,
737+
name,
738+
1,
739+
[uiManager](
740+
jsi::Runtime &runtime,
741+
jsi::Value const & /*thisValue*/,
742+
jsi::Value const *arguments,
743+
size_t /*count*/) noexcept -> jsi::Value {
744+
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
745+
*shadowNodeFromValue(runtime, arguments[0]),
746+
nullptr,
747+
{/* .includeTransform = */ true,
748+
/* .includeViewportOffset = */ true});
749+
750+
if (layoutMetrics == EmptyLayoutMetrics) {
751+
return jsi::Value::undefined();
752+
}
753+
754+
auto frame = layoutMetrics.frame;
755+
return jsi::Array::createWithElements(
756+
runtime,
757+
jsi::Value{runtime, (double)frame.origin.x},
758+
jsi::Value{runtime, (double)frame.origin.y},
759+
jsi::Value{runtime, (double)frame.size.width},
760+
jsi::Value{runtime, (double)frame.size.height});
761+
});
762+
}
763+
764+
if (methodName == "getParentNode") {
765+
// This is a React Native implementation of `Node.prototype.parentNode`
766+
// (see https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode).
767+
768+
// If a version of the given shadow node is present in the current revision
769+
// of an active shadow tree, it returns the instance handle of its parent.
770+
// Otherwise, it returns null.
771+
772+
// getParent(shadowNode: ShadowNode): ?InstanceHandle
773+
return jsi::Function::createFromHostFunction(
774+
runtime,
775+
name,
776+
1,
777+
[uiManager](
778+
jsi::Runtime &runtime,
779+
jsi::Value const & /*thisValue*/,
780+
jsi::Value const *arguments,
781+
size_t /*count*/) noexcept -> jsi::Value {
782+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
783+
auto parentShadowNode =
784+
uiManager->getNewestParentOfShadowNode(*shadowNode);
785+
786+
// shadowNode is a RootShadowNode
787+
if (!parentShadowNode) {
788+
return jsi::Value::null();
789+
}
790+
791+
return getInstanceHandleFromShadowNode(parentShadowNode, runtime);
792+
});
793+
}
794+
795+
if (methodName == "getChildNodes") {
796+
// This is a React Native implementation of `Node.prototype.childNodes`
797+
// (see https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes).
798+
799+
// If a version of the given shadow node is present in the current revision
800+
// of an active shadow tree, it returns an array of instance handles of its
801+
// children. Otherwise, it returns an empty array.
802+
803+
// getChildren(shadowNode: ShadowNode): Array<InstanceHandle>
804+
return jsi::Function::createFromHostFunction(
805+
runtime,
806+
name,
807+
1,
808+
[uiManager](
809+
jsi::Runtime &runtime,
810+
jsi::Value const & /*thisValue*/,
811+
jsi::Value const *arguments,
812+
size_t /*count*/) noexcept -> jsi::Value {
813+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
814+
815+
auto newestCloneOfShadowNode =
816+
uiManager->getNewestCloneOfShadowNode(*shadowNode);
817+
818+
// There's no version of this node in the current shadow tree
819+
if (newestCloneOfShadowNode == nullptr) {
820+
return jsi::Array(runtime, 0);
821+
}
822+
823+
auto childShadowNodes = newestCloneOfShadowNode->getChildren();
824+
return getArrayOfInstanceHandlesFromShadowNodes(
825+
childShadowNodes, runtime);
826+
});
827+
}
828+
760829
return jsi::Value::undefined();
761830
}
762831

0 commit comments

Comments
 (0)