Skip to content

Reset accessibility state when recycling Android views - #59047

Closed
YevheniiKotyrlo wants to merge 4 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-recycled-view-accessibility-state
Closed

YevheniiKotyrlo wants to merge 4 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-recycled-view-accessibility-state

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

With enableViewRecycling on, a recycled view keeps the accessibility state its previous props applied. BaseViewManager.prepareToRecycleView clears the accessibility tags, but not the content description, importantForAccessibility, the selected state, the live region, screenReaderFocusable, the role and collection tags, or the actions its accessibility delegate cached. The next view of that type is built from it and applies only the props it declares, so TalkBack announces a label and a role the view never had, the delegate sends TalkBack's click to an action only the previous props declared, and a view last used inside an aria-hidden subtree hides its new content from accessibility services. With experimental_accessibilityOrder on as well, a recycled ReactViewGroup keeps its previous order list, and a recycled child keeps the parent and focusability tags its former parent's order put on it.

This resets that state in prepareToRecycleView to what a new view has, clears the order list in ReactViewGroup.initView, and restores TextView's default importance in ReactTextView.recycleView. The delegate stays, since ReactScrollView, ReactHorizontalScrollView and ReactNestedScrollView install theirs in their constructors: a new ReactAccessibilityDelegate.prepareToRecycle() clears the actions it cached, and accessibilityLabelledBy, read only in the call that sets it, is a local there. It sits beside the enabled reset #58799 added.

Changelog:

[ANDROID] [FIXED] - Recycled views no longer keep the accessibility state of their previous props

Test Plan:

New cases in BaseViewManagerTest and ReactTextViewPropertyTest recycle a view through onDropViewInstance and createView and compare it with a new one. Before the change six fail: content description "Dismiss sheet", role tag LIST, a click dispatched to the previous props' activate action instead of performed, importance 4 where a new text view has 1, the order list ["first"], and the former parent in accessibility_order_parent. Another case recycles each scroll view and checks that it keeps the ReactScrollViewAccessibilityDelegate its constructor installed. With the change, ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true passes (727 tests) and the release sources compile with the same flag; the RNTester debug build and the rest of build_android were not run.

RNTester with view recycling on, on an Android 16 emulator with TalkBack, before and after: plain rows mounted after labelled rows were removed are read as a selected "Dismiss sheet" button without the change, and as their text with it.

On a Galaxy S23 (Android 14) app with view recycling on, plain rows mounted after rows carrying aria-label, role, aria-selected, aria-disabled and aria-hidden were removed reported the removed rows' label as their content description before the change. Built from source with it, the same rows report no label, role, selected or disabled state, and their text is in the accessibility tree.

Both device runs used the first revision of this change, which cleared the delegate on recycle instead of resetting it.

With view recycling on, prepareToRecycleView clears the accessibility
tags a view's props wrote but not the state those props applied to the
view itself. The next view built from it applies only the props it
declares, so it announces the previous view's content description and
role, reads as selected, keeps its live region and screen-reader
focusability, and stays hidden from accessibility services when the
previous view was.

Reset that state to a new view's, clear the accessibility delegate on
the view, and restore TextView's default importance in
ReactTextView.recycleView.
@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 13, 2026
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — once the labelled rows are removed, TalkBack focuses the plain rows' container, built from a removed row's view, as a selected "Dismiss sheet" button.

FIXED — TalkBack focuses the first plain row and reads "Row 1".

before — main at 977ca0a204 after — with this PR

RNTester debug builds from main at 977ca0a204 with and without this change, both with enableViewRecycling turned on in RNTesterApplication, on an Android 16 (API 36) emulator, 1140×2400 at density 480. The scene renders three rows with accessibilityLabel="Dismiss sheet", role="button" and aria-selected, removes them, then renders three plain rows with a Text each, and the first row is touched with TalkBack on. TalkBack's speech output shows one phrase at a time: without the change it speaks "selected", "Dismiss sheet", "Button", "Double-tap to activate"; with it, "Row 1".

With experimental_accessibilityOrder on, a recycled ReactViewGroup kept
the order list its previous props set, and a recycled child kept the
parent and focusability tags its former parent's order put on it, the
parent tag holding on to that view. ReactViewGroup.initView clears the
list and prepareToRecycleView clears both tags, as a new view has them.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 14, 2026 09:10
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 14, 2026
A delegate keeps the actions it registered while building node info, so
a view recycled from one that declared an activate action dispatched
that action on TalkBack's click instead of performing the click. The
case fails without prepareToRecycleView clearing the delegate.
@meta-codesync

meta-codesync Bot commented Oct 15, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D125427268.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft October 16, 2026 09:44
…tions

Clearing the whole delegate in prepareToRecycleView also removed the
ReactScrollViewAccessibilityDelegate that ReactScrollView,
ReactHorizontalScrollView and ReactNestedScrollView install in their
constructors, which recycleView never installs again.

The delegate now stays, and ReactAccessibilityDelegate.prepareToRecycle()
clears the actions it cached from the previous props. accessibilityLabelledBy
was only read in the call that set it, so it is a local there.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 10:23
@meta-codesync meta-codesync Bot closed this in 6f73542 Oct 16, 2026
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @YevheniiKotyrlo in 6f73542

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Oct 16, 2026
@meta-codesync

meta-codesync Bot commented Oct 16, 2026

Copy link
Copy Markdown

@javache merged this pull request in 6f73542.

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. Merged This PR has been merged. 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.

3 participants