From e5fd90462e575bcec5c86fe916dd7ae3327a81cc Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 25 Feb 2026 19:57:42 -0800 Subject: [PATCH 1/4] Refactor ParagraphComponentDescriptor to support subclassing Summary: Extract ParagraphComponentDescriptor logic into a template base class BaseParagraphComponentDescriptor so that other paragraph-like component descriptors can reuse the same TextLayoutManager wiring. Remove `final` from ParagraphShadowNode to allow subclassing. Move TextLayoutManagerKey from extern linkage in .cpp to constexpr in the new header. This is a pure refactor with no behavioral change. Changelog: [Internal] Differential Revision: D93829402 --- .../text/BaseParagraphComponentDescriptor.h | 49 +++++++++++++++++++ .../text/ParagraphComponentDescriptor.cpp | 14 ------ .../text/ParagraphComponentDescriptor.h | 31 ++---------- .../components/text/ParagraphShadowNode.h | 2 +- 4 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphComponentDescriptor.h delete mode 100644 packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphComponentDescriptor.h new file mode 100644 index 000000000000..8c7c8aab798f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphComponentDescriptor.h @@ -0,0 +1,49 @@ +/* + * 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 +#include +#include + +namespace facebook::react { + +constexpr const char *const TextLayoutManagerKey = "TextLayoutManager"; + +template +class BaseParagraphComponentDescriptor : public ConcreteComponentDescriptor { + public: + explicit BaseParagraphComponentDescriptor(const ComponentDescriptorParameters ¶meters) + : ConcreteComponentDescriptor(parameters), + textLayoutManager_(getManagerByName(this->contextContainer_, TextLayoutManagerKey)) + { + } + + ComponentName getComponentName() const override + { + return ShadowNodeT::Name(); + } + + protected: + void adopt(ShadowNode &shadowNode) const override + { + ConcreteComponentDescriptor::adopt(shadowNode); + + auto ¶graphShadowNode = static_cast(shadowNode); + + // `ParagraphShadowNode` uses `TextLayoutManager` to measure text content + // and communicate text rendering metrics to mounting layer. + paragraphShadowNode.setTextLayoutManager(textLayoutManager_); + } + + private: + // Every `ParagraphShadowNode` has a reference to a shared `TextLayoutManager` + const std::shared_ptr textLayoutManager_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.cpp deleted file mode 100644 index ee4e014b6564..000000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.cpp +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 "ParagraphComponentDescriptor.h" - -namespace facebook::react { - -extern const char TextLayoutManagerKey[] = "TextLayoutManager"; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.h index 3ed1a2a7aefc..3f64c6e8cb7a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphComponentDescriptor.h @@ -7,41 +7,16 @@ #pragma once +#include #include -#include -#include -#include namespace facebook::react { - -extern const char TextLayoutManagerKey[]; - /* * Descriptor for component. */ -class ParagraphComponentDescriptor final : public ConcreteComponentDescriptor { +class ParagraphComponentDescriptor final : public BaseParagraphComponentDescriptor { public: - explicit ParagraphComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters), - textLayoutManager_(getManagerByName(contextContainer_, TextLayoutManagerKey)) - { - } - - protected: - void adopt(ShadowNode &shadowNode) const override - { - ConcreteComponentDescriptor::adopt(shadowNode); - - auto ¶graphShadowNode = static_cast(shadowNode); - - // `ParagraphShadowNode` uses `TextLayoutManager` to measure text content - // and communicate text rendering metrics to mounting layer. - paragraphShadowNode.setTextLayoutManager(textLayoutManager_); - } - - private: - // Every `ParagraphShadowNode` has a reference to a shared `TextLayoutManager` - const std::shared_ptr textLayoutManager_; + using BaseParagraphComponentDescriptor::BaseParagraphComponentDescriptor; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index e8510a7705df..704088cbaae6 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -26,7 +26,7 @@ extern const char ParagraphComponentName[]; * containing and displaying text. Text content is represented as nested * and components. */ -class ParagraphShadowNode final +class ParagraphShadowNode : public ConcreteViewShadowNode, public BaseTextShadowNode { public: From b0f59f64479c6060606feff7f44b89c56a80b1c2 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 25 Feb 2026 19:57:42 -0800 Subject: [PATCH 2/4] Add SelectableParagraph component and SelectableTextViewManager Summary: Add the native components needed to route selectable text through ReactTextView instead of PreparedLayoutTextView when enablePreparedTextLayout is on. C++ side: Add SelectableParagraphShadowNode (inherits ParagraphShadowNode) and SelectableParagraphComponentDescriptor (inherits BaseParagraphComponentDescriptor). Register in CoreComponentsRegistry and componentNameByReactViewName. Android side: Make ReactTextViewManager open so it can be subclassed. Add getReactTextUpdateFromPreparedLayout to handle ReferenceStateWrapper holding PreparedLayout. Create SelectableTextViewManager (extends ReactTextViewManager, registered as RCTSelectableText). Add FabricNameComponentMapping entry for SelectableParagraph -> RCTSelectableText. No JS code references RCTSelectableText yet, so the new components are inert. Changelog: [Internal] Differential Revision: D93829400 --- .../ReactAndroid/api/ReactAndroid.api | 10 ++++- .../mountitems/FabricNameComponentMapping.kt | 1 + .../react/views/text/ReactTextViewManager.kt | 37 ++++++++++++++++++- .../views/text/SelectableTextViewManager.kt | 29 +++++++++++++++ .../react/fabric/CoreComponentsRegistry.cpp | 4 ++ .../componentNameByReactViewName.cpp | 3 ++ .../SelectableParagraphComponentDescriptor.h | 24 ++++++++++++ .../text/SelectableParagraphShadowNode.h | 32 ++++++++++++++++ 8 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SelectableTextViewManager.kt create mode 100644 packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphComponentDescriptor.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphShadowNode.h diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index b7c122d46066..e792e75d9af0 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6110,7 +6110,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi public fun updateView ()V } -public final class com/facebook/react/views/text/ReactTextViewManager : com/facebook/react/uimanager/BaseViewManager, com/facebook/react/uimanager/IViewManagerWithChildren, com/facebook/react/views/text/ReactTextViewManagerCallback { +public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/react/uimanager/BaseViewManager, com/facebook/react/uimanager/IViewManagerWithChildren, com/facebook/react/views/text/ReactTextViewManagerCallback { public static final field Companion Lcom/facebook/react/views/text/ReactTextViewManager$Companion; public static final field REACT_CLASS Ljava/lang/String; public fun ()V @@ -6123,11 +6123,14 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/text/ReactTextView; public fun getExportedCustomDirectEventTypeConstants ()Ljava/util/Map; public fun getName ()Ljava/lang/String; + protected final fun getReactTextViewManagerCallback ()Lcom/facebook/react/views/text/ReactTextViewManagerCallback; public fun getShadowNodeClass ()Ljava/lang/Class; public fun needsCustomLayoutForChildren ()Z public synthetic fun onAfterUpdateTransaction (Landroid/view/View;)V + protected fun onAfterUpdateTransaction (Lcom/facebook/react/views/text/ReactTextView;)V public fun onPostProcessSpannable (Landroid/text/Spannable;)V public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View; + protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/text/ReactTextView;)Lcom/facebook/react/views/text/ReactTextView; public final fun setAccessible (Lcom/facebook/react/views/text/ReactTextView;Z)V public final fun setAdjustFontSizeToFit (Lcom/facebook/react/views/text/ReactTextView;Z)V public final fun setAndroidHyphenationFrequency (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V @@ -6145,6 +6148,7 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face public final fun setOverflow (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V public synthetic fun setPadding (Landroid/view/View;IIII)V public fun setPadding (Lcom/facebook/react/views/text/ReactTextView;IIII)V + protected final fun setReactTextViewManagerCallback (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V public final fun setSelectable (Lcom/facebook/react/views/text/ReactTextView;Z)V public final fun setSelectionColor (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/Integer;)V public final fun setTextAlignVertical (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V @@ -6153,6 +6157,7 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face public synthetic fun updateState (Landroid/view/View;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object; public fun updateState (Lcom/facebook/react/views/text/ReactTextView;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object; public synthetic fun updateViewAccessibility (Landroid/view/View;)V + protected fun updateViewAccessibility (Lcom/facebook/react/views/text/ReactTextView;)V } public final class com/facebook/react/views/text/ReactTextViewManager$Companion { @@ -6170,6 +6175,9 @@ public final class com/facebook/react/views/text/ReactTypefaceUtils { public static final fun parseFontWeight (Ljava/lang/String;)I } +public final class com/facebook/react/views/text/SelectableTextViewManager$Companion { +} + public final class com/facebook/react/views/text/TextAttributeProps { public static final field Companion Lcom/facebook/react/views/text/TextAttributeProps$Companion; public static final field TA_KEY_ACCESSIBILITY_ROLE I diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/FabricNameComponentMapping.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/FabricNameComponentMapping.kt index c032448a7a33..31d93c32b695 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/FabricNameComponentMapping.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/FabricNameComponentMapping.kt @@ -18,6 +18,7 @@ internal object FabricNameComponentMapping { "Slider" to "RCTSlider", "ModalHostView" to "RCTModalHostView", "Paragraph" to "RCTText", + "SelectableParagraph" to "RCTSelectableText", "Text" to "RCTText", "RawText" to "RCTRawText", "ActivityIndicatorView" to "AndroidProgressBar", diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index 2e3669c113d7..e616cf5f6e75 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -12,6 +12,7 @@ package com.facebook.react.views.text import android.os.Build import android.text.Layout import android.text.Spannable +import android.text.SpannableString import android.text.Spanned import android.text.TextUtils import android.text.util.Linkify @@ -31,6 +32,7 @@ import com.facebook.react.uimanager.LayoutShadowNode import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ReferenceStateWrapper import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewDefaults @@ -46,7 +48,7 @@ import java.util.HashMap /** View manager for `` nodes. */ @ReactModule(name = ReactTextViewManager.REACT_CLASS) @OptIn(UnstableReactNativeAPI::class) -public class ReactTextViewManager +public open class ReactTextViewManager @JvmOverloads public constructor( protected var reactTextViewManagerCallback: ReactTextViewManagerCallback? = null @@ -131,6 +133,11 @@ public constructor( stateWrapper: StateWrapper, ): Any? { SystraceSection("ReactTextViewManager.updateState").use { s -> + val refState = (stateWrapper as? ReferenceStateWrapper)?.stateDataReference + if (refState is PreparedLayout) { + return getReactTextUpdateFromPreparedLayout(view, refState) + } + val stateMapBuffer = stateWrapper.stateDataMapBuffer return if (stateMapBuffer != null) { getReactTextUpdate(view, props, stateMapBuffer) @@ -176,6 +183,34 @@ public constructor( ) } + /** + * Constructs a [ReactTextUpdate] from a [PreparedLayout] received via [ReferenceStateWrapper]. + */ + private fun getReactTextUpdateFromPreparedLayout( + view: ReactTextView, + preparedLayout: PreparedLayout, + ): ReactTextUpdate { + val layout = preparedLayout.layout + val text = layout.text + val spanned = if (text is Spannable) text else SpannableString(text) + view.setSpanned(spanned) + + val textAlign = + when (layout.alignment) { + Layout.Alignment.ALIGN_CENTER -> Gravity.CENTER_HORIZONTAL + Layout.Alignment.ALIGN_OPPOSITE -> Gravity.END + else -> Gravity.START + } + + return ReactTextUpdate( + spanned, + -1, + textAlign, + Layout.BREAK_STRATEGY_HIGH_QUALITY, + 0, + ) + } + override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { val baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants() val eventTypeConstants = baseEventTypeConstants ?: HashMap() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SelectableTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SelectableTextViewManager.kt new file mode 100644 index 000000000000..b1f15c461903 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/SelectableTextViewManager.kt @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package com.facebook.react.views.text + +import com.facebook.react.common.annotations.UnstableReactNativeAPI + +/** + * A [ReactTextViewManager] registered under the name "RCTSelectableText". Used to route selectable + * text through [ReactTextView] (a real [android.widget.TextView]) instead of + * [PreparedLayoutTextView] when enablePreparedTextLayout is on, since [PreparedLayoutTextView] does + * not support native text selection. + */ +@UnstableReactNativeAPI +public class SelectableTextViewManager +@JvmOverloads +public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null) : + ReactTextViewManager(reactTextViewManagerCallback) { + + override fun getName(): String = REACT_CLASS + + public companion object { + public const val REACT_CLASS: String = "RCTSelectableText" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp index 03d455ee92f4..3b15aa501986 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +72,9 @@ void addCoreComponents( AndroidHorizontalScrollContentViewComponentDescriptor>()); providerRegistry->add( concreteComponentDescriptorProvider()); + providerRegistry->add( + concreteComponentDescriptorProvider< + SelectableParagraphComponentDescriptor>()); providerRegistry->add( concreteComponentDescriptorProvider< AndroidDrawerLayoutComponentDescriptor>()); diff --git a/packages/react-native/ReactCommon/react/renderer/componentregistry/componentNameByReactViewName.cpp b/packages/react-native/ReactCommon/react/renderer/componentregistry/componentNameByReactViewName.cpp index 25e3b2cb1072..fe63407de827 100644 --- a/packages/react-native/ReactCommon/react/renderer/componentregistry/componentNameByReactViewName.cpp +++ b/packages/react-native/ReactCommon/react/renderer/componentregistry/componentNameByReactViewName.cpp @@ -27,6 +27,9 @@ std::string componentNameByReactViewName(std::string viewName) { if (viewName == "Text") { return "Paragraph"; } + if (viewName == "SelectableText") { + return "SelectableParagraph"; + } if (viewName == "VirtualText") { return "Text"; diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphComponentDescriptor.h new file mode 100644 index 000000000000..b008580f5885 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphComponentDescriptor.h @@ -0,0 +1,24 @@ +/* + * 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 +#include + +namespace facebook::react { +/* + * Descriptor for component, which may render to a + * different native view than . + */ +class SelectableParagraphComponentDescriptor final + : public BaseParagraphComponentDescriptor { + public: + using BaseParagraphComponentDescriptor::BaseParagraphComponentDescriptor; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphShadowNode.h new file mode 100644 index 000000000000..8574dd601abd --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/SelectableParagraphShadowNode.h @@ -0,0 +1,32 @@ +/* + * 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 + +namespace facebook::react { + +/* + * ShadowNode for selectable Paragraph components, which may map to different native component than Paragraph. + */ +class SelectableParagraphShadowNode : public ParagraphShadowNode { + public: + using ParagraphShadowNode::ParagraphShadowNode; + + static constexpr ComponentName Name() + { + return "SelectableParagraph"; + } + + static ComponentHandle Handle() + { + return ComponentHandle(Name()); + } +}; + +} // namespace facebook::react From 2c70be3e666cf382ffcedc68ba408ce9c9a7f3d8 Mon Sep 17 00:00:00 2001 From: ngerlem <> Date: Wed, 25 Feb 2026 19:57:42 -0800 Subject: [PATCH 3/4] Intermediate commit hash for 1771849990 Differential Revision: D93829403 --- .../main/java/com/facebook/react/shell/MainReactPackage.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt index 9927bf571536..b3baad46a9aa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt @@ -15,6 +15,7 @@ import com.facebook.react.bridge.ModuleSpec import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.common.ClassFinder +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.module.annotations.ReactModule import com.facebook.react.module.annotations.ReactModuleList @@ -58,6 +59,7 @@ import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager import com.facebook.react.views.switchview.ReactSwitchManager import com.facebook.react.views.text.PreparedLayoutTextViewManager import com.facebook.react.views.text.ReactTextViewManager +import com.facebook.react.views.text.SelectableTextViewManager import com.facebook.react.views.textinput.ReactTextInputManager import com.facebook.react.views.unimplementedview.ReactUnimplementedViewManager import com.facebook.react.views.view.ReactViewManager @@ -96,6 +98,7 @@ import com.facebook.react.views.view.ReactViewManager WebSocketModule::class, ] ) +@OptIn(UnstableReactNativeAPI::class) public class MainReactPackage @JvmOverloads constructor(private val config: MainPackageConfig? = null) : @@ -150,6 +153,7 @@ constructor(private val config: MainPackageConfig? = null) : ReactTextInputManager(), if (ReactNativeFeatureFlags.enablePreparedTextLayout()) PreparedLayoutTextViewManager() else ReactTextViewManager(), + SelectableTextViewManager(), ReactViewManager(), ReactUnimplementedViewManager(), ) @@ -192,6 +196,8 @@ constructor(private val config: MainPackageConfig? = null) : PreparedLayoutTextViewManager() else ReactTextViewManager() }, + SelectableTextViewManager.REACT_CLASS to + ModuleSpec.viewManagerSpec { SelectableTextViewManager() }, ReactViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactViewManager() }, ReactUnimplementedViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactUnimplementedViewManager() }, From 0b57585c90ab9e37e7e48e0a4a8ca56ba756ede5 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Wed, 25 Feb 2026 20:02:31 -0800 Subject: [PATCH 4/4] Route selectable text through NativeSelectableText (#55678) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55678 Wire up the JS side to use the new SelectableTextViewManager native component. When enablePreparedTextLayout() is on, NativeSelectableText resolves to RCTSelectableText, routing selectable text through ReactTextView instead of PreparedLayoutTextView. When the flag is off, NativeSelectableText falls back to NativeText, so behavior is identical to before. Refactor NativePressableText and NativePressableVirtualText from arrow function component expressions to component declarations (PressableText and PressableVirtualText). PressableText now accepts a selectable prop to choose between NativeSelectableText and NativeText. Add a check(!isSelectable) guard in PreparedLayoutTextViewManager.setSelectable since selectable text now routes to SelectableTextViewManager instead. Changelog: [Internal] Reviewed By: javache Differential Revision: D93829401 --- packages/react-native/Libraries/Text/Text.js | 69 +++++++++---------- .../Libraries/Text/TextNativeComponent.js | 19 +++-- .../text/PreparedLayoutTextViewManager.kt | 5 +- packages/react-native/ReactNativeApi.d.ts | 10 +-- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text.js b/packages/react-native/Libraries/Text/Text.js index 56c9465aa379..5a1e760da1b2 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -22,14 +22,18 @@ import processColor from '../StyleSheet/processColor'; import StyleSheet from '../StyleSheet/StyleSheet'; import Platform from '../Utilities/Platform'; import TextAncestorContext from './TextAncestorContext'; -import {NativeText, NativeVirtualText} from './TextNativeComponent'; +import { + NativeSelectableText, + NativeText, + NativeVirtualText, +} from './TextNativeComponent'; import * as React from 'react'; import {useContext, useMemo, useState} from 'react'; export type {TextProps} from './TextProps'; type TextForwardRef = React.ElementRef< - typeof NativeText | typeof NativeVirtualText, + typeof NativeText | typeof NativeVirtualText | typeof NativeSelectableText, >; /** @@ -269,7 +273,7 @@ const TextImpl: component( processedProps.children = children; if (isPressable) { return ( - ); } else { - nativeText = ; + nativeText = + _selectable === true ? ( + + ) : ( + + ); } if (children == null) { @@ -463,28 +473,17 @@ function useTextPressability({ ); } -type NativePressableTextProps = Readonly<{ - textProps: NativeTextProps, - textPressabilityProps: TextPressabilityProps, -}>; - /** * Wrap the NativeVirtualText component and initialize pressability. * * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ -const NativePressableVirtualText: component( - ref: React.RefSetter, - ...props: NativePressableTextProps -) = ({ - ref: forwardedRef, - textProps, - textPressabilityProps, -}: { +component PressableVirtualText( ref?: React.RefSetter, - ...NativePressableTextProps, -}) => { + textProps: NativeTextProps, + textPressabilityProps: TextPressabilityProps, +) { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); @@ -495,42 +494,40 @@ const NativePressableVirtualText: component( {...eventHandlersForText} isHighlighted={isHighlighted} isPressable={true} - ref={forwardedRef} + ref={ref} /> ); -}; +} /** - * Wrap the NativeText component and initialize pressability. + * Wrap a NativeText component and initialize pressability. * * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ -const NativePressableText: component( - ref: React.RefSetter, - ...props: NativePressableTextProps -) = ({ - ref: forwardedRef, - textProps, - textPressabilityProps, -}: { +component PressableText( ref?: React.RefSetter, - ...NativePressableTextProps, -}) => { + selectable?: ?boolean, + textProps: NativeTextProps, + textPressabilityProps: TextPressabilityProps, +) { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); + const NativeComponent = + selectable === true ? NativeSelectableText : NativeText; + return ( - ); -}; +} const userSelectToSelectableMap = { auto: true, diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 020d5ceea8b6..55fc55e1560c 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -13,6 +13,7 @@ import type {ProcessedColorValue} from '../StyleSheet/processColor'; import type {GestureResponderEvent} from '../Types/CoreEventTypes'; import type {TextProps} from './TextProps'; +import {enablePreparedTextLayout} from '../../src/private/featureflags/ReactNativeFeatureFlags'; import {createViewConfig} from '../NativeComponent/ViewConfig'; import UIManager from '../ReactNative/UIManager'; import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; @@ -54,7 +55,7 @@ const textViewConfig = { }, }, uiViewClassName: 'RCTText', -}; +} as const; const virtualTextViewConfig = { validAttributes: { @@ -63,7 +64,7 @@ const virtualTextViewConfig = { maxFontSizeMultiplier: true, }, uiViewClassName: 'RCTVirtualText', -}; +} as const; /** * `NativeText` is an internal React Native host component, and is exported to @@ -77,8 +78,6 @@ const virtualTextViewConfig = { // and wrappers so that we no longer have any reason to export these APIs. export const NativeText: HostComponent = (createReactNativeComponentClass('RCTText', () => - /* $FlowFixMe[incompatible-type] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */ createViewConfig(textViewConfig), ): any); @@ -86,7 +85,15 @@ export const NativeVirtualText: HostComponent = !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') ? NativeText : (createReactNativeComponentClass('RCTVirtualText', () => - /* $FlowFixMe[incompatible-type] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */ createViewConfig(virtualTextViewConfig), ): any); + +export const NativeSelectableText: HostComponent = + enablePreparedTextLayout() + ? (createReactNativeComponentClass('RCTSelectableText', () => + createViewConfig({ + ...textViewConfig, + uiViewClassName: 'RCTSelectableText', + }), + ): any) + : NativeText; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt index 16e9eea42aa0..fe8bd56e6236 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt @@ -118,8 +118,9 @@ internal class PreparedLayoutTextViewManager : @ReactProp(name = "selectable", defaultBoolean = false) fun setSelectable(view: PreparedLayoutTextView, isSelectable: Boolean): Unit { - // T222052152: Implement fine-grained text selection for PreparedLayoutTextView - // view.setTextIsSelectable(isSelectable); + check(!isSelectable) { + "selectable Text should use SelectableTextViewManager instead of PreparedLayoutViewManager" + } } @ReactProp(name = "selectionColor", customType = "Color") diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 503687d9a78e..073c1eefdbf8 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<70e87b8481aeab750e8c747d9d4d28b1>> + * @generated SignedSource<<4d9de6bc6082c3ea5b39b8a5b685ac68>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -321,6 +321,7 @@ declare const NativeModules: typeof NativeModules_default declare let NativeModules_default: { [moduleName: string]: any } +declare const NativeSelectableText: HostComponent declare const NativeText: HostComponent declare const NativeTouchable: | typeof TouchableNativeFeedback @@ -3366,6 +3367,7 @@ declare type NativeScrollVelocity = { readonly x: number readonly y: number } +declare type NativeSelectableText = typeof NativeSelectableText declare type NativeSwitchChangeEvent = { readonly target: Int32 readonly value: boolean @@ -5157,7 +5159,7 @@ declare type TextContentType = | "URL" | "username" declare type TextForwardRef = React.ComponentRef< - typeof NativeText | typeof NativeVirtualText + typeof NativeSelectableText | typeof NativeText | typeof NativeVirtualText > declare type TextInput = typeof TextInput declare type TextInputAndroidProps = { @@ -5992,7 +5994,7 @@ export { AlertOptions, // a0cdac0f AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // ed7eb912 + Animated, // f39d3c6f AppConfig, // ebddad4b AppRegistry, // 6cdee1d6 AppState, // 12012be5 @@ -6212,7 +6214,7 @@ export { TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // e55ac2e2 + Text, // 0620c789 TextContentType, // 239b3ecc TextInput, // 2e89b91d TextInputAndroidProps, // 3f09ce49