From 19ddd80aec5acd8974840998304d31a846d3331f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 20 Mar 2024 08:59:53 -0700 Subject: [PATCH] Move DOM APIs to native module (#43512) Summary: Changelog: [internal] This moves all the new methods that were added to implement the DOM traversal and layout APIs (as per this RFC: https://github.com/react-native-community/discussions-and-proposals/pull/607) to a separate C++ native module to avoid bloating the UIManager interface, initialize lazily, provide automatic caching of methods, simplify the API for implementors, etc. Reviewed By: sammy-SC Differential Revision: D54903376 --- .../Libraries/ReactNative/FabricUIManager.js | 51 -- .../ReactFabricPublicInstance-test.js | 4 + .../ReactNative/__mocks__/FabricUIManager.js | 346 +--------- .../__snapshots__/public-api-test.js.snap | 13 - .../react/nativemodule/dom/NativeDOM.cpp | 355 ++++++++++ .../react/nativemodule/dom/NativeDOM.h | 94 +++ .../renderer/uimanager/UIManagerBinding.cpp | 611 +----------------- .../renderer/uimanager/UIManagerBinding.h | 1 + .../react/renderer/uimanager/primitives.h | 8 +- .../webapis/dom/nodes/ReactNativeElement.js | 7 +- .../dom/nodes/ReadOnlyCharacterData.js | 5 +- .../webapis/dom/nodes/ReadOnlyElement.js | 39 +- .../private/webapis/dom/nodes/ReadOnlyNode.js | 23 +- .../webapis/dom/nodes/specs/NativeDOM.js | 381 +++++++++++ .../nodes/specs/__mocks__/NativeDOMMock.js | 376 +++++++++++ 15 files changed, 1261 insertions(+), 1053 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp create mode 100644 packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h create mode 100644 packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js create mode 100644 packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 8233a12f3fe56d..a31fe41f232a4b 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -70,15 +70,7 @@ export interface Spec { locationY: number, callback: (instanceHandle: ?InternalInstanceHandle) => void, ) => 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; - +getTextContent: (node: Node) => string; +getBoundingClientRect: ( node: Node, includeTransform: boolean, @@ -88,36 +80,6 @@ export interface Spec { /* width: */ number, /* height: */ number, ]; - +getOffset: ( - node: Node, - ) => ?[ - /* offsetParent: */ InternalInstanceHandle, - /* offsetTop: */ number, - /* offsetLeft: */ number, - ]; - +getScrollPosition: ( - node: Node, - ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number]; - +getScrollSize: ( - node: Node, - ) => ?[/* scrollWidth: */ number, /* scrollHeight: */ number]; - +getInnerSize: (node: Node) => ?[/* width: */ number, /* height: */ number]; - +getBorderSize: ( - node: Node, - ) => ?[ - /* topWidth: */ number, - /* rightWidth: */ number, - /* bottomWidth: */ number, - /* leftWidth: */ number, - ]; - +getTagName: (node: Node) => string; - - /** - * Support methods for the Pointer Capture APIs. - */ - +hasPointerCapture: (node: Node, pointerId: number) => boolean; - +setPointerCapture: (node: Node, pointerId: number) => void; - +releasePointerCapture: (node: Node, pointerId: number) => void; } let nativeFabricUIManagerProxy: ?Spec; @@ -143,21 +105,8 @@ const CACHED_PROPERTIES = [ 'findShadowNodeByTag_DEPRECATED', 'setNativeProps', 'dispatchCommand', - 'getParentNode', - 'getChildNodes', - 'isConnected', 'compareDocumentPosition', - 'getTextContent', 'getBoundingClientRect', - 'getOffset', - 'getScrollPosition', - 'getScrollSize', - 'getInnerSize', - 'getBorderSize', - 'getTagName', - 'hasPointerCapture', - 'setPointerCapture', - 'releasePointerCapture', ]; // This is exposed as a getter because apps using the legacy renderer AND diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js index 12087f1b9854dc..c072d173e92c8a 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-test.js @@ -32,6 +32,10 @@ jest.mock('../../FabricUIManager', () => require('../../__mocks__/FabricUIManager'), ); +jest.mock('../../../../src/private/webapis/dom/nodes/specs/NativeDOM', () => + require('../../../../src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock'), +); + /** * Given a mocked function, get a correctly typed mock function that preserves * the original function's type. diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index 676bfb178304ad..1ab03a083c5c15 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -50,7 +50,7 @@ export function toNode(node: NodeMock): Node { const roots: Map = new Map(); const allocatedTags: Set = new Set(); -function ensureHostNode(node: Node): void { +export function ensureHostNode(node: Node): void { if (node == null || typeof node !== 'object') { throw new Error( `Expected node to be an object. Got ${ @@ -94,7 +94,7 @@ function getAncestorsInChildSet( return null; } -function getAncestorsInCurrentTree( +export function getAncestorsInCurrentTree( node: Node, ): ?$ReadOnlyArray<[Node, number]> { const childSet = roots.get(fromNode(node).rootTag); @@ -133,7 +133,7 @@ export function getNodeInChildSet(node: Node, childSet: NodeSet): ?Node { return nodeInCurrentTree; } -function getNodeInCurrentTree(node: Node): ?Node { +export function getNodeInCurrentTree(node: Node): ?Node { const childSet = roots.get(fromNode(node).rootTag); if (childSet == null) { return null; @@ -142,25 +142,6 @@ function getNodeInCurrentTree(node: Node): ?Node { return getNodeInChildSet(node, childSet); } -function* dfs(node: ?Node): Iterator { - if (node == null) { - return; - } - - yield node; - - for (const child of fromNode(node).children) { - yield* dfs(child); - } -} - -function hasDisplayNone(node: Node): boolean { - const props = fromNode(node).props; - // Style is flattened when passed to native, so there's no style object. - // $FlowFixMe[prop-missing] - return props != null && props.display === 'none'; -} - interface IFabricUIManagerMock extends FabricUIManager { getRoot(rootTag: RootTag | number): NodeSet; __getInstanceHandleFromNode(node: Node): InternalInstanceHandle; @@ -313,333 +294,16 @@ const FabricUIManagerMock: IFabricUIManagerMock = { /* y:*/ number, /* width:*/ number, /* height:*/ number, - ] => { - ensureHostNode(node); - - const nodeInCurrentTree = getNodeInCurrentTree(node); - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null) { - return null; - } - - const boundingClientRectForTests: ?{ - x: number, - y: number, - width: number, - height: number, - } = - // $FlowExpectedError[prop-missing] - currentProps.__boundingClientRectForTests; - - if (boundingClientRectForTests == null) { - return null; - } - - const {x, y, width, height} = boundingClientRectForTests; - return [x, y, width, height]; - }, + ] => {}, ), - hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false), - - setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}), - - releasePointerCapture: jest.fn((node: Node, pointerId: number): void => {}), - setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}), 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 nodeInCurrentTree = getNodeInCurrentTree(node); - - if (nodeInCurrentTree == null) { - return []; - } - - return fromNode(nodeInCurrentTree).children.map( - child => fromNode(child).instanceHandle, - ); - }, - ), - - isConnected: jest.fn((node: Node): boolean => { - return getNodeInCurrentTree(node) != null; - }), - - getTextContent: jest.fn((node: Node): string => { - const nodeInCurrentTree = getNodeInCurrentTree(node); - - let result = ''; - - if (nodeInCurrentTree == null) { - return result; - } - - for (const childNode of dfs(nodeInCurrentTree)) { - if (fromNode(childNode).viewName === 'RCTRawText') { - const props = fromNode(childNode).props; - // $FlowExpectedError[prop-missing] - const maybeString: ?string = props.text; - if (typeof maybeString === 'string') { - result += maybeString; - } - } - } - return result; - }), - - compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => { - /* eslint-disable no-bitwise */ - const ReadOnlyNode = - require('../../../src/private/webapis/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; - }), - - getOffset: jest.fn( - ( - node: Node, - ): ?[ - /* offsetParent: */ InternalInstanceHandle, - /* offsetTop: */ number, - /* offsetLeft: */ number, - ] => { - const ancestors = getAncestorsInCurrentTree(node); - if (ancestors == null) { - return null; - } - - const [parent, position] = ancestors[ancestors.length - 1]; - const nodeInCurrentTree = fromNode(parent).children[position]; - - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null || hasDisplayNone(nodeInCurrentTree)) { - return null; - } - - const offsetForTests: ?{ - top: number, - left: number, - } = - // $FlowExpectedError[prop-missing] - currentProps.__offsetForTests; - - if (offsetForTests == null) { - return null; - } - - let currentIndex = ancestors.length - 1; - while (currentIndex >= 0 && !hasDisplayNone(ancestors[currentIndex][0])) { - currentIndex--; - } - - if (currentIndex >= 0) { - // The node or one of its ancestors have display: none - return null; - } - - return [ - fromNode(parent).instanceHandle, - offsetForTests.top, - offsetForTests.left, - ]; - }, - ), - - getScrollPosition: jest.fn( - (node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => { - ensureHostNode(node); - - const nodeInCurrentTree = getNodeInCurrentTree(node); - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null) { - return null; - } - - const scrollForTests: ?{ - scrollLeft: number, - scrollTop: number, - ... - } = - // $FlowExpectedError[prop-missing] - currentProps.__scrollForTests; - - if (scrollForTests == null) { - return null; - } - - const {scrollLeft, scrollTop} = scrollForTests; - return [scrollLeft, scrollTop]; - }, - ), - - getScrollSize: jest.fn( - (node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => { - ensureHostNode(node); - - const nodeInCurrentTree = getNodeInCurrentTree(node); - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null) { - return null; - } - - const scrollForTests: ?{ - scrollWidth: number, - scrollHeight: number, - ... - } = - // $FlowExpectedError[prop-missing] - currentProps.__scrollForTests; - - if (scrollForTests == null) { - return null; - } - - const {scrollWidth, scrollHeight} = scrollForTests; - return [scrollWidth, scrollHeight]; - }, - ), - - getInnerSize: jest.fn( - (node: Node): ?[/* width: */ number, /* height: */ number] => { - ensureHostNode(node); - - const nodeInCurrentTree = getNodeInCurrentTree(node); - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null) { - return null; - } - - const innerSizeForTests: ?{ - width: number, - height: number, - ... - } = - // $FlowExpectedError[prop-missing] - currentProps.__innerSizeForTests; - - if (innerSizeForTests == null) { - return null; - } - - const {width, height} = innerSizeForTests; - return [width, height]; - }, - ), - - getBorderSize: jest.fn( - ( - node: Node, - ): ?[ - /* topWidth: */ number, - /* rightWidth: */ number, - /* bottomWidth: */ number, - /* leftWidth: */ number, - ] => { - ensureHostNode(node); - - const nodeInCurrentTree = getNodeInCurrentTree(node); - const currentProps = - nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; - if (currentProps == null) { - return null; - } - - const borderSizeForTests: ?{ - topWidth?: number, - rightWidth?: number, - bottomWidth?: number, - leftWidth?: number, - ... - } = - // $FlowExpectedError[prop-missing] - currentProps.__borderSizeForTests; - - if (borderSizeForTests == null) { - return null; - } - - const { - topWidth = 0, - rightWidth = 0, - bottomWidth = 0, - leftWidth = 0, - } = borderSizeForTests; - return [topWidth, rightWidth, bottomWidth, leftWidth]; - }, - ), - - getTagName: jest.fn((node: Node): string => { - ensureHostNode(node); - return 'RN:' + fromNode(node).viewName; - }), + compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => 0), getRoot(containerTag: RootTag | number): NodeSet { const tag = createRootTag(containerTag); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 482356444d6de5..56c0972ed05cfb 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -6605,24 +6605,11 @@ export interface Spec { locationY: number, callback: (instanceHandle: ?InternalInstanceHandle) => void ) => void; - +getParentNode: (node: Node) => ?InternalInstanceHandle; - +getChildNodes: (node: Node) => $ReadOnlyArray; - +isConnected: (node: Node) => boolean; +compareDocumentPosition: (node: Node, otherNode: Node) => number; - +getTextContent: (node: Node) => string; +getBoundingClientRect: ( node: Node, includeTransform: boolean ) => ?[number, number, number, number]; - +getOffset: (node: Node) => ?[InternalInstanceHandle, number, number]; - +getScrollPosition: (node: Node) => ?[number, number]; - +getScrollSize: (node: Node) => ?[number, number]; - +getInnerSize: (node: Node) => ?[number, number]; - +getBorderSize: (node: Node) => ?[number, number, number, number]; - +getTagName: (node: Node) => string; - +hasPointerCapture: (node: Node, pointerId: number) => boolean; - +setPointerCapture: (node: Node, pointerId: number) => void; - +releasePointerCapture: (node: Node, pointerId: number) => void; } declare export function getFabricUIManager(): ?Spec; " diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp new file mode 100644 index 00000000000000..2c26e9c415d97b --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -0,0 +1,355 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NativeDOM.h" +#include +#include +#include +#include +#include + +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER +#include "Plugins.h" +#endif + +std::shared_ptr NativeDOMModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared(std::move(jsInvoker)); +} + +namespace { +facebook::react::UIManager& getUIManagerFromRuntime( + facebook::jsi::Runtime& runtime) { + return facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); +} + +facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( + facebook::jsi::Runtime& runtime) { + return facebook::react::UIManagerBinding::getBinding(runtime) + ->getPointerEventsProcessor(); +} +} // namespace + +namespace facebook::react { + +NativeDOM::NativeDOM(std::shared_ptr jsInvoker) + : NativeDOMCxxSpec(std::move(jsInvoker)) {} + +std::optional NativeDOM::getParentNode( + jsi::Runtime& rt, + jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto parentShadowNode = + getUIManagerFromRuntime(rt).getNewestParentOfShadowNode(*shadowNode); + + // shadowNode is a RootShadowNode + if (!parentShadowNode) { + return std::nullopt; + } + + return (*parentShadowNode).getInstanceHandle(rt); +} + +std::optional> NativeDOM::getChildNodes( + jsi::Runtime& rt, + jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto newestCloneOfShadowNode = + getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); + + // There's no version of this node in the current shadow tree + if (newestCloneOfShadowNode == nullptr) { + return std::nullopt; + } + + auto childShadowNodes = newestCloneOfShadowNode->getChildren(); + return getArrayOfInstanceHandlesFromShadowNodes(childShadowNodes, rt); +} + +bool NativeDOM::isConnected(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto newestCloneOfShadowNode = + getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); + + return newestCloneOfShadowNode != nullptr; +} + +double NativeDOM::compareDocumentPosition( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + jsi::Value otherShadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto otherShadowNode = shadowNodeFromValue(rt, otherShadowNodeValue); + + auto documentPosition = getUIManagerFromRuntime(rt).compareDocumentPosition( + *shadowNode, *otherShadowNode); + + return documentPosition; +} + +std::string NativeDOM::getTextContent( + jsi::Runtime& rt, + jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto textContent = + getUIManagerFromRuntime(rt).getTextContentInNewestCloneOfShadowNode( + *shadowNode); + + return textContent; +} + +std::optional> +NativeDOM::getBoundingClientRect( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + bool includeTransform) { + auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( + *shadowNodeFromValue(rt, shadowNodeValue), + nullptr, + {/* .includeTransform = */ includeTransform, + /* .includeViewportOffset = */ true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto frame = layoutMetrics.frame; + return std::tuple{ + frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; +} + +std::optional> +NativeDOM::getOffset(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto& uiManager = getUIManagerFromRuntime(rt); + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto newestCloneOfShadowNode = + uiManager.getNewestCloneOfShadowNode(*shadowNode); + auto newestPositionedAncestorOfShadowNode = + uiManager.getNewestPositionedAncestorOfShadowNode(*shadowNode); + // The node is no longer part of an active shadow tree, or it is the + // root node + if (newestCloneOfShadowNode == nullptr || + newestPositionedAncestorOfShadowNode == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto shadowNodeLayoutMetricsRelativeToRoot = + uiManager.getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ false}); + if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto positionedAncestorLayoutMetricsRelativeToRoot = + uiManager.getRelativeLayoutMetrics( + *newestPositionedAncestorOfShadowNode, + nullptr, + {/* .includeTransform = */ false}); + if (positionedAncestorLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto shadowNodeOriginRelativeToRoot = + shadowNodeLayoutMetricsRelativeToRoot.frame.origin; + auto positionedAncestorOriginRelativeToRoot = + positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; + + // On the Web, offsets are computed from the inner border of the + // parent. + auto offsetTop = shadowNodeOriginRelativeToRoot.y - + positionedAncestorOriginRelativeToRoot.y - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; + auto offsetLeft = shadowNodeOriginRelativeToRoot.x - + positionedAncestorOriginRelativeToRoot.x - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; + + return std::tuple{ + (*newestPositionedAncestorOfShadowNode).getInstanceHandle(rt), + offsetTop, + offsetLeft}; +} + +std::optional> +NativeDOM::getScrollPosition(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto& uiManager = getUIManagerFromRuntime(rt); + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto newestCloneOfShadowNode = + uiManager.getNewestCloneOfShadowNode(*shadowNode); + // The node is no longer part of an active shadow tree, or it is the + // root node + if (newestCloneOfShadowNode == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = uiManager.getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto layoutableShadowNode = + dynamic_cast(newestCloneOfShadowNode.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); + + return std::tuple{ + scrollPosition.x == 0 ? 0 : -scrollPosition.x, + scrollPosition.y == 0 ? 0 : -scrollPosition.y}; +} + +std::optional> +NativeDOM::getScrollSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto& uiManager = getUIManagerFromRuntime(rt); + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + auto newestCloneOfShadowNode = + uiManager.getNewestCloneOfShadowNode(*shadowNode); + // The node is no longer part of an active shadow tree, or it is the + // root node + if (newestCloneOfShadowNode == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = uiManager.getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + newestCloneOfShadowNode.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + Size scrollSize = getScrollableContentBounds( + layoutableShadowNode->getContentBounds(), layoutMetrics) + .size; + + return std::tuple{ + std::round(scrollSize.width), std::round(scrollSize.height)}; +} + +std::optional> +NativeDOM::getInnerSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + return std::tuple{ + std::round(paddingFrame.size.width), + std::round(paddingFrame.size.height)}; +} + +std::optional> +NativeDOM::getBorderSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( + *shadowNode, nullptr, {/* .includeTransform = */ false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + return std::tuple{ + std::round(layoutMetrics.borderWidth.top), + std::round(layoutMetrics.borderWidth.right), + std::round(layoutMetrics.borderWidth.bottom), + std::round(layoutMetrics.borderWidth.left)}; +} + +std::string NativeDOM::getTagName( + jsi::Runtime& rt, + jsi::Value shadowNodeValue) { + auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + + std::string canonicalComponentName = shadowNode->getComponentName(); + + // FIXME(T162807327): Remove Android-specific prefixes and unify + // shadow node implementations + if (canonicalComponentName == "AndroidTextInput") { + canonicalComponentName = "TextInput"; + } else if (canonicalComponentName == "AndroidSwitch") { + canonicalComponentName = "Switch"; + } + + // Prefix with RN: + canonicalComponentName.insert(0, "RN:"); + + return canonicalComponentName; +} + +bool NativeDOM::hasPointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId) { + bool isCapturing = getPointerEventsProcessorFromRuntime(rt).hasPointerCapture( + pointerId, shadowNodeFromValue(rt, shadowNodeValue).get()); + return isCapturing; +} + +void NativeDOM::setPointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId) { + getPointerEventsProcessorFromRuntime(rt).setPointerCapture( + pointerId, shadowNodeFromValue(rt, shadowNodeValue)); +} + +void NativeDOM::releasePointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId) { + getPointerEventsProcessorFromRuntime(rt).releasePointerCapture( + pointerId, shadowNodeFromValue(rt, shadowNodeValue).get()); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h new file mode 100644 index 00000000000000..31de8d7c07c3fc --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#if __has_include("rncoreJSI.h") // Cmake headers on Android +#include "rncoreJSI.h" +#elif __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else +#include +#endif + +namespace facebook::react { + +class NativeDOM : public NativeDOMCxxSpec { + public: + NativeDOM(std::shared_ptr jsInvoker); + + std::optional getParentNode( + jsi::Runtime& rt, + jsi::Value shadowNodeValue); + + std::optional> getChildNodes( + jsi::Runtime& rt, + jsi::Value shadowNodeValue); + + bool isConnected(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + double compareDocumentPosition( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + jsi::Value otherShadowNodeValue); + + std::string getTextContent(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + std::optional> + getBoundingClientRect( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + bool includeTransform); + + std::optional> + getOffset(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + std::optional> + getScrollPosition(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + std::optional> + getScrollSize(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + std::optional> getInnerSize( + jsi::Runtime& rt, + jsi::Value shadowNodeValue); + + std::optional> + getBorderSize(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + std::string getTagName(jsi::Runtime& rt, jsi::Value shadowNodeValue); + + bool hasPointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId); + + void setPointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId); + + void releasePointerCapture( + jsi::Runtime& rt, + jsi::Value shadowNodeValue, + double pointerId); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 212184bc023f19..2f9e26e0868bd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -852,31 +852,10 @@ jsi::Value UIManagerBinding::get( }); } - /** - * DOM traversal and layout APIs - */ - if (methodName == "getBoundingClientRect") { - // This is a React Native implementation of - // `Element.prototype.getBoundingClientRect` (see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). - - // This is similar to `measureInWindow`, except it's explicitly synchronous - // (returns the result instead of passing it to a callback). - - // It allows indicating whether to include transforms so it can also be used - // to implement methods like - // [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) - // and - // [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight). - - // getBoundingClientRect(shadowNode: ShadowNode, includeTransform: boolean): - // [ - // /* x: */ number, - // /* y: */ number, - // /* width: */ number, - // /* height: */ number - // ] + // This has been moved to `NativeDOM` but we need to keep it here because + // there are still some callsites using this method in apps that don't have + // the DOM APIs enabled yet. auto paramCount = 2; return jsi::Function::createFromHostFunction( runtime, @@ -911,117 +890,10 @@ jsi::Value UIManagerBinding::get( }); } - 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 - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - auto parentShadowNode = - uiManager->getNewestParentOfShadowNode(*shadowNode); - - // shadowNode is a RootShadowNode - if (!parentShadowNode) { - return jsi::Value::null(); - } - - return (*parentShadowNode).getInstanceHandle(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 - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - 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); - }); - } - - if (methodName == "isConnected") { - // This is a React Native implementation of `Node.prototype.isConnected` - // (see https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected). - - // Indicates whether a version of the given shadow node is present in - // the current revision of an active shadow tree. - - // isConnected(shadowNode: ShadowNode): boolean - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - auto newestCloneOfShadowNode = - uiManager->getNewestCloneOfShadowNode(*shadowNode); - - return jsi::Value(newestCloneOfShadowNode != nullptr); - }); - } - 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 + // This has been moved to `NativeDOM` but we need to keep it here because + // there are still some callsites using this method in apps that don't have + // the DOM APIs enabled yet. auto paramCount = 2; return jsi::Function::createFromHostFunction( runtime, @@ -1044,473 +916,6 @@ jsi::Value UIManagerBinding::get( }); } - if (methodName == "getTextContent") { - // This is a React Native implementation of - // `Element.prototype.textContent` (see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/textContent). - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. - // If the version is present, is traverses all its children in DFS and - // concatenates all the text contents. Otherwise, it returns an empty - // string. - - // This is also used to access the text content of text nodes, which does - // not need any traversal. - - // getTextContent(shadowNode: ShadowNode): string - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - auto textContent = - uiManager->getTextContentInNewestCloneOfShadowNode(*shadowNode); - - return jsi::Value( - runtime, jsi::String::createFromUtf8(runtime, textContent)); - }); - } - - if (methodName == "getOffset") { - // This is a method to access the offset information for a shadow node, to - // implement these methods: - // * `HTMLElement.prototype.offsetParent`: see - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent. - // * `HTMLElement.prototype.offsetTop`: see - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop. - // * `HTMLElement.prototype.offsetLeft`: see - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft. - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. If the node is not present or is not - // displayed (because any of its ancestors or itself have 'display: none'), - // it returns undefined. Otherwise, it returns its parent (as all nodes in - // React Native are currently "positioned") and its offset relative to its - // parent. - - // getOffset(shadowNode: ShadowNode): - // ?[ - // /* parent: */ InstanceHandle, - // /* top: */ number, - // /* left: */ number, - // ] - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - auto newestCloneOfShadowNode = - uiManager->getNewestCloneOfShadowNode(*shadowNode); - auto newestPositionedAncestorOfShadowNode = - uiManager->getNewestPositionedAncestorOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr || - newestPositionedAncestorOfShadowNode == nullptr) { - return jsi::Value::undefined(); - } - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto shadowNodeLayoutMetricsRelativeToRoot = - uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { - return jsi::Value::undefined(); - } - - auto positionedAncestorLayoutMetricsRelativeToRoot = - uiManager->getRelativeLayoutMetrics( - *newestPositionedAncestorOfShadowNode, - nullptr, - {/* .includeTransform = */ false}); - if (positionedAncestorLayoutMetricsRelativeToRoot == - EmptyLayoutMetrics) { - return jsi::Value::undefined(); - } - - auto shadowNodeOriginRelativeToRoot = - shadowNodeLayoutMetricsRelativeToRoot.frame.origin; - auto positionedAncestorOriginRelativeToRoot = - positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; - - // On the Web, offsets are computed from the inner border of the - // parent. - auto offsetTop = shadowNodeOriginRelativeToRoot.y - - positionedAncestorOriginRelativeToRoot.y - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; - auto offsetLeft = shadowNodeOriginRelativeToRoot.x - - positionedAncestorOriginRelativeToRoot.x - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; - - return jsi::Array::createWithElements( - runtime, - (*newestPositionedAncestorOfShadowNode) - .getInstanceHandle(runtime), - jsi::Value{runtime, (double)offsetTop}, - jsi::Value{runtime, (double)offsetLeft}); - }); - } - - if (methodName == "getScrollPosition") { - // This is a method to access scroll information for a shadow node, to - // implement these methods: - // * `Element.prototype.scrollLeft`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft. - // * `Element.prototype.scrollTop`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop. - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. If the node is not present or is not - // displayed (because any of its ancestors or itself have 'display: none'), - // it returns undefined. Otherwise, it returns the scroll position. - - // getScrollPosition(shadowNode: ShadowNode): - // ?[ - // /* scrollLeft: */ number, - // /* scrollTop: */ number, - // ] - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - auto newestCloneOfShadowNode = - uiManager->getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { - return jsi::Value::undefined(); - } - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); - - if (layoutMetrics == EmptyLayoutMetrics) { - return jsi::Value::undefined(); - } - - auto layoutableShadowNode = dynamic_cast( - newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return jsi::Value::undefined(); - } - - auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); - - return jsi::Array::createWithElements( - runtime, - jsi::Value{ - runtime, - scrollPosition.x == 0 ? 0 : (double)-scrollPosition.x}, - jsi::Value{ - runtime, - scrollPosition.y == 0 ? 0 : (double)-scrollPosition.y}); - }); - } - - if (methodName == "getScrollSize") { - // This is a method to access the scroll information of a shadow node, to - // implement these methods: - // * `Element.prototype.scrollWidth`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth. - // * `Element.prototype.scrollHeight`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight. - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. If the node is not present or is not - // displayed (because any of its ancestors or itself have 'display: none'), - // it returns undefined. Otherwise, it returns the scroll size. - - // getScrollSize(shadowNode: ShadowNode): - // ?[ - // /* scrollWidth: */ number, - // /* scrollHeight: */ number, - // ] - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - auto newestCloneOfShadowNode = - uiManager->getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { - return jsi::Value::undefined(); - } - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { - return jsi::Value::undefined(); - } - - auto layoutableShadowNode = - dynamic_cast( - newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return jsi::Value::undefined(); - } - - Size scrollSize = getScrollSize( - layoutMetrics, layoutableShadowNode->getContentBounds()); - - return jsi::Array::createWithElements( - runtime, - jsi::Value{runtime, std::round(scrollSize.width)}, - jsi::Value{runtime, std::round(scrollSize.height)}); - }); - } - - if (methodName == "getInnerSize") { - // This is a method to access the inner size of a shadow node, to implement - // these methods: - // * `Element.prototype.clientWidth`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth. - // * `Element.prototype.clientHeight`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight. - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. If the node is not present, it is not - // displayed (because any of its ancestors or itself have 'display: none'), - // or it has an inline display, it returns undefined. - // Otherwise, it returns its inner size. - - // getInnerSize(shadowNode: ShadowNode): - // ?[ - // /* width: */ number, - // /* height: */ number, - // ] - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { - return jsi::Value::undefined(); - } - - auto paddingFrame = layoutMetrics.getPaddingFrame(); - - return jsi::Array::createWithElements( - runtime, - jsi::Value{runtime, std::round(paddingFrame.size.width)}, - jsi::Value{runtime, std::round(paddingFrame.size.height)}); - }); - } - - if (methodName == "getBorderSize") { - // This is a method to access the border size of a shadow node, to implement - // these methods: - // * `Element.prototype.clientLeft`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientLeft. - // * `Element.prototype.clientTop`: see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientTop. - - // It uses the version of the shadow node that is present in the current - // revision of the shadow tree. If the node is not present, it is not - // displayed (because any of its ancestors or itself have 'display: none'), - // or it has an inline display, it returns undefined. - // Otherwise, it returns its border size. - - // getBorderSize(shadowNode: ShadowNode): - // ?[ - // /* topWidth: */ number, - // /* rightWidth: */ number, - // /* bottomWidth: */ number, - // /* leftWidth: */ number, - // ] - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [uiManager, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { - return jsi::Value::undefined(); - } - - return jsi::Array::createWithElements( - runtime, - jsi::Value{runtime, std::round(layoutMetrics.borderWidth.top)}, - jsi::Value{runtime, std::round(layoutMetrics.borderWidth.right)}, - jsi::Value{runtime, std::round(layoutMetrics.borderWidth.bottom)}, - jsi::Value{runtime, std::round(layoutMetrics.borderWidth.left)}); - }); - } - - if (methodName == "getTagName") { - // This is a method to access the normalized tag name of a shadow node, to - // implement `Element.prototype.tagName` (see - // https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName). - - // getTagName(shadowNode: ShadowNode): string - auto paramCount = 1; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - - auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - - std::string canonicalComponentName = shadowNode->getComponentName(); - - // FIXME(T162807327): Remove Android-specific prefixes and unify - // shadow node implementations - if (canonicalComponentName == "AndroidTextInput") { - canonicalComponentName = "TextInput"; - } else if (canonicalComponentName == "AndroidSwitch") { - canonicalComponentName = "Switch"; - } - - // Prefix with RN: - canonicalComponentName.insert(0, "RN:"); - - return jsi::String::createFromUtf8(runtime, canonicalComponentName); - }); - } - - /** - * Pointer Capture APIs - */ - if (methodName == "hasPointerCapture") { - auto paramCount = 2; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [this, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - bool isCapturing = pointerEventsProcessor_.hasPointerCapture( - static_cast(arguments[1].asNumber()), - shadowNodeFromValue(runtime, arguments[0]).get()); - return jsi::Value(isCapturing); - }); - } - - if (methodName == "setPointerCapture") { - auto paramCount = 2; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [this, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - pointerEventsProcessor_.setPointerCapture( - static_cast(arguments[1].asNumber()), - shadowNodeFromValue(runtime, arguments[0])); - return jsi::Value::undefined(); - }); - } - - if (methodName == "releasePointerCapture") { - auto paramCount = 2; - return jsi::Function::createFromHostFunction( - runtime, - name, - paramCount, - [this, methodName, paramCount]( - jsi::Runtime& runtime, - const jsi::Value& /*thisValue*/, - const jsi::Value* arguments, - size_t count) -> jsi::Value { - validateArgumentCount(runtime, methodName, paramCount, count); - pointerEventsProcessor_.releasePointerCapture( - static_cast(arguments[1].asNumber()), - shadowNodeFromValue(runtime, arguments[0]).get()); - return jsi::Value::undefined(); - }); - } - return jsi::Value::undefined(); } @@ -1518,4 +923,8 @@ UIManager& UIManagerBinding::getUIManager() { return *uiManager_; } +PointerEventsProcessor& UIManagerBinding::getPointerEventsProcessor() { + return pointerEventsProcessor_; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h index 3ade1a02e4a008..8f8e6a387898ee 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.h @@ -70,6 +70,7 @@ class UIManagerBinding : public jsi::HostObject { jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override; UIManager& getUIManager(); + PointerEventsProcessor& getPointerEventsProcessor(); private: /* diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 5d2cd00a19b6f3..7984b902bc275d 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -167,7 +167,7 @@ inline static folly::dynamic commandArgsFromValue( return jsi::dynamicFromValue(runtime, value); } -inline static jsi::Value getArrayOfInstanceHandlesFromShadowNodes( +inline static std::vector getArrayOfInstanceHandlesFromShadowNodes( const ShadowNode::ListOfShared& nodes, jsi::Runtime& runtime) { // JSI doesn't support adding elements to an array after creation, @@ -182,11 +182,7 @@ inline static jsi::Value getArrayOfInstanceHandlesFromShadowNodes( } } - auto result = jsi::Array(runtime, nonNullInstanceHandles.size()); - for (size_t i = 0; i < nonNullInstanceHandles.size(); i++) { - result.setValueAtIndex(runtime, i, nonNullInstanceHandles[i]); - } - return result; + return nonNullInstanceHandles; } inline static void getTextContentInShadowNode( diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index a596de0ae37c07..de582ddb5cd4c2 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -31,6 +31,7 @@ import { getPublicInstanceFromInternalInstanceHandle, getShadowNode, } from './ReadOnlyNode'; +import NativeDOM from './specs/NativeDOM'; import nullthrows from 'nullthrows'; const noop = () => {}; @@ -67,7 +68,7 @@ export default class ReactNativeElement const node = getShadowNode(this); if (node != null) { - const offset = nullthrows(getFabricUIManager()).getOffset(node); + const offset = NativeDOM.getOffset(node); if (offset != null) { return Math.round(offset[2]); } @@ -80,7 +81,7 @@ export default class ReactNativeElement const node = getShadowNode(this); if (node != null) { - const offset = nullthrows(getFabricUIManager()).getOffset(node); + const offset = NativeDOM.getOffset(node); // For children of the root node we currently return offset data // but a `null` parent because the root node is not accessible // in JavaScript yet. @@ -102,7 +103,7 @@ export default class ReactNativeElement const node = getShadowNode(this); if (node != null) { - const offset = nullthrows(getFabricUIManager()).getOffset(node); + const offset = NativeDOM.getOffset(node); if (offset != null) { return Math.round(offset[1]); } diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js index aac54c9e2bf4c5..fc814f22852331 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js @@ -12,10 +12,9 @@ import type ReadOnlyElement from './ReadOnlyElement'; -import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; import ReadOnlyNode, {getShadowNode} from './ReadOnlyNode'; +import NativeDOM from './specs/NativeDOM'; import {getElementSibling} from './utilities/Traversal'; -import nullthrows from 'nullthrows'; export default class ReadOnlyCharacterData extends ReadOnlyNode { get nextElementSibling(): ReadOnlyElement | null { @@ -30,7 +29,7 @@ export default class ReadOnlyCharacterData extends ReadOnlyNode { const shadowNode = getShadowNode(this); if (shadowNode != null) { - return nullthrows(getFabricUIManager()).getTextContent(shadowNode); + return NativeDOM.getTextContent(shadowNode); } return ''; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js index e1b58a294fae5d..9db3d7a3735397 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js @@ -12,7 +12,6 @@ import type HTMLCollection from '../oldstylecollections/HTMLCollection'; -import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; import DOMRect from '../geometry/DOMRect'; import {createHTMLCollection} from '../oldstylecollections/HTMLCollection'; import ReadOnlyNode, { @@ -20,8 +19,8 @@ import ReadOnlyNode, { getInstanceHandle, getShadowNode, } from './ReadOnlyNode'; +import NativeDOM from './specs/NativeDOM'; import {getElementSibling} from './utilities/Traversal'; -import nullthrows from 'nullthrows'; export default class ReadOnlyElement extends ReadOnlyNode { get childElementCount(): number { @@ -36,7 +35,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node); + const innerSize = NativeDOM.getInnerSize(node); if (innerSize != null) { return innerSize[1]; } @@ -49,7 +48,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const borderSize = nullthrows(getFabricUIManager()).getBorderSize(node); + const borderSize = NativeDOM.getBorderSize(node); if (borderSize != null) { return borderSize[3]; } @@ -62,7 +61,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const borderSize = nullthrows(getFabricUIManager()).getBorderSize(node); + const borderSize = NativeDOM.getBorderSize(node); if (borderSize != null) { return borderSize[0]; } @@ -75,7 +74,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const innerSize = nullthrows(getFabricUIManager()).getInnerSize(node); + const innerSize = NativeDOM.getInnerSize(node); if (innerSize != null) { return innerSize[0]; } @@ -138,7 +137,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node); + const scrollSize = NativeDOM.getScrollSize(node); if (scrollSize != null) { return scrollSize[1]; } @@ -151,8 +150,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const scrollPosition = - nullthrows(getFabricUIManager()).getScrollPosition(node); + const scrollPosition = NativeDOM.getScrollPosition(node); if (scrollPosition != null) { return scrollPosition[0]; } @@ -165,8 +163,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const scrollPosition = - nullthrows(getFabricUIManager()).getScrollPosition(node); + const scrollPosition = NativeDOM.getScrollPosition(node); if (scrollPosition != null) { return scrollPosition[1]; } @@ -179,7 +176,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - const scrollSize = nullthrows(getFabricUIManager()).getScrollSize(node); + const scrollSize = NativeDOM.getScrollSize(node); if (scrollSize != null) { return scrollSize[0]; } @@ -192,7 +189,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const node = getShadowNode(this); if (node != null) { - return nullthrows(getFabricUIManager()).getTagName(node); + return NativeDOM.getTagName(node); } return ''; @@ -202,7 +199,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { const shadowNode = getShadowNode(this); if (shadowNode != null) { - return nullthrows(getFabricUIManager()).getTextContent(shadowNode); + return NativeDOM.getTextContent(shadowNode); } return ''; @@ -218,10 +215,7 @@ export default class ReadOnlyElement extends ReadOnlyNode { hasPointerCapture(pointerId: number): boolean { const node = getShadowNode(this); if (node != null) { - return nullthrows(getFabricUIManager()).hasPointerCapture( - node, - pointerId, - ); + return NativeDOM.hasPointerCapture(node, pointerId); } return false; } @@ -229,14 +223,14 @@ export default class ReadOnlyElement extends ReadOnlyNode { setPointerCapture(pointerId: number): void { const node = getShadowNode(this); if (node != null) { - nullthrows(getFabricUIManager()).setPointerCapture(node, pointerId); + NativeDOM.setPointerCapture(node, pointerId); } } releasePointerCapture(pointerId: number): void { const node = getShadowNode(this); if (node != null) { - nullthrows(getFabricUIManager()).releasePointerCapture(node, pointerId); + NativeDOM.releasePointerCapture(node, pointerId); } } } @@ -260,10 +254,7 @@ export function getBoundingClientRect( const shadowNode = getShadowNode(node); if (shadowNode != null) { - const rect = nullthrows(getFabricUIManager()).getBoundingClientRect( - shadowNode, - includeTransform, - ); + const rect = NativeDOM.getBoundingClientRect(shadowNode, includeTransform); if (rect) { return new DOMRect(rect[0], rect[1], rect[2], rect[3]); diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js index 2c29c2d105af85..45ec0ab7f6f660 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js @@ -17,9 +17,8 @@ import type { import type NodeList from '../oldstylecollections/NodeList'; import type ReadOnlyElement from './ReadOnlyElement'; -import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; import {createNodeList} from '../oldstylecollections/NodeList'; -import nullthrows from 'nullthrows'; +import NativeDOM from './specs/NativeDOM'; // We initialize this lazily to avoid a require cycle // (`ReadOnlyElement` also depends on `ReadOnlyNode`). @@ -52,7 +51,7 @@ export default class ReadOnlyNode { return false; } - return nullthrows(getFabricUIManager()).isConnected(shadowNode); + return NativeDOM.isConnected(shadowNode); } get lastChild(): ReadOnlyNode | null { @@ -125,8 +124,7 @@ export default class ReadOnlyNode { return null; } - const parentInstanceHandle = - nullthrows(getFabricUIManager()).getParentNode(shadowNode); + const parentInstanceHandle = NativeDOM.getParentNode(shadowNode); if (parentInstanceHandle == null) { return null; @@ -170,10 +168,7 @@ export default class ReadOnlyNode { return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; } - return nullthrows(getFabricUIManager()).compareDocumentPosition( - shadowNode, - otherShadowNode, - ); + return NativeDOM.compareDocumentPosition(shadowNode, otherShadowNode); } contains(otherNode: ReadOnlyNode): boolean { @@ -322,8 +317,14 @@ export function getChildNodes( return []; } - const childNodeInstanceHandles = - nullthrows(getFabricUIManager()).getChildNodes(shadowNode); + const childNodeInstanceHandles = NativeDOM.getChildNodes(shadowNode); + if ( + childNodeInstanceHandles == null || + childNodeInstanceHandles.length === 0 + ) { + return []; + } + return childNodeInstanceHandles .map(instanceHandle => getPublicInstanceFromInternalInstanceHandle(instanceHandle), diff --git a/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js new file mode 100644 index 00000000000000..a6aa62ceada9de --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js @@ -0,0 +1,381 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import type { + InternalInstanceHandle as InstanceHandle, + Node as ShadowNode, +} from '../../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type {TurboModule} from '../../../../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../../../../Libraries/TurboModule/TurboModuleRegistry'; +import nullthrows from 'nullthrows'; + +export interface Spec extends TurboModule { + +getParentNode: ( + shadowNode: mixed /* ShadowNode */, + ) => ?mixed /* ?InstanceHandle */; + + +getChildNodes: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?$ReadOnlyArray */; + + +isConnected: (shadowNode: mixed /* ShadowNode */) => boolean; + + +compareDocumentPosition: ( + shadowNode: mixed /* ShadowNode */, + otherShadowNode: mixed /* ShadowNode */, + ) => number; + + +getTextContent: (shadowNode: mixed /* ShadowNode */) => string; + + +getBoundingClientRect: ( + shadowNode: mixed /* ShadowNode */, + includeTransform: boolean, + ) => ?$ReadOnlyArray /* ?[x: number, y: number, width: number, height: number] */; + + +getOffset: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?[offsetParent: InstanceHandle, top: number, left: number] */; + + +getScrollPosition: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?[scrollLeft: number, scrollTop: number] */; + + +getScrollSize: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?[scrollWidth: number, scrollHeight: number] */; + + +getInnerSize: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?[width: number, height: number] */; + + +getBorderSize: ( + shadowNode: mixed /* ShadowNode */, + ) => ?$ReadOnlyArray /* ?[topWidth: number, rightWidth: number, bottomWidth: number, leftWidth: number] */; + + +getTagName: (shadowNode: mixed /* ShadowNode */) => string; + + +hasPointerCapture: ( + shadowNode: mixed /* ShadowNode */, + pointerId: number, + ) => boolean; + + +setPointerCapture: ( + shadowNode: mixed /* ShadowNode */, + pointerId: number, + ) => void; + + +releasePointerCapture: ( + shadowNode: mixed /* ShadowNode */, + pointerId: number, + ) => void; +} + +const RawNativeDOM = (TurboModuleRegistry.get('NativeDOMCxx'): ?Spec); + +// This is the actual interface of this module, but the native module codegen +// isn't expressive enough yet. +export interface RefinedSpec { + /** + * 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`. + */ + +getParentNode: (shadowNode: ShadowNode) => ?InstanceHandle; + + /** + * 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. + */ + +getChildNodes: (shadowNode: ShadowNode) => ?$ReadOnlyArray; + + /** + * This is a React Native implementation of `Node.prototype.isConnected` + * (see https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected). + * + * Indicates whether a version of the given shadow node is present in the + * current revision of an active shadow tree. + */ + +isConnected: (shadowNode: ShadowNode) => boolean; + + /** + * 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; + + /** + * This is a React Native implementation of `Element.prototype.textContent` + * (see https://developer.mozilla.org/en-US/docs/Web/API/Element/textContent). + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. + * If the version is present, is traverses all its children in DFS and + * concatenates all the text contents. Otherwise, it returns an empty string. + * + * This is also used to access the text content of text nodes, which does not + * need any traversal. + */ + +getTextContent: (shadowNode: ShadowNode) => string; + + /** + * This is a React Native implementation of `Element.prototype.getBoundingClientRect` + * (see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). + * + * This is similar to `measureInWindow`, except it's explicitly synchronous + * (returns the result instead of passing it to a callback). + * + * It allows indicating whether to include transforms so it can also be used + * to implement methods like [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) + * and [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight). + */ + +getBoundingClientRect: ( + shadowNode: ShadowNode, + includeTransform: boolean, + ) => ?$ReadOnly< + [ + /* x: */ number, + /* y: */ number, + /* width: */ number, + /* height: */ number, + ], + >; + + /** + * This is a method to access the offset information for a shadow node, to + * implement these methods: + * - `HTMLElement.prototype.offsetParent`: see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent. + * - `HTMLElement.prototype.offsetTop`: see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop. + * - `HTMLElement.prototype.offsetLeft`: see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft. + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. If the node is not present or is not + * displayed (because any of its ancestors or itself have 'display: none'), + * it returns `undefined`. Otherwise, it returns its parent (as all nodes in + * React Native are currently "positioned") and its offset relative to its + * parent. + */ + +getOffset: ( + shadowNode: ShadowNode, + ) => ?$ReadOnly< + [/* offsetParent: */ InstanceHandle, /* top: */ number, /* left: */ number], + >; + + /** + * This is a method to access scroll information for a shadow node, to + * implement these methods: + * - `Element.prototype.scrollLeft`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft. + * - `Element.prototype.scrollTop`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop. + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. If the node is not present or is not displayed + * (because any of its ancestors or itself have 'display: none'), it returns + * `undefined`. Otherwise, it returns the scroll position. + */ + +getScrollPosition: ( + shadowNode: ShadowNode, + ) => ?$ReadOnly<[/* scrollLeft: */ number, /* scrollTop: */ number]>; + + /** + * + * This is a method to access the scroll information of a shadow node, to + * implement these methods: + * - `Element.prototype.scrollWidth`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth. + * - `Element.prototype.scrollHeight`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight. + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. If the node is not present or is not displayed + * (because any of its ancestors or itself have 'display: none'), it returns + * `undefined`. Otherwise, it returns the scroll size. + */ + +getScrollSize: ( + shadowNode: ShadowNode, + ) => ?$ReadOnly<[/* scrollWidth: */ number, /* scrollHeight: */ number]>; + + /** + * This is a method to access the inner size of a shadow node, to implement + * these methods: + * - `Element.prototype.clientWidth`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth. + * - `Element.prototype.clientHeight`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight. + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. If the node is not present, it is not + * displayed (because any of its ancestors or itself have 'display: none'), or + * it has an inline display, it returns `undefined`. Otherwise, it returns its + * inner size. + */ + +getInnerSize: ( + shadowNode: ShadowNode, + ) => ?$ReadOnly<[/* width: */ number, /* height: */ number]>; + + /** + * This is a method to access the border size of a shadow node, to implement + * these methods: + * - `Element.prototype.clientLeft`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/clientLeft. + * - `Element.prototype.clientTop`: see https://developer.mozilla.org/en-US/docs/Web/API/Element/clientTop. + * + * It uses the version of the shadow node that is present in the current + * revision of the shadow tree. If the node is not present, it is not + * displayed (because any of its ancestors or itself have 'display: none'), or + * it has an inline display, it returns `undefined`. Otherwise, it returns its + * border size. + */ + +getBorderSize: ( + shadowNode: ShadowNode, + ) => ?$ReadOnly< + [ + /* topWidth: */ number, + /* rightWidth: */ number, + /* bottomWidth: */ number, + /* leftWidth: */ number, + ], + >; + + /** + * This is a method to access the normalized tag name of a shadow node, to + * implement `Element.prototype.tagName` (see https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName). + */ + +getTagName: (shadowNode: ShadowNode) => string; + + /** + * Pointer Capture APIs + */ + + +hasPointerCapture: (shadowNode: ShadowNode, pointerId: number) => boolean; + + +setPointerCapture: (shadowNode: ShadowNode, pointerId: number) => void; + + +releasePointerCapture: (shadowNode: ShadowNode, pointerId: number) => void; +} + +const NativeDOM: RefinedSpec = { + getParentNode(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getParentNode( + shadowNode, + ): ?InstanceHandle); + }, + + getChildNodes(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getChildNodes( + shadowNode, + ): ?$ReadOnlyArray); + }, + + isConnected(shadowNode) { + return nullthrows(RawNativeDOM).isConnected(shadowNode); + }, + + compareDocumentPosition(shadowNode, otherShadowNode) { + return nullthrows(RawNativeDOM).compareDocumentPosition( + shadowNode, + otherShadowNode, + ); + }, + + getTextContent(shadowNode) { + return nullthrows(RawNativeDOM).getTextContent(shadowNode); + }, + + getBoundingClientRect(shadowNode, includeTransform: boolean) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getBoundingClientRect( + shadowNode, + includeTransform, + ): ?$ReadOnly< + [ + /* x: */ number, + /* y: */ number, + /* width: */ number, + /* height: */ number, + ], + >); + }, + + getOffset(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getOffset(shadowNode): ?$ReadOnly< + [ + /* offsetParent: */ InstanceHandle, + /* top: */ number, + /* left: */ number, + ], + >); + }, + + getScrollPosition(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getScrollPosition(shadowNode): ?$ReadOnly< + [/* scrollLeft: */ number, /* scrollTop: */ number], + >); + }, + + getScrollSize(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getScrollSize(shadowNode): ?$ReadOnly< + [/* scrollWidth: */ number, /* scrollHeight: */ number], + >); + }, + + getInnerSize(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getInnerSize(shadowNode): ?$ReadOnly< + [/* width: */ number, /* height: */ number], + >); + }, + + getBorderSize(shadowNode) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM).getBorderSize(shadowNode): ?$ReadOnly< + [ + /* topWidth: */ number, + /* rightWidth: */ number, + /* bottomWidth: */ number, + /* leftWidth: */ number, + ], + >); + }, + + getTagName(shadowNode) { + return nullthrows(RawNativeDOM).getTagName(shadowNode); + }, + + hasPointerCapture(shadowNode, pointerId) { + return nullthrows(RawNativeDOM).hasPointerCapture(shadowNode, pointerId); + }, + + setPointerCapture(shadowNode, pointerId) { + return nullthrows(RawNativeDOM).setPointerCapture(shadowNode, pointerId); + }, + + releasePointerCapture(shadowNode, pointerId) { + return nullthrows(RawNativeDOM).releasePointerCapture( + shadowNode, + pointerId, + ); + }, +}; + +export default NativeDOM; diff --git a/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js b/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js new file mode 100644 index 00000000000000..8f6c64f4ee4c30 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/specs/__mocks__/NativeDOMMock.js @@ -0,0 +1,376 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import type { + InternalInstanceHandle, + Node, +} from '../../../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import typeof NativeDOM from '../NativeDOM'; + +import { + ensureHostNode, + fromNode, + getAncestorsInCurrentTree, + getNodeInCurrentTree, +} from '../../../../../../../Libraries/ReactNative/__mocks__/FabricUIManager'; + +function* dfs(node: ?Node): Iterator { + if (node == null) { + return; + } + + yield node; + + for (const child of fromNode(node).children) { + yield* dfs(child); + } +} + +function hasDisplayNone(node: Node): boolean { + const props = fromNode(node).props; + // Style is flattened when passed to native, so there's no style object. + // $FlowFixMe[prop-missing] + return props != null && props.display === 'none'; +} + +const NativeDOMMock: NativeDOM = { + getBoundingClientRect: jest.fn( + ( + node: Node, + includeTransform: boolean, + ): ?[ + /* x:*/ number, + /* y:*/ number, + /* width:*/ number, + /* height:*/ number, + ] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const boundingClientRectForTests: ?{ + x: number, + y: number, + width: number, + height: number, + } = + // $FlowExpectedError[prop-missing] + currentProps.__boundingClientRectForTests; + + if (boundingClientRectForTests == null) { + return null; + } + + const {x, y, width, height} = boundingClientRectForTests; + return [x, y, width, height]; + }, + ), + + hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false), + + setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}), + + releasePointerCapture: jest.fn((node: Node, pointerId: number): 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 nodeInCurrentTree = getNodeInCurrentTree(node); + + if (nodeInCurrentTree == null) { + return []; + } + + return fromNode(nodeInCurrentTree).children.map( + child => fromNode(child).instanceHandle, + ); + }, + ), + + isConnected: jest.fn((node: Node): boolean => { + return getNodeInCurrentTree(node) != null; + }), + + getTextContent: jest.fn((node: Node): string => { + const nodeInCurrentTree = getNodeInCurrentTree(node); + + let result = ''; + + if (nodeInCurrentTree == null) { + return result; + } + + for (const childNode of dfs(nodeInCurrentTree)) { + if (fromNode(childNode).viewName === 'RCTRawText') { + const props = fromNode(childNode).props; + // $FlowExpectedError[prop-missing] + const maybeString: ?string = props.text; + if (typeof maybeString === 'string') { + result += maybeString; + } + } + } + return result; + }), + + compareDocumentPosition: jest.fn((node: Node, otherNode: Node): number => { + /* eslint-disable no-bitwise */ + const ReadOnlyNode = require('../../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; + }), + + getOffset: jest.fn( + ( + node: Node, + ): ?[ + /* offsetParent: */ InternalInstanceHandle, + /* offsetTop: */ number, + /* offsetLeft: */ number, + ] => { + const ancestors = getAncestorsInCurrentTree(node); + if (ancestors == null) { + return null; + } + + const [parent, position] = ancestors[ancestors.length - 1]; + const nodeInCurrentTree = fromNode(parent).children[position]; + + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null || hasDisplayNone(nodeInCurrentTree)) { + return null; + } + + const offsetForTests: ?{ + top: number, + left: number, + } = + // $FlowExpectedError[prop-missing] + currentProps.__offsetForTests; + + if (offsetForTests == null) { + return null; + } + + let currentIndex = ancestors.length - 1; + while (currentIndex >= 0 && !hasDisplayNone(ancestors[currentIndex][0])) { + currentIndex--; + } + + if (currentIndex >= 0) { + // The node or one of its ancestors have display: none + return null; + } + + return [ + fromNode(parent).instanceHandle, + offsetForTests.top, + offsetForTests.left, + ]; + }, + ), + + getScrollPosition: jest.fn( + (node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const scrollForTests: ?{ + scrollLeft: number, + scrollTop: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__scrollForTests; + + if (scrollForTests == null) { + return null; + } + + const {scrollLeft, scrollTop} = scrollForTests; + return [scrollLeft, scrollTop]; + }, + ), + + getScrollSize: jest.fn( + (node: Node): ?[/* scrollLeft: */ number, /* scrollTop: */ number] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const scrollForTests: ?{ + scrollWidth: number, + scrollHeight: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__scrollForTests; + + if (scrollForTests == null) { + return null; + } + + const {scrollWidth, scrollHeight} = scrollForTests; + return [scrollWidth, scrollHeight]; + }, + ), + + getInnerSize: jest.fn( + (node: Node): ?[/* width: */ number, /* height: */ number] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const innerSizeForTests: ?{ + width: number, + height: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__innerSizeForTests; + + if (innerSizeForTests == null) { + return null; + } + + const {width, height} = innerSizeForTests; + return [width, height]; + }, + ), + + getBorderSize: jest.fn( + ( + node: Node, + ): ?[ + /* topWidth: */ number, + /* rightWidth: */ number, + /* bottomWidth: */ number, + /* leftWidth: */ number, + ] => { + ensureHostNode(node); + + const nodeInCurrentTree = getNodeInCurrentTree(node); + const currentProps = + nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null; + if (currentProps == null) { + return null; + } + + const borderSizeForTests: ?{ + topWidth?: number, + rightWidth?: number, + bottomWidth?: number, + leftWidth?: number, + ... + } = + // $FlowExpectedError[prop-missing] + currentProps.__borderSizeForTests; + + if (borderSizeForTests == null) { + return null; + } + + const { + topWidth = 0, + rightWidth = 0, + bottomWidth = 0, + leftWidth = 0, + } = borderSizeForTests; + return [topWidth, rightWidth, bottomWidth, leftWidth]; + }, + ), + + getTagName: jest.fn((node: Node): string => { + ensureHostNode(node); + return 'RN:' + fromNode(node).viewName; + }), +}; + +export default NativeDOMMock;