Skip to content

Commit b2ccbb2

Browse files
NickGerlemanmeta-codesync[bot]
authored andcommitted
Teach ReactTextView Hit Testing/a11y about Facsimile State (Fixes hit testing/a11y for selectable Text in Facsimile) (#55559)
Summary: Pull Request resolved: #55559 When enablePreparedTextLayout is true, we may reuse layouts (including backing `Spannable`), even when ShadowNode generation changes. React tags are subject to change during this period, so we have an extra layer of indirection, that users of `PreparedLayout` based state currently have to deal with. This adds that logic so that hit testing, a11y delegate, and spans against FragmentIndex, all work in the scenario of ReactTextView with Facsimile state, used for selectable text. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D93346617 fbshipit-source-id: a9d01b1b560430396dcf23962be56d07e2dec838
1 parent 4185ff8 commit b2ccbb2

File tree

4 files changed

+66
-12
lines changed

4 files changed

+66
-12
lines changed

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

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.facebook.react.bridge.ReactContext;
3838
import com.facebook.react.bridge.WritableMap;
3939
import com.facebook.react.common.ReactConstants;
40+
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
4041
import com.facebook.react.common.build.ReactBuildConfig;
4142
import com.facebook.react.internal.SystraceSection;
4243
import com.facebook.react.uimanager.BackgroundStyleApplicator;
@@ -52,6 +53,7 @@
5253
import com.facebook.react.uimanager.style.BorderStyle;
5354
import com.facebook.react.uimanager.style.LogicalEdge;
5455
import com.facebook.react.uimanager.style.Overflow;
56+
import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan;
5557
import com.facebook.react.views.text.internal.span.ReactTagSpan;
5658
import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
5759
import com.facebook.yoga.YogaMeasureMode;
@@ -77,6 +79,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
7779
private Overflow mOverflow = Overflow.VISIBLE;
7880

7981
private @Nullable Spannable mSpanned;
82+
private @Nullable PreparedLayout mPreparedLayout;
8083

8184
public ReactTextView(Context context) {
8285
super(context);
@@ -100,6 +103,7 @@ private void initView() {
100103
mLetterSpacing = 0.f;
101104
mOverflow = Overflow.VISIBLE;
102105
mSpanned = null;
106+
mPreparedLayout = null;
103107
}
104108

105109
/* package */ void recycleView() {
@@ -448,16 +452,33 @@ public int reactTagForTouch(float touchX, float touchY) {
448452
// if no such span can be found we will send the textview's react id as a touch handler
449453
// In case when there are more than one spans with same length we choose the last one
450454
// from the spans[] array, since it correspond to the most inner react element
451-
ReactTagSpan[] spans = spannedText.getSpans(index, index, ReactTagSpan.class);
452-
453-
if (spans != null) {
454-
int targetSpanTextLength = text.length();
455-
for (int i = 0; i < spans.length; i++) {
456-
int spanStart = spannedText.getSpanStart(spans[i]);
457-
int spanEnd = spannedText.getSpanEnd(spans[i]);
458-
if (spanEnd >= index && (spanEnd - spanStart) <= targetSpanTextLength) {
459-
target = spans[i].getReactTag();
460-
targetSpanTextLength = (spanEnd - spanStart);
455+
if (mPreparedLayout != null) {
456+
ReactFragmentIndexSpan[] fragmentSpans =
457+
spannedText.getSpans(index, index, ReactFragmentIndexSpan.class);
458+
459+
if (fragmentSpans != null) {
460+
int targetSpanTextLength = text.length();
461+
for (int i = 0; i < fragmentSpans.length; i++) {
462+
int spanStart = spannedText.getSpanStart(fragmentSpans[i]);
463+
int spanEnd = spannedText.getSpanEnd(fragmentSpans[i]);
464+
if (spanEnd >= index && (spanEnd - spanStart) <= targetSpanTextLength) {
465+
target = mPreparedLayout.getReactTags()[fragmentSpans[i].getFragmentIndex()];
466+
targetSpanTextLength = (spanEnd - spanStart);
467+
}
468+
}
469+
}
470+
} else {
471+
ReactTagSpan[] spans = spannedText.getSpans(index, index, ReactTagSpan.class);
472+
473+
if (spans != null) {
474+
int targetSpanTextLength = text.length();
475+
for (int i = 0; i < spans.length; i++) {
476+
int spanStart = spannedText.getSpanStart(spans[i]);
477+
int spanEnd = spannedText.getSpanEnd(spans[i]);
478+
if (spanEnd >= index && (spanEnd - spanStart) <= targetSpanTextLength) {
479+
target = spans[i].getReactTag();
480+
targetSpanTextLength = (spanEnd - spanStart);
481+
}
461482
}
462483
}
463484
}
@@ -627,6 +648,22 @@ public void setSpanned(Spannable spanned) {
627648
return mSpanned;
628649
}
629650

651+
/**
652+
* Get the PreparedLayout originally generated by the Fabric renderer, if using {@code
653+
* enablePreparedTextLayout()}
654+
*
655+
* <p>TODO: Should be made internal when ReactTextView is converted to Kotlin
656+
*/
657+
@UnstableReactNativeAPI
658+
@Nullable
659+
public PreparedLayout getPreparedLayout() {
660+
return mPreparedLayout;
661+
}
662+
663+
/* package */ void setPreparedLayout(@Nullable PreparedLayout preparedLayout) {
664+
mPreparedLayout = preparedLayout;
665+
}
666+
630667
public void setLinkifyMask(int mask) {
631668
mLinkifyMaskType = mask;
632669
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import androidx.core.view.ViewCompat
1818
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
1919
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
2020
import com.facebook.react.R
21+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
2122
import com.facebook.react.uimanager.ReactAccessibilityDelegate
2223
import com.facebook.react.views.text.internal.span.ReactClickableSpan
2324

25+
@OptIn(UnstableReactNativeAPI::class)
2426
internal class ReactTextViewAccessibilityDelegate(
2527
view: View,
2628
originalFocus: Boolean,
@@ -154,6 +156,8 @@ internal class ReactTextViewAccessibilityDelegate(
154156
private fun getLayoutFromHost(): Layout? {
155157
return if (hostView is PreparedLayoutTextView) {
156158
(hostView as PreparedLayoutTextView).preparedLayout?.layout
159+
} else if (hostView is ReactTextView && (hostView as ReactTextView).preparedLayout != null) {
160+
(hostView as ReactTextView).preparedLayout?.layout
157161
} else if (hostView is TextView) {
158162
(hostView as TextView).layout
159163
} else {
@@ -171,6 +175,8 @@ internal class ReactTextViewAccessibilityDelegate(
171175
val host = hostView
172176
return if (host is PreparedLayoutTextView) {
173177
host.preparedLayout?.layout?.text as? Spanned
178+
} else if (host is ReactTextView && host.preparedLayout != null) {
179+
host.preparedLayout?.layout?.text as? Spanned
174180
} else if (host is TextView) {
175181
host.text as? Spanned
176182
} else {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ public constructor(
167167
paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SIZE).toFloat()
168168
view.setMinimumFontSize(minimumFontSize)
169169

170+
// Clear any stale PreparedLayout from a previous update
171+
view.setPreparedLayout(null)
172+
170173
val textBreakStrategy =
171174
TextAttributeProps.getTextBreakStrategy(
172175
paragraphAttributes.getString(TextLayoutManager.PA_KEY_TEXT_BREAK_STRATEGY)
@@ -194,6 +197,7 @@ public constructor(
194197
val text = layout.text
195198
val spanned = if (text is Spannable) text else SpannableString(text)
196199
view.setSpanned(spanned)
200+
view.setPreparedLayout(preparedLayout)
197201

198202
val textAlign =
199203
when (layout.alignment) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactLinkSpan.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import android.text.TextPaint
1111
import android.text.style.ClickableSpan
1212
import android.view.View
1313
import com.facebook.react.bridge.ReactContext
14+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
1415
import com.facebook.react.uimanager.UIManagerHelper
1516
import com.facebook.react.views.text.PreparedLayoutTextView
17+
import com.facebook.react.views.text.ReactTextView
1618
import com.facebook.react.views.text.TextLayoutManager
1719
import com.facebook.react.views.view.ViewGroupClickEvent
1820

@@ -29,11 +31,16 @@ import com.facebook.react.views.view.ViewGroupClickEvent
2931
* </Text>
3032
* ```
3133
*/
34+
@OptIn(UnstableReactNativeAPI::class)
3235
internal class ReactLinkSpan(val fragmentIndex: Int) : ClickableSpan(), ReactSpan {
3336
override fun onClick(view: View) {
3437
val context = view.context as ReactContext
35-
val textView = view as? PreparedLayoutTextView ?: return
36-
val preparedLayout = textView.preparedLayout ?: return
38+
val preparedLayout =
39+
when (view) {
40+
is PreparedLayoutTextView -> view.preparedLayout
41+
is ReactTextView -> view.preparedLayout
42+
else -> null
43+
} ?: return
3744
val reactTag = preparedLayout.reactTags[fragmentIndex]
3845
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, reactTag)
3946
eventDispatcher?.dispatchEvent(

0 commit comments

Comments
 (0)