diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index 461f7707c7f..8884550145d 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -83,6 +83,16 @@ component View(ref?: React.RefSetter, ...props: ViewProps) { resolvedProps.focusable = !tabIndex; } + // For web compatibility, a `role` implicitly makes the element accessible. + 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 3c46277f3a8..5974928c5eb 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 b20bdfa5f6f..8c9c2d3a587 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -284,6 +284,13 @@ let BaseImage: AbstractImageAndroid = ({ nativeProps.accessible = true; } else if (accessible != null) { nativeProps.accessible = accessible; + } else if ( + // For web compatibility, a `role` implicitly makes the element accessible. + 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 3fc14f2bcf1..3e1657a0de1 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -168,9 +168,23 @@ 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, a `role` implicitly makes the element accessible. + 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 33ac94c9456..cb52e0c094c 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 ; }