diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 2ffaee8e118..169216cdecd 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6856,6 +6856,7 @@ public final class com/facebook/react/views/text/ReactTextUpdate$Companion { public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/widget/AppCompatTextView, com/facebook/react/uimanager/ReactCompoundView { public fun (Landroid/content/Context;)V protected fun dispatchHoverEvent (Landroid/view/MotionEvent;)Z + public fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z public fun getSpanned ()Landroid/text/Spannable; public fun hasOverlappingRendering ()Z public fun invalidateDrawable (Landroid/graphics/drawable/Drawable;)V @@ -6863,6 +6864,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi public fun onDetachedFromWindow ()V protected fun onDraw (Landroid/graphics/Canvas;)V public fun onFinishTemporaryDetach ()V + public final fun onFocusChanged (ZILandroid/graphics/Rect;)V protected fun onLayout (ZIIII)V protected fun onMeasure (II)V public fun onStartTemporaryDetach ()V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index cb735fc8563..cd2b25044bd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -9,6 +9,7 @@ package com.facebook.react.views.text; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Layout; @@ -19,6 +20,7 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.TypedValue; import android.view.Gravity; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -756,6 +758,30 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie return super.dispatchHoverEvent(event); } + @Override + public final void onFocusChanged( + boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + AccessibilityDelegateCompat accessibilityDelegateCompat = + ViewCompat.getAccessibilityDelegate(this); + if (accessibilityDelegateCompat != null + && accessibilityDelegateCompat instanceof ReactTextViewAccessibilityDelegate) { + ((ReactTextViewAccessibilityDelegate) accessibilityDelegateCompat) + .onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + AccessibilityDelegateCompat accessibilityDelegateCompat = + ViewCompat.getAccessibilityDelegate(this); + return (accessibilityDelegateCompat != null + && accessibilityDelegateCompat instanceof ReactTextViewAccessibilityDelegate + && ((ReactTextViewAccessibilityDelegate) accessibilityDelegateCompat) + .dispatchKeyEvent(event)) + || super.dispatchKeyEvent(event); + } + private void applyTextAttributes() { // Workaround for an issue where text can be cut off with an ellipsis when // using certain font sizes and padding. Sets the provided text size and diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt index 72730f0aec9..2f207e49178 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt @@ -9,6 +9,7 @@ package com.facebook.react.views.text import android.graphics.Paint import android.graphics.Rect +import android.os.Bundle import android.text.Spannable import android.text.Spanned import android.text.style.AbsoluteSizeSpan @@ -20,6 +21,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeProviderCompat import com.facebook.react.R import com.facebook.react.uimanager.ReactAccessibilityDelegate +import com.facebook.react.views.text.internal.span.ReactClickableSpan import kotlin.math.ceil internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate { @@ -68,6 +70,49 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate { } } + override fun onVirtualViewKeyboardFocusChanged(virtualViewId: Int, hasFocus: Boolean) { + if (accessibilityLinks == null) { + return + } + + val link = accessibilityLinks?.getLinkById(virtualViewId) ?: return + + val span = getFirstSpan(link.start, link.end, ClickableSpan::class.java) + if (span == null || span !is ReactClickableSpan || hostView !is ReactTextView) { + return + } + + // TODO: When we refactor ReactTextView, implement this using + // https://developer.android.com/reference/android/text/Layout + span.isKeyboardFocused = hasFocus + span.focusBgColor = (hostView as TextView).highlightColor + hostView.invalidate() + } + + override fun onPerformActionForVirtualView( + virtualViewId: Int, + action: Int, + arguments: Bundle? + ): Boolean { + if (accessibilityLinks == null) { + return false + } + + val link = accessibilityLinks?.getLinkById(virtualViewId) ?: return false + + val span = getFirstSpan(link.start, link.end, ClickableSpan::class.java) + if (span == null || span !is ReactClickableSpan) { + return false + } + + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + span.onClick(hostView) + return true + } + + return false + } + override fun getVisibleVirtualViews(virtualViewIds: MutableList) { val accessibilityLinks = accessibilityLinks ?: return diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactClickableSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactClickableSpan.kt index 21211a586a7..b3c43a04a89 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactClickableSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactClickableSpan.kt @@ -7,6 +7,7 @@ package com.facebook.react.views.text.internal.span +import android.graphics.Color import android.text.TextPaint import android.text.style.ClickableSpan import android.view.View @@ -35,6 +36,9 @@ import com.facebook.react.views.view.ViewGroupClickEvent * menu). */ public class ReactClickableSpan(public val reactTag: Int) : ClickableSpan(), ReactSpan { + public var isKeyboardFocused: Boolean = false + public var focusBgColor: Int = Color.TRANSPARENT + public override fun onClick(view: View) { val context = view.context as ReactContext val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, reactTag) @@ -43,7 +47,10 @@ public class ReactClickableSpan(public val reactTag: Int) : ClickableSpan(), Rea } public override fun updateDrawState(ds: TextPaint) { - // no-op to make sure we don't change the link color or add an underline by default, as the + // no super call so we don't change the link color or add an underline by default, as the // superclass does. + if (isKeyboardFocused) { + ds.bgColor = focusBgColor + } } }