diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 69cb482e9c3..21c621f6fad 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -137,7 +137,13 @@ export default class ReadOnlyElement extends ReadOnlyNode { } get tagName(): string { - throw new TypeError('Unimplemented'); + const node = getShadowNode(this); + + if (node != null) { + return nullthrows(getFabricUIManager()).getTagName(node); + } + + return ''; } get textContent(): string | null { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index eab57429cb4..a938dc6d962 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -91,6 +91,7 @@ export interface Spec { +getScrollPosition: ( node: Node, ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number]; + +getTagName: (node: Node) => string; /** * Support methods for the Pointer Capture APIs. @@ -131,6 +132,10 @@ const CACHED_PROPERTIES = [ 'getBoundingClientRect', 'getOffset', 'getScrollPosition', + '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/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index 5aebd90a205..d20d9165e05 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -497,6 +497,11 @@ const FabricUIManagerMock: IFabricUIManagerMock = { }, ), + getTagName: jest.fn((node: Node): string => { + ensureHostNode(node); + return 'RN:' + fromNode(node).viewName; + }), + __getInstanceHandleFromNode(node: Node): InternalInstanceHandle { return fromNode(node).instanceHandle; }, diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index ca6f816844f..87adb43f2bd 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -1207,6 +1207,43 @@ jsi::Value UIManagerBinding::get( }); } + 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 */