From d65d846ccff07bdfb26fbfb90e5fbb05c46f0d97 Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Fri, 21 Feb 2025 16:10:03 -0800 Subject: [PATCH] Move text-specific a11y logic in ReactAccessibilityDelegate to subclass (#49377) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49377 ReactAccessibilityDelegate exists to handle much of the accessibility tasks in the various Views in RN. There is quite a bit of text specific logic, mostly related to virtual views and nested links within a TextView. I decided to subclass this into a TextView-specific version because I need this delegate to reference TextView or ReactClickableSpan, which live under `react/views` while ReactAccessibilityDelegate live under `react/uimanager`. The former depends on the latter, so making the latter depend on the former would for a dependency cycle that would break builds. I thought about making a separate package for this but both `react/views` and `react/uimanager` need to include ReactAccessibilityDelegate so we would still have a cycle. mAccessibilityLinks is only set on ReactTextViewManager, so this is purely a text thing. Subclassing is not the most ideal as it extends the inheritance chain some more but I do not see a better option. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D69499115 fbshipit-source-id: 1720d20bb56ba1e1b5bd114d32bc70e80e3b4558 --- .../ReactAndroid/api/ReactAndroid.api | 13 +- .../react/uimanager/BaseViewManager.java | 2 +- .../uimanager/ReactAccessibilityDelegate.java | 242 +-------------- .../ReactTextViewAccessibilityDelegate.kt | 290 ++++++++++++++++++ .../views/text/ReactTextViewManager.java | 11 +- 5 files changed, 319 insertions(+), 239 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f613a00c7c4..2ffaee8e118 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3822,6 +3822,7 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo public fun setTranslateY (Landroid/view/View;F)V public fun setViewState (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V public fun setZIndex (Landroid/view/View;F)V + protected fun updateViewAccessibility (Landroid/view/View;)V } public abstract class com/facebook/react/uimanager/BaseViewManagerDelegate : com/facebook/react/uimanager/ViewManagerDelegate { @@ -4106,7 +4107,7 @@ public class com/facebook/react/uimanager/ReactAccessibilityDelegate : androidx/ public fun (Landroid/view/View;ZI)V public static fun createNodeInfoFromView (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat; public fun getAccessibilityNodeProvider (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat; - protected fun getFirstSpan (IILjava/lang/Class;)Ljava/lang/Object; + protected fun getHostView ()Landroid/view/View; public static fun getTalkbackDescription (Landroid/view/View;Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat;)Ljava/lang/CharSequence; protected fun getVirtualViewAt (FF)I protected fun getVisibleVirtualViews (Ljava/util/List;)V @@ -4124,13 +4125,7 @@ public class com/facebook/react/uimanager/ReactAccessibilityDelegate : androidx/ public static fun resetDelegate (Landroid/view/View;ZI)V public static fun setDelegate (Landroid/view/View;ZI)V public static fun setRole (Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat;Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;Landroid/content/Context;)V -} - -public class com/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityLinks { - public fun ([Landroid/text/style/ClickableSpan;Landroid/text/Spannable;)V - public fun getLinkById (I)Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityLinks$AccessibleLink; - public fun getLinkBySpanPos (II)Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityLinks$AccessibleLink; - public fun size ()I + public fun superGetAccessibilityNodeProvider (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat; } public final class com/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole : java/lang/Enum { @@ -6922,6 +6917,8 @@ public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/r public fun updateExtraData (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/Object;)V public synthetic fun updateState (Landroid/view/View;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object; public fun updateState (Lcom/facebook/react/views/text/ReactTextView;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object; + protected synthetic fun updateViewAccessibility (Landroid/view/View;)V + protected fun updateViewAccessibility (Lcom/facebook/react/views/text/ReactTextView;)V } public abstract interface class com/facebook/react/views/text/ReactTextViewManagerCallback { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index d55d7f3f3c9..421bb777ab2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -626,7 +626,7 @@ public abstract class BaseViewManager virtualViewIds) { - if (mAccessibilityLinks == null) { - return; - } - - for (int i = 0; i < mAccessibilityLinks.size(); i++) { - virtualViewIds.add(i); - } - } + protected void getVisibleVirtualViews(List virtualViewIds) {} @Override protected void onPopulateNodeForVirtualView( int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) { - // If we get an invalid virtualViewId for some reason (which is known to happen in API 19 and - // below), return an "empty" node to prevent from crashing. This will never be presented to - // the user, as Talkback filters out nodes with no content to announce. - if (mAccessibilityLinks == null) { - node.setContentDescription(""); - node.setBoundsInParent(new Rect(0, 0, 1, 1)); - return; - } - - final AccessibilityLinks.AccessibleLink accessibleTextSpan = - mAccessibilityLinks.getLinkById(virtualViewId); - if (accessibleTextSpan == null) { - node.setContentDescription(""); - node.setBoundsInParent(new Rect(0, 0, 1, 1)); - return; - } - - // NOTE: The span may not actually have visible bounds within its parent, - // due to line limits, etc. - final Rect bounds = getBoundsInParent(accessibleTextSpan); - if (bounds == null) { - node.setContentDescription(""); - node.setBoundsInParent(new Rect(0, 0, 1, 1)); - return; - } - - node.setContentDescription(accessibleTextSpan.description); - node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - node.setBoundsInParent(bounds); - node.setRoleDescription(mView.getResources().getString(R.string.link_description)); - node.setClassName(AccessibilityRole.getValue(AccessibilityRole.BUTTON)); - } - - private Rect getBoundsInParent(AccessibilityLinks.AccessibleLink accessibleLink) { - // This view is not a text view, so return the entire views bounds. - if (!(mView instanceof TextView)) { - return new Rect(0, 0, mView.getWidth(), mView.getHeight()); - } - - TextView textView = (TextView) mView; - Layout textViewLayout = textView.getLayout(); - if (textViewLayout == null) { - return new Rect(0, 0, textView.getWidth(), textView.getHeight()); - } - - double startOffset = accessibleLink.start; - double endOffset = accessibleLink.end; - - // Ensure the link hasn't been ellipsized away; in such cases, - // getPrimaryHorizontal will crash (and the link isn't rendered anyway). - int startOffsetLineNumber = textViewLayout.getLineForOffset((int) startOffset); - int lineEndOffset = textViewLayout.getLineEnd(startOffsetLineNumber); - if (startOffset > lineEndOffset) { - return null; - } - - Rect rootRect = new Rect(); - - double startXCoordinates = textViewLayout.getPrimaryHorizontal((int) startOffset); - - final Paint paint = new Paint(); - AbsoluteSizeSpan sizeSpan = - getFirstSpan(accessibleLink.start, accessibleLink.end, AbsoluteSizeSpan.class); - float textSize = sizeSpan != null ? sizeSpan.getSize() : textView.getTextSize(); - paint.setTextSize(textSize); - int textWidth = (int) Math.ceil(paint.measureText(accessibleLink.description)); - - int endOffsetLineNumber = textViewLayout.getLineForOffset((int) endOffset); - boolean isMultiline = startOffsetLineNumber != endOffsetLineNumber; - textViewLayout.getLineBounds(startOffsetLineNumber, rootRect); - - int verticalOffset = textView.getScrollY() + textView.getTotalPaddingTop(); - rootRect.top += verticalOffset; - rootRect.bottom += verticalOffset; - rootRect.left += startXCoordinates + textView.getTotalPaddingLeft() - textView.getScrollX(); - - // The bounds for multi-line strings should *only* include the first line. This is because for - // API 25 and below, Talkback's click is triggered at the center point of these bounds, and if - // that center point is outside the spannable, it will click on something else. There is no - // harm in not outlining the wrapped part of the string, as the text for the whole string will - // be read regardless of the bounding box. - if (isMultiline) { - return new Rect(rootRect.left, rootRect.top, rootRect.right, rootRect.bottom); - } - - return new Rect(rootRect.left, rootRect.top, rootRect.left + textWidth, rootRect.bottom); + node.setContentDescription(""); + node.setBoundsInParent(new Rect(0, 0, 1, 1)); } @Override @@ -861,97 +729,17 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { return false; } - protected @Nullable T getFirstSpan(int start, int end, Class classType) { - if (!(mView instanceof TextView) || !(((TextView) mView).getText() instanceof Spanned)) { - return null; - } - - Spanned spanned = (Spanned) ((TextView) mView).getText(); - T[] spans = spanned.getSpans(start, end, classType); - return spans.length > 0 ? spans[0] : null; - } - - public static class AccessibilityLinks { - private final List mLinks; - - public AccessibilityLinks(ClickableSpan[] spans, Spannable text) { - ArrayList links = new ArrayList<>(); - for (int i = 0; i < spans.length; i++) { - ClickableSpan span = spans[i]; - int start = text.getSpanStart(span); - int end = text.getSpanEnd(span); - // zero length spans, and out of range spans should not be included. - if (start == end || start < 0 || end < 0 || start > text.length() || end > text.length()) { - continue; - } - - final AccessibleLink link = new AccessibleLink(); - link.description = text.subSequence(start, end).toString(); - link.start = start; - link.end = end; - - // ID is the reverse of what is expected, since the ClickableSpans are returned in reverse - // order due to being added in reverse order. If we don't do this, focus will move to the - // last link first and move backwards. - // - // If this approach becomes unreliable, we should instead look at their start position and - // order them manually. - link.id = spans.length - 1 - i; - links.add(link); - } - mLinks = links; - } - - @Nullable - public AccessibleLink getLinkById(int id) { - for (AccessibleLink link : mLinks) { - if (link.id == id) { - return link; - } - } - - return null; - } - - @Nullable - public AccessibleLink getLinkBySpanPos(int start, int end) { - for (AccessibleLink link : mLinks) { - if (link.start == start && link.end == end) { - return link; - } - } - - return null; - } - - public int size() { - return mLinks.size(); - } - - private static class AccessibleLink { - public String description; - public int start; - public int end; - public int id; - } - } - @Override public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { - // Only set a NodeProvider if we have virtual views, otherwise just return null here so that - // we fall back to the View class's default behavior. If we don't do this, then Views with - // no virtual children will fall back to using ExploreByTouchHelper's onPopulateNodeForHost - // method to populate their AccessibilityNodeInfo, which defaults to doing nothing, so no - // AccessibilityNodeInfo will be created. Alternatively, we could override - // onPopulateNodeForHost instead, and have it create an AccessibilityNodeInfo for the host - // but this is what the default View class does by itself, so we may as well defer to it. - if (mAccessibilityLinks != null) { - return super.getAccessibilityNodeProvider(host); - } - return null; } + // This exists so classes that extend this can properly call super's impl of this method while + // still being able to override it properly for this class + public @Nullable AccessibilityNodeProviderCompat superGetAccessibilityNodeProvider(View host) { + return super.getAccessibilityNodeProvider(host); + } + /** * Determines if the supplied {@link View} and {@link AccessibilityNodeInfoCompat} has any * children which are not independently accessibility focusable and also have a spoken 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 new file mode 100644 index 00000000000..72730f0aec9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewAccessibilityDelegate.kt @@ -0,0 +1,290 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.graphics.Paint +import android.graphics.Rect +import android.text.Spannable +import android.text.Spanned +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.view.View +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import androidx.core.view.accessibility.AccessibilityNodeProviderCompat +import com.facebook.react.R +import com.facebook.react.uimanager.ReactAccessibilityDelegate +import kotlin.math.ceil + +internal class ReactTextViewAccessibilityDelegate : ReactAccessibilityDelegate { + public constructor( + view: View, + originalFocus: Boolean, + originalImportantForAccessibility: Int + ) : super(view, originalFocus, originalImportantForAccessibility) { + accessibilityLinks = hostView.getTag(R.id.accessibility_links) as AccessibilityLinks? + } + + private var accessibilityLinks: AccessibilityLinks? = null + + public companion object { + public fun setDelegate( + view: View, + originalFocus: Boolean, + originalImportantForAccessibility: Int + ) { + // if a view already has an accessibility delegate, replacing it could cause + // problems,so leave it alone. + if (!ViewCompat.hasAccessibilityDelegate(view) && + (view.getTag(R.id.accessibility_role) != null || + view.getTag(R.id.accessibility_state) != null || + view.getTag(R.id.accessibility_actions) != null || + view.getTag(R.id.react_test_id) != null || + view.getTag(R.id.accessibility_collection_item) != null || + view.getTag(R.id.accessibility_links) != null || + view.getTag(R.id.role) != null)) { + ViewCompat.setAccessibilityDelegate( + view, + ReactTextViewAccessibilityDelegate( + view, originalFocus, originalImportantForAccessibility)) + } + } + + public fun resetDelegate( + view: View, + originalFocus: Boolean, + originalImportantForAccessibility: Int + ) { + ViewCompat.setAccessibilityDelegate( + view, + ReactTextViewAccessibilityDelegate( + view, originalFocus, originalImportantForAccessibility)) + } + } + + override fun getVisibleVirtualViews(virtualViewIds: MutableList) { + val accessibilityLinks = accessibilityLinks ?: return + + for (i in 0 until accessibilityLinks.size()) { + virtualViewIds.add(i) + } + } + + override fun getVirtualViewAt(x: Float, y: Float): Int { + val accessibilityLinks = accessibilityLinks ?: return INVALID_ID + if (accessibilityLinks.size() == 0 || hostView !is TextView) { + return INVALID_ID + } + + var x = x + var y = y + + val textView = hostView as TextView + if (textView.text !is Spanned) { + return INVALID_ID + } + + val layout = textView.layout ?: return INVALID_ID + + x -= textView.totalPaddingLeft.toFloat() + y -= textView.totalPaddingTop.toFloat() + x += textView.scrollX.toFloat() + y += textView.scrollY.toFloat() + + val line = layout.getLineForVertical(y.toInt()) + val charOffset = layout.getOffsetForHorizontal(line, x) + + val clickableSpan = + getFirstSpan(charOffset, charOffset, ClickableSpan::class.java) ?: return INVALID_ID + + val spanned = textView.text as Spanned + val start = spanned.getSpanStart(clickableSpan) + val end = spanned.getSpanEnd(clickableSpan) + + val link: AccessibilityLinks.AccessibleLink? = accessibilityLinks.getLinkBySpanPos(start, end) + return link?.id ?: INVALID_ID + } + + protected fun getFirstSpan(start: Int, end: Int, classType: Class?): T? { + if (hostView !is TextView || (hostView as TextView).text !is Spanned) { + return null + } + + val spanned = (hostView as TextView).text as Spanned + val spans = spanned.getSpans(start, end, classType) + return if (spans.isNotEmpty()) spans[0] else null + } + + @Suppress("DEPRECATION") + override fun onPopulateNodeForVirtualView(virtualViewId: Int, node: AccessibilityNodeInfoCompat) { + // If we get an invalid virtualViewId for some reason (which is known to happen in API 19 and + // below), return an "empty" node to prevent from crashing. This will never be presented to + // the user, as Talkback filters out nodes with no content to announce. + val accessibilityLinks = accessibilityLinks + if (accessibilityLinks == null) { + node.contentDescription = "" + node.setBoundsInParent(Rect(0, 0, 1, 1)) + return + } + + val accessibleTextSpan: AccessibilityLinks.AccessibleLink? = + accessibilityLinks.getLinkById(virtualViewId) + if (accessibleTextSpan == null) { + node.contentDescription = "" + node.setBoundsInParent(Rect(0, 0, 1, 1)) + return + } + + // NOTE: The span may not actually have visible bounds within its parent, + // due to line limits, etc. + val bounds = getBoundsInParent(accessibleTextSpan) + if (bounds == null) { + node.contentDescription = "" + node.setBoundsInParent(Rect(0, 0, 1, 1)) + return + } + + node.contentDescription = accessibleTextSpan.description + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK) + node.setBoundsInParent(bounds) + node.roleDescription = hostView.resources.getString(R.string.link_description) + node.className = AccessibilityRole.getValue(AccessibilityRole.BUTTON) + } + + private fun getBoundsInParent(accessibleLink: AccessibilityLinks.AccessibleLink): Rect? { + // This view is not a text view, so return the entire views bounds. + if (hostView !is TextView) { + return Rect(0, 0, hostView.width, hostView.height) + } + + val textView = hostView as TextView + val textViewLayout = textView.layout ?: return Rect(0, 0, textView.width, textView.height) + + val startOffset = accessibleLink.start + val endOffset = accessibleLink.end + + // Ensure the link hasn't been ellipsized away; in such cases, + // getPrimaryHorizontal will crash (and the link isn't rendered anyway). + val startOffsetLineNumber = textViewLayout.getLineForOffset(startOffset) + val lineEndOffset = textViewLayout.getLineEnd(startOffsetLineNumber) + if (startOffset > lineEndOffset) { + return null + } + + val rootRect = Rect() + + val startXCoordinates = textViewLayout.getPrimaryHorizontal(startOffset).toDouble() + + val paint = Paint() + val sizeSpan = + getFirstSpan(accessibleLink.start, accessibleLink.end, AbsoluteSizeSpan::class.java) + val textSize = sizeSpan?.size?.toFloat() ?: textView.textSize + paint.textSize = textSize + val textWidth = ceil(paint.measureText(accessibleLink.description).toDouble()).toInt() + + val endOffsetLineNumber = textViewLayout.getLineForOffset(endOffset) + val isMultiline = startOffsetLineNumber != endOffsetLineNumber + textViewLayout.getLineBounds(startOffsetLineNumber, rootRect) + + val verticalOffset = textView.scrollY + textView.totalPaddingTop + rootRect.top += verticalOffset + rootRect.bottom += verticalOffset + rootRect.left = + (rootRect.left + (startXCoordinates + textView.totalPaddingLeft - textView.scrollX)).toInt() + + // The bounds for multi-line strings should *only* include the first line. This is because for + // API 25 and below, Talkback's click is triggered at the center point of these bounds, and if + // that center point is outside the spannable, it will click on something else. There is no + // harm in not outlining the wrapped part of the string, as the text for the whole string will + // be read regardless of the bounding box. + if (isMultiline) { + return Rect(rootRect.left, rootRect.top, rootRect.right, rootRect.bottom) + } + + return Rect(rootRect.left, rootRect.top, rootRect.left + textWidth, rootRect.bottom) + } + + override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProviderCompat? { + // Only set a NodeProvider if we have virtual views, otherwise just return null here so that + // we fall back to the View class's default behavior. If we don't do this, then Views with + // no virtual children will fall back to using ExploreByTouchHelper's onPopulateNodeForHost + // method to populate their AccessibilityNodeInfo, which defaults to doing nothing, so no + // AccessibilityNodeInfo will be created. Alternatively, we could override + // onPopulateNodeForHost instead, and have it create an AccessibilityNodeInfo for the host + // but this is what the default View class does by itself, so we may as well defer to it. + if (accessibilityLinks != null) { + return superGetAccessibilityNodeProvider(host) + } + + return null + } + + public class AccessibilityLinks(spans: Array, text: Spannable) { + private val links: List + + init { + val accessibleLinks = mutableListOf() + for (i in spans.indices) { + val span = spans[i] + val start = text.getSpanStart(span) + val end = text.getSpanEnd(span) + // zero length spans, and out of range spans should not be included. + if (start == end || start < 0 || end < 0 || start > text.length || end > text.length) { + continue + } + + val link = AccessibleLink() + link.description = text.subSequence(start, end).toString() + link.start = start + link.end = end + + // ID is the reverse of what is expected, since the ClickableSpans are returned in reverse + // order due to being added in reverse order. If we don't do this, focus will move to the + // last link first and move backwards. + // + // If this approach becomes unreliable, we should instead look at their start position and + // order them manually. + link.id = spans.size - 1 - i + accessibleLinks.add(link) + } + links = accessibleLinks + } + + public fun getLinkById(id: Int): AccessibleLink? { + for (link in links) { + if (link.id == id) { + return link + } + } + + return null + } + + public fun getLinkBySpanPos(start: Int, end: Int): AccessibleLink? { + for (link in links) { + if (link.start == start && link.end == end) { + return link + } + } + + return null + } + + public fun size(): Int { + return links.size + } + + public class AccessibleLink { + public var description: String? = null + public var start: Int = 0 + public var end: Int = 0 + public var id: Int = 0 + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index 2c00dc7a459..dc1db61bb0d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -21,7 +21,6 @@ import com.facebook.react.internal.SystraceSection; import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.IViewManagerWithChildren; -import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; @@ -83,6 +82,12 @@ public class ReactTextViewManager return REACT_CLASS; } + @Override + protected void updateViewAccessibility(@NonNull ReactTextView view) { + ReactTextViewAccessibilityDelegate.Companion.setDelegate( + view, view.isFocusable(), view.getImportantForAccessibility()); + } + @Override public ReactTextView createViewInstance(ThemedReactContext context) { return new ReactTextView(context); @@ -106,8 +111,8 @@ public class ReactTextViewManager if (clickableSpans.length > 0) { view.setTag( R.id.accessibility_links, - new ReactAccessibilityDelegate.AccessibilityLinks(clickableSpans, spannable)); - ReactAccessibilityDelegate.resetDelegate( + new ReactTextViewAccessibilityDelegate.AccessibilityLinks(clickableSpans, spannable)); + ReactTextViewAccessibilityDelegate.Companion.resetDelegate( view, view.isFocusable(), view.getImportantForAccessibility()); } }