Skip to content

Commit 48ce5e5

Browse files
NickGerlemanmeta-codesync[bot]
authored andcommitted
Initial support for selectable text with enablePreparedTextLayout (#55552)
Summary: Pull Request resolved: #55552 enablePreparedTextLayout replaces ReactTextView (a real TextView) with PreparedLayoutTextView (a ViewGroup that draws a pre-computed Layout). PreparedLayoutTextView does not support native text selection, so selectable text was broken when the flag was on (T222052152). This diff adds support for selectable text by routing it through ReactTextView when enablePreparedTextLayout is enabled. A new JS component NativeSelectableText resolves to native name RCTSelectableText when the flag is on, or falls back to RCTText when it is off. Text.js uses NativeSelectableText whenever text is selectable, and a new SelectableTextViewManager (which extends ReactTextViewManager) is registered as RCTSelectableText in all ReactPackage sites. ReactTextViewManager.updateState() is also updated to handle ReferenceStateWrapper holding PreparedLayout, so that it can process state delivered through the PreparedLayout path. Note that this change relies on facebook/react#35780 to avoid warnings from React Changelog: [General][Changed] - Text Can Conditionally Use "RCTSelectableText" Native Component Reviewed By: mdvacca Differential Revision: D92928315
1 parent b92d378 commit 48ce5e5

File tree

17 files changed

+252
-87
lines changed

17 files changed

+252
-87
lines changed

packages/react-native/Libraries/Text/Text.js

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ import flattenStyle from '../StyleSheet/flattenStyle';
2020
import processColor from '../StyleSheet/processColor';
2121
import Platform from '../Utilities/Platform';
2222
import TextAncestorContext from './TextAncestorContext';
23-
import {NativeText, NativeVirtualText} from './TextNativeComponent';
23+
import {
24+
NativeSelectableText,
25+
NativeText,
26+
NativeVirtualText,
27+
} from './TextNativeComponent';
2428
import * as React from 'react';
2529
import {useContext, useMemo, useState} from 'react';
2630

2731
export type {TextProps} from './TextProps';
2832

2933
type TextForwardRef = React.ElementRef<
30-
typeof NativeText | typeof NativeVirtualText,
34+
typeof NativeText | typeof NativeVirtualText | typeof NativeSelectableText,
3135
>;
3236

3337
/**
@@ -263,7 +267,7 @@ const TextImpl: component(
263267
processedProps.children = children;
264268
if (isPressable) {
265269
return (
266-
<NativePressableVirtualText
270+
<PressableVirtualText
267271
ref={forwardedRef}
268272
textProps={processedProps}
269273
textPressabilityProps={textPressabilityProps ?? {}}
@@ -283,14 +287,20 @@ const TextImpl: component(
283287

284288
if (isPressable) {
285289
nativeText = (
286-
<NativePressableText
290+
<PressableText
287291
ref={forwardedRef}
292+
selectable={_selectable}
288293
textProps={processedProps}
289294
textPressabilityProps={textPressabilityProps ?? {}}
290295
/>
291296
);
292297
} else {
293-
nativeText = <NativeText {...processedProps} ref={forwardedRef} />;
298+
nativeText =
299+
_selectable === true ? (
300+
<NativeSelectableText {...processedProps} ref={forwardedRef} />
301+
) : (
302+
<NativeText {...processedProps} ref={forwardedRef} />
303+
);
294304
}
295305

296306
if (children == null) {
@@ -457,28 +467,17 @@ function useTextPressability({
457467
);
458468
}
459469

460-
type NativePressableTextProps = Readonly<{
461-
textProps: NativeTextProps,
462-
textPressabilityProps: TextPressabilityProps,
463-
}>;
464-
465470
/**
466471
* Wrap the NativeVirtualText component and initialize pressability.
467472
*
468473
* This logic is split out from the main Text component to enable the more
469474
* expensive pressability logic to be only initialized when needed.
470475
*/
471-
const NativePressableVirtualText: component(
472-
ref: React.RefSetter<TextForwardRef>,
473-
...props: NativePressableTextProps
474-
) = ({
475-
ref: forwardedRef,
476-
textProps,
477-
textPressabilityProps,
478-
}: {
476+
component PressableVirtualText(
479477
ref?: React.RefSetter<TextForwardRef>,
480-
...NativePressableTextProps,
481-
}) => {
478+
textProps: NativeTextProps,
479+
textPressabilityProps: TextPressabilityProps,
480+
) {
482481
const [isHighlighted, eventHandlersForText] = useTextPressability(
483482
textPressabilityProps,
484483
);
@@ -489,42 +488,40 @@ const NativePressableVirtualText: component(
489488
{...eventHandlersForText}
490489
isHighlighted={isHighlighted}
491490
isPressable={true}
492-
ref={forwardedRef}
491+
ref={ref}
493492
/>
494493
);
495-
};
494+
}
496495

497496
/**
498-
* Wrap the NativeText component and initialize pressability.
497+
* Wrap a NativeText component and initialize pressability.
499498
*
500499
* This logic is split out from the main Text component to enable the more
501500
* expensive pressability logic to be only initialized when needed.
502501
*/
503-
const NativePressableText: component(
504-
ref: React.RefSetter<TextForwardRef>,
505-
...props: NativePressableTextProps
506-
) = ({
507-
ref: forwardedRef,
508-
textProps,
509-
textPressabilityProps,
510-
}: {
502+
component PressableText(
511503
ref?: React.RefSetter<TextForwardRef>,
512-
...NativePressableTextProps,
513-
}) => {
504+
selectable?: ?boolean,
505+
textProps: NativeTextProps,
506+
textPressabilityProps: TextPressabilityProps,
507+
) {
514508
const [isHighlighted, eventHandlersForText] = useTextPressability(
515509
textPressabilityProps,
516510
);
517511

512+
const NativeComponent =
513+
selectable === true ? NativeSelectableText : NativeText;
514+
518515
return (
519-
<NativeText
516+
<NativeComponent
520517
{...textProps}
521518
{...eventHandlersForText}
522519
isHighlighted={isHighlighted}
523520
isPressable={true}
524-
ref={forwardedRef}
521+
ref={ref}
525522
/>
526523
);
527-
};
524+
}
528525

529526
const userSelectToSelectableMap = {
530527
auto: true,

packages/react-native/Libraries/Text/TextNativeComponent.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {ProcessedColorValue} from '../StyleSheet/processColor';
1313
import type {GestureResponderEvent} from '../Types/CoreEventTypes';
1414
import type {TextProps} from './TextProps';
1515

16+
import {enablePreparedTextLayout} from '../../src/private/featureflags/ReactNativeFeatureFlags';
1617
import {createViewConfig} from '../NativeComponent/ViewConfig';
1718
import UIManager from '../ReactNative/UIManager';
1819
import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass';
@@ -90,3 +91,15 @@ export const NativeVirtualText: HostComponent<NativeTextProps> =
9091
* https://fburl.com/workplace/6291gfvu */
9192
createViewConfig(virtualTextViewConfig),
9293
): any);
94+
95+
export const NativeSelectableText: HostComponent<NativeTextProps> =
96+
enablePreparedTextLayout()
97+
? (createReactNativeComponentClass('RCTSelectableText', () =>
98+
/* $FlowFixMe[incompatible-type] Natural Inference rollout. See
99+
* https://fburl.com/workplace/6291gfvu */
100+
createViewConfig({
101+
...textViewConfig,
102+
uiViewClassName: 'RCTSelectableText',
103+
}),
104+
): any)
105+
: NativeText;

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6112,7 +6112,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
61126112
public fun updateView ()V
61136113
}
61146114

6115-
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 {
6115+
public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/react/uimanager/BaseViewManager, com/facebook/react/uimanager/IViewManagerWithChildren, com/facebook/react/views/text/ReactTextViewManagerCallback {
61166116
public static final field Companion Lcom/facebook/react/views/text/ReactTextViewManager$Companion;
61176117
public static final field REACT_CLASS Ljava/lang/String;
61186118
public fun <init> ()V
@@ -6125,11 +6125,14 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face
61256125
public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/text/ReactTextView;
61266126
public fun getExportedCustomDirectEventTypeConstants ()Ljava/util/Map;
61276127
public fun getName ()Ljava/lang/String;
6128+
protected final fun getReactTextViewManagerCallback ()Lcom/facebook/react/views/text/ReactTextViewManagerCallback;
61286129
public fun getShadowNodeClass ()Ljava/lang/Class;
61296130
public fun needsCustomLayoutForChildren ()Z
61306131
public synthetic fun onAfterUpdateTransaction (Landroid/view/View;)V
6132+
protected fun onAfterUpdateTransaction (Lcom/facebook/react/views/text/ReactTextView;)V
61316133
public fun onPostProcessSpannable (Landroid/text/Spannable;)V
61326134
public synthetic fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)Landroid/view/View;
6135+
protected fun prepareToRecycleView (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/text/ReactTextView;)Lcom/facebook/react/views/text/ReactTextView;
61336136
public final fun setAccessible (Lcom/facebook/react/views/text/ReactTextView;Z)V
61346137
public final fun setAdjustFontSizeToFit (Lcom/facebook/react/views/text/ReactTextView;Z)V
61356138
public final fun setAndroidHyphenationFrequency (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
@@ -6147,6 +6150,7 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face
61476150
public final fun setOverflow (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
61486151
public synthetic fun setPadding (Landroid/view/View;IIII)V
61496152
public fun setPadding (Lcom/facebook/react/views/text/ReactTextView;IIII)V
6153+
protected final fun setReactTextViewManagerCallback (Lcom/facebook/react/views/text/ReactTextViewManagerCallback;)V
61506154
public final fun setSelectable (Lcom/facebook/react/views/text/ReactTextView;Z)V
61516155
public final fun setSelectionColor (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/Integer;)V
61526156
public final fun setTextAlignVertical (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
@@ -6155,6 +6159,7 @@ public final class com/facebook/react/views/text/ReactTextViewManager : com/face
61556159
public synthetic fun updateState (Landroid/view/View;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object;
61566160
public fun updateState (Lcom/facebook/react/views/text/ReactTextView;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object;
61576161
public synthetic fun updateViewAccessibility (Landroid/view/View;)V
6162+
protected fun updateViewAccessibility (Lcom/facebook/react/views/text/ReactTextView;)V
61586163
}
61596164

61606165
public final class com/facebook/react/views/text/ReactTextViewManager$Companion {
@@ -6172,6 +6177,9 @@ public final class com/facebook/react/views/text/ReactTypefaceUtils {
61726177
public static final fun parseFontWeight (Ljava/lang/String;)I
61736178
}
61746179

6180+
public final class com/facebook/react/views/text/SelectableTextViewManager$Companion {
6181+
}
6182+
61756183
public final class com/facebook/react/views/text/TextAttributeProps {
61766184
public static final field Companion Lcom/facebook/react/views/text/TextAttributeProps$Companion;
61776185
public static final field TA_KEY_ACCESSIBILITY_ROLE I

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/FabricNameComponentMapping.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal object FabricNameComponentMapping {
1818
"Slider" to "RCTSlider",
1919
"ModalHostView" to "RCTModalHostView",
2020
"Paragraph" to "RCTText",
21+
"SelectableParagraph" to "RCTSelectableText",
2122
"Text" to "RCTText",
2223
"RawText" to "RCTRawText",
2324
"ActivityIndicatorView" to "AndroidProgressBar",

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.facebook.react.bridge.ModuleSpec
1515
import com.facebook.react.bridge.NativeModule
1616
import com.facebook.react.bridge.ReactApplicationContext
1717
import com.facebook.react.common.ClassFinder
18+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
1819
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1920
import com.facebook.react.module.annotations.ReactModule
2021
import com.facebook.react.module.annotations.ReactModuleList
@@ -58,6 +59,7 @@ import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager
5859
import com.facebook.react.views.switchview.ReactSwitchManager
5960
import com.facebook.react.views.text.PreparedLayoutTextViewManager
6061
import com.facebook.react.views.text.ReactTextViewManager
62+
import com.facebook.react.views.text.SelectableTextViewManager
6163
import com.facebook.react.views.textinput.ReactTextInputManager
6264
import com.facebook.react.views.unimplementedview.ReactUnimplementedViewManager
6365
import com.facebook.react.views.view.ReactViewManager
@@ -96,6 +98,7 @@ import com.facebook.react.views.view.ReactViewManager
9698
WebSocketModule::class,
9799
]
98100
)
101+
@OptIn(UnstableReactNativeAPI::class)
99102
public class MainReactPackage
100103
@JvmOverloads
101104
constructor(private val config: MainPackageConfig? = null) :
@@ -150,6 +153,7 @@ constructor(private val config: MainPackageConfig? = null) :
150153
ReactTextInputManager(),
151154
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) PreparedLayoutTextViewManager()
152155
else ReactTextViewManager(),
156+
SelectableTextViewManager(),
153157
ReactViewManager(),
154158
ReactUnimplementedViewManager(),
155159
)
@@ -192,6 +196,8 @@ constructor(private val config: MainPackageConfig? = null) :
192196
PreparedLayoutTextViewManager()
193197
else ReactTextViewManager()
194198
},
199+
SelectableTextViewManager.REACT_CLASS to
200+
ModuleSpec.viewManagerSpec { SelectableTextViewManager() },
195201
ReactViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactViewManager() },
196202
ReactUnimplementedViewManager.REACT_CLASS to
197203
ModuleSpec.viewManagerSpec { ReactUnimplementedViewManager() },

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ internal class PreparedLayoutTextViewManager :
118118

119119
@ReactProp(name = "selectable", defaultBoolean = false)
120120
fun setSelectable(view: PreparedLayoutTextView, isSelectable: Boolean): Unit {
121-
// T222052152: Implement fine-grained text selection for PreparedLayoutTextView
122-
// view.setTextIsSelectable(isSelectable);
121+
check(!isSelectable) {
122+
"selectable Text should use SelectableTextViewManager instead of PreparedLayoutViewManager"
123+
}
123124
}
124125

125126
@ReactProp(name = "selectionColor", customType = "Color")

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package com.facebook.react.views.text
1212
import android.os.Build
1313
import android.text.Layout
1414
import android.text.Spannable
15+
import android.text.SpannableString
1516
import android.text.Spanned
1617
import android.text.TextUtils
1718
import android.text.util.Linkify
@@ -31,6 +32,7 @@ import com.facebook.react.uimanager.LayoutShadowNode
3132
import com.facebook.react.uimanager.LengthPercentage
3233
import com.facebook.react.uimanager.LengthPercentageType
3334
import com.facebook.react.uimanager.ReactStylesDiffMap
35+
import com.facebook.react.uimanager.ReferenceStateWrapper
3436
import com.facebook.react.uimanager.StateWrapper
3537
import com.facebook.react.uimanager.ThemedReactContext
3638
import com.facebook.react.uimanager.ViewDefaults
@@ -46,7 +48,7 @@ import java.util.HashMap
4648
/** View manager for `<Text>` nodes. */
4749
@ReactModule(name = ReactTextViewManager.REACT_CLASS)
4850
@OptIn(UnstableReactNativeAPI::class)
49-
public class ReactTextViewManager
51+
public open class ReactTextViewManager
5052
@JvmOverloads
5153
public constructor(
5254
protected var reactTextViewManagerCallback: ReactTextViewManagerCallback? = null
@@ -131,6 +133,11 @@ public constructor(
131133
stateWrapper: StateWrapper,
132134
): Any? {
133135
SystraceSection("ReactTextViewManager.updateState").use { s ->
136+
val refState = (stateWrapper as? ReferenceStateWrapper)?.stateDataReference
137+
if (refState is PreparedLayout) {
138+
return getReactTextUpdateFromPreparedLayout(view, refState)
139+
}
140+
134141
val stateMapBuffer = stateWrapper.stateDataMapBuffer
135142
return if (stateMapBuffer != null) {
136143
getReactTextUpdate(view, props, stateMapBuffer)
@@ -176,6 +183,34 @@ public constructor(
176183
)
177184
}
178185

186+
/**
187+
* Constructs a [ReactTextUpdate] from a [PreparedLayout] received via [ReferenceStateWrapper].
188+
*/
189+
private fun getReactTextUpdateFromPreparedLayout(
190+
view: ReactTextView,
191+
preparedLayout: PreparedLayout,
192+
): ReactTextUpdate {
193+
val layout = preparedLayout.layout
194+
val text = layout.text
195+
val spanned = if (text is Spannable) text else SpannableString(text)
196+
view.setSpanned(spanned)
197+
198+
val textAlign =
199+
when (layout.alignment) {
200+
Layout.Alignment.ALIGN_CENTER -> Gravity.CENTER_HORIZONTAL
201+
Layout.Alignment.ALIGN_OPPOSITE -> Gravity.END
202+
else -> Gravity.START
203+
}
204+
205+
return ReactTextUpdate(
206+
spanned,
207+
-1,
208+
textAlign,
209+
Layout.BREAK_STRATEGY_HIGH_QUALITY,
210+
0,
211+
)
212+
}
213+
179214
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
180215
val baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants()
181216
val eventTypeConstants = baseEventTypeConstants ?: HashMap()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text
9+
10+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
11+
12+
/**
13+
* A [ReactTextViewManager] registered under the name "RCTSelectableText". Used to route selectable
14+
* text through [ReactTextView] (a real [android.widget.TextView]) instead of
15+
* [PreparedLayoutTextView] when enablePreparedTextLayout is on, since [PreparedLayoutTextView] does
16+
* not support native text selection.
17+
*/
18+
@UnstableReactNativeAPI
19+
public class SelectableTextViewManager
20+
@JvmOverloads
21+
public constructor(reactTextViewManagerCallback: ReactTextViewManagerCallback? = null) :
22+
ReactTextViewManager(reactTextViewManagerCallback) {
23+
24+
override fun getName(): String = REACT_CLASS
25+
26+
public companion object {
27+
public const val REACT_CLASS: String = "RCTSelectableText"
28+
}
29+
}

packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
2222
#include <react/renderer/components/text/ParagraphComponentDescriptor.h>
2323
#include <react/renderer/components/text/RawTextComponentDescriptor.h>
24+
#include <react/renderer/components/text/SelectableParagraphComponentDescriptor.h>
2425
#include <react/renderer/components/text/TextComponentDescriptor.h>
2526
#include <react/renderer/components/view/LayoutConformanceComponentDescriptor.h>
2627
#include <react/renderer/components/view/ViewComponentDescriptor.h>
@@ -71,6 +72,9 @@ void addCoreComponents(
7172
AndroidHorizontalScrollContentViewComponentDescriptor>());
7273
providerRegistry->add(
7374
concreteComponentDescriptorProvider<ParagraphComponentDescriptor>());
75+
providerRegistry->add(
76+
concreteComponentDescriptorProvider<
77+
SelectableParagraphComponentDescriptor>());
7478
providerRegistry->add(
7579
concreteComponentDescriptorProvider<
7680
AndroidDrawerLayoutComponentDescriptor>());

0 commit comments

Comments
 (0)