From 51c53b78a666b6f3b04cdcb6a752c616e761141a Mon Sep 17 00:00:00 2001 From: doracawl Date: Sat, 13 Jun 2026 22:43:17 +0900 Subject: [PATCH 1/2] fix(iOS): reset accessibilityViewIsModal on view recycle --- .../Mounting/ComponentViews/View/RCTViewComponentView.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5571b5350bff..1d1ed3c0041c 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -723,6 +723,13 @@ - (void)prepareForRecycle _removeClippedSubviews = NO; _reactSubviews = [NSMutableArray new]; _layoutMetrics = {}; + + // Reset accessibilityViewIsModal so it does not leak across recycled views. + // updateProps only writes this property when (oldProps != newProps); after a + // view is recycled _props resets to the default (NO), so a recycled view that + // is reused without the prop keeps the stale YES and traps VoiceOver / UI + // automation against a now-unrelated subtree (empty accessibility tree). + self.accessibilityElement.accessibilityViewIsModal = NO; } - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet *_Nullable)props From c4ab261fdeafc34614909dc994703fb94fc70263 Mon Sep 17 00:00:00 2001 From: doracawl Date: Thu, 25 Jun 2026 23:04:14 +0900 Subject: [PATCH 2/2] fix(iOS): reset accessibility state on view recycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../View/RCTViewComponentView.mm | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 1d1ed3c0041c..e79978ffc76f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -724,12 +724,52 @@ - (void)prepareForRecycle _reactSubviews = [NSMutableArray new]; _layoutMetrics = {}; - // Reset accessibilityViewIsModal so it does not leak across recycled views. - // updateProps only writes this property when (oldProps != newProps); after a - // view is recycled _props resets to the default (NO), so a recycled view that - // is reused without the prop keeps the stale YES and traps VoiceOver / UI - // automation against a now-unrelated subtree (empty accessibility tree). + // Reset accessibility state so it does not leak across recycled views. + // updateProps writes most accessibility properties only when + // (oldProps != newProps); after recycle _props is reset to the + // AccessibilityProps default-constructed values, so any UIKit-side + // state previously set stays stale unless cleared here. + // + // The most visible failure this guards against is + // `accessibilityViewIsModal` / `accessibilityElementsHidden` trapping + // VoiceOver against a now-unrelated subtree (empty accessibility + // tree). + // + // Skipped on purpose: + // * `isAccessibilityElement` and `accessibilityTraits` — their + // UIKit defaults depend on the underlying view subclass + // (UIControl / UILabel / UIImageView start with different + // defaults than UIView). For RCTText/Image ComponentView + // `accessibilityElement` resolves to an inner UILabel / + // UIImageView whose natural a11y behavior would be clobbered + // by forcing NO / None here. Per-subclass reset is a separate + // follow-up. + self.accessibilityElement.accessibilityLabel = nil; + self.accessibilityElement.accessibilityLanguage = nil; + self.accessibilityElement.accessibilityHint = nil; + self.accessibilityElement.accessibilityValue = nil; self.accessibilityElement.accessibilityViewIsModal = NO; + self.accessibilityElement.accessibilityElementsHidden = NO; + // UIView default for accessibilityRespondsToUserInteraction is YES. + self.accessibilityElement.accessibilityRespondsToUserInteraction = YES; + self.accessibilityIgnoresInvertColors = NO; + // Clear only the `accessibilityState` bits the setter writes via + // self.accessibilityTraits (.NotEnabled / .Selected). Subclass + // default traits are preserved. + self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected); + if ([self.accessibilityElement respondsToSelector:@selector(setAccessibilityIdentifier:)]) { + ((UIView *)self.accessibilityElement).accessibilityIdentifier = nil; + } else { + self.accessibilityIdentifier = nil; + } +#if !TARGET_OS_TV + if (@available(iOS 13.0, *)) { + self.showsLargeContentViewer = NO; + self.largeContentTitle = nil; + } +#endif + _accessibilityOrderNativeIDs = nil; + self.accessibilityElements = nil; } - (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet *_Nullable)props