mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
6be4449a27
commit
43864a1d60
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user