Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/react-native/Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ component View(ref?: React.RefSetter<ViewInstance>, ...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 ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,59 @@ describe('<View>', () => {
).toEqual(null);
});
});
describe('role', () => {
it('implicitly makes the view accessible', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<View role="button" />);
});

expect(
root.getRenderedOutput({props: ['accessible']}).toJSX(),
).toEqual(<rn-view accessible="true" />);
});

it('does not make the view accessible for the "none" role', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<View role="none" collapsable={false} />);
});

expect(
root.getRenderedOutput({props: ['accessible']}).toJSX(),
).toEqual(<rn-view />);
});

it('does not make the view accessible for the "presentation" role', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<View role="presentation" collapsable={false} />);
});

expect(
root.getRenderedOutput({props: ['accessible']}).toJSX(),
).toEqual(<rn-view />);
});

it('does not override an explicit "accessible" prop', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
<View role="button" accessible={false} collapsable={false} />,
);
});

// `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(<rn-view />);
});
});
describe('aria-label', () => {
it('is mapped to accessibilityLabel', () => {
const root = Fantom.createRoot();
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native/Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
20 changes: 17 additions & 3 deletions packages/react-native/Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions packages/react-native/Libraries/Image/__tests__/Image-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,52 @@ describe('<Image>', () => {
});
});

describe('role', () => {
it('implicitly marks the image accessible', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image role="button" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
<rn-image accessible="true" />,
);
});

it('does not mark the image accessible for the "none" role', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image role="none" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
<rn-image />,
);
});

it('does not mark the image accessible for the "presentation" role', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<Image role="presentation" source={LOGO_SOURCE} />);
});
expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual(
<rn-image />,
);
});

it('does not override an explicit "accessible" prop', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<Image role="button" accessible={false} source={LOGO_SOURCE} />,
);
});
// `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(
<rn-image />,
);
});
});

component TestComponent(testID?: ?string, ...props: AccessibilityProps) {
return <Image {...props} testID={testID} source={LOGO_SOURCE} />;
}
Expand Down
Loading