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
This commit is contained in:
Nick Gerleman
2025-05-27 17:23:36 -07:00
committed by Facebook GitHub Bot
parent 83226431c2
commit 6e8ce60bd4
3 changed files with 35 additions and 16 deletions
@@ -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<ClickableSpan> = 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
}
}
}
@@ -62,7 +62,7 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> {
fontSize: this.state.fontSize,
};
return (
<View>
<View testID="text-with-toggle-attributes">
<RNTesterText style={curStyle}>
Tap the controls below to change attributes.
</RNTesterText>
@@ -75,11 +75,12 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> {
</RNTesterText>
</RNTesterText>
<RNTesterText>
<RNTesterText onPress={this.toggleWeight}>Toggle Weight</RNTesterText>
{' (with highlight onPress)'}
<RNTesterText onPress={this.toggleWeight} testID="toggle-weight">
Toggle Weight
</RNTesterText>
</RNTesterText>
<RNTesterText onPress={this.increaseSize} suppressHighlighting={true}>
Increase Size (suppressHighlighting true)
<RNTesterText onPress={this.increaseSize} testID="increase-size">
Increase Size
</RNTesterText>
</View>
);
@@ -1478,9 +1479,7 @@ const examples = [
{
title: 'Toggling Attributes',
name: 'togglingAttributes',
render(): React.Node {
return <AttributeToggler />;
},
render: AttributeToggler,
},
{
title: 'backgroundColor attribute',
@@ -117,7 +117,7 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> {
fontSize: this.state.fontSize,
};
return (
<View>
<View testID="text-with-toggle-attributes">
{/* $FlowFixMe[incompatible-type] */}
<Text style={curStyle}>
Tap the controls below to change attributes.
@@ -130,12 +130,14 @@ class AttributeToggler extends React.Component<{...}, $FlowFixMeState> {
</Text>
<Text
style={{backgroundColor: '#ffaaaa', marginTop: 5}}
onPress={this.toggleWeight}>
onPress={this.toggleWeight}
testID="toggle-weight">
Toggle Weight
</Text>
<Text
style={{backgroundColor: '#aaaaff', marginTop: 5}}
onPress={this.increaseSize}>
onPress={this.increaseSize}
testID="increase-size">
Increase Size
</Text>
</View>
@@ -1064,9 +1066,8 @@ const examples = [
},
{
title: 'Toggling Attributes',
render: function (): React.MixedElement {
return <AttributeToggler />;
},
name: 'togglingAttributes',
render: AttributeToggler,
},
{
title: 'backgroundColor attribute',