Skip to content

Commit b847be8

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Allow text links to be navigatable via keyboard by default v2 (#49381)
Summary: A much improved version of my previous attempt in D68306316 (#48773). Instead of LinkMovementMethod which makes TextViews scrollable if they overflow, this implementation uses `ExploreByTouchHelper`'s `onVirtualViewKeyboardFocusChanged` and `onPerformActionForVirtualView` to handle focus changes and clicks on virtual views (aka spans in our case). This impl will correctly ellipsize text and allow tab to nav through the links. Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D69551206
1 parent eed7025 commit b847be8

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6869,13 +6869,15 @@ public final class com/facebook/react/views/text/ReactTextUpdate$Companion {
68696869
public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/widget/AppCompatTextView, com/facebook/react/uimanager/ReactCompoundView {
68706870
public fun <init> (Landroid/content/Context;)V
68716871
protected fun dispatchHoverEvent (Landroid/view/MotionEvent;)Z
6872+
public fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z
68726873
public fun getSpanned ()Landroid/text/Spannable;
68736874
public fun hasOverlappingRendering ()Z
68746875
public fun invalidateDrawable (Landroid/graphics/drawable/Drawable;)V
68756876
public fun onAttachedToWindow ()V
68766877
public fun onDetachedFromWindow ()V
68776878
protected fun onDraw (Landroid/graphics/Canvas;)V
68786879
public fun onFinishTemporaryDetach ()V
6880+
public final fun onFocusChanged (ZILandroid/graphics/Rect;)V
68796881
protected fun onLayout (ZIIII)V
68806882
protected fun onMeasure (II)V
68816883
public fun onStartTemporaryDetach ()V

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import android.content.Context;
1111
import android.graphics.Canvas;
12+
import android.graphics.Rect;
1213
import android.graphics.drawable.Drawable;
1314
import android.os.Build;
1415
import android.text.Layout;
@@ -19,6 +20,7 @@
1920
import android.text.util.Linkify;
2021
import android.util.TypedValue;
2122
import android.view.Gravity;
23+
import android.view.KeyEvent;
2224
import android.view.MotionEvent;
2325
import android.view.View;
2426
import android.view.ViewGroup;
@@ -756,6 +758,30 @@ protected boolean dispatchHoverEvent(MotionEvent event) {
756758
return super.dispatchHoverEvent(event);
757759
}
758760

761+
@Override
762+
public final void onFocusChanged(
763+
boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
764+
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
765+
AccessibilityDelegateCompat accessibilityDelegateCompat =
766+
ViewCompat.getAccessibilityDelegate(this);
767+
if (accessibilityDelegateCompat != null
768+
&& accessibilityDelegateCompat instanceof ReactTextViewAccessibilityDelegate) {
769+
((ReactTextViewAccessibilityDelegate) accessibilityDelegateCompat)
770+
.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
771+
}
772+
}
773+
774+
@Override
775+
public boolean dispatchKeyEvent(KeyEvent event) {
776+
AccessibilityDelegateCompat accessibilityDelegateCompat =
777+
ViewCompat.getAccessibilityDelegate(this);
778+
return (accessibilityDelegateCompat != null
779+
&& accessibilityDelegateCompat instanceof ReactTextViewAccessibilityDelegate
780+
&& ((ReactTextViewAccessibilityDelegate) accessibilityDelegateCompat)
781+
.dispatchKeyEvent(event))
782+
|| super.dispatchKeyEvent(event);
783+
}
784+
759785
private void applyTextAttributes() {
760786
// Workaround for an issue where text can be cut off with an ellipsis when
761787
// using certain font sizes and padding. Sets the provided text size and

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.facebook.react.views.text
99

1010
import android.graphics.Paint
1111
import android.graphics.Rect
12+
import android.os.Bundle
1213
import android.text.Spannable
1314
import android.text.Spanned
1415
import android.text.style.AbsoluteSizeSpan
@@ -20,6 +21,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
2021
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
2122
import com.facebook.react.R
2223
import com.facebook.react.uimanager.ReactAccessibilityDelegate
24+
import com.facebook.react.views.text.internal.span.ReactClickableSpan
2325
import kotlin.math.ceil
2426

2527
internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
@@ -68,6 +70,49 @@ internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
6870
}
6971
}
7072

73+
override fun onVirtualViewKeyboardFocusChanged(virtualViewId: Int, hasFocus: Boolean) {
74+
if (accessibilityLinks == null) {
75+
return
76+
}
77+
78+
val link = accessibilityLinks?.getLinkById(virtualViewId) ?: return
79+
80+
val span = getFirstSpan(link.start, link.end, ClickableSpan::class.java)
81+
if (span == null || span !is ReactClickableSpan || hostView !is ReactTextView) {
82+
return
83+
}
84+
85+
// TODO: When we refactor ReactTextView, implement this using
86+
// https://developer.android.com/reference/android/text/Layout
87+
span.isKeyboardFocused = hasFocus
88+
span.focusBgColor = (hostView as TextView).highlightColor
89+
hostView.invalidate()
90+
}
91+
92+
override fun onPerformActionForVirtualView(
93+
virtualViewId: Int,
94+
action: Int,
95+
arguments: Bundle?
96+
): Boolean {
97+
if (accessibilityLinks == null) {
98+
return false
99+
}
100+
101+
val link = accessibilityLinks?.getLinkById(virtualViewId) ?: return false
102+
103+
val span = getFirstSpan(link.start, link.end, ClickableSpan::class.java)
104+
if (span == null || span !is ReactClickableSpan) {
105+
return false
106+
}
107+
108+
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
109+
span.onClick(hostView)
110+
return true
111+
}
112+
113+
return false
114+
}
115+
71116
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int?>) {
72117
val accessibilityLinks = accessibilityLinks ?: return
73118

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.facebook.react.views.text.internal.span
99

10+
import android.graphics.Color
1011
import android.text.TextPaint
1112
import android.text.style.ClickableSpan
1213
import android.view.View
@@ -35,6 +36,9 @@ import com.facebook.react.views.view.ViewGroupClickEvent
3536
* menu).
3637
*/
3738
public class ReactClickableSpan(public val reactTag: Int) : ClickableSpan(), ReactSpan {
39+
public var isKeyboardFocused: Boolean = false
40+
public var focusBgColor: Int = Color.TRANSPARENT
41+
3842
public override fun onClick(view: View) {
3943
val context = view.context as ReactContext
4044
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, reactTag)
@@ -43,7 +47,10 @@ public class ReactClickableSpan(public val reactTag: Int) : ClickableSpan(), Rea
4347
}
4448

4549
public override fun updateDrawState(ds: TextPaint) {
46-
// no-op to make sure we don't change the link color or add an underline by default, as the
50+
// no super call so we don't change the link color or add an underline by default, as the
4751
// superclass does.
52+
if (isKeyboardFocused) {
53+
ds.bgColor = focusBgColor
54+
}
4855
}
4956
}

0 commit comments

Comments
 (0)