Skip to content

Commit b352e2d

Browse files
Emily Janzerfacebook-github-bot
Emily Janzer
authored andcommitted
Create a ClickableSpan for nested Text components
Summary: Right now nested Text components are not accessible on Android. This is because we only create a native ReactTextView for the parent component; the styling and touch handling for the child component are handled using spans. In order for TalkBack to announce the link, we need to linkify the text using a ClickableSpan. This diff adds ReactClickableSpan, which TextLayoutManager uses to linkify a span of text when its corresponding React component has `accessibilityRole="link"`. For example: <Text> A paragraph with some <Text accessible={true} accessibilityRole="link" onPress={onPress} onClick={onClick}>links</Text> surrounded by other text. </Text> With this diff, the child Text component will be announced by TalkBack ('links available') and exposed as an option in the context menu. Clicking on the link in the context menu fires the Text component's onClick, which we're explicitly forwarding to onPress in Text.js (for now - ideally this would probably use a separate event, but that would involve wiring it up in the renderer as well). ReactClickableSpan also applies text color from React if it exists; this is to override the default Android link styling (teal + underline). Changelog: [Android][Fixed] Make nested Text components accessible as links Reviewed By: yungsters, mdvacca Differential Revision: D23553222 fbshipit-source-id: a962b2833d73ec81047e86cfb41846513c486d87
1 parent b705eaf commit b352e2d

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

Libraries/Text/Text.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ class TouchableText extends React.Component<Props, State> {
151151
<TextAncestor.Consumer>
152152
{hasTextAncestor =>
153153
hasTextAncestor ? (
154-
<RCTVirtualText {...props} ref={props.forwardedRef} />
154+
<RCTVirtualText
155+
{...props}
156+
// This is used on Android to call a nested Text component's press handler from the context menu.
157+
// TODO T75145059 Clean this up once Text is migrated off of Touchable
158+
onClick={props.onPress}
159+
ref={props.forwardedRef}
160+
/>
155161
) : (
156162
<TextAncestor.Provider value={true}>
157163
<RCTText {...props} ref={props.forwardedRef} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text;
9+
10+
import android.text.TextPaint;
11+
import android.text.style.ClickableSpan;
12+
import android.view.View;
13+
import androidx.annotation.NonNull;
14+
import com.facebook.react.bridge.ReactContext;
15+
import com.facebook.react.uimanager.UIManagerHelper;
16+
import com.facebook.react.uimanager.events.EventDispatcher;
17+
import com.facebook.react.views.view.ViewGroupClickEvent;
18+
19+
/**
20+
* This class is used in {@link TextLayoutManager} to linkify and style a span of text with
21+
* accessibilityRole="link". This is needed to make nested Text components accessible.
22+
*
23+
* <p>For example, if your React component looks like this:
24+
*
25+
* <pre>{@code
26+
* <Text>
27+
* Some text with
28+
* <Text onPress={onPress} accessible={true} accessibilityRole="link">a link</Text>
29+
* in the middle.
30+
* </Text>
31+
* }</pre>
32+
*
33+
* then only one {@link ReactTextView} will be created, for the parent. The child Text component
34+
* does not exist as a native view, and therefore has no accessibility properties. Instead, we have
35+
* to use spans on the parent's {@link ReactTextView} to properly style the child, and to make it
36+
* accessible (TalkBack announces that the text has links available, and the links are exposed in
37+
* the context menu).
38+
*/
39+
class ReactClickableSpan extends ClickableSpan implements ReactSpan {
40+
41+
private final int mReactTag;
42+
private final int mForegroundColor;
43+
44+
ReactClickableSpan(int reactTag, int foregroundColor) {
45+
mReactTag = reactTag;
46+
mForegroundColor = foregroundColor;
47+
}
48+
49+
@Override
50+
public void onClick(@NonNull View view) {
51+
ReactContext context = (ReactContext) view.getContext();
52+
EventDispatcher eventDispatcher =
53+
UIManagerHelper.getEventDispatcherForReactTag(context, mReactTag);
54+
if (eventDispatcher != null) {
55+
eventDispatcher.dispatchEvent(new ViewGroupClickEvent(mReactTag));
56+
}
57+
}
58+
59+
@Override
60+
public void updateDrawState(@NonNull TextPaint ds) {
61+
super.updateDrawState(ds);
62+
ds.setColor(mForegroundColor);
63+
ds.setUnderlineText(false);
64+
}
65+
66+
public int getReactTag() {
67+
return mReactTag;
68+
}
69+
}

ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java

+13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.facebook.react.bridge.ReadableArray;
1818
import com.facebook.react.bridge.ReadableMap;
1919
import com.facebook.react.uimanager.PixelUtil;
20+
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
2021
import com.facebook.react.uimanager.ReactStylesDiffMap;
2122
import com.facebook.react.uimanager.ViewProps;
2223
import com.facebook.yoga.YogaDirection;
@@ -72,6 +73,9 @@ public class TextAttributeProps {
7273
protected boolean mIsLineThroughTextDecorationSet = false;
7374
protected boolean mIncludeFontPadding = true;
7475

76+
protected @Nullable ReactAccessibilityDelegate.AccessibilityRole mAccessibilityRole = null;
77+
protected boolean mIsAccessibilityRoleSet = false;
78+
7579
/**
7680
* mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}. mFontWeight can be {@link
7781
* Typeface#NORMAL} or {@link Typeface#BOLD}.
@@ -134,6 +138,7 @@ public TextAttributeProps(ReactStylesDiffMap props) {
134138
setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR));
135139
setTextTransform(getStringProp(PROP_TEXT_TRANSFORM));
136140
setLayoutDirection(getStringProp(ViewProps.LAYOUT_DIRECTION));
141+
setAccessibilityRole(getStringProp(ViewProps.ACCESSIBILITY_ROLE));
137142
}
138143

139144
public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL) {
@@ -412,6 +417,14 @@ public void setTextTransform(@Nullable String textTransform) {
412417
}
413418
}
414419

420+
public void setAccessibilityRole(@Nullable String accessibilityRole) {
421+
if (accessibilityRole != null) {
422+
mIsAccessibilityRoleSet = accessibilityRole != null;
423+
mAccessibilityRole =
424+
ReactAccessibilityDelegate.AccessibilityRole.fromValue(accessibilityRole);
425+
}
426+
}
427+
415428
public static int getTextBreakStrategy(@Nullable String textBreakStrategy) {
416429
int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY;
417430
if (textBreakStrategy != null) {

ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.facebook.react.bridge.ReadableNativeMap;
3030
import com.facebook.react.config.ReactFeatureFlags;
3131
import com.facebook.react.uimanager.PixelUtil;
32+
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
3233
import com.facebook.react.uimanager.ReactStylesDiffMap;
3334
import com.facebook.react.uimanager.ViewProps;
3435
import com.facebook.yoga.YogaConstants;
@@ -115,7 +116,12 @@ private static void buildSpannableFromFragment(
115116
sb.length(),
116117
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
117118
} else if (end >= start) {
118-
if (textAttributes.mIsColorSet) {
119+
if (ReactAccessibilityDelegate.AccessibilityRole.LINK.equals(
120+
textAttributes.mAccessibilityRole)) {
121+
ops.add(
122+
new SetSpanOperation(
123+
start, end, new ReactClickableSpan(reactTag, textAttributes.mColor)));
124+
} else if (textAttributes.mIsColorSet) {
119125
ops.add(
120126
new SetSpanOperation(
121127
start, end, new ReactForegroundColorSpan(textAttributes.mColor)));

ReactCommon/react/renderer/attributedstring/conversions.h

+4
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,10 @@ inline folly::dynamic toDynamic(const TextAttributes &textAttributes) {
768768
_textAttributes(
769769
"layoutDirection", toString(*textAttributes.layoutDirection));
770770
}
771+
if (textAttributes.accessibilityRole.has_value()) {
772+
_textAttributes(
773+
"accessibilityRole", toString(*textAttributes.accessibilityRole));
774+
}
771775
return _textAttributes;
772776
}
773777

0 commit comments

Comments
 (0)