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 https://github.com/react-native-community/discussions-and-proposals/pull/607.

Changelog: [internal]

bypass-github-export-checks

Reviewed By: sammy-SC

Differential Revision: D44022477

fbshipit-source-id: 34fbab0fbce4d57a93b8ed1dcd0ef89e9f542237
This commit is contained in:
Rubén Norte
2023-04-13 09:19:00 -07:00
committed by Facebook GitHub Bot
parent 6be4449a27
commit 43864a1d60
8 changed files with 254 additions and 39 deletions
@@ -11,6 +11,7 @@
'use strict';
import type {
InternalInstanceHandle,
LayoutAnimationConfig,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
@@ -21,14 +22,13 @@ import type {RootTag} from '../Types/RootTagTypes';
export type NodeSet = Array<Node>;
export type NodeProps = {...};
export type InstanceHandle = {...};
export type Spec = {|
+createNode: (
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InstanceHandle,
instanceHandle: InternalInstanceHandle,
) => Node,
+cloneNode: (node: Node) => Node,
+cloneNodeWithNewChildren: (node: Node) => Node,
@@ -70,6 +70,8 @@ export type Spec = {|
commandName: string,
args: Array<mixed>,
) => void,
+getParentNode: (node: Node) => ?InternalInstanceHandle,
+getChildNodes: (node: Node) => $ReadOnlyArray<InternalInstanceHandle>,
|};
// This is exposed as a getter because apps using the legacy renderer AND
@@ -10,6 +10,7 @@
*/
import type {
InternalInstanceHandle,
LayoutAnimationConfig,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
@@ -18,7 +19,6 @@ import type {
} from '../../Renderer/shims/ReactNativeTypes';
import type {RootTag} from '../../Types/RootTagTypes';
import type {
InstanceHandle,
NodeProps,
NodeSet,
Spec as FabricUIManager,
@@ -26,7 +26,7 @@ import type {
type NodeMock = {
children: NodeSet,
instanceHandle: InstanceHandle,
instanceHandle: InternalInstanceHandle,
props: NodeProps,
reactTag: number,
rootTag: RootTag,
@@ -66,6 +66,53 @@ function ensureHostNode(node: Node): void {
}
}
function getAncestorsInCurrentTree(
node: Node,
): ?$ReadOnlyArray<[Node, number]> {
const childSet = roots.get(fromNode(node).rootTag);
if (childSet == null) {
return null;
}
const rootNode = toNode({
reactTag: 0,
rootTag: fromNode(node).rootTag,
viewName: 'RootNode',
// $FlowExpectedError
instanceHandle: null,
props: {},
children: childSet,
});
let position = 0;
for (const child of childSet) {
const ancestors = getAncestors(child, node);
if (ancestors) {
return [[rootNode, position]].concat(ancestors);
}
position++;
}
return null;
}
function getAncestors(root: Node, node: Node): ?$ReadOnlyArray<[Node, number]> {
if (fromNode(root).reactTag === fromNode(node).reactTag) {
return [];
}
let position = 0;
for (const child of fromNode(root).children) {
const ancestors = getAncestors(child, node);
if (ancestors != null) {
return [[root, position]].concat(ancestors);
}
position++;
}
return null;
}
const FabricUIManagerMock: FabricUIManager = {
createNode: jest.fn(
(
@@ -73,7 +120,7 @@ const FabricUIManagerMock: FabricUIManager = {
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InstanceHandle,
instanceHandle: InternalInstanceHandle,
): Node => {
if (allocatedTags.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
@@ -183,6 +230,30 @@ const FabricUIManagerMock: FabricUIManager = {
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) {
return null;
}
const [parentOfParent, position] = ancestors[ancestors.length - 2];
const parentInCurrentTree = fromNode(parentOfParent).children[position];
return fromNode(parentInCurrentTree).instanceHandle;
}),
getChildNodes: jest.fn(
(node: Node): $ReadOnlyArray<InternalInstanceHandle> => {
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null) {
return [];
}
const [parent, position] = ancestors[ancestors.length - 1];
const nodeInCurrentTree = fromNode(parent).children[position];
return fromNode(nodeInCurrentTree).children.map(
child => fromNode(child).instanceHandle,
);
},
),
};
global.nativeFabricUIManager = FabricUIManagerMock;
@@ -131,4 +131,8 @@ void EventEmitter::setEnabled(bool enabled) const {
}
}
const SharedEventTarget &EventEmitter::getEventTarget() const {
return eventTarget_;
}
} // namespace facebook::react
@@ -56,6 +56,8 @@ class EventEmitter {
*/
void setEnabled(bool enabled) const;
SharedEventTarget const &getEventTarget() const;
protected:
#ifdef ANDROID
// We need this temporarily due to lack of Java-counterparts for particular
@@ -260,6 +260,34 @@ ShadowNode::Shared UIManager::getNewestCloneOfShadowNode(
return pair->first.get().getChildren().at(pair->second);
}
ShadowNode::Shared UIManager::getNewestParentOfShadowNode(
ShadowNode const &shadowNode) const {
auto ancestorShadowNode = ShadowNode::Shared{};
shadowTreeRegistry_.visit(
shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) {
ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
if (!ancestorShadowNode) {
return nullptr;
}
auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode);
if (ancestors.empty()) {
return nullptr;
}
if (ancestors.size() == 1) {
// The parent is the shadow root
return ancestorShadowNode;
}
auto parentOfParentPair = ancestors[ancestors.size() - 2];
return parentOfParentPair.first.get().getChildren().at(
parentOfParentPair.second);
}
ShadowNode::Shared UIManager::findNodeAtPoint(
ShadowNode::Shared const &node,
Point point) const {
@@ -85,6 +85,9 @@ class UIManager final : public ShadowTreeDelegate {
ShadowNode::Shared getNewestCloneOfShadowNode(
ShadowNode const &shadowNode) const;
ShadowNode::Shared getNewestParentOfShadowNode(
ShadowNode const &shadowNode) const;
#pragma mark - Surface Start & Stop
void startSurface(
@@ -642,40 +642,6 @@ jsi::Value UIManagerBinding::get(
});
}
if (methodName == "getBoundingClientRect") {
// This is similar to `measureInWindow`, except it's explicitly synchronous
// (returns the result instead of passing it to a callback).
// The behavior is similar to `Element.prototype.getBoundingClientRect` from
// Web.
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const & /*thisValue*/,
jsi::Value const *arguments,
size_t /*count*/) noexcept -> jsi::Value {
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNodeFromValue(runtime, arguments[0]),
nullptr,
{/* .includeTransform = */ true,
/* .includeViewportOffset = */ true});
if (layoutMetrics == EmptyLayoutMetrics) {
return jsi::Value::undefined();
}
auto frame = layoutMetrics.frame;
return jsi::Array::createWithElements(
runtime,
jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height});
});
}
if (methodName == "sendAccessibilityEvent") {
return jsi::Function::createFromHostFunction(
runtime,
@@ -757,6 +723,109 @@ jsi::Value UIManagerBinding::get(
});
}
/**
* DOM traversal and layout APIs
*/
if (methodName == "getBoundingClientRect") {
// This is similar to `measureInWindow`, except it's explicitly synchronous
// (returns the result instead of passing it to a callback).
// The behavior is similar to `Element.prototype.getBoundingClientRect` from
// Web.
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const & /*thisValue*/,
jsi::Value const *arguments,
size_t /*count*/) noexcept -> jsi::Value {
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNodeFromValue(runtime, arguments[0]),
nullptr,
{/* .includeTransform = */ true,
/* .includeViewportOffset = */ true});
if (layoutMetrics == EmptyLayoutMetrics) {
return jsi::Value::undefined();
}
auto frame = layoutMetrics.frame;
return jsi::Array::createWithElements(
runtime,
jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height});
});
}
if (methodName == "getParentNode") {
// This is a React Native implementation of `Node.prototype.parentNode`
// (see https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode).
// If a version of the given shadow node is present in the current revision
// of an active shadow tree, it returns the instance handle of its parent.
// Otherwise, it returns null.
// getParent(shadowNode: ShadowNode): ?InstanceHandle
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const & /*thisValue*/,
jsi::Value const *arguments,
size_t /*count*/) noexcept -> jsi::Value {
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
auto parentShadowNode =
uiManager->getNewestParentOfShadowNode(*shadowNode);
// shadowNode is a RootShadowNode
if (!parentShadowNode) {
return jsi::Value::null();
}
return getInstanceHandleFromShadowNode(parentShadowNode, runtime);
});
}
if (methodName == "getChildNodes") {
// This is a React Native implementation of `Node.prototype.childNodes`
// (see https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes).
// If a version of the given shadow node is present in the current revision
// of an active shadow tree, it returns an array of instance handles of its
// children. Otherwise, it returns an empty array.
// getChildren(shadowNode: ShadowNode): Array<InstanceHandle>
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const & /*thisValue*/,
jsi::Value const *arguments,
size_t /*count*/) noexcept -> jsi::Value {
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
auto newestCloneOfShadowNode =
uiManager->getNewestCloneOfShadowNode(*shadowNode);
// There's no version of this node in the current shadow tree
if (newestCloneOfShadowNode == nullptr) {
return jsi::Array(runtime, 0);
}
auto childShadowNodes = newestCloneOfShadowNode->getChildren();
return getArrayOfInstanceHandlesFromShadowNodes(
childShadowNodes, runtime);
});
}
return jsi::Value::undefined();
}
@@ -183,4 +183,40 @@ inline static folly::dynamic commandArgsFromValue(
return jsi::dynamicFromValue(runtime, value);
}
inline static jsi::Value getInstanceHandleFromShadowNode(
ShadowNode::Shared shadowNode,
jsi::Runtime &runtime) {
auto eventTarget = shadowNode->getEventEmitter()->getEventTarget();
// shadowNode is probably a RootShadowNode and they don't have
// event targets.
if (eventTarget == nullptr) {
return jsi::Value::null();
}
eventTarget->retain(runtime);
auto instanceHandle = eventTarget->getInstanceHandle(runtime);
eventTarget->release(runtime);
return instanceHandle;
}
inline static jsi::Value getArrayOfInstanceHandlesFromShadowNodes(
ShadowNode::ListOfShared const &nodes,
jsi::Runtime &runtime) {
// JSI doesn't support adding elements to an array after creation,
// so we need to accumulate the values in a vector and then create
// the array when we know the size.
std::vector<jsi::Value> nonNullInstanceHandles;
nonNullInstanceHandles.reserve(nodes.size());
for (auto const &shadowNode : nodes) {
auto instanceHandle = getInstanceHandleFromShadowNode(shadowNode, runtime);
if (!instanceHandle.isNull()) {
nonNullInstanceHandles.push_back(std::move(instanceHandle));
}
}
auto result = jsi::Array(runtime, nonNullInstanceHandles.size());
for (size_t i = 0; i < nonNullInstanceHandles.size(); i++) {
result.setValueAtIndex(runtime, i, nonNullInstanceHandles[i]);
}
return result;
}
} // namespace facebook::react