uikit-react-public 0.14.21 → 0.17.4
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/README.md +4 -2
- package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
- package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
- package/dist/components/Accordion/Accordion.d.ts +1 -1
- package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
- package/dist/components/Accordion/index.d.ts +2 -0
- package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
- package/dist/components/Button/Button.d.ts +1 -0
- package/dist/components/Calendar/index.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
- package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
- package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
- package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
- package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
- package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
- package/dist/components/Datepicker/utils/index.d.ts +0 -1
- package/dist/components/Dialog/BaseDialog.d.ts +2 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -0
- package/dist/components/Header/Header.d.ts +4 -1
- package/dist/components/Header/Header.stories.d.ts +40 -0
- package/dist/components/Main/Main.d.ts +21 -0
- package/dist/components/Main/Main.stories.d.ts +15 -0
- package/dist/components/Main/index.d.ts +2 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
- package/dist/components/NativeDatepicker/index.d.ts +2 -0
- package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
- package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
- package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
- package/dist/components/Select/Select.stories.d.ts +154 -2
- package/dist/components/Select/Select.types.d.ts +51 -22
- package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
- package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
- package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
- package/dist/components/Select/subcomponents/index.d.ts +1 -0
- package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
- package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
- package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
- package/dist/components/WeekPicker/index.d.ts +1 -0
- package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4366 -3768
- package/dist/utils/__tests__/announce.test.d.ts +1 -0
- package/dist/utils/announce.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
- package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
- package/lib/components/Accordion/Accordion.stories.tsx +139 -0
- package/lib/components/Accordion/Accordion.tsx +10 -8
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
- package/lib/components/Accordion/index.ts +2 -0
- package/lib/components/Alert/Alert.stories.tsx +1 -1
- package/lib/components/Avatar/Avatar.mdx +117 -0
- package/lib/components/Avatar/Avatar.stories.tsx +110 -2
- package/lib/components/Blanket/Blanket.stories.tsx +1 -1
- package/lib/components/Button/Button.stories.tsx +1 -1
- package/lib/components/Button/Button.tsx +1 -0
- package/lib/components/Calendar/Calendar.stories.tsx +12 -32
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
- package/lib/components/Calendar/index.ts +1 -5
- package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
- package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
- package/lib/components/Calendar/subcomponents/index.ts +1 -1
- package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
- package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
- package/lib/components/Datepicker/Datepicker.lld.md +108 -0
- package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
- package/lib/components/Datepicker/Datepicker.tsx +14 -36
- package/lib/components/Datepicker/Datepicker.types.ts +5 -14
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
- package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
- package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
- package/lib/components/Datepicker/subcomponents/index.ts +0 -1
- package/lib/components/Datepicker/utils/index.ts +0 -1
- package/lib/components/Dialog/BaseDialog.tsx +11 -0
- package/lib/components/Dialog/Dialog.tsx +8 -1
- package/lib/components/Dialog/DialogBody.tsx +5 -1
- package/lib/components/Dialog/DialogHeader.tsx +2 -1
- package/lib/components/Divider/Divider.stories.tsx +1 -1
- package/lib/components/Field/ErrorText.tsx +1 -0
- package/lib/components/Field/Field.stories.tsx +1 -1
- package/lib/components/Field/__tests__/Field.test.tsx +13 -0
- package/lib/components/FileInput/FileInput.stories.tsx +1 -1
- package/lib/components/Footer/Footer.stories.tsx +1 -1
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
- package/lib/components/Header/Header.mdx +52 -0
- package/lib/components/Header/Header.stories.tsx +98 -0
- package/lib/components/Header/Header.tsx +51 -6
- package/lib/components/Header/__tests__/Header.test.tsx +17 -1
- package/lib/components/Heading/Heading.stories.tsx +1 -1
- package/lib/components/Icon/Icon.stories.tsx +1 -1
- package/lib/components/IconButton/IconButton.stories.tsx +1 -1
- package/lib/components/Input/Input.stories.tsx +1 -1
- package/lib/components/Label/Label.stories.tsx +1 -1
- package/lib/components/Main/Main.stories.tsx +36 -0
- package/lib/components/Main/Main.tsx +46 -0
- package/lib/components/Main/__tests__/Main.test.tsx +80 -0
- package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
- package/lib/components/Main/index.ts +2 -0
- package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
- package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
- package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
- package/lib/components/NativeDatepicker/index.ts +2 -0
- package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
- package/lib/components/NativeDatepicker/utils/index.ts +1 -0
- package/lib/components/Pagination/PaginationControls.tsx +55 -12
- package/lib/components/Pagination/PaginationInfo.tsx +5 -1
- package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
- package/lib/components/Search/Search.stories.tsx +1 -1
- package/lib/components/Search/Search.tsx +4 -1
- package/lib/components/Search/__tests__/Search.test.tsx +19 -1
- package/lib/components/Select/Select.mdx +169 -0
- package/lib/components/Select/Select.stories.tsx +191 -43
- package/lib/components/Select/Select.tsx +36 -12
- package/lib/components/Select/Select.types.ts +66 -48
- package/lib/components/Select/__tests__/Select.test.tsx +448 -7
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
- package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
- package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
- package/lib/components/Select/subcomponents/index.tsx +1 -0
- package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
- package/lib/components/Spinner/Spinner.stories.tsx +1 -1
- package/lib/components/Textarea/Textarea.stories.tsx +1 -1
- package/lib/components/Timepicker/Timepicker.tsx +4 -0
- package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
- package/lib/components/Toggle/Toggle.stories.tsx +1 -1
- package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
- package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
- package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
- package/lib/components/WeekPicker/index.ts +1 -0
- package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
- package/lib/components/common/Common.mdx +1 -1
- package/lib/components/index.ts +11 -2
- package/lib/hooks/useFocusTrap.ts +40 -4
- package/lib/index.ts +1 -0
- package/lib/utils/__tests__/announce.test.ts +121 -0
- package/lib/utils/announce.ts +134 -0
- package/lib/utils/index.ts +1 -0
- package/package.json +3 -6
- package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
- package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
- /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
- /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
1
|
+
import { describe, expect, test, vi, beforeAll } from 'vitest';
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { render } from '@testing-library/react';
|
|
3
4
|
import userEvent from '@testing-library/user-event';
|
|
4
5
|
import Select from '../Select';
|
|
@@ -11,6 +12,13 @@ const defaultOptions = [
|
|
|
11
12
|
];
|
|
12
13
|
|
|
13
14
|
describe('Select', () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
|
17
|
+
value: vi.fn(),
|
|
18
|
+
writable: true,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
14
22
|
// Snapshot tests
|
|
15
23
|
|
|
16
24
|
test('Snapshot: default', () => {
|
|
@@ -19,7 +27,7 @@ describe('Select', () => {
|
|
|
19
27
|
<Select
|
|
20
28
|
options={defaultOptions}
|
|
21
29
|
value=''
|
|
22
|
-
|
|
30
|
+
onValueChange={() => {}}
|
|
23
31
|
/>
|
|
24
32
|
</ThemeContextProvider>
|
|
25
33
|
);
|
|
@@ -35,7 +43,7 @@ describe('Select', () => {
|
|
|
35
43
|
<Select
|
|
36
44
|
options={defaultOptions}
|
|
37
45
|
value=''
|
|
38
|
-
|
|
46
|
+
onValueChange={() => {}}
|
|
39
47
|
/>
|
|
40
48
|
</ThemeContextProvider>
|
|
41
49
|
);
|
|
@@ -50,7 +58,7 @@ describe('Select', () => {
|
|
|
50
58
|
native
|
|
51
59
|
options={defaultOptions}
|
|
52
60
|
value=''
|
|
53
|
-
|
|
61
|
+
nativeHtmlAttributes={{ onChange: () => {} }}
|
|
54
62
|
/>
|
|
55
63
|
</ThemeContextProvider>
|
|
56
64
|
);
|
|
@@ -66,7 +74,7 @@ describe('Select', () => {
|
|
|
66
74
|
<Select
|
|
67
75
|
options={defaultOptions}
|
|
68
76
|
value=''
|
|
69
|
-
|
|
77
|
+
onValueChange={() => {}}
|
|
70
78
|
/>
|
|
71
79
|
</ThemeContextProvider>
|
|
72
80
|
);
|
|
@@ -91,7 +99,7 @@ describe('Select', () => {
|
|
|
91
99
|
disabled
|
|
92
100
|
options={defaultOptions}
|
|
93
101
|
value=''
|
|
94
|
-
|
|
102
|
+
onValueChange={() => {}}
|
|
95
103
|
/>
|
|
96
104
|
</ThemeContextProvider>
|
|
97
105
|
);
|
|
@@ -109,11 +117,444 @@ describe('Select', () => {
|
|
|
109
117
|
native
|
|
110
118
|
options={defaultOptions}
|
|
111
119
|
value='1'
|
|
112
|
-
|
|
120
|
+
nativeHtmlAttributes={{ onChange: () => {} }}
|
|
113
121
|
/>
|
|
114
122
|
</ThemeContextProvider>
|
|
115
123
|
);
|
|
116
124
|
const select = renderResult.getByTestId('ucl-uikit-select--native');
|
|
117
125
|
expect(select).toHaveProperty('disabled');
|
|
118
126
|
});
|
|
127
|
+
|
|
128
|
+
test('Filters options when filterable is true', async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
const renderResult = render(
|
|
131
|
+
<ThemeContextProvider>
|
|
132
|
+
<Select
|
|
133
|
+
filterable
|
|
134
|
+
options={defaultOptions}
|
|
135
|
+
value=''
|
|
136
|
+
onValueChange={() => {}}
|
|
137
|
+
/>
|
|
138
|
+
</ThemeContextProvider>
|
|
139
|
+
);
|
|
140
|
+
await user.click(renderResult.getByTestId('ucl-uikit-select'));
|
|
141
|
+
const input = renderResult.container.querySelector('input');
|
|
142
|
+
expect(input).not.toBeNull();
|
|
143
|
+
await user.type(input as HTMLInputElement, '2');
|
|
144
|
+
|
|
145
|
+
const options = await renderResult.findAllByRole('option');
|
|
146
|
+
expect(options.length).toBe(1);
|
|
147
|
+
expect(options[0].textContent).toBe('Option 2');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('shows empty state when filter yields no options', async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
const result = render(
|
|
153
|
+
<ThemeContextProvider>
|
|
154
|
+
<Select
|
|
155
|
+
filterable
|
|
156
|
+
options={defaultOptions}
|
|
157
|
+
value=''
|
|
158
|
+
onValueChange={() => {}}
|
|
159
|
+
/>
|
|
160
|
+
</ThemeContextProvider>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
164
|
+
await user.type(result.getByRole('searchbox'), 'zzz');
|
|
165
|
+
|
|
166
|
+
expect(result.getByText('No options')).toBeInTheDocument();
|
|
167
|
+
expect(result.queryAllByTestId('ucl-uikit-select__option')).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('clears filter text after selecting an option', async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
const ControlledSelect = () => {
|
|
173
|
+
const [value, setValue] = useState('');
|
|
174
|
+
return (
|
|
175
|
+
<Select
|
|
176
|
+
filterable
|
|
177
|
+
options={defaultOptions}
|
|
178
|
+
value={value}
|
|
179
|
+
onValueChange={(next) => setValue(next as string)}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = render(
|
|
185
|
+
<ThemeContextProvider>
|
|
186
|
+
<ControlledSelect />
|
|
187
|
+
</ThemeContextProvider>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
191
|
+
await user.type(result.getByRole('searchbox'), '2');
|
|
192
|
+
await user.click(result.getByText('Option 2'));
|
|
193
|
+
|
|
194
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
195
|
+
expect(result.getByRole('searchbox')).toHaveValue('');
|
|
196
|
+
expect(result.getAllByTestId('ucl-uikit-select__option')).toHaveLength(
|
|
197
|
+
defaultOptions.length
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('clears filter text when dropdown closes', async () => {
|
|
202
|
+
const user = userEvent.setup();
|
|
203
|
+
const result = render(
|
|
204
|
+
<ThemeContextProvider>
|
|
205
|
+
<Select
|
|
206
|
+
filterable
|
|
207
|
+
options={defaultOptions}
|
|
208
|
+
value=''
|
|
209
|
+
onValueChange={() => {}}
|
|
210
|
+
/>
|
|
211
|
+
</ThemeContextProvider>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
215
|
+
await user.type(result.getByRole('searchbox'), '1');
|
|
216
|
+
|
|
217
|
+
await user.click(document.body);
|
|
218
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
219
|
+
|
|
220
|
+
expect(result.getByRole('searchbox')).toHaveValue('');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('keyboard navigation uses filtered options', async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const options = [
|
|
226
|
+
{ label: 'Alpha', value: 'alpha' },
|
|
227
|
+
{ label: 'Echo', value: 'echo' },
|
|
228
|
+
{ label: 'Gamma', value: 'gamma' },
|
|
229
|
+
];
|
|
230
|
+
const changeSpy = vi.fn();
|
|
231
|
+
|
|
232
|
+
const ControlledSelect = () => {
|
|
233
|
+
const [value, setValue] = useState('');
|
|
234
|
+
const handleChange = vi.fn((next, ev) => {
|
|
235
|
+
setValue(next as string);
|
|
236
|
+
changeSpy(next, ev);
|
|
237
|
+
});
|
|
238
|
+
return (
|
|
239
|
+
<Select
|
|
240
|
+
filterable
|
|
241
|
+
options={options}
|
|
242
|
+
value={value}
|
|
243
|
+
onValueChange={handleChange}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = render(
|
|
249
|
+
<ThemeContextProvider>
|
|
250
|
+
<ControlledSelect />
|
|
251
|
+
</ThemeContextProvider>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
255
|
+
await user.type(result.getByRole('searchbox'), 'a');
|
|
256
|
+
expect(result.getAllByTestId('ucl-uikit-select__option')).toHaveLength(2);
|
|
257
|
+
|
|
258
|
+
await user.keyboard('{ArrowDown}');
|
|
259
|
+
await user.keyboard('{ArrowDown}');
|
|
260
|
+
await user.keyboard('{ArrowDown}');
|
|
261
|
+
|
|
262
|
+
expect(changeSpy.mock.calls.map(([val]) => val)).toEqual([
|
|
263
|
+
'alpha',
|
|
264
|
+
'gamma',
|
|
265
|
+
'alpha',
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('selectionBehaviour=commit does not call onValueChange on arrow keys', async () => {
|
|
270
|
+
const user = userEvent.setup();
|
|
271
|
+
const changeSpy = vi.fn();
|
|
272
|
+
|
|
273
|
+
const ControlledSelect = () => {
|
|
274
|
+
const [value, setValue] = useState('');
|
|
275
|
+
return (
|
|
276
|
+
<Select
|
|
277
|
+
options={defaultOptions}
|
|
278
|
+
value={value}
|
|
279
|
+
selectionBehaviour='commit'
|
|
280
|
+
onValueChange={(next, ev) => {
|
|
281
|
+
setValue(next as string);
|
|
282
|
+
changeSpy(next, ev);
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = render(
|
|
289
|
+
<ThemeContextProvider>
|
|
290
|
+
<ControlledSelect />
|
|
291
|
+
</ThemeContextProvider>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
295
|
+
await user.keyboard('{ArrowDown}');
|
|
296
|
+
await user.keyboard('{ArrowDown}');
|
|
297
|
+
await user.keyboard('{ArrowDown}');
|
|
298
|
+
|
|
299
|
+
expect(changeSpy).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('selectionBehaviour=commit commits highlighted option on Enter', async () => {
|
|
303
|
+
const user = userEvent.setup();
|
|
304
|
+
const changeSpy = vi.fn();
|
|
305
|
+
|
|
306
|
+
const ControlledSelect = () => {
|
|
307
|
+
const [value, setValue] = useState('');
|
|
308
|
+
return (
|
|
309
|
+
<Select
|
|
310
|
+
options={defaultOptions}
|
|
311
|
+
value={value}
|
|
312
|
+
selectionBehaviour='commit'
|
|
313
|
+
onValueChange={(next, ev) => {
|
|
314
|
+
setValue(next as string);
|
|
315
|
+
changeSpy(next, ev);
|
|
316
|
+
}}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const result = render(
|
|
322
|
+
<ThemeContextProvider>
|
|
323
|
+
<ControlledSelect />
|
|
324
|
+
</ThemeContextProvider>
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
328
|
+
await user.keyboard('{ArrowDown}');
|
|
329
|
+
await user.keyboard('{ArrowDown}');
|
|
330
|
+
await user.keyboard('{Enter}');
|
|
331
|
+
|
|
332
|
+
expect(changeSpy.mock.calls.map(([val]) => val)).toEqual(['2']);
|
|
333
|
+
expect(
|
|
334
|
+
result.queryByTestId('ucl-uikit-select__panel')
|
|
335
|
+
).not.toBeInTheDocument();
|
|
336
|
+
expect(result.getByTestId('ucl-uikit-select')).toHaveTextContent(
|
|
337
|
+
'Option 2'
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('keyboard navigation does not get stuck on duplicate values', async () => {
|
|
342
|
+
const user = userEvent.setup();
|
|
343
|
+
const options = [
|
|
344
|
+
{ label: 'First duplicate', value: 'dup' },
|
|
345
|
+
{ label: 'Second duplicate', value: 'dup' },
|
|
346
|
+
{ label: 'Unique', value: 'unique' },
|
|
347
|
+
];
|
|
348
|
+
const changeSpy = vi.fn();
|
|
349
|
+
|
|
350
|
+
const ControlledSelect = () => {
|
|
351
|
+
const [value, setValue] = useState('');
|
|
352
|
+
const handleChange = vi.fn((next, ev) => {
|
|
353
|
+
setValue(next as string);
|
|
354
|
+
changeSpy(next, ev);
|
|
355
|
+
});
|
|
356
|
+
return (
|
|
357
|
+
<Select
|
|
358
|
+
options={options}
|
|
359
|
+
value={value}
|
|
360
|
+
onValueChange={handleChange}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const result = render(
|
|
366
|
+
<ThemeContextProvider>
|
|
367
|
+
<ControlledSelect />
|
|
368
|
+
</ThemeContextProvider>
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
372
|
+
await user.keyboard('{ArrowDown}');
|
|
373
|
+
await user.keyboard('{ArrowDown}');
|
|
374
|
+
await user.keyboard('{ArrowDown}');
|
|
375
|
+
|
|
376
|
+
expect(changeSpy.mock.calls.map(([val]) => val)).toEqual([
|
|
377
|
+
'dup',
|
|
378
|
+
'dup',
|
|
379
|
+
'unique',
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
const optionElements = result.getAllByRole('option');
|
|
383
|
+
expect(
|
|
384
|
+
optionElements.filter((el) => el.getAttribute('aria-selected') === 'true')
|
|
385
|
+
).toHaveLength(1);
|
|
386
|
+
expect(optionElements[2]).toHaveAttribute('aria-selected', 'true');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('keeps duplicate label identity when selecting by click', async () => {
|
|
390
|
+
const user = userEvent.setup();
|
|
391
|
+
const options = [
|
|
392
|
+
{ label: 'First duplicate', value: 'dup' },
|
|
393
|
+
{ label: 'Second duplicate', value: 'dup' },
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const ControlledSelect = () => {
|
|
397
|
+
const [value, setValue] = useState('');
|
|
398
|
+
return (
|
|
399
|
+
<Select
|
|
400
|
+
options={options}
|
|
401
|
+
value={value}
|
|
402
|
+
onValueChange={(next) => setValue(next as string)}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const result = render(
|
|
408
|
+
<ThemeContextProvider>
|
|
409
|
+
<ControlledSelect />
|
|
410
|
+
</ThemeContextProvider>
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
await user.click(result.getByTestId('ucl-uikit-select'));
|
|
414
|
+
await user.click(result.getAllByRole('option')[1]);
|
|
415
|
+
|
|
416
|
+
expect(result.getByTestId('ucl-uikit-select')).toHaveTextContent(
|
|
417
|
+
'Second duplicate'
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('forwards id and title attributes to the combobox', () => {
|
|
422
|
+
const result = render(
|
|
423
|
+
<ThemeContextProvider>
|
|
424
|
+
<Select
|
|
425
|
+
id='my-select'
|
|
426
|
+
title='Select an option'
|
|
427
|
+
options={defaultOptions}
|
|
428
|
+
value=''
|
|
429
|
+
onValueChange={() => {}}
|
|
430
|
+
/>
|
|
431
|
+
</ThemeContextProvider>
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const combobox = result.getByRole('combobox');
|
|
435
|
+
expect(combobox).toHaveAttribute('id', 'my-select');
|
|
436
|
+
expect(combobox).toHaveAttribute('title', 'Select an option');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('applies filterInputProps to the filter input', async () => {
|
|
440
|
+
const user = userEvent.setup();
|
|
441
|
+
const result = render(
|
|
442
|
+
<ThemeContextProvider>
|
|
443
|
+
<div>
|
|
444
|
+
<span id='filter-hint'>Filter options by text</span>
|
|
445
|
+
<Select
|
|
446
|
+
filterable
|
|
447
|
+
options={defaultOptions}
|
|
448
|
+
value=''
|
|
449
|
+
onValueChange={() => {}}
|
|
450
|
+
filterInputProps={{ 'aria-describedby': 'filter-hint' }}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</ThemeContextProvider>
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
await user.click(result.getByRole('combobox'));
|
|
457
|
+
const filterInput = result.getByRole('searchbox');
|
|
458
|
+
expect(filterInput).toHaveAttribute('aria-describedby', 'filter-hint');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('opens filter input on keyboard focus', async () => {
|
|
462
|
+
const user = userEvent.setup();
|
|
463
|
+
const result = render(
|
|
464
|
+
<ThemeContextProvider>
|
|
465
|
+
<Select
|
|
466
|
+
filterable
|
|
467
|
+
options={defaultOptions}
|
|
468
|
+
value=''
|
|
469
|
+
onValueChange={() => {}}
|
|
470
|
+
/>
|
|
471
|
+
</ThemeContextProvider>
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
await user.tab();
|
|
475
|
+
|
|
476
|
+
const filterInput = await result.findByRole('searchbox');
|
|
477
|
+
expect(filterInput).toHaveFocus();
|
|
478
|
+
expect(result.getByRole('listbox')).toBeInTheDocument();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('pressing Escape closes dropdown and clears filter', async () => {
|
|
482
|
+
const user = userEvent.setup();
|
|
483
|
+
const result = render(
|
|
484
|
+
<ThemeContextProvider>
|
|
485
|
+
<Select
|
|
486
|
+
filterable
|
|
487
|
+
options={defaultOptions}
|
|
488
|
+
value=''
|
|
489
|
+
onValueChange={() => {}}
|
|
490
|
+
/>
|
|
491
|
+
</ThemeContextProvider>
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const combobox = result.getByTestId('ucl-uikit-select');
|
|
495
|
+
await user.click(combobox);
|
|
496
|
+
await user.type(result.getByRole('searchbox'), '1');
|
|
497
|
+
await user.keyboard('{Escape}');
|
|
498
|
+
|
|
499
|
+
expect(
|
|
500
|
+
result.queryByTestId('ucl-uikit-select__panel')
|
|
501
|
+
).not.toBeInTheDocument();
|
|
502
|
+
expect(combobox).toHaveFocus();
|
|
503
|
+
|
|
504
|
+
await user.click(combobox);
|
|
505
|
+
expect(result.getByRole('searchbox')).toHaveValue('');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('announces active option and option position metadata', async () => {
|
|
509
|
+
const user = userEvent.setup();
|
|
510
|
+
const result = render(
|
|
511
|
+
<ThemeContextProvider>
|
|
512
|
+
<Select
|
|
513
|
+
options={defaultOptions}
|
|
514
|
+
value=''
|
|
515
|
+
onValueChange={() => {}}
|
|
516
|
+
/>
|
|
517
|
+
</ThemeContextProvider>
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const combobox = result.getByRole('combobox');
|
|
521
|
+
await user.click(combobox);
|
|
522
|
+
|
|
523
|
+
const options = result.getAllByRole('option');
|
|
524
|
+
expect(options[0]).toHaveAttribute('aria-posinset', '1');
|
|
525
|
+
expect(options[0]).toHaveAttribute('aria-setsize', '3');
|
|
526
|
+
expect(options[1]).toHaveAttribute('aria-posinset', '2');
|
|
527
|
+
expect(options[1]).toHaveAttribute('aria-setsize', '3');
|
|
528
|
+
|
|
529
|
+
await user.keyboard('{ArrowDown}');
|
|
530
|
+
expect(combobox).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
531
|
+
|
|
532
|
+
await user.keyboard('{ArrowDown}');
|
|
533
|
+
expect(combobox).toHaveAttribute('aria-activedescendant', options[1].id);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('filterable input tracks active descendant while navigating options', async () => {
|
|
537
|
+
const user = userEvent.setup();
|
|
538
|
+
const result = render(
|
|
539
|
+
<ThemeContextProvider>
|
|
540
|
+
<Select
|
|
541
|
+
filterable
|
|
542
|
+
options={defaultOptions}
|
|
543
|
+
value=''
|
|
544
|
+
onValueChange={() => {}}
|
|
545
|
+
/>
|
|
546
|
+
</ThemeContextProvider>
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
await user.click(result.getByRole('combobox'));
|
|
550
|
+
const filterInput = result.getByRole('searchbox');
|
|
551
|
+
expect(filterInput).toHaveFocus();
|
|
552
|
+
|
|
553
|
+
const options = result.getAllByRole('option');
|
|
554
|
+
await user.keyboard('{ArrowDown}');
|
|
555
|
+
expect(filterInput).toHaveAttribute('aria-activedescendant', options[0].id);
|
|
556
|
+
|
|
557
|
+
await user.keyboard('{ArrowDown}');
|
|
558
|
+
expect(filterInput).toHaveAttribute('aria-activedescendant', options[1].id);
|
|
559
|
+
});
|
|
119
560
|
});
|
|
@@ -7,6 +7,7 @@ const NAME = 'ucl-uikit-select__option';
|
|
|
7
7
|
|
|
8
8
|
const CustomOption = <T extends string | number>({
|
|
9
9
|
value,
|
|
10
|
+
optionIndex,
|
|
10
11
|
isSelected = false,
|
|
11
12
|
onSelect,
|
|
12
13
|
lineBreak = false,
|
|
@@ -29,7 +30,7 @@ const CustomOption = <T extends string | number>({
|
|
|
29
30
|
}, [isSelected]);
|
|
30
31
|
|
|
31
32
|
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
32
|
-
onSelect(event, value);
|
|
33
|
+
onSelect(event, value, optionIndex);
|
|
33
34
|
event.stopPropagation(); // Otherwise the panel will open again instantaneously
|
|
34
35
|
};
|
|
35
36
|
|