From 16253014d06e3de266c2ede793cd9a0fd387c967 Mon Sep 17 00:00:00 2001 From: Dmitry Zhigulin <104712148+faciledictu@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:26:39 +0400 Subject: [PATCH] fix(View): do not flatten a view whose accessibility props a container uses Keep the view when iOS or Android reads one of its accessibility props from a container: label, role, state, actions, escape, magic tap, Large Content Viewer and ignoresInvertColors, as #59335 did for accessibilityLiveRegion. --- .../Components/View/__tests__/View-itest.js | 67 +++++++++++++++++-- .../components/view/ViewShadowNode.cpp | 17 ++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index a26f29766aed..bea0383b5840 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -481,7 +481,7 @@ describe('', () => { ); }); - it('does not unflatten view', () => { + it('unflattens view', () => { const root = Fantom.createRoot(); Fantom.runTask(() => { @@ -496,8 +496,10 @@ describe('', () => { }); expect( - root.getRenderedOutput({props: ['accessibilityRole']}).toJSX(), - ).toEqual(null); + root.getRenderedOutput({props: ['accessibilityActions']}).toJSX(), + ).toEqual( + , + ); }); }); @@ -596,7 +598,7 @@ describe('', () => { ).toEqual(); }); - it('does not unflatten view', () => { + it('unflattens view', () => { const root = Fantom.createRoot(); Fantom.runTask(() => { @@ -605,7 +607,7 @@ describe('', () => { expect( root.getRenderedOutput({props: ['accessibilityLabel']}).toJSX(), - ).toEqual(null); + ).toEqual(); }); }); @@ -656,7 +658,7 @@ describe('', () => { ).toEqual(); }); - it('does not unflatten view', () => { + it('unflattens view', () => { const root = Fantom.createRoot(); Fantom.runTask(() => { @@ -665,9 +667,22 @@ describe('', () => { expect( root.getRenderedOutput({props: ['accessibilityRole']}).toJSX(), - ).toEqual(null); + ).toEqual(); }); }); + + describe('role', () => { + it('unflattens view', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual(); + }); + }); + describe('aria-label', () => { it('is mapped to accessibilityLabel', () => { const root = Fantom.createRoot(); @@ -914,6 +929,18 @@ describe('', () => { root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), ).toEqual(); }); + + it('does not unflatten view', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual(null); + }); }); describe('aria-disabled', () => { @@ -1050,6 +1077,32 @@ describe('', () => { root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), ).toEqual(); }); + + it('unflattens view', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('does not unflatten view when mixed', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual(null); + }); }); }); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index b3d396e263c1..af8d60ec0cb0 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -41,6 +41,21 @@ void ViewShadowNode::initialize() noexcept { return false; }; + auto hasContainerAccessibilityProps = [&]() { + const auto& state = viewProps.accessibilityState; + bool hasState = state.has_value() && + (state->disabled || state->selected.has_value() || + state->expanded.has_value() || + state->checked == AccessibilityState::Checked || + state->checked == AccessibilityState::Unchecked); + return !viewProps.accessibilityLabel.empty() || + !viewProps.accessibilityRole.empty() || viewProps.role.has_value() || + hasState || !viewProps.accessibilityActions.empty() || + viewProps.onAccessibilityEscape || viewProps.onAccessibilityMagicTap || + viewProps.accessibilityShowsLargeContentViewer || + viewProps.accessibilityIgnoresInvertColors; + }; + bool formsStackingContext = !viewProps.collapsable || viewProps.pointerEvents == PointerEventsMode::BoxOnly || viewProps.pointerEvents == PointerEventsMode::None || @@ -60,7 +75,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) || - !viewProps.accessibilityOrder.empty(); + !viewProps.accessibilityOrder.empty() || hasContainerAccessibilityProps(); bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || hasBorder() ||