Skip to content

Commit

Permalink
feat: aria-modal support (#1481)
Browse files Browse the repository at this point in the history
* feat: `aria-modal` support

* chore: add API assumptions tests
  • Loading branch information
mdjastrzebski committed Sep 6, 2023
1 parent 5a7c693 commit 5dbd04f
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 36 deletions.
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 @@ -3,7 +3,6 @@ import { defaultMapProps } from '../format-default';
describe('mapPropsForQueryError', () => {
test('preserves props that are helpful for debugging', () => {
const props = {
'aria-hidden': true,
accessibilityElementsHidden: true,
accessibilityViewIsModal: true,
importantForAccessibility: 'yes',
Expand All @@ -17,8 +16,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 @@ -871,6 +871,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.

0 comments on commit 5dbd04f

Please sign in to comment.