Implement element.textContent

Summary:
This implements the `textContent` property in `ReadOnlyElement` as defined in https://github.com/react-native-community/discussions-and-proposals/pull/607.

This requires a new method in Fabric because we can't derive that from the existing methods.

Changelog: [internal]

bypass-github-export-checks

Reviewed By: rshest

Differential Revision: D44464637

fbshipit-source-id: de7e4539b4ef70f3586f802fd07888d22678c239
This commit is contained in:
Rubén Norte
2023-04-13 09:19:00 -07:00
committed by Facebook GitHub Bot
parent 528edd82bb
commit 245c405bd6
10 changed files with 99 additions and 3 deletions
@@ -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 {
@@ -70,6 +70,7 @@ export type Spec = {|
+getChildNodes: (node: Node) => $ReadOnlyArray<InternalInstanceHandle>,
+isConnected: (node: Node) => boolean,
+compareDocumentPosition: (node: Node, otherNode: Node) => number,
+getTextContent: (node: Node) => string,
+getBoundingClientRect: (
node: Node,
) => ?[
@@ -124,6 +124,18 @@ function getNodeInCurrentTree(node: Node): ?Node {
return nodeInCurrentTree;
}
function* dfs(node: ?Node): Iterator<Node> {
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;
@@ -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"),
@@ -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"),
],
@@ -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"),
@@ -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 {
@@ -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;
@@ -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();
}
@@ -11,9 +11,11 @@
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/components/text/RawTextShadowNode.h>
#include <react/renderer/core/CoreFeatures.h>
#include <react/renderer/core/EventHandler.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/core/TraitCast.h>
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<RawTextShadowNode const *>(&shadowNode);
if (rawTextShadowNode != nullptr) {
result.append(rawTextShadowNode->getConcreteProps().text);
}
for (auto const &childNode : shadowNode.getChildren()) {
getTextContentInShadowNode(*childNode.get(), result);
}
}
} // namespace facebook::react