Fix: a11y crash when an accessible link is ellipsized away (#37050)

Summary:
If an accessible link is ellipsized out of being rendered, the AccessibilityDelegate will still attempt to populate an accessibility node for it; doing so results in an invalid request to a TextLayout API, however, causing a crash. This crash occurs as soon as the element is rendered, so long as a Screen Reader (or app using similar a11y APIs) is enabled. This change uses a technique similar to those existing to make the node "blank" in such cases, so Talkback can filter it out—and, more importantly, not crash.

## Changelog:

[Android] [Fixed] - Fix links hidden via ellipsis crashing screen readers

Pull Request resolved: https://github.com/facebook/react-native/pull/37050

Test Plan:
- Added a block to the "Accessibility Android APIs" page in the rn-tester app. Without the changes to `ReactAccessibilityDelegate`, this component crashes the app; with the changes, the component renders without problem.
- You can also see the crash "in the wild" using [this Expo Snack](https://snack.expo.dev/dhleong/2d1407) that I put together when trying to isolate this issue.

Reviewed By: rshest

Differential Revision: D46206673

Pulled By: NickGerleman

fbshipit-source-id: 0eb3e735202ee6be5f931bbb4bb92c24e7458ea6
This commit is contained in:
Daniel Leong
2023-06-04 00:03:01 -07:00
committed by Facebook GitHub Bot
parent fd9e295bef
commit d54f486fe6
2 changed files with 35 additions and 4 deletions
@@ -786,9 +786,18 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
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(getBoundsInParent(accessibleTextSpan));
node.setBoundsInParent(bounds);
node.setRoleDescription(mView.getResources().getString(R.string.link_description));
node.setClassName(AccessibilityRole.getValue(AccessibilityRole.BUTTON));
}
@@ -805,10 +814,19 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
return new Rect(0, 0, textView.getWidth(), textView.getHeight());
}
Rect rootRect = new Rect();
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();
@@ -818,7 +836,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
paint.setTextSize(textSize);
int textWidth = (int) Math.ceil(paint.measureText(accessibleLink.description));
int startOffsetLineNumber = textViewLayout.getLineForOffset((int) startOffset);
int endOffsetLineNumber = textViewLayout.getLineForOffset((int) endOffset);
boolean isMultiline = startOffsetLineNumber != endOffsetLineNumber;
textViewLayout.getLineBounds(startOffsetLineNumber, rootRect);
@@ -65,6 +65,20 @@ class AccessibilityAndroidExample extends React.Component<
render(): React.Node {
return (
<RNTesterPage title={'Accessibility Android APIs'}>
<RNTesterBlock title="Ellipsized Accessible Links">
<Text numberOfLines={3}>
<Text>
Bacon {this.state.count} Ipsum{'\n'}
</Text>
<Text>Dolor sit amet{'\n'}</Text>
<Text>Eggsecetur{'\n'}</Text>
<Text>{'\n'}</Text>
<Text accessibilityRole="link" onPress={this._addOne}>
http://github.com
</Text>
</Text>
</RNTesterBlock>
<RNTesterBlock title="LiveRegion">
<TouchableWithoutFeedback onPress={this._addOne}>
<View style={styles.embedded}>