Skip to content

Commit 5dbd04f

Browse files
feat: aria-modal support (#1481)
* feat: `aria-modal` support * chore: add API assumptions tests
1 parent 5a7c693 commit 5dbd04f

File tree

7 files changed

+195
-36
lines changed

7 files changed

+195
-36
lines changed

Diff for: src/__tests__/react-native-api.test.tsx

+144
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,150 @@ test('React Native API assumption: <Switch> renders single host element', () =>
132132
`);
133133
});
134134

135+
test('React Native API assumption: aria-* props render on host View', () => {
136+
const view = render(
137+
<View
138+
testID="test"
139+
aria-busy
140+
aria-checked
141+
aria-disabled
142+
aria-expanded
143+
aria-hidden
144+
aria-label="Label"
145+
aria-labelledby="LabelledBy"
146+
aria-live="polite"
147+
aria-modal
148+
aria-pressed
149+
aria-readonly
150+
aria-required
151+
aria-selected
152+
aria-valuemax={10}
153+
aria-valuemin={0}
154+
aria-valuenow={5}
155+
aria-valuetext="ValueText"
156+
/>
157+
);
158+
159+
expect(view.toJSON()).toMatchInlineSnapshot(`
160+
<View
161+
aria-busy={true}
162+
aria-checked={true}
163+
aria-disabled={true}
164+
aria-expanded={true}
165+
aria-hidden={true}
166+
aria-label="Label"
167+
aria-labelledby="LabelledBy"
168+
aria-live="polite"
169+
aria-modal={true}
170+
aria-pressed={true}
171+
aria-readonly={true}
172+
aria-required={true}
173+
aria-selected={true}
174+
aria-valuemax={10}
175+
aria-valuemin={0}
176+
aria-valuenow={5}
177+
aria-valuetext="ValueText"
178+
testID="test"
179+
/>
180+
`);
181+
});
182+
183+
test('React Native API assumption: aria-* props render on host Text', () => {
184+
const view = render(
185+
<Text
186+
testID="test"
187+
aria-busy
188+
aria-checked
189+
aria-disabled
190+
aria-expanded
191+
aria-hidden
192+
aria-label="Label"
193+
aria-labelledby="LabelledBy"
194+
aria-live="polite"
195+
aria-modal
196+
aria-pressed
197+
aria-readonly
198+
aria-required
199+
aria-selected
200+
aria-valuemax={10}
201+
aria-valuemin={0}
202+
aria-valuenow={5}
203+
aria-valuetext="ValueText"
204+
/>
205+
);
206+
207+
expect(view.toJSON()).toMatchInlineSnapshot(`
208+
<Text
209+
aria-busy={true}
210+
aria-checked={true}
211+
aria-disabled={true}
212+
aria-expanded={true}
213+
aria-hidden={true}
214+
aria-label="Label"
215+
aria-labelledby="LabelledBy"
216+
aria-live="polite"
217+
aria-modal={true}
218+
aria-pressed={true}
219+
aria-readonly={true}
220+
aria-required={true}
221+
aria-selected={true}
222+
aria-valuemax={10}
223+
aria-valuemin={0}
224+
aria-valuenow={5}
225+
aria-valuetext="ValueText"
226+
testID="test"
227+
/>
228+
`);
229+
});
230+
231+
test('React Native API assumption: aria-* props render on host TextInput', () => {
232+
const view = render(
233+
<TextInput
234+
testID="test"
235+
aria-busy
236+
aria-checked
237+
aria-disabled
238+
aria-expanded
239+
aria-hidden
240+
aria-label="Label"
241+
aria-labelledby="LabelledBy"
242+
aria-live="polite"
243+
aria-modal
244+
aria-pressed
245+
aria-readonly
246+
aria-required
247+
aria-selected
248+
aria-valuemax={10}
249+
aria-valuemin={0}
250+
aria-valuenow={5}
251+
aria-valuetext="ValueText"
252+
/>
253+
);
254+
255+
expect(view.toJSON()).toMatchInlineSnapshot(`
256+
<TextInput
257+
aria-busy={true}
258+
aria-checked={true}
259+
aria-disabled={true}
260+
aria-expanded={true}
261+
aria-hidden={true}
262+
aria-label="Label"
263+
aria-labelledby="LabelledBy"
264+
aria-live="polite"
265+
aria-modal={true}
266+
aria-pressed={true}
267+
aria-readonly={true}
268+
aria-required={true}
269+
aria-selected={true}
270+
aria-valuemax={10}
271+
aria-valuemin={0}
272+
aria-valuenow={5}
273+
aria-valuetext="ValueText"
274+
testID="test"
275+
/>
276+
`);
277+
});
278+
135279
test('ScrollView renders correctly', () => {
136280
const screen = render(
137281
<ScrollView testID="scrollView">

Diff for: src/helpers/__tests__/accessiblity.test.tsx

+23-32
Original file line numberDiff line numberDiff line change
@@ -245,71 +245,62 @@ describe('isHiddenFromAccessibility', () => {
245245
);
246246
expect(
247247
isHiddenFromAccessibility(
248-
view.getByTestId('subject', {
249-
includeHiddenElements: true,
250-
})
248+
view.getByTestId('subject', { includeHiddenElements: true })
251249
)
252250
).toBe(true);
253251
});
254252

255-
test('is not triggered for element with accessibilityViewIsModal prop', () => {
253+
test('detects siblings of element with "aria-modal" prop', () => {
256254
const view = render(
257255
<View>
258-
<View accessibilityViewIsModal testID="subject" />
256+
<View aria-modal />
257+
<View testID="subject" />
259258
</View>
260259
);
261260
expect(
262261
isHiddenFromAccessibility(
263-
view.getByTestId('subject', {
264-
includeHiddenElements: true,
265-
})
262+
view.getByTestId('subject', { includeHiddenElements: true })
266263
)
267-
).toBe(false);
264+
).toBe(true);
265+
});
266+
267+
test('is not triggered for element with accessibilityViewIsModal prop', () => {
268+
const view = render(<View accessibilityViewIsModal testID="subject" />);
269+
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
268270
});
269271

270272
test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
271273
const view = render(
272-
<View>
273-
<View accessibilityViewIsModal>
274-
<View testID="subject" />
275-
</View>
274+
<View accessibilityViewIsModal>
275+
<View testID="subject" />
276276
</View>
277277
);
278-
expect(
279-
isHiddenFromAccessibility(
280-
view.getByTestId('subject', {
281-
includeHiddenElements: true,
282-
})
283-
)
284-
).toBe(false);
278+
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
285279
});
286280

287281
test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
288282
const view = render(
289-
<View>
290-
<View accessibilityViewIsModal>
283+
<View accessibilityViewIsModal>
284+
<View>
291285
<View>
292-
<View>
293-
<View testID="subject" />
294-
</View>
286+
<View testID="subject" />
295287
</View>
296288
</View>
297289
</View>
298290
);
299-
expect(
300-
isHiddenFromAccessibility(
301-
view.getByTestId('subject', {
302-
includeHiddenElements: true,
303-
})
304-
)
305-
).toBe(false);
291+
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
306292
});
307293

308294
test('has isInaccessible alias', () => {
309295
expect(isInaccessible).toBe(isHiddenFromAccessibility);
310296
});
311297
});
312298

299+
test('is not triggered for element with "aria-modal" prop', () => {
300+
const view = render(<View aria-modal testID="subject" />);
301+
expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
302+
});
303+
313304
describe('isAccessibilityElement', () => {
314305
test('matches View component properly', () => {
315306
const { getByTestId } = render(

Diff for: src/helpers/__tests__/format-default.test.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { defaultMapProps } from '../format-default';
33
describe('mapPropsForQueryError', () => {
44
test('preserves props that are helpful for debugging', () => {
55
const props = {
6-
'aria-hidden': true,
76
accessibilityElementsHidden: true,
87
accessibilityViewIsModal: true,
98
importantForAccessibility: 'yes',
@@ -17,8 +16,10 @@ describe('mapPropsForQueryError', () => {
1716
'aria-checked': 'ARIA-CHECKED',
1817
'aria-disabled': 'ARIA-DISABLED',
1918
'aria-expanded': 'ARIA-EXPANDED',
19+
'aria-hidden': true,
2020
'aria-label': 'ARIA_LABEL',
2121
'aria-labelledby': 'ARIA_LABELLED_BY',
22+
'aria-modal': true,
2223
'aria-selected': 'ARIA-SELECTED',
2324
placeholder: 'PLACEHOLDER',
2425
value: 'VALUE',

Diff for: src/helpers/accessiblity.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean {
8383
const flatStyle = StyleSheet.flatten(element.props.style) ?? {};
8484
if (flatStyle.display === 'none') return true;
8585

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

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

119+
export function getAccessibilityViewIsModal(element: ReactTestInstance) {
120+
return element.props['aria-modal'] ?? element.props.accessibilityViewIsModal;
121+
}
122+
119123
export function getAccessibilityLabel(
120124
element: ReactTestInstance
121125
): string | undefined {

Diff for: src/helpers/format-default.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const propsToDisplay = [
1515
'aria-hidden',
1616
'aria-label',
1717
'aria-labelledby',
18+
'aria-modal',
1819
'aria-selected',
1920
'defaultValue',
2021
'importantForAccessibility',

Diff for: src/queries/__tests__/makeQueries.test.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,21 @@ describe('printing element tree', () => {
2727
accessibilityHint="HINT"
2828
accessibilityRole="summary"
2929
accessibilityViewIsModal
30+
aria-busy={false}
31+
aria-checked="mixed"
32+
aria-disabled={false}
33+
aria-expanded={false}
3034
aria-hidden
3135
aria-label="ARIA_LABEL"
3236
aria-labelledby="ARIA_LABELLED_BY"
37+
aria-modal
38+
aria-selected={false}
39+
aria-valuemin={10}
40+
aria-valuemax={30}
41+
aria-valuenow={20}
42+
aria-valuetext="Hello Value"
3343
importantForAccessibility="yes"
44+
role="summary"
3445
>
3546
<TextInput
3647
placeholder="PLACEHOLDER"
@@ -51,11 +62,18 @@ describe('printing element tree', () => {
5162
accessibilityLabelledBy="LABELLED_BY"
5263
accessibilityRole="summary"
5364
accessibilityViewIsModal={true}
65+
aria-busy={false}
66+
aria-checked="mixed"
67+
aria-disabled={false}
68+
aria-expanded={false}
5469
aria-hidden={true}
5570
aria-label="ARIA_LABEL"
5671
aria-labelledby="ARIA_LABELLED_BY"
72+
aria-modal={true}
73+
aria-selected={false}
5774
importantForAccessibility="yes"
5875
nativeID="NATIVE_ID"
76+
role="summary"
5977
testID="TEST_ID"
6078
>
6179
<TextInput

Diff for: website/docs/API.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,6 @@ For the scope of this function, element is inaccessible when it, or any of its a
871871
- it has [`aria-hidden`](https://reactnative.dev/docs/accessibility#aria-hidden) prop set to `true`
872872
- it has [`accessibilityElementsHidden`](https://reactnative.dev/docs/accessibility#accessibilityelementshidden-ios) prop set to `true`
873873
- it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants`
874-
- it has sibling host element with [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true`
874+
- 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`
875875

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

0 commit comments

Comments
 (0)