From 6e8ce60bd4e80913e7485dfe66087d0697b0e6c8 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 27 May 2025 17:23:36 -0700 Subject: [PATCH] Implement ReactCompoundView for PreparedLayoutTextView (#51551) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51551 This allows hit RN's hit testing to find nested spans, and click them. This mechanism is fully separate from the one used by a11y virtual views, and ClickableSpan, such as those added for links via dataDetectorType (and also the `link` role). When we do have a link accessibilityRole, that ClickableSpan hit test seems to prevent the React one, and we only activate the onPress once (but then add keyboard interaction, press visual, and add to the a11y tree). Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D75257326 fbshipit-source-id: 0c693f581ec121cf4b4e3e2040d141985118224f --- .../views/text/PreparedLayoutTextView.kt | 23 +++++++++++++++++-- .../js/examples/Text/TextExample.android.js | 15 ++++++------ .../js/examples/Text/TextExample.ios.js | 13 ++++++----- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index b23e2d5b190..9dc06d3111d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -24,8 +24,11 @@ import androidx.annotation.DoNotInline import androidx.annotation.RequiresApi import androidx.core.view.ViewCompat import com.facebook.react.uimanager.BackgroundStyleApplicator +import com.facebook.react.uimanager.ReactCompoundView import com.facebook.react.uimanager.style.Overflow +import com.facebook.react.views.text.internal.span.ReactTagSpan import kotlin.collections.ArrayList +import kotlin.math.roundToInt /** * A custom version of Android's TextView, providing React Native with lower-level hooks for text @@ -33,7 +36,7 @@ import kotlin.collections.ArrayList * existing layout, previously generated for measurement by Fabric, to ensure consistency of * measurements, and avoid duplicate work. */ -internal class PreparedLayoutTextView(context: Context) : ViewGroup(context) { +internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), ReactCompoundView { private var clickableSpans: List = emptyList() private var selection: TextSelection? = null @@ -143,7 +146,6 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context) { invalidate() } - // T222163602: We should reconcile this hit testing with ReactCompoundView hit testing override fun onTouchEvent(event: MotionEvent): Boolean { if (!isEnabled || clickableSpans.isEmpty()) { return super.onTouchEvent(event) @@ -322,4 +324,21 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context) { return spans } } + + override fun reactTagForTouch(touchX: Float, touchY: Float): Int { + val offset = getTextOffsetAt(touchX.roundToInt(), touchY.roundToInt()) + if (offset < 0) { + return id + } + + val spanned = text as? Spanned ?: return id + val reactSpans = spanned.getSpans(offset, offset, ReactTagSpan::class.java) + check(reactSpans.size <= 1) + + return if (reactSpans.isNotEmpty()) { + reactSpans[0].reactTag + } else { + id + } + } } diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index 070e3fd7596..271156139b7 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -62,7 +62,7 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> { fontSize: this.state.fontSize, }; return ( - + Tap the controls below to change attributes. @@ -75,11 +75,12 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> { - Toggle Weight - {' (with highlight onPress)'} + + Toggle Weight + - - Increase Size (suppressHighlighting true) + + Increase Size ); @@ -1478,9 +1479,7 @@ const examples = [ { title: 'Toggling Attributes', name: 'togglingAttributes', - render(): React.Node { - return ; - }, + render: AttributeToggler, }, { title: 'backgroundColor attribute', diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 1515727f37e..869b111297e 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -117,7 +117,7 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> { fontSize: this.state.fontSize, }; return ( - + {/* $FlowFixMe[incompatible-type] */} Tap the controls below to change attributes. @@ -130,12 +130,14 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> { + onPress={this.toggleWeight} + testID="toggle-weight"> Toggle Weight + onPress={this.increaseSize} + testID="increase-size"> Increase Size @@ -1064,9 +1066,8 @@ const examples = [ }, { title: 'Toggling Attributes', - render: function (): React.MixedElement { - return ; - }, + name: 'togglingAttributes', + render: AttributeToggler, }, { title: 'backgroundColor attribute',