@transferwise/components 46.71.1 → 46.71.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/button/Button.js.map +1 -1
- package/build/button/Button.mjs.map +1 -1
- package/build/inputs/SelectInput.js +33 -4
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +33 -4
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/instructionsList/InstructionsList.js +4 -3
- package/build/instructionsList/InstructionsList.js.map +1 -1
- package/build/instructionsList/InstructionsList.mjs +4 -3
- package/build/instructionsList/InstructionsList.mjs.map +1 -1
- package/build/types/button/Button.d.ts +1 -1
- package/build/types/button/Button.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/accordion/AccordionItem/__snapshots__/AccordionItem.spec.js.snap +4 -4
- package/src/avatarWrapper/__snapshots__/AvatarWrapper.spec.tsx.snap +8 -8
- package/src/button/Button.tsx +1 -1
- package/src/chips/__snapshots__/Chips.spec.tsx.snap +2 -2
- package/src/circularButton/__snapshots__/CircularButton.spec.tsx.snap +20 -20
- package/src/common/bottomSheet/__snapshots__/BottomSheet.spec.tsx.snap +2 -2
- package/src/common/closeButton/__snapshots__/CloseButton.spec.tsx.snap +2 -2
- package/src/drawer/__snapshots__/Drawer.rtl.spec.tsx.snap +2 -2
- package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +4 -4
- package/src/inputs/SelectInput.docs.mdx +21 -0
- package/src/inputs/SelectInput.spec.tsx +94 -1
- package/src/inputs/SelectInput.story.tsx +2 -2
- package/src/inputs/SelectInput.tsx +39 -2
- package/src/instructionsList/InstructionsList.spec.tsx +21 -5
- package/src/instructionsList/InstructionsList.tsx +11 -3
- package/src/moneyInput/MoneyInput.rtl.spec.tsx +9 -0
- package/src/overlayHeader/__snapshots__/OverlayHeader.spec.tsx.snap +2 -2
|
@@ -11,15 +11,15 @@ exports[`CircularButton defaults renders a button of type accent and priority pr
|
|
|
11
11
|
type="button"
|
|
12
12
|
/>
|
|
13
13
|
<span
|
|
14
|
-
aria-hidden="true"
|
|
15
14
|
class="tw-icon tw-icon-plus "
|
|
16
15
|
data-testid="plus-icon"
|
|
17
|
-
role="presentation"
|
|
18
16
|
>
|
|
19
17
|
<svg
|
|
18
|
+
aria-hidden="true"
|
|
20
19
|
fill="currentColor"
|
|
21
20
|
focusable="false"
|
|
22
21
|
height="24"
|
|
22
|
+
role="none"
|
|
23
23
|
viewBox="0 0 24 24"
|
|
24
24
|
width="24"
|
|
25
25
|
>
|
|
@@ -48,15 +48,15 @@ exports[`CircularButton priorities renders primary buttons 1`] = `
|
|
|
48
48
|
type="button"
|
|
49
49
|
/>
|
|
50
50
|
<span
|
|
51
|
-
aria-hidden="true"
|
|
52
51
|
class="tw-icon tw-icon-plus "
|
|
53
52
|
data-testid="plus-icon"
|
|
54
|
-
role="presentation"
|
|
55
53
|
>
|
|
56
54
|
<svg
|
|
55
|
+
aria-hidden="true"
|
|
57
56
|
fill="currentColor"
|
|
58
57
|
focusable="false"
|
|
59
58
|
height="24"
|
|
59
|
+
role="none"
|
|
60
60
|
viewBox="0 0 24 24"
|
|
61
61
|
width="24"
|
|
62
62
|
>
|
|
@@ -85,15 +85,15 @@ exports[`CircularButton priorities renders primary buttons 2`] = `
|
|
|
85
85
|
type="button"
|
|
86
86
|
/>
|
|
87
87
|
<span
|
|
88
|
-
aria-hidden="true"
|
|
89
88
|
class="tw-icon tw-icon-plus "
|
|
90
89
|
data-testid="plus-icon"
|
|
91
|
-
role="presentation"
|
|
92
90
|
>
|
|
93
91
|
<svg
|
|
92
|
+
aria-hidden="true"
|
|
94
93
|
fill="currentColor"
|
|
95
94
|
focusable="false"
|
|
96
95
|
height="24"
|
|
96
|
+
role="none"
|
|
97
97
|
viewBox="0 0 24 24"
|
|
98
98
|
width="24"
|
|
99
99
|
>
|
|
@@ -122,15 +122,15 @@ exports[`CircularButton priorities renders primary buttons 3`] = `
|
|
|
122
122
|
type="button"
|
|
123
123
|
/>
|
|
124
124
|
<span
|
|
125
|
-
aria-hidden="true"
|
|
126
125
|
class="tw-icon tw-icon-plus "
|
|
127
126
|
data-testid="plus-icon"
|
|
128
|
-
role="presentation"
|
|
129
127
|
>
|
|
130
128
|
<svg
|
|
129
|
+
aria-hidden="true"
|
|
131
130
|
fill="currentColor"
|
|
132
131
|
focusable="false"
|
|
133
132
|
height="24"
|
|
133
|
+
role="none"
|
|
134
134
|
viewBox="0 0 24 24"
|
|
135
135
|
width="24"
|
|
136
136
|
>
|
|
@@ -159,15 +159,15 @@ exports[`CircularButton priorities renders secondary buttons 1`] = `
|
|
|
159
159
|
type="button"
|
|
160
160
|
/>
|
|
161
161
|
<span
|
|
162
|
-
aria-hidden="true"
|
|
163
162
|
class="tw-icon tw-icon-plus "
|
|
164
163
|
data-testid="plus-icon"
|
|
165
|
-
role="presentation"
|
|
166
164
|
>
|
|
167
165
|
<svg
|
|
166
|
+
aria-hidden="true"
|
|
168
167
|
fill="currentColor"
|
|
169
168
|
focusable="false"
|
|
170
169
|
height="24"
|
|
170
|
+
role="none"
|
|
171
171
|
viewBox="0 0 24 24"
|
|
172
172
|
width="24"
|
|
173
173
|
>
|
|
@@ -196,15 +196,15 @@ exports[`CircularButton priorities renders secondary buttons 2`] = `
|
|
|
196
196
|
type="button"
|
|
197
197
|
/>
|
|
198
198
|
<span
|
|
199
|
-
aria-hidden="true"
|
|
200
199
|
class="tw-icon tw-icon-plus "
|
|
201
200
|
data-testid="plus-icon"
|
|
202
|
-
role="presentation"
|
|
203
201
|
>
|
|
204
202
|
<svg
|
|
203
|
+
aria-hidden="true"
|
|
205
204
|
fill="currentColor"
|
|
206
205
|
focusable="false"
|
|
207
206
|
height="24"
|
|
207
|
+
role="none"
|
|
208
208
|
viewBox="0 0 24 24"
|
|
209
209
|
width="24"
|
|
210
210
|
>
|
|
@@ -233,15 +233,15 @@ exports[`CircularButton priorities renders secondary buttons 3`] = `
|
|
|
233
233
|
type="button"
|
|
234
234
|
/>
|
|
235
235
|
<span
|
|
236
|
-
aria-hidden="true"
|
|
237
236
|
class="tw-icon tw-icon-plus "
|
|
238
237
|
data-testid="plus-icon"
|
|
239
|
-
role="presentation"
|
|
240
238
|
>
|
|
241
239
|
<svg
|
|
240
|
+
aria-hidden="true"
|
|
242
241
|
fill="currentColor"
|
|
243
242
|
focusable="false"
|
|
244
243
|
height="24"
|
|
244
|
+
role="none"
|
|
245
245
|
viewBox="0 0 24 24"
|
|
246
246
|
width="24"
|
|
247
247
|
>
|
|
@@ -270,15 +270,15 @@ exports[`CircularButton types renders accent buttons 1`] = `
|
|
|
270
270
|
type="button"
|
|
271
271
|
/>
|
|
272
272
|
<span
|
|
273
|
-
aria-hidden="true"
|
|
274
273
|
class="tw-icon tw-icon-plus "
|
|
275
274
|
data-testid="plus-icon"
|
|
276
|
-
role="presentation"
|
|
277
275
|
>
|
|
278
276
|
<svg
|
|
277
|
+
aria-hidden="true"
|
|
279
278
|
fill="currentColor"
|
|
280
279
|
focusable="false"
|
|
281
280
|
height="24"
|
|
281
|
+
role="none"
|
|
282
282
|
viewBox="0 0 24 24"
|
|
283
283
|
width="24"
|
|
284
284
|
>
|
|
@@ -307,15 +307,15 @@ exports[`CircularButton types renders negative buttons 1`] = `
|
|
|
307
307
|
type="button"
|
|
308
308
|
/>
|
|
309
309
|
<span
|
|
310
|
-
aria-hidden="true"
|
|
311
310
|
class="tw-icon tw-icon-plus "
|
|
312
311
|
data-testid="plus-icon"
|
|
313
|
-
role="presentation"
|
|
314
312
|
>
|
|
315
313
|
<svg
|
|
314
|
+
aria-hidden="true"
|
|
316
315
|
fill="currentColor"
|
|
317
316
|
focusable="false"
|
|
318
317
|
height="24"
|
|
318
|
+
role="none"
|
|
319
319
|
viewBox="0 0 24 24"
|
|
320
320
|
width="24"
|
|
321
321
|
>
|
|
@@ -344,15 +344,15 @@ exports[`CircularButton types renders positive buttons 1`] = `
|
|
|
344
344
|
type="button"
|
|
345
345
|
/>
|
|
346
346
|
<span
|
|
347
|
-
aria-hidden="true"
|
|
348
347
|
class="tw-icon tw-icon-plus "
|
|
349
348
|
data-testid="plus-icon"
|
|
350
|
-
role="presentation"
|
|
351
349
|
>
|
|
352
350
|
<svg
|
|
351
|
+
aria-hidden="true"
|
|
353
352
|
fill="currentColor"
|
|
354
353
|
focusable="false"
|
|
355
354
|
height="24"
|
|
355
|
+
role="none"
|
|
356
356
|
viewBox="0 0 24 24"
|
|
357
357
|
width="24"
|
|
358
358
|
>
|
|
@@ -39,15 +39,15 @@ exports[`BottomSheet renders content when open 1`] = `
|
|
|
39
39
|
type="button"
|
|
40
40
|
>
|
|
41
41
|
<span
|
|
42
|
-
aria-hidden="true"
|
|
43
42
|
class="tw-icon tw-icon-cross "
|
|
44
43
|
data-testid="cross-icon"
|
|
45
|
-
role="presentation"
|
|
46
44
|
>
|
|
47
45
|
<svg
|
|
46
|
+
aria-hidden="true"
|
|
48
47
|
fill="currentColor"
|
|
49
48
|
focusable="false"
|
|
50
49
|
height="16"
|
|
50
|
+
role="none"
|
|
51
51
|
viewBox="0 0 24 24"
|
|
52
52
|
width="16"
|
|
53
53
|
>
|
|
@@ -8,15 +8,15 @@ exports[`CloseButton renders as expected 1`] = `
|
|
|
8
8
|
type="button"
|
|
9
9
|
>
|
|
10
10
|
<span
|
|
11
|
-
aria-hidden="true"
|
|
12
11
|
class="tw-icon tw-icon-cross "
|
|
13
12
|
data-testid="cross-icon"
|
|
14
|
-
role="presentation"
|
|
15
13
|
>
|
|
16
14
|
<svg
|
|
15
|
+
aria-hidden="true"
|
|
17
16
|
fill="currentColor"
|
|
18
17
|
focusable="false"
|
|
19
18
|
height="24"
|
|
19
|
+
role="none"
|
|
20
20
|
viewBox="0 0 24 24"
|
|
21
21
|
width="24"
|
|
22
22
|
>
|
|
@@ -24,15 +24,15 @@ exports[`Drawer renders content when open 1`] = `
|
|
|
24
24
|
type="button"
|
|
25
25
|
>
|
|
26
26
|
<span
|
|
27
|
-
aria-hidden="true"
|
|
28
27
|
class="tw-icon tw-icon-cross "
|
|
29
28
|
data-testid="cross-icon"
|
|
30
|
-
role="presentation"
|
|
31
29
|
>
|
|
32
30
|
<svg
|
|
31
|
+
aria-hidden="true"
|
|
33
32
|
fill="currentColor"
|
|
34
33
|
focusable="false"
|
|
35
34
|
height="24"
|
|
35
|
+
role="none"
|
|
36
36
|
viewBox="0 0 24 24"
|
|
37
37
|
width="24"
|
|
38
38
|
>
|
|
@@ -38,15 +38,15 @@ exports[`FlowNavigation on mobile renders as expected 1`] = `
|
|
|
38
38
|
type="button"
|
|
39
39
|
>
|
|
40
40
|
<span
|
|
41
|
-
aria-hidden="true"
|
|
42
41
|
class="tw-icon tw-icon-cross "
|
|
43
42
|
data-testid="cross-icon"
|
|
44
|
-
role="presentation"
|
|
45
43
|
>
|
|
46
44
|
<svg
|
|
45
|
+
aria-hidden="true"
|
|
47
46
|
fill="currentColor"
|
|
48
47
|
focusable="false"
|
|
49
48
|
height="24"
|
|
49
|
+
role="none"
|
|
50
50
|
viewBox="0 0 24 24"
|
|
51
51
|
width="24"
|
|
52
52
|
>
|
|
@@ -153,15 +153,15 @@ exports[`FlowNavigation renders as expected 1`] = `
|
|
|
153
153
|
type="button"
|
|
154
154
|
>
|
|
155
155
|
<span
|
|
156
|
-
aria-hidden="true"
|
|
157
156
|
class="tw-icon tw-icon-cross "
|
|
158
157
|
data-testid="cross-icon"
|
|
159
|
-
role="presentation"
|
|
160
158
|
>
|
|
161
159
|
<svg
|
|
160
|
+
aria-hidden="true"
|
|
162
161
|
fill="currentColor"
|
|
163
162
|
focusable="false"
|
|
164
163
|
height="24"
|
|
164
|
+
role="none"
|
|
165
165
|
viewBox="0 0 24 24"
|
|
166
166
|
width="24"
|
|
167
167
|
>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
import { MoneyInput } from '..';
|
|
3
|
+
|
|
4
|
+
<Meta title="Forms/SelectInput/Accessibility" />
|
|
5
|
+
|
|
6
|
+
# Accessibility
|
|
7
|
+
|
|
8
|
+
## Labelling
|
|
9
|
+
|
|
10
|
+
In order for the `<SelectInput />` to be considered accessible, it must be provided with a matching label, preferably via the <a href="/?path=/docs/field--docs">Field</a> component.
|
|
11
|
+
|
|
12
|
+
Additionally, the `listbox` container that holds all the options is also expected to have its own label, which the component will attempt to resolve by looking in the following places:
|
|
13
|
+
|
|
14
|
+
1. `UNSAFE_triggerButtonProps['aria-label']` prop, which reuses the custom label provided for the trigger button.
|
|
15
|
+
<br /> A good example of this strategy is the `<MoneyInput />` component, internally setting
|
|
16
|
+
`"Select currency"` as a trigger button label, which then gets automatically applied to the
|
|
17
|
+
`listbox`.
|
|
18
|
+
2. `UNSAFE_triggerButtonProps['aria-labelledby']` prop, which holds the id of the element labelling the trigger button.
|
|
19
|
+
3. Correctly paired input `<label />` text, ideally via the `Field` component.
|
|
20
|
+
|
|
21
|
+
> Using option group heading is possible but complicated as we can have multiple groups, and those are not necessarily rendered consistently if search or virtualisation are enabled.
|
|
@@ -3,7 +3,7 @@ import { userEvent } from '@testing-library/user-event';
|
|
|
3
3
|
|
|
4
4
|
import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
|
|
5
5
|
|
|
6
|
-
import { SelectInput } from './SelectInput';
|
|
6
|
+
import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
|
|
7
7
|
import { Field } from '../field/Field';
|
|
8
8
|
|
|
9
9
|
mockMatchMedia();
|
|
@@ -216,4 +216,97 @@ describe('SelectInput', () => {
|
|
|
216
216
|
);
|
|
217
217
|
expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
describe('listbox label', () => {
|
|
221
|
+
const fieldLabel = 'Fruits';
|
|
222
|
+
const triggerLabel = 'Select fruit';
|
|
223
|
+
const options: SelectInputOptionItem[] = [
|
|
224
|
+
{ type: 'option', value: 'Banana' },
|
|
225
|
+
{ type: 'option', value: 'Orange' },
|
|
226
|
+
{ type: 'option', value: 'Olive' },
|
|
227
|
+
];
|
|
228
|
+
const requiredTriggerButtonProps = {
|
|
229
|
+
id: undefined,
|
|
230
|
+
'aria-labelledby': undefined,
|
|
231
|
+
'aria-describedby': undefined,
|
|
232
|
+
'aria-invalid': undefined,
|
|
233
|
+
'aria-label': undefined,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const renderSelectInput = (props: Omit<SelectInputProps<string | null>, 'items'> = {}) =>
|
|
237
|
+
render(
|
|
238
|
+
<Field label={fieldLabel} id="selectId">
|
|
239
|
+
<SelectInput {...props} items={options} />
|
|
240
|
+
</Field>,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
it("should propagate trigger's label if nothing is selected", async () => {
|
|
244
|
+
renderSelectInput({
|
|
245
|
+
UNSAFE_triggerButtonProps: {
|
|
246
|
+
...requiredTriggerButtonProps,
|
|
247
|
+
'aria-label': triggerLabel,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
const trigger = screen.getByRole('combobox');
|
|
251
|
+
await userEvent.click(trigger);
|
|
252
|
+
expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should propagate trigger's label if an option is selected", async () => {
|
|
256
|
+
renderSelectInput({
|
|
257
|
+
UNSAFE_triggerButtonProps: {
|
|
258
|
+
...requiredTriggerButtonProps,
|
|
259
|
+
'aria-label': triggerLabel,
|
|
260
|
+
},
|
|
261
|
+
value: options[1].value,
|
|
262
|
+
});
|
|
263
|
+
const trigger = screen.getByRole('combobox');
|
|
264
|
+
await userEvent.click(trigger);
|
|
265
|
+
expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should propagate trigger's label by id", async () => {
|
|
269
|
+
const customLabelId = 'customLabelId';
|
|
270
|
+
renderSelectInput({
|
|
271
|
+
UNSAFE_triggerButtonProps: {
|
|
272
|
+
...requiredTriggerButtonProps,
|
|
273
|
+
'aria-labelledby': customLabelId,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const trigger = screen.getByRole('combobox');
|
|
277
|
+
await userEvent.click(trigger);
|
|
278
|
+
expect(screen.getByRole('listbox')).toHaveAttribute('aria-labelledby', customLabelId);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should propagate input's label by id", async () => {
|
|
282
|
+
renderSelectInput();
|
|
283
|
+
const trigger = screen.getByRole('combobox');
|
|
284
|
+
await userEvent.click(trigger);
|
|
285
|
+
expect(screen.getByRole('listbox', { name: fieldLabel })).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should prefer explicit label over label ids', async () => {
|
|
289
|
+
const customLabelId = 'customLabelId';
|
|
290
|
+
renderSelectInput({
|
|
291
|
+
UNSAFE_triggerButtonProps: {
|
|
292
|
+
...requiredTriggerButtonProps,
|
|
293
|
+
'aria-labelledby': customLabelId,
|
|
294
|
+
'aria-label': triggerLabel,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const trigger = screen.getByRole('combobox');
|
|
298
|
+
await userEvent.click(trigger);
|
|
299
|
+
expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
|
|
300
|
+
expect(screen.getByRole('listbox')).not.toHaveAttribute('aria-labelledby');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should have no label if none of the above are provided', async () => {
|
|
304
|
+
render(<SelectInput items={options} />);
|
|
305
|
+
const trigger = screen.getByRole('combobox');
|
|
306
|
+
await userEvent.click(trigger);
|
|
307
|
+
const listBox = screen.getByRole('listbox');
|
|
308
|
+
expect(listBox).not.toHaveAttribute('aria-label');
|
|
309
|
+
expect(listBox).not.toHaveAttribute('aria-labelledby');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
219
312
|
});
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from './SelectInput';
|
|
21
21
|
|
|
22
22
|
const meta = {
|
|
23
|
-
title: '
|
|
23
|
+
title: 'Forms/SelectInput',
|
|
24
24
|
component: SelectInput,
|
|
25
25
|
tags: ['autodocs'],
|
|
26
26
|
parameters: { actions: { argTypesRegex: '' } },
|
|
@@ -347,7 +347,7 @@ export const WithinField = {
|
|
|
347
347
|
args: Months.args,
|
|
348
348
|
decorators: [
|
|
349
349
|
(Story) => (
|
|
350
|
-
<Field message="Something went wrong" sentiment="negative">
|
|
350
|
+
<Field message="Something went wrong" sentiment="negative" label="Month">
|
|
351
351
|
<Story />
|
|
352
352
|
</Field>
|
|
353
353
|
),
|
|
@@ -258,8 +258,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
258
258
|
onClose,
|
|
259
259
|
onClear,
|
|
260
260
|
}: SelectInputProps<T, M>) {
|
|
261
|
-
const inputAttributes = useInputAttributes();
|
|
261
|
+
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
262
262
|
const id = idProp ?? inputAttributes.id;
|
|
263
|
+
|
|
263
264
|
const [open, setOpen] = useState(false);
|
|
264
265
|
|
|
265
266
|
const initialized = useRef(false);
|
|
@@ -295,6 +296,35 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
295
296
|
const listboxRef = useRef<HTMLDivElement>(null);
|
|
296
297
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
297
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Attempts to resolve the `listbox` label
|
|
301
|
+
* @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
|
|
302
|
+
*/
|
|
303
|
+
const getListBoxLabelProps = (): {
|
|
304
|
+
listBoxLabel?: string;
|
|
305
|
+
listBoxLabelledBy?: string;
|
|
306
|
+
} => {
|
|
307
|
+
if (UNSAFE_triggerButtonProps?.['aria-label']) {
|
|
308
|
+
return {
|
|
309
|
+
listBoxLabel: UNSAFE_triggerButtonProps['aria-label'],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (UNSAFE_triggerButtonProps?.['aria-labelledby']) {
|
|
314
|
+
return {
|
|
315
|
+
listBoxLabelledBy: UNSAFE_triggerButtonProps['aria-labelledby'],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (inputAttributes['aria-labelledby']) {
|
|
320
|
+
return {
|
|
321
|
+
listBoxLabelledBy: inputAttributes['aria-labelledby'],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {};
|
|
326
|
+
};
|
|
327
|
+
|
|
298
328
|
return (
|
|
299
329
|
<ListboxBase
|
|
300
330
|
name={name}
|
|
@@ -388,7 +418,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
388
418
|
}}
|
|
389
419
|
>
|
|
390
420
|
<SelectInputOptions
|
|
391
|
-
id={`${id}Search`}
|
|
421
|
+
id={id ? `${id}Search` : undefined}
|
|
392
422
|
items={items}
|
|
393
423
|
renderValue={renderValue}
|
|
394
424
|
renderFooter={renderFooter}
|
|
@@ -398,6 +428,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
398
428
|
listboxRef={listboxRef}
|
|
399
429
|
filterQuery={deferredFilterQuery}
|
|
400
430
|
onFilterChange={setFilterQuery}
|
|
431
|
+
{...getListBoxLabelProps()}
|
|
401
432
|
/>
|
|
402
433
|
</OptionsOverlay>
|
|
403
434
|
);
|
|
@@ -496,6 +527,8 @@ interface SelectInputOptionsProps<T = string>
|
|
|
496
527
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
497
528
|
filterQuery: string;
|
|
498
529
|
onFilterChange: (query: string) => void;
|
|
530
|
+
listBoxLabel?: string;
|
|
531
|
+
listBoxLabelledBy?: string;
|
|
499
532
|
}
|
|
500
533
|
|
|
501
534
|
function SelectInputOptions<T = string>({
|
|
@@ -509,6 +542,8 @@ function SelectInputOptions<T = string>({
|
|
|
509
542
|
listboxRef,
|
|
510
543
|
filterQuery,
|
|
511
544
|
onFilterChange,
|
|
545
|
+
listBoxLabel,
|
|
546
|
+
listBoxLabelledBy,
|
|
512
547
|
}: SelectInputOptionsProps<T>) {
|
|
513
548
|
const intl = useIntl();
|
|
514
549
|
|
|
@@ -662,6 +697,8 @@ function SelectInputOptions<T = string>({
|
|
|
662
697
|
id={listboxId}
|
|
663
698
|
role="listbox"
|
|
664
699
|
aria-orientation="vertical"
|
|
700
|
+
aria-label={listBoxLabel}
|
|
701
|
+
aria-labelledby={listBoxLabelledBy}
|
|
665
702
|
tabIndex={0}
|
|
666
703
|
className="np-select-input-listbox"
|
|
667
704
|
>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '../test-utils';
|
|
1
|
+
import { render, screen, within } from '../test-utils';
|
|
2
2
|
|
|
3
3
|
import InstructionsList from '.';
|
|
4
4
|
|
|
@@ -34,8 +34,16 @@ describe('InstructionsList', () => {
|
|
|
34
34
|
|
|
35
35
|
const instructions = container.querySelectorAll('.instruction');
|
|
36
36
|
expect(instructions).toHaveLength(2);
|
|
37
|
-
expect(
|
|
38
|
-
|
|
37
|
+
expect(
|
|
38
|
+
within(instructions[0] as HTMLElement).getByRole('graphics-symbol', {
|
|
39
|
+
name: dos[0]['aria-label'],
|
|
40
|
+
}),
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
within(instructions[1] as HTMLElement).getByRole('graphics-symbol', {
|
|
44
|
+
name: donts[0]['aria-label'],
|
|
45
|
+
}),
|
|
46
|
+
).toBeInTheDocument();
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
it('should render donts first when sort is set to `dontsFirst`', () => {
|
|
@@ -48,7 +56,15 @@ describe('InstructionsList', () => {
|
|
|
48
56
|
|
|
49
57
|
const instructions = container.querySelectorAll('.instruction');
|
|
50
58
|
expect(instructions).toHaveLength(2);
|
|
51
|
-
expect(
|
|
52
|
-
|
|
59
|
+
expect(
|
|
60
|
+
within(instructions[0] as HTMLElement).getByRole('graphics-symbol', {
|
|
61
|
+
name: donts[0]['aria-label'],
|
|
62
|
+
}),
|
|
63
|
+
).toBeInTheDocument();
|
|
64
|
+
expect(
|
|
65
|
+
within(instructions[1] as HTMLElement).getByRole('graphics-symbol', {
|
|
66
|
+
name: dos[0]['aria-label'],
|
|
67
|
+
}),
|
|
68
|
+
).toBeInTheDocument();
|
|
53
69
|
});
|
|
54
70
|
});
|
|
@@ -57,11 +57,19 @@ function Instruction({ item, type }: { item: ReactNode | InstructionNode; type:
|
|
|
57
57
|
const isInstructionNode =
|
|
58
58
|
typeof item === 'object' && item !== null && 'content' in item && 'aria-label' in item;
|
|
59
59
|
return (
|
|
60
|
-
<li className="instruction"
|
|
60
|
+
<li className="instruction">
|
|
61
61
|
{type === 'do' ? (
|
|
62
|
-
<DoIcon
|
|
62
|
+
<DoIcon
|
|
63
|
+
size={24}
|
|
64
|
+
className={type}
|
|
65
|
+
title={isInstructionNode ? item['aria-label'] : undefined}
|
|
66
|
+
/>
|
|
63
67
|
) : (
|
|
64
|
-
<DontIcon
|
|
68
|
+
<DontIcon
|
|
69
|
+
size={24}
|
|
70
|
+
className={type}
|
|
71
|
+
title={isInstructionNode ? item['aria-label'] : undefined}
|
|
72
|
+
/>
|
|
65
73
|
)}
|
|
66
74
|
<Body className="text-primary" type={Typography.BODY_LARGE}>
|
|
67
75
|
{isInstructionNode ? item.content : item}
|
|
@@ -137,4 +137,13 @@ describe('MoneyInput', () => {
|
|
|
137
137
|
messages.selectCurrencyLabel.defaultMessage,
|
|
138
138
|
);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
it('should have a listbox label', async () => {
|
|
142
|
+
render(<MoneyInput {...props} />);
|
|
143
|
+
const trigger = screen.getByRole('combobox');
|
|
144
|
+
await userEvent.click(trigger);
|
|
145
|
+
const triggerLabel = trigger.getAttribute('aria-label');
|
|
146
|
+
expect(triggerLabel).toBeTruthy();
|
|
147
|
+
expect(screen.getByRole('listbox', { name: triggerLabel ?? '' })).toBeInTheDocument();
|
|
148
|
+
});
|
|
140
149
|
});
|
|
@@ -35,15 +35,15 @@ exports[`OverlayHeader renders as expected 1`] = `
|
|
|
35
35
|
type="button"
|
|
36
36
|
>
|
|
37
37
|
<span
|
|
38
|
-
aria-hidden="true"
|
|
39
38
|
class="tw-icon tw-icon-cross "
|
|
40
39
|
data-testid="cross-icon"
|
|
41
|
-
role="presentation"
|
|
42
40
|
>
|
|
43
41
|
<svg
|
|
42
|
+
aria-hidden="true"
|
|
44
43
|
fill="currentColor"
|
|
45
44
|
focusable="false"
|
|
46
45
|
height="24"
|
|
46
|
+
role="none"
|
|
47
47
|
viewBox="0 0 24 24"
|
|
48
48
|
width="24"
|
|
49
49
|
>
|