Skip to content

Commit da0a888

Browse files
feat: support aria-label and aria-labelledby props (#1475)
* feat: support aria-label and aria-labelledby props * refactor: tweaks * chore: revert unintended docs changes
1 parent 054f36d commit da0a888

File tree

10 files changed

+146
-36
lines changed

10 files changed

+146
-36
lines changed

src/helpers/__tests__/format-default.tsx renamed to src/helpers/__tests__/format-default.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ describe('mapPropsForQueryError', () => {
2020
accessibilityLabelledBy: 'LABELLED_BY',
2121
accessibilityRole: 'ROLE',
2222
accessibilityHint: 'HINT',
23+
'aria-label': 'ARIA_LABEL',
24+
'aria-labelledby': 'ARIA_LABELLED_BY',
2325
placeholder: 'PLACEHOLDER',
2426
value: 'VALUE',
2527
defaultValue: 'DEFAULT_VALUE',

src/helpers/accessiblity.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,17 @@ export function isAccessibilityElement(
115115
export function getAccessibilityRole(element: ReactTestInstance) {
116116
return element.props.role ?? element.props.accessibilityRole;
117117
}
118+
119+
export function getAccessibilityLabel(
120+
element: ReactTestInstance
121+
): string | undefined {
122+
return element.props['aria-label'] ?? element.props.accessibilityLabel;
123+
}
124+
125+
export function getAccessibilityLabelledBy(
126+
element: ReactTestInstance
127+
): string | undefined {
128+
return (
129+
element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy
130+
);
131+
}

src/helpers/format-default.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import { StyleSheet, ViewStyle } from 'react-native';
22
import { MapPropsFunction } from './format';
33

44
const propsToDisplay = [
5-
'testID',
6-
'nativeID',
75
'accessibilityElementsHidden',
8-
'accessibilityViewIsModal',
9-
'importantForAccessibility',
10-
'accessibilityRole',
6+
'accessibilityHint',
117
'accessibilityLabel',
128
'accessibilityLabelledBy',
13-
'accessibilityHint',
14-
'role',
9+
'accessibilityRole',
10+
'accessibilityViewIsModal',
1511
'aria-hidden',
16-
'placeholder',
17-
'value',
12+
'aria-label',
13+
'aria-labelledby',
1814
'defaultValue',
15+
'importantForAccessibility',
16+
'nativeID',
17+
'placeholder',
18+
'role',
19+
'testID',
1920
'title',
21+
'value',
2022
];
2123

2224
/**

src/helpers/matchers/matchLabelText.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matches, TextMatch, TextMatchOptions } from '../../matches';
3+
import {
4+
getAccessibilityLabel,
5+
getAccessibilityLabelledBy,
6+
} from '../accessiblity';
37
import { findAll } from '../findAll';
48
import { matchTextContent } from './matchTextContent';
59

610
export function matchLabelText(
711
root: ReactTestInstance,
812
element: ReactTestInstance,
9-
text: TextMatch,
13+
expectedText: TextMatch,
1014
options: TextMatchOptions = {}
1115
) {
1216
return (
13-
matchAccessibilityLabel(element, text, options) ||
17+
matchAccessibilityLabel(element, expectedText, options) ||
1418
matchAccessibilityLabelledBy(
1519
root,
16-
element.props.accessibilityLabelledBy,
17-
text,
20+
getAccessibilityLabelledBy(element),
21+
expectedText,
1822
options
1923
)
2024
);
2125
}
2226

2327
function matchAccessibilityLabel(
2428
element: ReactTestInstance,
25-
text: TextMatch,
29+
extpectedLabel: TextMatch,
2630
options: TextMatchOptions
2731
) {
28-
const { exact, normalizer } = options;
29-
return matches(text, element.props.accessibilityLabel, normalizer, exact);
32+
return matches(
33+
extpectedLabel,
34+
getAccessibilityLabel(element),
35+
options.normalizer,
36+
options.exact
37+
);
3038
}
3139

3240
function matchAccessibilityLabelledBy(

src/matches.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type TextMatchOptions = {
88

99
export function matches(
1010
matcher: TextMatch,
11-
text: string,
11+
text: string | undefined,
1212
normalizer: NormalizerFn = getDefaultNormalizer(),
1313
exact: boolean = true
1414
): boolean {

src/queries/__tests__/labelText.test.tsx

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ const TEXT_HINT = 'static text';
1010
const NO_MATCHES_TEXT: any = 'not-existent-element';
1111

1212
const getMultipleInstancesFoundMessage = (value: string) => {
13-
return `Found multiple elements with accessibilityLabel: ${value}`;
13+
return `Found multiple elements with accessibility label: ${value}`;
1414
};
1515

1616
const getNoInstancesFoundMessage = (value: string) => {
17-
return `Unable to find an element with accessibilityLabel: ${value}`;
17+
return `Unable to find an element with accessibility label: ${value}`;
1818
};
1919

2020
const Typography = ({ children, ...rest }: any) => {
@@ -161,7 +161,7 @@ test('byLabelText queries support hidden option', () => {
161161
).toBeFalsy();
162162
expect(() => getByLabelText('hidden', { includeHiddenElements: false }))
163163
.toThrowErrorMatchingInlineSnapshot(`
164-
"Unable to find an element with accessibilityLabel: hidden
164+
"Unable to find an element with accessibility label: hidden
165165
166166
<Text
167167
accessibilityLabel="hidden"
@@ -176,6 +176,24 @@ test('byLabelText queries support hidden option', () => {
176176
`);
177177
});
178178

179+
test('getByLabelText supports aria-label', async () => {
180+
const screen = render(
181+
<>
182+
<View testID="view" aria-label="view-label" />
183+
<Text testID="text" aria-label="text-label">
184+
Text
185+
</Text>
186+
<TextInput testID="text-input" aria-label="text-input-label" />
187+
</>
188+
);
189+
190+
expect(screen.getByLabelText('view-label')).toBe(screen.getByTestId('view'));
191+
expect(screen.getByLabelText('text-label')).toBe(screen.getByTestId('text'));
192+
expect(screen.getByLabelText('text-input-label')).toBe(
193+
screen.getByTestId('text-input')
194+
);
195+
});
196+
179197
test('getByLabelText supports accessibilityLabelledBy', async () => {
180198
const { getByLabelText, getByTestId } = render(
181199
<>
@@ -202,11 +220,45 @@ test('getByLabelText supports nested accessibilityLabelledBy', async () => {
202220
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
203221
});
204222

223+
test('getByLabelText supports aria-labelledby', async () => {
224+
const screen = render(
225+
<>
226+
<Text nativeID="label">Text Label</Text>
227+
<TextInput testID="text-input" aria-labelledby="label" />
228+
</>
229+
);
230+
231+
expect(screen.getByLabelText('Text Label')).toBe(
232+
screen.getByTestId('text-input')
233+
);
234+
expect(screen.getByLabelText(/text label/i)).toBe(
235+
screen.getByTestId('text-input')
236+
);
237+
});
238+
239+
test('getByLabelText supports nested aria-labelledby', async () => {
240+
const screen = render(
241+
<>
242+
<View nativeID="label">
243+
<Text>Nested Text Label</Text>
244+
</View>
245+
<TextInput testID="text-input" aria-labelledby="label" />
246+
</>
247+
);
248+
249+
expect(screen.getByLabelText('Nested Text Label')).toBe(
250+
screen.getByTestId('text-input')
251+
);
252+
expect(screen.getByLabelText(/nested text label/i)).toBe(
253+
screen.getByTestId('text-input')
254+
);
255+
});
256+
205257
test('error message renders the element tree, preserving only helpful props', async () => {
206258
const view = render(<TouchableOpacity accessibilityLabel="LABEL" key="3" />);
207259

208260
expect(() => view.getByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(`
209-
"Unable to find an element with accessibilityLabel: FOO
261+
"Unable to find an element with accessibility label: FOO
210262
211263
<View
212264
accessibilityLabel="LABEL"
@@ -215,7 +267,7 @@ test('error message renders the element tree, preserving only helpful props', as
215267

216268
expect(() => view.getAllByLabelText('FOO'))
217269
.toThrowErrorMatchingInlineSnapshot(`
218-
"Unable to find an element with accessibilityLabel: FOO
270+
"Unable to find an element with accessibility label: FOO
219271
220272
<View
221273
accessibilityLabel="LABEL"
@@ -224,7 +276,7 @@ test('error message renders the element tree, preserving only helpful props', as
224276

225277
await expect(view.findByLabelText('FOO')).rejects
226278
.toThrowErrorMatchingInlineSnapshot(`
227-
"Unable to find an element with accessibilityLabel: FOO
279+
"Unable to find an element with accessibility label: FOO
228280
229281
<View
230282
accessibilityLabel="LABEL"
@@ -233,7 +285,7 @@ test('error message renders the element tree, preserving only helpful props', as
233285

234286
await expect(view.findAllByLabelText('FOO')).rejects
235287
.toThrowErrorMatchingInlineSnapshot(`
236-
"Unable to find an element with accessibilityLabel: FOO
288+
"Unable to find an element with accessibility label: FOO
237289
238290
<View
239291
accessibilityLabel="LABEL"

src/queries/__tests__/makeQueries.test.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ describe('printing element tree', () => {
1818
test('prints helpful props but not others', async () => {
1919
const { getByText } = render(
2020
<View
21-
aria-hidden
22-
accessibilityElementsHidden
23-
accessibilityViewIsModal
24-
importantForAccessibility="yes"
21+
key="this is filtered"
2522
testID="TEST_ID"
2623
nativeID="NATIVE_ID"
24+
accessibilityElementsHidden
2725
accessibilityLabel="LABEL"
2826
accessibilityLabelledBy="LABELLED_BY"
29-
accessibilityRole="summary"
3027
accessibilityHint="HINT"
31-
key="this is filtered"
28+
accessibilityRole="summary"
29+
accessibilityViewIsModal
30+
aria-hidden
31+
aria-label="ARIA_LABEL"
32+
aria-labelledby="ARIA_LABELLED_BY"
33+
importantForAccessibility="yes"
3234
>
3335
<TextInput
3436
placeholder="PLACEHOLDER"
@@ -50,6 +52,8 @@ describe('printing element tree', () => {
5052
accessibilityRole="summary"
5153
accessibilityViewIsModal={true}
5254
aria-hidden={true}
55+
aria-label="ARIA_LABEL"
56+
aria-labelledby="ARIA_LABELLED_BY"
5357
importantForAccessibility="yes"
5458
nativeID="NATIVE_ID"
5559
testID="TEST_ID"

src/queries/__tests__/role.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,34 @@ describe('supports name option', () => {
166166
);
167167
});
168168

169+
test('returns an element that has the corresponding role and a children with a matching aria-label', () => {
170+
const { getByRole } = render(
171+
<TouchableOpacity accessibilityRole="button" testID="target-button">
172+
<Text aria-label="Save" />
173+
</TouchableOpacity>
174+
);
175+
176+
// assert on the testId to be sure that the returned element is the one with the accessibilityRole
177+
expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
178+
'target-button'
179+
);
180+
});
181+
182+
test('returns an element that has the corresponding role and a matching aria-label', () => {
183+
const { getByRole } = render(
184+
<TouchableOpacity
185+
accessibilityRole="button"
186+
testID="target-button"
187+
aria-label="Save"
188+
></TouchableOpacity>
189+
);
190+
191+
// assert on the testId to be sure that the returned element is the one with the accessibilityRole
192+
expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
193+
'target-button'
194+
);
195+
});
196+
169197
test('returns an element when the direct child is text', () => {
170198
const { getByRole, getByTestId } = render(
171199
<Text accessibilityRole="header" testID="target-header">

src/queries/labelText.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ function queryAllByLabelText(instance: ReactTestInstance) {
2626
}
2727

2828
const getMultipleError = (labelText: TextMatch) =>
29-
`Found multiple elements with accessibilityLabel: ${String(labelText)} `;
29+
`Found multiple elements with accessibility label: ${String(labelText)} `;
3030
const getMissingError = (labelText: TextMatch) =>
31-
`Unable to find an element with accessibilityLabel: ${String(labelText)}`;
31+
`Unable to find an element with accessibility label: ${String(labelText)}`;
3232

3333
const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
3434
queryAllByLabelText,

website/docs/Queries.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ title: Queries
3030
- [Precision](#precision)
3131
- [Normalization](#normalization)
3232
- [Unit testing helpers](#unit-testing-helpers)
33-
- [`UNSAFE_ByType`](#unsafebytype)
34-
- [`UNSAFE_ByProps`](#unsafebyprops)
33+
- [`UNSAFE_ByType`](#unsafe_bytype)
34+
- [`UNSAFE_ByProps`](#unsafe_byprops)
3535

3636
## Variants
3737

@@ -272,8 +272,8 @@ getByLabelText(
272272
```
273273

274274
Returns a `ReactTestInstance` with matching label:
275-
- either by matching [`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
276-
- or by matching text content of view referenced by [`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
275+
- either by matching [`aria-label`](https://reactnative.dev/docs/accessibility#aria-label)/[`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
276+
- or by matching text content of view referenced by [`aria-labelledby`](https://reactnative.dev/docs/accessibility#aria-labelledby-android)/[`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
277277

278278
```jsx
279279
import { render, screen } from '@testing-library/react-native';

0 commit comments

Comments
 (0)