diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 7bb26450215..29bdfa06faa 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -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; 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, ) => void, + +getParentNode: (node: Node) => ?InternalInstanceHandle, + +getChildNodes: (node: Node) => $ReadOnlyArray, |}; // This is exposed as a getter because apps using the legacy renderer AND diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index c1cfe324bd7..f1aeb3cf03a 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -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): 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 => { + 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; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp index 282b25f8d67..2426cea3ca2 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp @@ -131,4 +131,8 @@ void EventEmitter::setEnabled(bool enabled) const { } } +const SharedEventTarget &EventEmitter::getEventTarget() const { + return eventTarget_; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h index 1f5afceab2f..d0fba333089 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h @@ -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 diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 67de51a9db6..78c74ad98be 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -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 { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index afccfd1ad3a..517399e14b6 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -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( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 27092d35f7c..7de51a69aa3 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -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 + 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(); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 07622b8549c..5f7178eb451 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -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 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