Skip to content

Commit 4043e81

Browse files
mdjastrzebskimeta-codesync[bot]
authored andcommitted
feat(a11y): automatically set accessible prop with role prop (#57633)
Summary: Setting `role` prop (except `none`/`presentation`) automatically sets `accessible` prop on `View` and `Image`. This aligns web behavior when setting `role` attribute is enough to add the element into accessibility tree. This aligns also with `Image` `alt` web-compat prop which also enables `accessible` when set. The change skips `Text`, `TextInput`, etc as they are implicitly members of accessibility tree by default. This change also intentionally skips `accessibilityRole` as legacy, non-web version. This also helps when using React Strict DOM, as it exposes `role` prop, but does not expose `accessible` prop, hence setting `role` on `h.div` basically has no effect. I think this changes is better suited for RN repo though. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [CHANGED] Automatically set `acessible` prop when `role` prop is set (except `none`/`presenation`) Pull Request resolved: #57633 Test Plan: Added relevant Fantom tests. Reviewed By: andrewdacenko Differential Revision: D113754156 Pulled By: javache fbshipit-source-id: 4805b92d026d21e8a40b69fcb02f75e55254a996
1 parent 5e40a17 commit 4043e81

5 files changed

Lines changed: 133 additions & 3 deletions

File tree

packages/react-native/Libraries/Components/View/View.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ component View(ref?: React.RefSetter<ViewInstance>, ...props: ViewProps) {
8383
resolvedProps.focusable = !tabIndex;
8484
}
8585

86+
// For web compatibility, a `role` implicitly makes the element accessible.
87+
if (
88+
resolvedProps.accessible == null &&
89+
resolvedProps.role != null &&
90+
resolvedProps.role !== 'none' &&
91+
resolvedProps.role !== 'presentation'
92+
) {
93+
resolvedProps.accessible = true;
94+
}
95+
8696
if (
8797
accessibilityState != null ||
8898
ariaBusy != null ||

packages/react-native/Libraries/Components/View/__tests__/View-itest.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,59 @@ describe('<View>', () => {
643643
).toEqual(null);
644644
});
645645
});
646+
describe('role', () => {
647+
it('implicitly makes the view accessible', () => {
648+
const root = Fantom.createRoot();
649+
650+
Fantom.runTask(() => {
651+
root.render(<View role="button" />);
652+
});
653+
654+
expect(
655+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
656+
).toEqual(<rn-view accessible="true" />);
657+
});
658+
659+
it('does not make the view accessible for the "none" role', () => {
660+
const root = Fantom.createRoot();
661+
662+
Fantom.runTask(() => {
663+
root.render(<View role="none" collapsable={false} />);
664+
});
665+
666+
expect(
667+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
668+
).toEqual(<rn-view />);
669+
});
670+
671+
it('does not make the view accessible for the "presentation" role', () => {
672+
const root = Fantom.createRoot();
673+
674+
Fantom.runTask(() => {
675+
root.render(<View role="presentation" collapsable={false} />);
676+
});
677+
678+
expect(
679+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
680+
).toEqual(<rn-view />);
681+
});
682+
683+
it('does not override an explicit "accessible" prop', () => {
684+
const root = Fantom.createRoot();
685+
686+
Fantom.runTask(() => {
687+
root.render(
688+
<View role="button" accessible={false} collapsable={false} />,
689+
);
690+
});
691+
692+
// `accessible={false}` is not propagated to the mounting layer (it is
693+
// the default), so the implicit `role` behavior must not force it on.
694+
expect(
695+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
696+
).toEqual(<rn-view />);
697+
});
698+
});
646699
describe('aria-label', () => {
647700
it('is mapped to accessibilityLabel', () => {
648701
const root = Fantom.createRoot();

packages/react-native/Libraries/Image/Image.android.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,13 @@ let BaseImage: AbstractImageAndroid = ({
284284
nativeProps.accessible = true;
285285
} else if (accessible != null) {
286286
nativeProps.accessible = accessible;
287+
} else if (
288+
// For web compatibility, a `role` implicitly makes the element accessible.
289+
nativeProps.role != null &&
290+
nativeProps.role !== 'none' &&
291+
nativeProps.role !== 'presentation'
292+
) {
293+
nativeProps.accessible = true;
287294
}
288295

289296
if (

packages/react-native/Libraries/Image/Image.ios.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,23 @@ let BaseImage: AbstractImageIOS = ({
168168
selected: ariaSelected ?? props.accessibilityState?.selected,
169169
};
170170

171-
// In order for `aria-hidden` to work on iOS we must set `accessible` to false (`accessibilityElementsHidden` is not enough).
172-
const accessible =
173-
ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
171+
let accessible;
172+
if (ariaHidden === true) {
173+
// In order for `aria-hidden` to work on iOS we must set `accessible` to
174+
// false (`accessibilityElementsHidden` is not enough).
175+
accessible = false;
176+
} else if (props.alt !== undefined) {
177+
accessible = true;
178+
} else if (props.accessible != null) {
179+
accessible = props.accessible;
180+
} else if (
181+
// For web compatibility, a `role` implicitly makes the element accessible.
182+
props.role != null &&
183+
props.role !== 'none' &&
184+
props.role !== 'presentation'
185+
) {
186+
accessible = true;
187+
}
174188
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
175189

176190
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);

packages/react-native/Libraries/Image/__tests__/Image-itest.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,52 @@ describe('<Image>', () => {
648648
});
649649
});
650650

651+
describe('role', () => {
652+
it('implicitly marks the image accessible', () => {
653+
const root = Fantom.createRoot();
654+
Fantom.runTask(() => {
655+
root.render(<Image role="button" source={LOGO_SOURCE} />);
656+
});
657+
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
658+
<rn-image accessible="true" />,
659+
);
660+
});
661+
662+
it('does not mark the image accessible for the "none" role', () => {
663+
const root = Fantom.createRoot();
664+
Fantom.runTask(() => {
665+
root.render(<Image role="none" source={LOGO_SOURCE} />);
666+
});
667+
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
668+
<rn-image />,
669+
);
670+
});
671+
672+
it('does not mark the image accessible for the "presentation" role', () => {
673+
const root = Fantom.createRoot();
674+
Fantom.runTask(() => {
675+
root.render(<Image role="presentation" source={LOGO_SOURCE} />);
676+
});
677+
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
678+
<rn-image />,
679+
);
680+
});
681+
682+
it('does not override an explicit "accessible" prop', () => {
683+
const root = Fantom.createRoot();
684+
Fantom.runTask(() => {
685+
root.render(
686+
<Image role="button" accessible={false} source={LOGO_SOURCE} />,
687+
);
688+
});
689+
// `accessible={false}` is not propagated to the mounting layer (it is
690+
// the default), so the implicit `role` behavior must not force it on.
691+
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
692+
<rn-image />,
693+
);
694+
});
695+
});
696+
651697
component TestComponent(testID?: ?string, ...props: AccessibilityProps) {
652698
return <Image {...props} testID={testID} source={LOGO_SOURCE} />;
653699
}

0 commit comments

Comments
 (0)