mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Allow text links to be navigatable via keyboard by default v2 (#49381)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49381 A much improved version of my previous attempt in D68306316 (https://github.com/facebook/react-native/pull/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 fbshipit-source-id: 6a88ccd507dc5534c1f494e3303c6bfc2bae5e9f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
c9e6567881
commit
a003be0f23
@@ -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 <init> (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
|
||||
|
||||
+26
@@ -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
|
||||
|
||||
+45
@@ -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<Int?>) {
|
||||
val accessibilityLinks = accessibilityLinks ?: return
|
||||
|
||||
|
||||
+8
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user