From 37d73aef577c8419620a926fbac0d163cdfab3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 22 Jul 2026 11:18:27 +0200 Subject: [PATCH 1/3] feat: automatically set `accessible` when setting `role` prop --- .../Libraries/Components/View/View.js | 12 +++++ .../Components/View/__tests__/View-itest.js | 53 +++++++++++++++++++ .../Libraries/Image/Image.android.js | 8 +++ .../react-native/Libraries/Image/Image.ios.js | 21 ++++++-- .../Libraries/Image/__tests__/Image-itest.js | 46 ++++++++++++++++ 5 files changed, 137 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index 461f7707c7fa..2f8cd0dd1bf9 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -83,6 +83,18 @@ component View(ref?: React.RefSetter, ...props: ViewProps) { resolvedProps.focusable = !tabIndex; } + // For web compatibility, setting a `role` implicitly makes the element + // accessible, unless the role is `none`/`presentation` (which explicitly + // removes semantics) or `accessible` was set explicitly. + if ( + resolvedProps.accessible == null && + resolvedProps.role != null && + resolvedProps.role !== 'none' && + resolvedProps.role !== 'presentation' + ) { + resolvedProps.accessible = true; + } + if ( accessibilityState != null || ariaBusy != null || 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 3c46277f3a84..bfc67c159114 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -643,6 +643,59 @@ describe('', () => { ).toEqual(null); }); }); + describe('role', () => { + it('implicitly makes the view accessible', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); + }); + + it('does not make the view accessible for the "none" role', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); + }); + + it('does not make the view accessible for the "presentation" role', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); + }); + + it('does not override an explicit "accessible" prop', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + // `accessible={false}` is not propagated to the mounting layer (it is + // the default), so the implicit `role` behavior must not force it on. + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); + }); + }); describe('aria-label', () => { it('is mapped to accessibilityLabel', () => { const root = Fantom.createRoot(); diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index b20bdfa5f6fa..e8f0d31f94cc 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -284,6 +284,14 @@ let BaseImage: AbstractImageAndroid = ({ nativeProps.accessible = true; } else if (accessible != null) { nativeProps.accessible = accessible; + } else if ( + // For web compatibility, setting a `role` implicitly makes the element + // accessible, unless the role is `none`/`presentation`. + nativeProps.role != null && + nativeProps.role !== 'none' && + nativeProps.role !== 'presentation' + ) { + nativeProps.accessible = true; } if ( diff --git a/packages/react-native/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js index 3fc14f2bcf14..489374fcdbb7 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -168,9 +168,24 @@ let BaseImage: AbstractImageIOS = ({ selected: ariaSelected ?? props.accessibilityState?.selected, }; - // In order for `aria-hidden` to work on iOS we must set `accessible` to false (`accessibilityElementsHidden` is not enough). - const accessible = - ariaHidden !== true && (props.alt !== undefined ? true : props.accessible); + let accessible; + if (ariaHidden === true) { + // In order for `aria-hidden` to work on iOS we must set `accessible` to + // false (`accessibilityElementsHidden` is not enough). + accessible = false; + } else if (props.alt !== undefined) { + accessible = true; + } else if (props.accessible != null) { + accessible = props.accessible; + } else if ( + // For web compatibility, setting a `role` implicitly makes the element + // accessible, unless the role is `none`/`presentation`. + props.role != null && + props.role !== 'none' && + props.role !== 'presentation' + ) { + accessible = true; + } const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel; const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef); diff --git a/packages/react-native/Libraries/Image/__tests__/Image-itest.js b/packages/react-native/Libraries/Image/__tests__/Image-itest.js index 33ac94c94566..d30a9659bd17 100644 --- a/packages/react-native/Libraries/Image/__tests__/Image-itest.js +++ b/packages/react-native/Libraries/Image/__tests__/Image-itest.js @@ -648,6 +648,52 @@ describe('', () => { }); }); + describe('role', () => { + it('implicitly marks the image accessible', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); + }); + + it('does not mark the image accessible for the "none" role', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); + }); + + it('does not mark the image accessible for the "presentation" role', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); + }); + + it('does not override an explicit "accessible" prop', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + , + ); + }); + // `accessible={false}` is not propagated to the mounting layer (it is + // the default), so the implicit `role` behavior must not force it on. + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); + }); + }); + component TestComponent(testID?: ?string, ...props: AccessibilityProps) { return ; } From d72a04b156d3b7ee1a7089d05886dec71d3b2ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 22 Jul 2026 11:26:41 +0200 Subject: [PATCH 2/3] tweaks --- packages/react-native/Libraries/Components/View/View.js | 4 +--- packages/react-native/Libraries/Image/Image.android.js | 3 +-- packages/react-native/Libraries/Image/Image.ios.js | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index 2f8cd0dd1bf9..8884550145de 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -83,9 +83,7 @@ component View(ref?: React.RefSetter, ...props: ViewProps) { resolvedProps.focusable = !tabIndex; } - // For web compatibility, setting a `role` implicitly makes the element - // accessible, unless the role is `none`/`presentation` (which explicitly - // removes semantics) or `accessible` was set explicitly. + // For web compatibility, a `role` implicitly makes the element accessible. if ( resolvedProps.accessible == null && resolvedProps.role != null && diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index e8f0d31f94cc..8c9c2d3a5878 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -285,8 +285,7 @@ let BaseImage: AbstractImageAndroid = ({ } else if (accessible != null) { nativeProps.accessible = accessible; } else if ( - // For web compatibility, setting a `role` implicitly makes the element - // accessible, unless the role is `none`/`presentation`. + // For web compatibility, a `role` implicitly makes the element accessible. nativeProps.role != null && nativeProps.role !== 'none' && nativeProps.role !== 'presentation' diff --git a/packages/react-native/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js index 489374fcdbb7..3e1657a0de11 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -178,8 +178,7 @@ let BaseImage: AbstractImageIOS = ({ } else if (props.accessible != null) { accessible = props.accessible; } else if ( - // For web compatibility, setting a `role` implicitly makes the element - // accessible, unless the role is `none`/`presentation`. + // For web compatibility, a `role` implicitly makes the element accessible. props.role != null && props.role !== 'none' && props.role !== 'presentation' From 59f1947a3b8a1215ebcb416f64c023fc189ff251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 22 Jul 2026 11:44:08 +0200 Subject: [PATCH 3/3] format --- .../Components/View/__tests__/View-itest.js | 24 +++++++++---------- .../Libraries/Image/__tests__/Image-itest.js | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 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 bfc67c159114..5974928c5ebe 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -651,9 +651,9 @@ describe('', () => { root.render(); }); - expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); }); it('does not make the view accessible for the "none" role', () => { @@ -663,9 +663,9 @@ describe('', () => { root.render(); }); - expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); }); it('does not make the view accessible for the "presentation" role', () => { @@ -675,9 +675,9 @@ describe('', () => { root.render(); }); - expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); }); it('does not override an explicit "accessible" prop', () => { @@ -691,9 +691,9 @@ describe('', () => { // `accessible={false}` is not propagated to the mounting layer (it is // the default), so the implicit `role` behavior must not force it on. - expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( - , - ); + expect( + root.getRenderedOutput({props: ['accessible']}).toJSX(), + ).toEqual(); }); }); describe('aria-label', () => { diff --git a/packages/react-native/Libraries/Image/__tests__/Image-itest.js b/packages/react-native/Libraries/Image/__tests__/Image-itest.js index d30a9659bd17..cb52e0c094c2 100644 --- a/packages/react-native/Libraries/Image/__tests__/Image-itest.js +++ b/packages/react-native/Libraries/Image/__tests__/Image-itest.js @@ -654,9 +654,9 @@ describe('', () => { Fantom.runTask(() => { root.render(); }); - expect( - root.getRenderedOutput({props: ['accessible']}).toJSX(), - ).toEqual(); + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); }); it('does not mark the image accessible for the "none" role', () => { @@ -664,9 +664,9 @@ describe('', () => { Fantom.runTask(() => { root.render(); }); - expect( - root.getRenderedOutput({props: ['accessible']}).toJSX(), - ).toEqual(); + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); }); it('does not mark the image accessible for the "presentation" role', () => { @@ -674,9 +674,9 @@ describe('', () => { Fantom.runTask(() => { root.render(); }); - expect( - root.getRenderedOutput({props: ['accessible']}).toJSX(), - ).toEqual(); + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); }); it('does not override an explicit "accessible" prop', () => { @@ -688,9 +688,9 @@ describe('', () => { }); // `accessible={false}` is not propagated to the mounting layer (it is // the default), so the implicit `role` behavior must not force it on. - expect( - root.getRenderedOutput({props: ['accessible']}).toJSX(), - ).toEqual(); + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); }); });