diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index cce9cb3d9547e0..7d34417875ece5 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -151,7 +151,13 @@ class TouchableText extends React.Component { {hasTextAncestor => hasTextAncestor ? ( - + ) : ( diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java new file mode 100644 index 00000000000000..37f7000a915b9c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactClickableSpan.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.text.TextPaint; +import android.text.style.ClickableSpan; +import android.view.View; +import androidx.annotation.NonNull; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.view.ViewGroupClickEvent; + +/** + * This class is used in {@link TextLayoutManager} to linkify and style a span of text with + * accessibilityRole="link". This is needed to make nested Text components accessible. + * + *

For example, if your React component looks like this: + * + *

{@code
+ * 
+ *   Some text with
+ *   a link
+ *   in the middle.
+ * 
+ * }
+ * + * then only one {@link ReactTextView} will be created, for the parent. The child Text component + * does not exist as a native view, and therefore has no accessibility properties. Instead, we have + * to use spans on the parent's {@link ReactTextView} to properly style the child, and to make it + * accessible (TalkBack announces that the text has links available, and the links are exposed in + * the context menu). + */ +class ReactClickableSpan extends ClickableSpan implements ReactSpan { + + private final int mReactTag; + private final int mForegroundColor; + + ReactClickableSpan(int reactTag, int foregroundColor) { + mReactTag = reactTag; + mForegroundColor = foregroundColor; + } + + @Override + public void onClick(@NonNull View view) { + ReactContext context = (ReactContext) view.getContext(); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(context, mReactTag); + if (eventDispatcher != null) { + eventDispatcher.dispatchEvent(new ViewGroupClickEvent(mReactTag)); + } + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setColor(mForegroundColor); + ds.setUnderlineText(false); + } + + public int getReactTag() { + return mReactTag; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 29e3e2969fb879..f99e24019e5236 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -17,6 +17,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaDirection; @@ -72,6 +73,9 @@ public class TextAttributeProps { protected boolean mIsLineThroughTextDecorationSet = false; protected boolean mIncludeFontPadding = true; + protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null; + protected boolean mIsAccessibilityRoleSet = false; + /** * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link * Typeface#NORMAL} or {@link Typeface#BOLD}. @@ -134,6 +138,7 @@ public TextAttributeProps(ReactStylesDiffMap props) { setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); setTextTransform(getStringProp(PROP_TEXT_TRANSFORM)); setLayoutDirection(getStringProp(ViewProps.LAYOUT_DIRECTION)); + setAccessibilityRole(getStringProp(ViewProps.ACCESSIBILITY_ROLE)); } public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL) { @@ -412,6 +417,14 @@ public void setTextTransform(@Nullable String textTransform) { } } + public void setAccessibilityRole(@Nullable String accessibilityRole) { + if (accessibilityRole != null) { + mIsAccessibilityRoleSet = accessibilityRole != null; + mAccessibilityRole = + ReactAccessibilityDelegate.AccessibilityRole.fromValue(accessibilityRole); + } + } + public static int getTextBreakStrategy(@Nullable String textBreakStrategy) { int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY; if (textBreakStrategy != null) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 9c524d9f078059..c1143b430830e7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -29,6 +29,7 @@ import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; import com.facebook.yoga.YogaConstants; @@ -115,7 +116,12 @@ private static void buildSpannableFromFragment( sb.length(), new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); } else if (end >= start) { - if (textAttributes.mIsColorSet) { + if (ReactAccessibilityDelegate.AccessibilityRole.LINK.equals( + textAttributes.mAccessibilityRole)) { + ops.add( + new SetSpanOperation( + start, end, new ReactClickableSpan(reactTag, textAttributes.mColor))); + } else if (textAttributes.mIsColorSet) { ops.add( new SetSpanOperation( start, end, new ReactForegroundColorSpan(textAttributes.mColor))); diff --git a/ReactCommon/react/renderer/attributedstring/conversions.h b/ReactCommon/react/renderer/attributedstring/conversions.h index 26c8b5f37f0799..a121a09028e2ea 100644 --- a/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/ReactCommon/react/renderer/attributedstring/conversions.h @@ -768,6 +768,10 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) { _textAttributes( "layoutDirection", toString(*textAttributes.layoutDirection)); } + if (textAttributes.accessibilityRole.has_value()) { + _textAttributes( + "accessibilityRole", toString(*textAttributes.accessibilityRole)); + } return _textAttributes; }