Skip to content

Commit c4ab261

Browse files
committed
fix(iOS): reset accessibility state on view recycle
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 #57196.
1 parent 51c53b7 commit c4ab261

1 file changed

Lines changed: 45 additions & 5 deletions

File tree

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -724,12 +724,52 @@ - (void)prepareForRecycle
724724
_reactSubviews = [NSMutableArray new];
725725
_layoutMetrics = {};
726726

727-
// Reset accessibilityViewIsModal so it does not leak across recycled views.
728-
// updateProps only writes this property when (oldProps != newProps); after a
729-
// view is recycled _props resets to the default (NO), so a recycled view that
730-
// is reused without the prop keeps the stale YES and traps VoiceOver / UI
731-
// automation against a now-unrelated subtree (empty accessibility tree).
727+
// Reset accessibility state so it does not leak across recycled views.
728+
// updateProps writes most accessibility properties only when
729+
// (oldProps != newProps); after recycle _props is reset to the
730+
// AccessibilityProps default-constructed values, so any UIKit-side
731+
// state previously set stays stale unless cleared here.
732+
//
733+
// The most visible failure this guards against is
734+
// `accessibilityViewIsModal` / `accessibilityElementsHidden` trapping
735+
// VoiceOver against a now-unrelated subtree (empty accessibility
736+
// tree).
737+
//
738+
// Skipped on purpose:
739+
// * `isAccessibilityElement` and `accessibilityTraits` — their
740+
// UIKit defaults depend on the underlying view subclass
741+
// (UIControl / UILabel / UIImageView start with different
742+
// defaults than UIView). For RCTText/Image ComponentView
743+
// `accessibilityElement` resolves to an inner UILabel /
744+
// UIImageView whose natural a11y behavior would be clobbered
745+
// by forcing NO / None here. Per-subclass reset is a separate
746+
// follow-up.
747+
self.accessibilityElement.accessibilityLabel = nil;
748+
self.accessibilityElement.accessibilityLanguage = nil;
749+
self.accessibilityElement.accessibilityHint = nil;
750+
self.accessibilityElement.accessibilityValue = nil;
732751
self.accessibilityElement.accessibilityViewIsModal = NO;
752+
self.accessibilityElement.accessibilityElementsHidden = NO;
753+
// UIView default for accessibilityRespondsToUserInteraction is YES.
754+
self.accessibilityElement.accessibilityRespondsToUserInteraction = YES;
755+
self.accessibilityIgnoresInvertColors = NO;
756+
// Clear only the `accessibilityState` bits the setter writes via
757+
// self.accessibilityTraits (.NotEnabled / .Selected). Subclass
758+
// default traits are preserved.
759+
self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected);
760+
if ([self.accessibilityElement respondsToSelector:@selector(setAccessibilityIdentifier:)]) {
761+
((UIView *)self.accessibilityElement).accessibilityIdentifier = nil;
762+
} else {
763+
self.accessibilityIdentifier = nil;
764+
}
765+
#if !TARGET_OS_TV
766+
if (@available(iOS 13.0, *)) {
767+
self.showsLargeContentViewer = NO;
768+
self.largeContentTitle = nil;
769+
}
770+
#endif
771+
_accessibilityOrderNativeIDs = nil;
772+
self.accessibilityElements = nil;
733773
}
734774

735775
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props

0 commit comments

Comments
 (0)