Skip to content

Commit 3fa6c45

Browse files
feat: aria-value* props support (#1480)
* feat: aria-value* props support * refactor: tweaks * chore: fix code cov
1 parent 5dbd04f commit 3fa6c45

File tree

10 files changed

+155
-13
lines changed

10 files changed

+155
-13
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ describe('mapPropsForQueryError', () => {
2121
'aria-labelledby': 'ARIA_LABELLED_BY',
2222
'aria-modal': true,
2323
'aria-selected': 'ARIA-SELECTED',
24+
'aria-valuemax': 'ARIA-VALUEMAX',
25+
'aria-valuemin': 'ARIA-VALUEMIN',
26+
'aria-valuenow': 'ARIA-VALUENOW',
27+
'aria-valuetext': 'ARIA-VALUETEXT',
2428
placeholder: 'PLACEHOLDER',
2529
value: 'VALUE',
2630
defaultValue: 'DEFAULT_VALUE',

src/helpers/accessiblity.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ export function isAccessibilityElement(
112112
);
113113
}
114114

115-
export function getAccessibilityRole(element: ReactTestInstance) {
115+
export function getAccessibilityRole(
116+
element: ReactTestInstance
117+
): string | undefined {
116118
return element.props.role ?? element.props.accessibilityRole;
117119
}
118120

@@ -134,7 +136,9 @@ export function getAccessibilityLabelledBy(
134136
);
135137
}
136138

137-
export function getAccessibilityState(element: ReactTestInstance) {
139+
export function getAccessibilityState(
140+
element: ReactTestInstance
141+
): AccessibilityState | undefined {
138142
const {
139143
accessibilityState,
140144
'aria-busy': ariaBusy,
@@ -171,3 +175,33 @@ export function getAccessibilityCheckedState(
171175
const { accessibilityState, 'aria-checked': ariaChecked } = element.props;
172176
return ariaChecked ?? accessibilityState?.checked;
173177
}
178+
179+
export function getAccessibilityValue(
180+
element: ReactTestInstance
181+
): AccessibilityValue | undefined {
182+
const {
183+
accessibilityValue,
184+
'aria-valuemax': ariaValueMax,
185+
'aria-valuemin': ariaValueMin,
186+
'aria-valuenow': ariaValueNow,
187+
'aria-valuetext': ariaValueText,
188+
} = element.props;
189+
190+
const hasAnyAccessibilityValueProps =
191+
accessibilityValue != null ||
192+
ariaValueMax != null ||
193+
ariaValueMin != null ||
194+
ariaValueNow != null ||
195+
ariaValueText != null;
196+
197+
if (!hasAnyAccessibilityValueProps) {
198+
return undefined;
199+
}
200+
201+
return {
202+
max: ariaValueMax ?? accessibilityValue?.max,
203+
min: ariaValueMin ?? accessibilityValue?.min,
204+
now: ariaValueNow ?? accessibilityValue?.now,
205+
text: ariaValueText ?? accessibilityValue?.text,
206+
};
207+
}

src/helpers/format-default.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const propsToDisplay = [
1717
'aria-labelledby',
1818
'aria-modal',
1919
'aria-selected',
20+
'aria-valuemax',
21+
'aria-valuemin',
22+
'aria-valuenow',
23+
'aria-valuetext',
2024
'defaultValue',
2125
'importantForAccessibility',
2226
'nativeID',

src/helpers/matchers/accessibilityValue.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { AccessibilityValue } from 'react-native';
21
import { ReactTestInstance } from 'react-test-renderer';
2+
import { getAccessibilityValue } from '../accessiblity';
33
import { TextMatch } from '../../matches';
44
import { matchStringProp } from './matchStringProp';
55

@@ -14,11 +14,11 @@ export function matchAccessibilityValue(
1414
node: ReactTestInstance,
1515
matcher: AccessibilityValueMatcher
1616
): boolean {
17-
const value: AccessibilityValue = node.props.accessibilityValue ?? {};
17+
const value = getAccessibilityValue(node);
1818
return (
19-
(matcher.min === undefined || matcher.min === value.min) &&
20-
(matcher.max === undefined || matcher.max === value.max) &&
21-
(matcher.now === undefined || matcher.now === value.now) &&
22-
(matcher.text === undefined || matchStringProp(value.text, matcher.text))
19+
(matcher.min === undefined || matcher.min === value?.min) &&
20+
(matcher.max === undefined || matcher.max === value?.max) &&
21+
(matcher.now === undefined || matcher.now === value?.now) &&
22+
(matcher.text === undefined || matchStringProp(value?.text, matcher.text))
2323
);
2424
}

src/matchers/__tests__/to-be-checked.test.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,12 @@ test('throws error for invalid role', () => {
176176
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
177177
);
178178
});
179+
180+
test('throws error for non-accessibility element', () => {
181+
render(<View testID="test" />);
182+
183+
const view = screen.getByTestId('test');
184+
expect(() => expect(view).toBeChecked()).toThrowErrorMatchingInlineSnapshot(
185+
`"toBeChecked() works only on accessibility elements with "checkbox" or "radio" role."`
186+
);
187+
});

src/matchers/to-be-checked.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ export function toBeChecked(
3535
};
3636
}
3737

38-
const VALID_ROLES = new Set(['checkbox', 'radio']);
39-
4038
function hasValidAccessibilityRole(element: ReactTestInstance) {
39+
if (!isAccessibilityElement(element)) {
40+
return false;
41+
}
42+
4143
const role = getAccessibilityRole(element);
42-
return isAccessibilityElement(element) && VALID_ROLES.has(role);
44+
return role === 'checkbox' || role === 'radio';
4345
}

src/queries/__tests__/a11yValue.test.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,37 @@ test('error message renders the element tree, preserving only helpful props', as
307307
/>"
308308
`);
309309
});
310+
311+
describe('getByAccessibilityValue supports "aria-*" props', () => {
312+
test('supports "aria-valuemax"', () => {
313+
const screen = render(<View aria-valuemax={10} />);
314+
expect(screen.getByAccessibilityValue({ max: 10 })).toBeTruthy();
315+
});
316+
317+
test('supports "aria-valuemin"', () => {
318+
const screen = render(<View aria-valuemin={20} />);
319+
expect(screen.getByAccessibilityValue({ min: 20 })).toBeTruthy();
320+
});
321+
322+
test('supports "aria-valuenow"', () => {
323+
const screen = render(<View aria-valuenow={30} />);
324+
expect(screen.getByAccessibilityValue({ now: 30 })).toBeTruthy();
325+
});
326+
327+
test('supports "aria-valuetext"', () => {
328+
const screen = render(<View aria-valuetext="Hello World" />);
329+
expect(
330+
screen.getByAccessibilityValue({ text: 'Hello World' })
331+
).toBeTruthy();
332+
expect(screen.getByAccessibilityValue({ text: /hello/i })).toBeTruthy();
333+
});
334+
335+
test('supports multiple "aria-value*" props', () => {
336+
const screen = render(
337+
<View aria-valuenow={50} aria-valuemin={0} aria-valuemax={100} />
338+
);
339+
expect(
340+
screen.getByAccessibilityValue({ now: 50, min: 0, max: 100 })
341+
).toBeTruthy();
342+
});
343+
});

src/queries/__tests__/makeQueries.test.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ describe('printing element tree', () => {
7171
aria-labelledby="ARIA_LABELLED_BY"
7272
aria-modal={true}
7373
aria-selected={false}
74+
aria-valuemax={30}
75+
aria-valuemin={10}
76+
aria-valuenow={20}
77+
aria-valuetext="Hello Value"
7478
importantForAccessibility="yes"
7579
nativeID="NATIVE_ID"
7680
role="summary"

src/queries/__tests__/role-value.test.tsx

+51
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,55 @@ describe('accessibility value', () => {
173173
</Text>"
174174
`);
175175
});
176+
177+
test('supports "aria-valuemax" prop', () => {
178+
const screen = render(<View accessible role="slider" aria-valuemax={10} />);
179+
expect(screen.getByRole('slider', { value: { max: 10 } })).toBeTruthy();
180+
expect(screen.queryByRole('slider', { value: { max: 20 } })).toBeNull();
181+
});
182+
183+
test('supports "aria-valuemin" prop', () => {
184+
const screen = render(<View accessible role="slider" aria-valuemin={20} />);
185+
expect(screen.getByRole('slider', { value: { min: 20 } })).toBeTruthy();
186+
expect(screen.queryByRole('slider', { value: { min: 30 } })).toBeNull();
187+
});
188+
189+
test('supports "aria-valuenow" prop', () => {
190+
const screen = render(<View accessible role="slider" aria-valuenow={30} />);
191+
expect(screen.getByRole('slider', { value: { now: 30 } })).toBeTruthy();
192+
expect(screen.queryByRole('slider', { value: { now: 10 } })).toBeNull();
193+
});
194+
195+
test('supports "aria-valuetext" prop', () => {
196+
const screen = render(
197+
<View accessible role="slider" aria-valuetext="Hello World" />
198+
);
199+
expect(
200+
screen.getByRole('slider', { value: { text: 'Hello World' } })
201+
).toBeTruthy();
202+
expect(
203+
screen.getByRole('slider', { value: { text: /hello/i } })
204+
).toBeTruthy();
205+
expect(
206+
screen.queryByRole('slider', { value: { text: 'Hello' } })
207+
).toBeNull();
208+
expect(
209+
screen.queryByRole('slider', { value: { text: /salut/i } })
210+
).toBeNull();
211+
});
212+
213+
test('supports multiple "aria-value*" props', () => {
214+
const screen = render(
215+
<View
216+
accessible
217+
role="slider"
218+
aria-valuenow={50}
219+
aria-valuemin={0}
220+
aria-valuemax={100}
221+
/>
222+
);
223+
expect(
224+
screen.getByRole('slider', { value: { now: 50, min: 0, max: 100 } })
225+
).toBeTruthy();
226+
});
176227
});

website/docs/Queries.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const element3 = screen.getByRole('button', { name: 'Hello', disabled: true });
121121

122122
`expanded`: You can filter elements by their expanded state (coming either from `aria-expanded` prop or `accessbilityState.expanded` prop). The possible values are `true` or `false`. See [React Native's accessibilityState](https://reactnative.dev/docs/accessibility#accessibilitystate) docs to learn more about the `expanded` state.
123123

124-
`value`: Filter elements by their accessibility, available value entries include numeric `min`, `max` & `now`, as well as string or regex `text` key. See React Native [accessibilityValue](https://reactnative.dev/docs/accessibility#accessibilityvalue) docs to learn more about this prop.
124+
`value`: Filter elements by their accessibility value, based on either `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` or `accessibilityValue` props. Accessiblity value conceptually consists of numeric `min`, `max` and `now` entries, as well as string `text` entry. See React Native [accessibilityValue](https://reactnative.dev/docs/accessibility#accessibilityvalue) docs to learn more about the accessibility value concept.
125125

126126
### `ByText`
127127

@@ -371,7 +371,7 @@ getByA11yValue(
371371
): ReactTestInstance;
372372
```
373373

374-
Returns a host element with matching `accessibilityValue` prop entries. Only entires provided to the query will be used to match elements. Element might have additional accessibility value entries and still be matched.
374+
Returns a host element with matching accessibility value based on `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` & `accessibilityValue` props. Only value entires provided to the query will be used to match elements. Element might have additional accessibility value entries and still be matched.
375375

376376
When querying by `text` entry a string or regex might be used.
377377

0 commit comments

Comments
 (0)