Fix Android Text accessibility leaking style spans#56881
Fix Android Text accessibility leaking style spans#56881danyalahmed1995 wants to merge 2 commits into
Conversation
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D105680819. |
|
This regresses accessibility for single-link elements where developers haven't explicitly set accessibilityRole="link" — TalkBack won't provide link earcons or link navigation for these nodes. When the entire text is a single ClickableSpan (isWholeTextSingleLink returns true), no virtual link nodes are created and TalkBack relies on the ClickableSpan in info.text to identify the node as a link — this change strips that span, so TalkBack will no longer announce these elements as links. |
|
@javache Yes, you are absolutely right. I missed the whole-text single-link path where I agree the fix should be narrower than converting the accessibility node text to a plain One safer direction could be to preserve semantic/action spans such as Does that sound like the right direction? If so, I can update the PR and add coverage for the whole-text single-link case. |
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sanitize Android Text accessibility node text by removing known visual spans while preserving ClickableSpan/URLSpan semantics. This avoids leaking style metadata to TalkBack without regressing whole-text single-link accessibility.
|
@javache Thank you for pointing me in the right direction. I updated the PR to avoid converting The new approach creates an accessibility-only I also added coverage for:
I verified fail-before/pass-after against the previous plain-string implementation, and reran the focused Android accessibility delegate test plus formatting/diff checks. |
|
Does the original issue repro on your device? |
|
I was not able to reproduce the exact original spoken output on this OPPO/Android 13 device before the PR fix. What I validated on the physical device was a smoke test of the PR build: RNTester remained navigable with TalkBack enabled across the text-heavy/styled examples, clickable/link-style examples, Image, APIs, Playground, and AccessibilityAndroid screens, and the app did not crash or lose focus behavior. The deterministic repro/verification for the original issue is covered by the focused unit tests: with the production sanitizer reverted to the previous plain/old behavior, the style-span leak tests fail because So the physical device test was mainly for TalkBack smoke/safety, while the unit tests cover the original style-span leak and the whole-text single-link regression concern. |
javache
left a comment
There was a problem hiding this comment.
I'm not confident that this is the right fix, given that the original root cause is not well understood, and the manual mapping of all the different Span types is very error-prone and likely to break int he future.
Can you research if AOSP does something similar in the default accessibility implementation?
|
@javache I looked into AOSP’s default From what I found, info.setText(getTextForAccessibility());and getTextForAccessibility() returns the displayed/transformed text via: TextUtils.trimToParcelableSize(mTransformed)So TextView itself does not appear to maintain a manual allowlist of visual spans. The span normalization seems to happen inside AccessibilityNodeInfo.setText(CharSequence). If the text is Spanned, it stores mOriginalText, then calls replaceClickableSpan(...) and replaceReplacementSpan(...) before assigning mText. AccessibilityNodeInfo.getText() also documents that if the text contains ClickableSpan or URLSpan, those spans are replaced with accessibility-specific span wrappers. So AOSP appears to avoid a broad visual-span allowlist and instead normalizes accessibility-relevant spans, especially clickable/url spans and replacement spans. Given that, I agree the manual span allowlist in my current revision is probably too fragile. Should I look for a React Native-side approach that better matches this AOSP model rather than maintaining a list of visual span classes ? Or If you have any better suggestions kindly let me know |
Summary
Fixes #56873.
This also addresses the underlying Android
<Text>accessibility issue previously reported in #55150.On Android, TalkBack could announce React Native
<Text>styling metadata as part of the accessible name. For example, a plain text node could be announced with implementation details such as pixel size and color before the actual visible text.The root cause is that the host accessibility node was receiving the rendered
Spannedtext object. React Native text layout attaches visual spans to that object, such as absolute size and color spans. Those spans are correct for rendering, but they should not become part of the host node's spoken accessibility text.This PR converts the host accessibility node text to plain text when the value is
Spanned, while leaving the rendered/source text untouched.Background / relation to previous reports
#55150 reported the same Android TalkBack behavior on React Native 0.78.2. That issue was later closed as
Type: Too Old Version, but the underlying Android accessibility delegate path still exposed styledSpannedtext to the accessibility node.#56873 confirms the issue is still reproducible on a current React Native version, 0.85.3, using a minimal
<Text>example with no custom styles.#56872 provides a reproducer for the current issue, but it does not change the Android accessibility code path.
This PR fixes the root cause directly in
ReactTextViewAccessibilityDelegaterather than relying on app-level labels or repro-only changes.What changed
ReactTextViewAccessibilityDelegatenow normalizes the host node's accessibility text:Spanned, exposetoString()toAccessibilityNodeInfoCompat.textCharSequencePreparedLayoutTextViewpathcontentDescription/ accessibility label behaviorThe important part is that this only affects the accessibility node text. It does not mutate the rendered text.
Why this is safe
The fix only changes the value assigned to
AccessibilityNodeInfoCompat.text.It does not:
setText()on the host viewTextView.textClickableSpansInline link behavior is preserved because the link/accessibility virtual view logic still reads from the original source
Spannedtext on the host view / prepared layout text.Specifically:
SpannedgetVirtualViewAt()still findsClickableSpans from the source textonPerformActionForVirtualView()still callsspan.onClick(hostView)So this strips visual/style spans only from the host node's spoken text, not from rendering, link data, virtual links, or click actions.
Tests
Added
ReactTextViewAccessibilityDelegateTestcoverage for:<Text>accessibility node textcontentDescriptionVerification
Ran the focused Android unit test:
Verified fail-before / pass-after behavior:
with only the production fix temporarily reverted, the focused test failed
after restoring the production fix, the same focused test passed
reran with --rerun-tasks to avoid relying on cached results
The old implementation failed because nodeInfo.text was still a Spanned.
Additional checks:
Manual TalkBack smoke validation was later performed on a physical Android device:
adb reverse tcp:8081 tcp:8081Validated that RNTester remained navigable with TalkBack focus across Components, APIs, Playground, Image, and AccessibilityAndroid examples, including styled/text-heavy and clickable/link-style examples. Screenshots and a sanitized log were attached in #56873.
Changelog:
[Android] [Fixed] - Prevent TalkBack from announcing React Native Text style spans as Android Text accessibility text.