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
This commit is contained in:
Rubén Norte
2023-04-13 09:19:00 -07:00
committed by Facebook GitHub Bot
parent 03430ca660
commit 528edd82bb
6 changed files with 184 additions and 11 deletions
+21 -2
View File
@@ -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 {
@@ -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<mixed>,
) => void,
/**
* Support methods for the DOM-compatible APIs.
*/
+getParentNode: (node: Node) => ?InternalInstanceHandle,
+getChildNodes: (node: Node) => $ReadOnlyArray<InternalInstanceHandle>,
+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<mixed>,
) => void,
+getParentNode: (node: Node) => ?InternalInstanceHandle,
+getChildNodes: (node: Node) => $ReadOnlyArray<InternalInstanceHandle>,
+isConnected: (node: Node) => boolean,
|};
// This is exposed as a getter because apps using the legacy renderer AND
@@ -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;
@@ -21,6 +21,14 @@
#include <utility>
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 {
@@ -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(
@@ -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();
}