Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aria-modal support #1481

Merged
merged 2 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/__tests__/react-native-api.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,150 @@ test('React Native API assumption: <Switch> renders single host element', () =>
`);
});

test('React Native API assumption: aria-* props render on host View', () => {
const view = render(
<View
testID="test"
aria-busy
aria-checked
aria-disabled
aria-expanded
aria-hidden
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal
aria-pressed
aria-readonly
aria-required
aria-selected
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
/>
);

expect(view.toJSON()).toMatchInlineSnapshot(`
<View
aria-busy={true}
aria-checked={true}
aria-disabled={true}
aria-expanded={true}
aria-hidden={true}
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal={true}
aria-pressed={true}
aria-readonly={true}
aria-required={true}
aria-selected={true}
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
testID="test"
/>
`);
});

test('React Native API assumption: aria-* props render on host Text', () => {
const view = render(
<Text
testID="test"
aria-busy
aria-checked
aria-disabled
aria-expanded
aria-hidden
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal
aria-pressed
aria-readonly
aria-required
aria-selected
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
/>
);

expect(view.toJSON()).toMatchInlineSnapshot(`
<Text
aria-busy={true}
aria-checked={true}
aria-disabled={true}
aria-expanded={true}
aria-hidden={true}
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal={true}
aria-pressed={true}
aria-readonly={true}
aria-required={true}
aria-selected={true}
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
testID="test"
/>
`);
});

test('React Native API assumption: aria-* props render on host TextInput', () => {
const view = render(
<TextInput
testID="test"
aria-busy
aria-checked
aria-disabled
aria-expanded
aria-hidden
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal
aria-pressed
aria-readonly
aria-required
aria-selected
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
/>
);

expect(view.toJSON()).toMatchInlineSnapshot(`
<TextInput
aria-busy={true}
aria-checked={true}
aria-disabled={true}
aria-expanded={true}
aria-hidden={true}
aria-label="Label"
aria-labelledby="LabelledBy"
aria-live="polite"
aria-modal={true}
aria-pressed={true}
aria-readonly={true}
aria-required={true}
aria-selected={true}
aria-valuemax={10}
aria-valuemin={0}
aria-valuenow={5}
aria-valuetext="ValueText"
testID="test"
/>
`);
});

test('ScrollView renders correctly', () => {
const screen = render(
<ScrollView testID="scrollView">
Expand Down
55 changes: 23 additions & 32 deletions src/helpers/__tests__/accessiblity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,71 +245,62 @@ describe('isHiddenFromAccessibility', () => {
);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
view.getByTestId('subject', { includeHiddenElements: true })
)
).toBe(true);
});

test('is not triggered for element with accessibilityViewIsModal prop', () => {
test('detects siblings of element with "aria-modal" prop', () => {
const view = render(
<View>
<View accessibilityViewIsModal testID="subject" />
<View aria-modal />
<View testID="subject" />
</View>
);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
view.getByTestId('subject', { includeHiddenElements: true })
)
).toBe(false);
).toBe(true);
});

test('is not triggered for element with accessibilityViewIsModal prop', () => {
const view = render(<View accessibilityViewIsModal testID="subject" />);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
});

test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
const view = render(
<View>
<View accessibilityViewIsModal>
<View testID="subject" />
</View>
<View accessibilityViewIsModal>
<View testID="subject" />
</View>
);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
});

test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
const view = render(
<View>
<View accessibilityViewIsModal>
<View accessibilityViewIsModal>
<View>
<View>
<View>
<View testID="subject" />
</View>
<View testID="subject" />
</View>
</View>
</View>
);
expect(
isHiddenFromAccessibility(
view.getByTestId('subject', {
includeHiddenElements: true,
})
)
).toBe(false);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
});

test('has isInaccessible alias', () => {
expect(isInaccessible).toBe(isHiddenFromAccessibility);
});
});

test('is not triggered for element with "aria-modal" prop', () => {
const view = render(<View aria-modal testID="subject" />);
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
});

describe('isAccessibilityElement', () => {
test('matches View component properly', () => {
const { getByTestId } = render(
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/__tests__/format-default.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const node: ReactTestRendererJSON = {
describe('mapPropsForQueryError', () => {
test('preserves props that are helpful for debugging', () => {
const props = {
'aria-hidden': true,
accessibilityElementsHidden: true,
accessibilityViewIsModal: true,
importantForAccessibility: 'yes',
Expand All @@ -24,8 +23,10 @@ describe('mapPropsForQueryError', () => {
'aria-checked': 'ARIA-CHECKED',
'aria-disabled': 'ARIA-DISABLED',
'aria-expanded': 'ARIA-EXPANDED',
'aria-hidden': true,
'aria-label': 'ARIA_LABEL',
'aria-labelledby': 'ARIA_LABELLED_BY',
'aria-modal': true,
'aria-selected': 'ARIA-SELECTED',
placeholder: 'PLACEHOLDER',
value: 'VALUE',
Expand Down
8 changes: 6 additions & 2 deletions src/helpers/accessiblity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean {
const flatStyle = StyleSheet.flatten(element.props.style) ?? {};
if (flatStyle.display === 'none') return true;

// iOS: accessibilityViewIsModal
// iOS: accessibilityViewIsModal or aria-modal
// See: https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios
const hostSiblings = getHostSiblings(element);
if (hostSiblings.some((sibling) => sibling.props.accessibilityViewIsModal)) {
if (hostSiblings.some((sibling) => getAccessibilityViewIsModal(sibling))) {
return true;
}

Expand Down Expand Up @@ -116,6 +116,10 @@ export function getAccessibilityRole(element: ReactTestInstance) {
return element.props.role ?? element.props.accessibilityRole;
}

export function getAccessibilityViewIsModal(element: ReactTestInstance) {
return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal;
}

export function getAccessibilityLabel(
element: ReactTestInstance
): string | undefined {
Expand Down
1 change: 1 addition & 0 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const propsToDisplay = [
'aria-hidden',
'aria-label',
'aria-labelledby',
'aria-modal',
'aria-selected',
'defaultValue',
'importantForAccessibility',
Expand Down
18 changes: 18 additions & 0 deletions src/queries/__tests__/makeQueries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,21 @@ describe('printing element tree', () => {
accessibilityHint="HINT"
accessibilityRole="summary"
accessibilityViewIsModal
aria-busy={false}
aria-checked="mixed"
aria-disabled={false}
aria-expanded={false}
aria-hidden
aria-label="ARIA_LABEL"
aria-labelledby="ARIA_LABELLED_BY"
aria-modal
aria-selected={false}
aria-valuemin={10}
aria-valuemax={30}
aria-valuenow={20}
aria-valuetext="Hello Value"
importantForAccessibility="yes"
role="summary"
>
<TextInput
placeholder="PLACEHOLDER"
Expand All @@ -51,11 +62,18 @@ describe('printing element tree', () => {
accessibilityLabelledBy="LABELLED_BY"
accessibilityRole="summary"
accessibilityViewIsModal={true}
aria-busy={false}
aria-checked="mixed"
aria-disabled={false}
aria-expanded={false}
aria-hidden={true}
aria-label="ARIA_LABEL"
aria-labelledby="ARIA_LABELLED_BY"
aria-modal={true}
aria-selected={false}
importantForAccessibility="yes"
nativeID="NATIVE_ID"
role="summary"
testID="TEST_ID"
>
<TextInput
Expand Down
2 changes: 1 addition & 1 deletion website/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,6 @@ For the scope of this function, element is inaccessible when it, or any of its a
- it has [`aria-hidden`](https://reactnative.dev/docs/accessibility#aria-hidden) prop set to `true`
- it has [`accessibilityElementsHidden`](https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios) prop set to `true`
- it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants`
- it has sibling host element with [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true`
- it has sibling host element with either [`aria-modal`](https://reactnative.dev/docs/accessibility#aria-modal-ios) or [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true`

Specifying `accessible={false}`, `accessiblityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible.