Skip to content

Commit 37d73ae

Browse files
committed
feat: automatically set accessible when setting role prop
1 parent eb4d389 commit 37d73ae

5 files changed

Lines changed: 137 additions & 3 deletions

File tree

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

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

86+
// For web compatibility, setting a `role` implicitly makes the element
87+
// accessible, unless the role is `none`/`presentation` (which explicitly
88+
// removes semantics) or `accessible` was set explicitly.
89+
if (
90+
resolvedProps.accessible == null &&
91+
resolvedProps.role != null &&
92+
resolvedProps.role !== 'none' &&
93+
resolvedProps.role !== 'presentation'
94+
) {
95+
resolvedProps.accessible = true;
96+
}
97+
8698
if (
8799
accessibilityState != null ||
88100
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(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
655+
<rn-view accessible="true" />,
656+
);
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(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
667+
<rn-view />,
668+
);
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(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
679+
<rn-view />,
680+
);
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(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
695+
<rn-view />,
696+
);
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ let BaseImage: AbstractImageAndroid = ({
284284
nativeProps.accessible = true;
285285
} else if (accessible != null) {
286286
nativeProps.accessible = accessible;
287+
} else if (
288+
// For web compatibility, setting a `role` implicitly makes the element
289+
// accessible, unless the role is `none`/`presentation`.
290+
nativeProps.role != null &&
291+
nativeProps.role !== 'none' &&
292+
nativeProps.role !== 'presentation'
293+
) {
294+
nativeProps.accessible = true;
287295
}
288296

289297
if (

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,24 @@ 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, setting a `role` implicitly makes the element
182+
// accessible, unless the role is `none`/`presentation`.
183+
props.role != null &&
184+
props.role !== 'none' &&
185+
props.role !== 'presentation'
186+
) {
187+
accessible = true;
188+
}
174189
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
175190

176191
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(
658+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
659+
).toEqual(<rn-image accessible="true" />);
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(
668+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
669+
).toEqual(<rn-image />);
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(
678+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
679+
).toEqual(<rn-image />);
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(
692+
root.getRenderedOutput({props: ['accessible']}).toJSX(),
693+
).toEqual(<rn-image />);
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)