Skip to content

Commit 83cafe4

Browse files
refactor(v13): a11y label helpers (#1666)
1 parent 5a10b31 commit 83cafe4

File tree

7 files changed

+63
-71
lines changed

7 files changed

+63
-71
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
- name: Setup Node.js and deps
7373
uses: ./.github/actions/setup-deps
7474

75-
- name: Test in concurrent mode
75+
- name: Test in legacy mode
7676
run: CONCURRENT_MODE=0 yarn test:ci
7777

7878
test-website:

src/helpers/__tests__/accessiblity.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { View, Text, TextInput, Pressable, Switch, TouchableOpacity } from 'react-native';
33
import { render, isHiddenFromAccessibility, isInaccessible, screen } from '../..';
4-
import { isAccessibilityElement } from '../accessibility';
4+
import { computeAriaLabel, isAccessibilityElement } from '../accessibility';
55

66
describe('isHiddenFromAccessibility', () => {
77
test('returns false for accessible elements', () => {
@@ -371,3 +371,39 @@ describe('isAccessibilityElement', () => {
371371
expect(isAccessibilityElement(null)).toEqual(false);
372372
});
373373
});
374+
375+
describe('computeAriaLabel', () => {
376+
test('supports basic usage', () => {
377+
render(
378+
<View>
379+
<View testID="label" aria-label="Internal Label" />
380+
<View testID="label-by-id" aria-labelledby="external-label" />
381+
<View nativeID="external-label">
382+
<Text>External Text</Text>
383+
</View>
384+
<View testID="no-label" />
385+
<View testID="text-content">
386+
<Text>Text Content</Text>
387+
</View>
388+
</View>,
389+
);
390+
391+
expect(computeAriaLabel(screen.getByTestId('label'))).toEqual('Internal Label');
392+
expect(computeAriaLabel(screen.getByTestId('label-by-id'))).toEqual('External Text');
393+
expect(computeAriaLabel(screen.getByTestId('no-label'))).toBeUndefined();
394+
expect(computeAriaLabel(screen.getByTestId('text-content'))).toBeUndefined();
395+
});
396+
397+
test('label priority', () => {
398+
render(
399+
<View>
400+
<View testID="subject" aria-label="Internal Label" aria-labelledby="external-content" />
401+
<View nativeID="external-content">
402+
<Text>External Label</Text>
403+
</View>
404+
</View>,
405+
);
406+
407+
expect(computeAriaLabel(screen.getByTestId('subject'))).toEqual('External Label');
408+
});
409+
});

src/helpers/__tests__/component-tree.test.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,4 @@ describe('getUnsafeRootElement()', () => {
228228
const view = screen.getByTestId('view');
229229
expect(getUnsafeRootElement(view)).toEqual(screen.UNSAFE_root);
230230
});
231-
232-
it('returns null for null', () => {
233-
expect(getUnsafeRootElement(null)).toEqual(null);
234-
});
235231
});

src/helpers/accessibility.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
Role,
66
StyleSheet,
77
} from 'react-native';
8-
import { ReactTestInstance } from 'react-test-renderer';
9-
import { getHostSiblings, getUnsafeRootElement } from './component-tree';
8+
import type { ReactTestInstance } from 'react-test-renderer';
9+
import { getHostSiblings, getUnsafeRootElement, isHostElement } from './component-tree';
10+
import { findAll } from './find-all';
1011
import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names';
1112
import { getTextContent } from './text-content';
1213
import { isTextInputEditable } from './text-input';
@@ -158,6 +159,19 @@ export function computeAriaModal(element: ReactTestInstance): boolean | undefine
158159
}
159160

160161
export function computeAriaLabel(element: ReactTestInstance): string | undefined {
162+
const labelElementId = element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
163+
if (labelElementId) {
164+
const rootElement = getUnsafeRootElement(element);
165+
const labelElement = findAll(
166+
rootElement,
167+
(node) => isHostElement(node) && node.props.nativeID === labelElementId,
168+
{ includeHiddenElements: true },
169+
);
170+
if (labelElement.length > 0) {
171+
return getTextContent(labelElement[0]);
172+
}
173+
}
174+
161175
const explicitLabel = element.props['aria-label'] ?? element.props.accessibilityLabel;
162176
if (explicitLabel) {
163177
return explicitLabel;
@@ -171,10 +185,6 @@ export function computeAriaLabel(element: ReactTestInstance): string | undefined
171185
return undefined;
172186
}
173187

174-
export function computeAriaLabelledBy(element: ReactTestInstance): string | undefined {
175-
return element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
176-
}
177-
178188
// See: https://github.com/callstack/react-native-testing-library/wiki/Accessibility:-State#busy-state
179189
export function computeAriaBusy({ props }: ReactTestInstance): boolean {
180190
return props['aria-busy'] ?? props.accessibilityState?.busy ?? false;
@@ -234,21 +244,7 @@ export function computeAriaValue(element: ReactTestInstance): AccessibilityValue
234244
}
235245

236246
export function computeAccessibleName(element: ReactTestInstance): string | undefined {
237-
const label = computeAriaLabel(element);
238-
if (label) {
239-
return label;
240-
}
241-
242-
const labelElementId = computeAriaLabelledBy(element);
243-
if (labelElementId) {
244-
const rootElement = getUnsafeRootElement(element);
245-
const labelElement = rootElement?.findByProps({ nativeID: labelElementId });
246-
if (labelElement) {
247-
return getTextContent(labelElement);
248-
}
249-
}
250-
251-
return getTextContent(element);
247+
return computeAriaLabel(element) ?? getTextContent(element);
252248
}
253249

254250
type RoleSupportMap = Partial<Record<Role | AccessibilityRole, true>>;

src/helpers/component-tree.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function isHostElement(element?: ReactTestInstance | null): element is Ho
1313
return typeof element?.type === 'string';
1414
}
1515

16-
export function isElementMounted(element: ReactTestInstance | null) {
16+
export function isElementMounted(element: ReactTestInstance) {
1717
return getUnsafeRootElement(element) === screen.UNSAFE_root;
1818
}
1919

@@ -91,11 +91,7 @@ export function getHostSiblings(element: ReactTestInstance | null): HostTestInst
9191
* @param element The element start traversing from.
9292
* @returns The root element of the tree (host or composite).
9393
*/
94-
export function getUnsafeRootElement(element: ReactTestInstance | null) {
95-
if (element == null) {
96-
return null;
97-
}
98-
94+
export function getUnsafeRootElement(element: ReactTestInstance) {
9995
let current = element;
10096
while (current.parent) {
10197
current = current.parent;
Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
11
import { ReactTestInstance } from 'react-test-renderer';
22
import { matches, TextMatch, TextMatchOptions } from '../../matches';
3-
import { computeAriaLabel, computeAriaLabelledBy } from '../accessibility';
4-
import { findAll } from '../find-all';
5-
import { matchTextContent } from './match-text-content';
3+
import { computeAriaLabel } from '../accessibility';
64

7-
export function matchLabelText(
8-
root: ReactTestInstance,
9-
element: ReactTestInstance,
10-
expectedText: TextMatch,
11-
options: TextMatchOptions = {},
12-
) {
13-
return (
14-
matchAccessibilityLabel(element, expectedText, options) ||
15-
matchAccessibilityLabelledBy(root, computeAriaLabelledBy(element), expectedText, options)
16-
);
17-
}
18-
19-
function matchAccessibilityLabel(
5+
export function matchAccessibilityLabel(
206
element: ReactTestInstance,
217
expectedLabel: TextMatch,
22-
options: TextMatchOptions,
8+
options?: TextMatchOptions,
239
) {
24-
return matches(expectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
25-
}
26-
27-
function matchAccessibilityLabelledBy(
28-
root: ReactTestInstance,
29-
nativeId: string | undefined,
30-
text: TextMatch,
31-
options: TextMatchOptions,
32-
) {
33-
if (!nativeId) {
34-
return false;
35-
}
36-
37-
return (
38-
findAll(
39-
root,
40-
(node) => node.props.nativeID === nativeId && matchTextContent(node, text, options),
41-
).length > 0
42-
);
10+
return matches(expectedLabel, computeAriaLabel(element), options?.normalizer, options?.exact);
4311
}

src/queries/label-text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { findAll } from '../helpers/find-all';
33
import { TextMatch, TextMatchOptions } from '../matches';
4-
import { matchLabelText } from '../helpers/matchers/match-label-text';
4+
import { matchAccessibilityLabel } from '../helpers/matchers/match-label-text';
55
import { makeQueries } from './make-queries';
66
import type {
77
FindAllByQuery,
@@ -19,7 +19,7 @@ function queryAllByLabelText(instance: ReactTestInstance) {
1919
return (text: TextMatch, queryOptions?: ByLabelTextOptions) => {
2020
return findAll(
2121
instance,
22-
(node) => matchLabelText(instance, node, text, queryOptions),
22+
(node) => matchAccessibilityLabel(node, text, queryOptions),
2323
queryOptions,
2424
);
2525
};

0 commit comments

Comments
 (0)