Skip to content

Keep the Text accessibility delegate when the text has no links - #59521

Open
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-text-keep-accessibility-delegate
Open

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-text-keep-accessibility-delegate

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Contributor

Summary:

On Android, every Text update goes through updateExtraData in the Text view manager (PreparedLayoutTextViewManager, and ReactTextViewManager for the older text view). There, the text's clickable spans are stored in a view tag and the accessibility delegate is reset, so that TalkBack can reach the links.

Current behavior. The comment says this is for a text view that "contains any clickable spans", but PreparedLayoutTextViewManager checks is Spanned, which is true for every Text, and ReactTextViewManager has no check at all. So every update of every Text creates a new ReactTextViewAccessibilityDelegate, even when TalkBack is off and the text has no links. Creating one is an ExploreByTouchHelper: it looks up the AccessibilityManager, makes the view focusable and then restores it, and allocates a Handler and a map. When the text has no links, nothing changes for the delegate, since it reads the links only when it's created.

With this change. Both managers call a new ReactTextViewAccessibilityDelegate.updateAccessibilityLinks. When the view already has a ReactTextViewAccessibilityDelegate and the text has no links, before or after the update, it keeps that delegate. In every other case (the first update, links added, changed or removed, or another delegate on the view) it resets the delegate as before. A plain Text still gets the delegate on its first update; PreparedLayoutTextView needs it to expose its text to TalkBack.

Setting a delegate through ViewCompat also makes a view whose importantForAccessibility is auto important. The kept path does the same, so a Text set back to auto (by the prop, or by view recycling) still ends up important, as before. The one difference: the accessibility actions a delegate caches from the accessibilityActions prop now stay between text updates, as they already do for every other view, instead of being dropped with the old delegate.

Changelog:

[ANDROID] [CHANGED] - Text updates keep the accessibility delegate when the text has no links

Test Plan:

  • The pinned ktfmt (--dry-run --set-exit-if-changed) passes on all changed files.
  • Added ReactTextViewAccessibilityDelegateTest (4 Robolectric tests: the first update installs the text delegate; updates without links keep the same delegate; adding and removing links resets it; a view set back to auto is important after an update). With the old code only "updates without links keep the delegate" fails. PreparedLayoutTextViewAccessibilityTextTest and ReactTextViewPropertyTest still pass.
  • Device: Samsung Galaxy A16 (SM-A165F, Android 16), RNTester built with Gradle in Release (assembleRelease, arm64-v8a) and compiled with cmd package compile -m speed. The scenarios are a local Playground screen, not part of this PR: 200 numbers that change every 100 ms, and 60 lines of mixed-script text (emoji, CJK, Arabic, Hebrew, Polish) that change every 100 ms.
  • A/B setup: a single APK with a local switch (an adb shell setprop, not part of this PR) that runs either the old reset on every update or this change. Launches alternate A B B A A B B A, 4 per arm and scenario; 15 s Perfetto traces with the app's trace sections. The changes are medians.
before this PR change
updateExtraData per call, numbers 36.6–39.1 µs 28.4–30.0 µs −21%
Mount per commit, numbers 12.65–13.20 ms 10.74–11.35 ms −13%
Main thread CPU in 15 s, numbers 3,614–3,654 ms 3,484–3,625 ms −3%
updateExtraData per call, mixed scripts 48.3–55.4 µs 38.5–41.1 µs −28%
Mount per commit, mixed scripts 5.49–6.06 ms 4.79–5.07 ms −18%
Main thread CPU in 15 s, mixed scripts 2,858–2,950 ms 2,804–2,849 ms −4%

These times include the cost of the trace sections themselves, about half of updateExtraData's samples in a sampled profile, so without tracing the relative gain is larger.

  • Accessibility: uiautomator dump of TextExample, AccessibilityExample and AccessibilityAndroidExample, 4 scroll positions each, with the old and the new code: the same 166 nodes, with the same text, content description and focusable, clickable and important-for-accessibility flags (positions differ only by the scroll). I couldn't run TalkBack on the device I used.

🤖 Generated with Claude Code

Every Text update reset the accessibility delegate, creating a new
ExploreByTouchHelper even when TalkBack is off and the text has no links.
Keep the existing delegate when the text has no links before or after the
update, and make an "auto" view important as setting the delegate does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 26, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown

Visual regression

No visual differences at 834bd43677d7.

android: no baseline for this environment

ios: no capture

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant