fix(iOS): reset accessibilityViewIsModal on view recycle to avoid trapping VoiceOver#57196
fix(iOS): reset accessibilityViewIsModal on view recycle to avoid trapping VoiceOver#57196doracawl wants to merge 2 commits into
Conversation
|
Hi @doracawl! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
Can we make this more complete and revert the defaults for the other |
Per @javache review feedback on react#57196: extend prepareForRecycle to clear all accessibilityElement-side state that leaks across recycled views, not just accessibilityViewIsModal. Defaults sourced from AccessibilityProps.h; UIKit-side resets cover the props that updateProps writes via direct UIKit setters. Note: accessibilityRespondsToUserInteraction's C++ default is true, so the reset value is YES (not NO). Skipped: accessibilityActions / accessibilityLabelledBy / accessibilityLiveRegion have no direct UIKit-side state to reset.
|
Thanks for the review @javache — pushed c4ab261 to extend Intentionally skipped |
When a Fabric ComponentView is recycled, _props resets to the AccessibilityProps default-constructed values. updateProps writes most accessibility properties only when (oldProps != newProps), so any UIKit-side state previously set stays stale across recycle unless cleared in prepareForRecycle. The most visible failure is `accessibilityViewIsModal` / `accessibilityElementsHidden` trapping VoiceOver against a now-unrelated subtree. Reset only the properties whose UIKit default does NOT depend on the underlying view subclass. `isAccessibilityElement` and `accessibilityTraits` are intentionally skipped: UIControl / UILabel / UIImageView each start with different defaults from UIView, and for RCTText/Image ComponentView `accessibilityElement` resolves to an inner UILabel / UIImageView whose natural a11y behavior would be clobbered by a blanket reset. Per-subclass reset is left as a follow-up. `accessibilityState` is cleared via the (.NotEnabled | .Selected) trait bit mask the setter writes on self.accessibilityTraits — safe across all subclasses. Addresses review feedback on react#57196.
d4a788c to
c4ab261
Compare
|
@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D109810097. |
Summary
RCTViewComponentViewappliesaccessibilityViewIsModalinupdatePropsonly when the prop changes:…but
prepareForRecyclenever resets it. So when a view that hadaria-modal/accessibilityViewIsModalis recycled and later reused for a different view that does not set the prop, the diff isNO (recycled default) == NO (new)andupdatePropsskips it — the staleaccessibilityViewIsModal = YESleaks onto the recycled view.A leftover modal view makes UIKit treat its subtree as the only accessible content, so VoiceOver and UI automation see an empty accessibility tree for the rest of the window until an unrelated layout pass recovers it.
This is easy to hit with transparent modals (e.g. a
react-native-screenstransparentModal) on iOS: open a modal → close it → reopen it. The recycled content view keeps the stale flag and competes with the modal's own transition view, collapsing the whole window's accessibility tree. It is intermittent because it depends on which pooled view instance is reused.Changelog:
[iOS] [Fixed] - Reset
accessibilityViewIsModalinprepareForRecycleso a stale modal flag no longer leaks onto recycled views and traps VoiceOverTest Plan
Repro (iOS, Fabric):
<View aria-modal>(for example the content of a transparent modal), then unmount it so its underlyingRCTViewComponentViewreturns to the recycle pool.aria-modal.accessibilityViewIsModal == YES; VoiceOver /Accessibility Inspector/ amaestro hierarchydump show a collapsed (near-empty) accessibility tree for the window.accessibilityViewIsModal == NO; the accessibility tree is intact.Verified on an iOS 26 simulator with a transparent-modal open → close → reopen cycle: UI-automation flows that read the accessibility tree go from intermittently failing to passing every run.