From 528edd82bbfe2b268d9ae85c5d77a7bb3fd5e1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 13 Apr 2023 09:19:00 -0700 Subject: [PATCH] Implement node.compareDocumentPosition and node.contains Summary: This implements the `compareDocumentPosition` and `contains` methods (contains is implemented in JS) as defined in https://github.com/react-native-community/discussions-and-proposals/pull/607. This requires a new method in Fabric because we can't derive that from the existing methods. Changelog: [internal] bypass-github-export-checks Reviewed By: rshest Differential Revision: D44370433 fbshipit-source-id: a7c12a35955e9ccbf8eb19f394125bf05cb995ce --- .../Libraries/DOM/Nodes/ReadOnlyNode.js | 23 ++++++- .../Libraries/ReactNative/FabricUIManager.js | 23 ++++--- .../ReactNative/__mocks__/FabricUIManager.js | 53 ++++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 62 +++++++++++++++++++ .../react/renderer/uimanager/UIManager.h | 4 ++ .../renderer/uimanager/UIManagerBinding.cpp | 30 +++++++++ 6 files changed, 184 insertions(+), 11 deletions(-) diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js index e7e0192c83a..e92a298be91 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js @@ -158,13 +158,32 @@ export default class ReadOnlyNode { } compareDocumentPosition(otherNode: ReadOnlyNode): number { - throw new TypeError('Unimplemented'); + // Quick check to avoid having to call into Fabric if the nodes are the same. + if (otherNode === this) { + return 0; + } + + const shadowNode = getShadowNode(this); + const otherShadowNode = getShadowNode(otherNode); + + if (shadowNode == null || otherShadowNode == null) { + return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; + } + + return nullthrows(getFabricUIManager()).compareDocumentPosition( + shadowNode, + otherShadowNode, + ); } contains(otherNode: ReadOnlyNode): boolean { + if (otherNode === this) { + return true; + } + const position = this.compareDocumentPosition(otherNode); // eslint-disable-next-line no-bitwise - return (position & ReadOnlyNode.DOCUMENT_POSITION_CONTAINS) !== 0; + return (position & ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY) !== 0; } getRootNode(): ReadOnlyNode { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 23ae2a53d7c..a608e3ab0d7 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -56,6 +56,20 @@ export type Spec = {| ) => void, +sendAccessibilityEvent: (node: Node, eventType: string) => void, +findShadowNodeByTag_DEPRECATED: (reactTag: number) => ?Node, + +setNativeProps: (node: Node, newProps: NodeProps) => void, + +dispatchCommand: ( + node: Node, + commandName: string, + args: Array, + ) => void, + + /** + * Support methods for the DOM-compatible APIs. + */ + +getParentNode: (node: Node) => ?InternalInstanceHandle, + +getChildNodes: (node: Node) => $ReadOnlyArray, + +isConnected: (node: Node) => boolean, + +compareDocumentPosition: (node: Node, otherNode: Node) => number, +getBoundingClientRect: ( node: Node, ) => ?[ @@ -64,15 +78,6 @@ export type Spec = {| /* width:*/ number, /* height:*/ number, ], - +setNativeProps: (node: Node, newProps: NodeProps) => void, - +dispatchCommand: ( - node: Node, - commandName: string, - args: Array, - ) => void, - +getParentNode: (node: Node) => ?InternalInstanceHandle, - +getChildNodes: (node: Node) => $ReadOnlyArray, - +isConnected: (node: Node) => boolean, |}; // 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 5e75910d060..e35d15af61d 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -288,6 +288,59 @@ const FabricUIManagerMock: FabricUIManager = { isConnected: jest.fn((node: Node): boolean => { return getNodeInCurrentTree(node) != null; }), + compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => { + /* eslint-disable no-bitwise */ + const ReadOnlyNode = require('../../DOM/Nodes/ReadOnlyNode').default; + + // Quick check for node vs. itself + if (fromNode(node).reactTag === fromNode(otherNode).reactTag) { + return 0; + } + + if (fromNode(node).rootTag !== fromNode(otherNode).rootTag) { + return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; + } + + const ancestors = getAncestorsInCurrentTree(node); + if (ancestors == null) { + return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; + } + + const otherAncestors = getAncestorsInCurrentTree(otherNode); + if (otherAncestors == null) { + return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; + } + + // Consume all common ancestors + let i = 0; + while ( + i < ancestors.length && + i < otherAncestors.length && + ancestors[i][1] === otherAncestors[i][1] + ) { + i++; + } + + if (i === ancestors.length) { + return ( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING + ); + } + + if (i === otherAncestors.length) { + return ( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING + ); + } + + if (ancestors[i][1] > otherAncestors[i][1]) { + return ReadOnlyNode.DOCUMENT_POSITION_PRECEDING; + } + + return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING; + }), }; global.nativeFabricUIManager = FabricUIManagerMock; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 78c74ad98be..c8e5045808d 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -21,6 +21,14 @@ #include +namespace { +constexpr int DOCUMENT_POSITION_DISCONNECTED = 1; +constexpr int DOCUMENT_POSITION_PRECEDING = 2; +constexpr int DOCUMENT_POSITION_FOLLOWING = 4; +constexpr int DOCUMENT_POSITION_CONTAINS = 8; +constexpr int DOCUMENT_POSITION_CONTAINED_BY = 16; +} // namespace + namespace facebook::react { // Explicitly define destructors here, as they to exist in order to act as a @@ -288,6 +296,60 @@ ShadowNode::Shared UIManager::getNewestParentOfShadowNode( parentOfParentPair.second); } +int UIManager::compareDocumentPosition( + ShadowNode const &shadowNode, + ShadowNode const &otherShadowNode) const { + // Quick check for node vs. itself + if (&shadowNode == &otherShadowNode) { + return 0; + } + + if (shadowNode.getSurfaceId() != otherShadowNode.getSurfaceId()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + auto ancestorShadowNode = ShadowNode::Shared{}; + shadowTreeRegistry_.visit( + shadowNode.getSurfaceId(), [&](ShadowTree const &shadowTree) { + ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + if (!ancestorShadowNode) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + auto otherAncestors = + otherShadowNode.getFamily().getAncestors(*ancestorShadowNode); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + // Consume all common ancestors + size_t i = 0; + while (i < ancestors.size() && i < otherAncestors.size() && + ancestors[i].second == otherAncestors[i].second) { + i++; + } + + if (i == ancestors.size()) { + return (DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + } + + if (i == otherAncestors.size()) { + return (DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + } + + if (ancestors[i].second > otherAncestors[i].second) { + return DOCUMENT_POSITION_PRECEDING; + } + + return DOCUMENT_POSITION_FOLLOWING; +} + 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 517399e14b6..7e196b8db24 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -88,6 +88,10 @@ class UIManager final : public ShadowTreeDelegate { ShadowNode::Shared getNewestParentOfShadowNode( ShadowNode const &shadowNode) const; + int compareDocumentPosition( + ShadowNode const &shadowNode, + ShadowNode const &otherShadowNode) 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 b328b8fa20c..78e8d2ee419 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -862,6 +862,36 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "compareDocumentPosition") { + // This is a React Native implementation of + // `Node.prototype.compareDocumentPosition` (see + // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition). + + // It uses the version of the shadow nodes that are present in the current + // revision of the shadow tree (if any). If any of the nodes is not present, + // it just indicates they are disconnected. + + // compareDocumentPosition(shadowNode: ShadowNode, otherShadowNode: + // ShadowNode): number + 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 otherShadowNode = shadowNodeFromValue(runtime, arguments[1]); + + auto documentPosition = + uiManager->compareDocumentPosition(*shadowNode, *otherShadowNode); + + return jsi::Value(documentPosition); + }); + } + return jsi::Value::undefined(); }