diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 33b01190e97..ca60bdf1149 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -124,7 +124,13 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get textContent(): string | null { - throw new TypeError('Unimplemented'); + const shadowNode = getShadowNode(this); + + if (shadowNode != null) { + return nullthrows(getFabricUIManager()).getTextContent(shadowNode); + } + + return ''; } getBoundingClientRect(): DOMRect { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index a608e3ab0d7..3ff2ae7d218 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -70,6 +70,7 @@ export type Spec = {| +getChildNodes: (node: Node) => $ReadOnlyArray, +isConnected: (node: Node) => boolean, +compareDocumentPosition: (node: Node, otherNode: Node) => number, + +getTextContent: (node: Node) => string, +getBoundingClientRect: ( node: Node, ) => ?[ diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index e35d15af61d..4674931a840 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -124,6 +124,18 @@ function getNodeInCurrentTree(node: Node): ?Node { return nodeInCurrentTree; } +function* dfs(node: ?Node): Iterator { + if (node == null) { + return; + } + + yield node; + + for (const child of fromNode(node).children) { + yield* dfs(child); + } +} + const FabricUIManagerMock: FabricUIManager = { createNode: jest.fn( ( @@ -288,6 +300,27 @@ const FabricUIManagerMock: FabricUIManager = { 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('../../DOM/Nodes/ReadOnlyNode').default; diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BUCK b/packages/react-native/ReactCommon/react/renderer/components/text/BUCK index 27bc1edacc8..aca9bb00799 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BUCK +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BUCK @@ -64,7 +64,6 @@ rn_xplat_cxx_library( react_native_xplat_target("react/renderer/graphics:graphics"), react_native_xplat_target("react/renderer/textlayoutmanager:textlayoutmanager"), react_native_xplat_target("react/renderer/components/view:view"), - react_native_xplat_target("react/renderer/uimanager:uimanager"), react_native_xplat_target("react/renderer/mounting:mounting"), react_native_xplat_target("react/renderer/componentregistry:componentregistry"), react_native_xplat_target("react/utils:utils"), diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/BUCK b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/BUCK index 11657d056ca..37faaeb512b 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/BUCK +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/BUCK @@ -125,7 +125,6 @@ rn_xplat_cxx_library( react_native_xplat_target("react/utils:utils"), react_native_xplat_target("react/renderer/debug:debug"), react_native_xplat_target("react/renderer/graphics:graphics"), - react_native_xplat_target("react/renderer/uimanager:uimanager"), react_native_xplat_target("react/renderer/mounting:mounting"), react_native_xplat_target("react/renderer/componentregistry:componentregistry"), ], diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/BUCK b/packages/react-native/ReactCommon/react/renderer/uimanager/BUCK index 1020c8107e2..dfa12a5c3f6 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/BUCK +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/BUCK @@ -56,6 +56,7 @@ rn_xplat_cxx_library( "//xplat/jsi:jsi", react_native_xplat_target("react/config:config"), react_native_xplat_target("react/debug:debug"), + react_native_xplat_target("react/renderer/components/text:text"), react_native_xplat_target("react/renderer/components/view:view"), react_native_xplat_target("react/renderer/mounting:mounting"), react_native_xplat_target("react/renderer/core:core"), diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index c8e5045808d..6434457b9a1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -296,6 +296,14 @@ ShadowNode::Shared UIManager::getNewestParentOfShadowNode( parentOfParentPair.second); } +std::string UIManager::getTextContentInNewestCloneOfShadowNode( + ShadowNode const &shadowNode) const { + auto newestCloneOfShadowNode = getNewestCloneOfShadowNode(shadowNode); + std::string result; + getTextContentInShadowNode(*newestCloneOfShadowNode, result); + return result; +} + int UIManager::compareDocumentPosition( ShadowNode const &shadowNode, ShadowNode const &otherShadowNode) const { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 7e196b8db24..e4f89e855c1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -88,6 +88,9 @@ class UIManager final : public ShadowTreeDelegate { ShadowNode::Shared getNewestParentOfShadowNode( ShadowNode const &shadowNode) const; + std::string getTextContentInNewestCloneOfShadowNode( + ShadowNode const &shadowNode) const; + int compareDocumentPosition( ShadowNode const &shadowNode, ShadowNode const &otherShadowNode) const; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 78e8d2ee419..1f261fc2148 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -892,6 +892,37 @@ 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. + + // getTextContent(shadowNode: ShadowNode): string + 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 textContent = + uiManager->getTextContentInNewestCloneOfShadowNode(*shadowNode); + + return jsi::Value( + runtime, jsi::String::createFromUtf8(runtime, textContent)); + }); + } + return jsi::Value::undefined(); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 5f7178eb451..6c4893dccf5 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include namespace facebook::react { @@ -219,4 +221,17 @@ inline static jsi::Value getArrayOfInstanceHandlesFromShadowNodes( } return result; } + +inline static void getTextContentInShadowNode( + ShadowNode const &shadowNode, + std::string &result) { + auto rawTextShadowNode = traitCast(&shadowNode); + if (rawTextShadowNode != nullptr) { + result.append(rawTextShadowNode->getConcreteProps().text); + } + + for (auto const &childNode : shadowNode.getChildren()) { + getTextContentInShadowNode(*childNode.get(), result); + } +} } // namespace facebook::react