From d54f486fe66d01450d3c7a04fb0a025319a3014c Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Sun, 4 Jun 2023 00:03:01 -0700 Subject: [PATCH] Fix: a11y crash when an accessible link is ellipsized away (#37050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../uimanager/ReactAccessibilityDelegate.java | 25 ++++++++++++++++--- .../AccessibilityAndroidExample.android.js | 14 +++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 531e4591ba6..b8b08c9bf74 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -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); diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js index 2daf0ba27c6..f5da9efc20e 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js @@ -65,6 +65,20 @@ class AccessibilityAndroidExample extends React.Component< render(): React.Node { return ( + + + + Bacon {this.state.count} Ipsum{'\n'} + + Dolor sit amet{'\n'} + Eggsecetur{'\n'} + {'\n'} + + http://github.com + + + +