Skip to content

Commit 6b605d8

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Allow text links to be navigatable via keyboard by default v2 (facebook#49381)
Summary: A much improved version of my previous attempt in D68306316 (facebook#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. Differential Revision: D69551206
1 parent 71f2f3b commit 6b605d8

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7023,13 +7023,15 @@ public final class com/facebook/react/views/text/ReactTextUpdate$Companion {
70237023
public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/widget/AppCompatTextView, com/facebook/react/uimanager/ReactCompoundView {
70247024
public fun <init> (Landroid/content/Context;)V
70257025
protected fun dispatchHoverEvent (Landroid/view/MotionEvent;)Z
7026+
public fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z
70267027
public fun getSpanned ()Landroid/text/Spannable;
70277028
public fun hasOverlappingRendering ()Z
70287029
public fun invalidateDrawable (Landroid/graphics/drawable/Drawable;)V
70297030
public fun onAttachedToWindow ()V
70307031
public fun onDetachedFromWindow ()V
70317032
protected fun onDraw (Landroid/graphics/Canvas;)V
70327033
public fun onFinishTemporaryDetach ()V
7034+
public final fun onFocusChanged (ZILandroid/graphics/Rect;)V
70337035
protected fun onLayout (ZIIII)V
70347036
protected fun onMeasure (II)V
70357037
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: 43 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
public class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate {
@@ -68,6 +70,47 @@ public 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 TextView) {
82+
return
83+
}
84+
85+
span.isKeyboardFocused = hasFocus
86+
span.focusBgColor = (hostView as TextView).highlightColor
87+
hostView.invalidate()
88+
}
89+
90+
override fun onPerformActionForVirtualView(
91+
virtualViewId: Int,
92+
action: Int,
93+
arguments: Bundle?
94+
): Boolean {
95+
if (accessibilityLinks == null) {
96+
return false
97+
}
98+
99+
val link = accessibilityLinks?.getLinkById(virtualViewId) ?: return false
100+
101+
val span = getFirstSpan(link.start, link.end, ClickableSpan::class.java)
102+
if (span == null || span !is ReactClickableSpan) {
103+
return false
104+
}
105+
106+
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
107+
span.onClick(hostView)
108+
return true
109+
}
110+
111+
return false
112+
}
113+
71114
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int?>) {
72115
val accessibilityLinks = accessibilityLinks ?: return
73116

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)