kempo-ui 0.3.13 → 0.3.15
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/dist/components/Combobox.js +97 -0
- package/dist/components/Option.js +1 -0
- package/docs/components/accordion.html +2 -0
- package/docs/components/aside.html +2 -0
- package/docs/components/card.html +2 -0
- package/docs/components/code-editor.html +2 -0
- package/docs/components/color-picker.html +2 -0
- package/docs/components/combobox.html +612 -0
- package/docs/components/content-slider.html +2 -0
- package/docs/components/context.html +2 -0
- package/docs/components/dialog.html +2 -0
- package/docs/components/dropdown.html +2 -0
- package/docs/components/filter-list.html +2 -0
- package/docs/components/focus-capture.html +2 -0
- package/docs/components/html-editor.html +2 -0
- package/docs/components/hybrid-component.html +2 -0
- package/docs/components/icon.html +2 -0
- package/docs/components/import.html +2 -0
- package/docs/components/light-component.html +2 -0
- package/docs/components/nav-spacer.html +2 -0
- package/docs/components/nav.html +2 -0
- package/docs/components/photo-viewer.html +2 -0
- package/docs/components/resize.html +2 -0
- package/docs/components/shadow-component.html +2 -0
- package/docs/components/show-more.html +2 -0
- package/docs/components/sortable.html +2 -0
- package/docs/components/spinner.html +2 -0
- package/docs/components/split.html +2 -0
- package/docs/components/table.html +2 -0
- package/docs/components/tableControls.html +2 -0
- package/docs/components/tableCustomFields.html +2 -0
- package/docs/components/tableFetchRecords.html +2 -0
- package/docs/components/tableFieldSortHide.html +2 -0
- package/docs/components/tablePagination.html +2 -0
- package/docs/components/tablePlaceholder.html +2 -0
- package/docs/components/tableRecordEditing.html +2 -0
- package/docs/components/tableRecordFiltering.html +2 -0
- package/docs/components/tableRecordHiding.html +2 -0
- package/docs/components/tableRecordSearching.html +2 -0
- package/docs/components/tableRecordSelection.html +2 -0
- package/docs/components/tableRowControls.html +2 -0
- package/docs/components/tableServerSync.html +2 -0
- package/docs/components/tableSorting.html +2 -0
- package/docs/components/tabs.html +2 -0
- package/docs/components/tags.html +2 -0
- package/docs/components/theme-select.html +2 -0
- package/docs/components/theme-switcher.html +2 -0
- package/docs/components/timestamp.html +2 -0
- package/docs/components/toast.html +2 -0
- package/docs/components/toggle.html +2 -0
- package/docs/components/tree.html +2 -0
- package/docs/index.html +8 -0
- package/docs/src/components/Combobox.js +97 -0
- package/docs/src/components/Option.js +1 -0
- package/docs/utils/context.html +2 -0
- package/docs/utils/cookie.html +2 -0
- package/docs/utils/debounce.html +2 -0
- package/docs/utils/drag.html +2 -0
- package/docs/utils/elevation.html +2 -0
- package/docs/utils/formatTimestamp.html +2 -0
- package/docs/utils/object.html +2 -0
- package/docs/utils/propConverters.html +2 -0
- package/docs/utils/string.html +2 -0
- package/docs/utils/theme.html +2 -0
- package/docs/utils/toTitleCase.html +2 -0
- package/docs/utils/type.html +2 -0
- package/docs/utils/wait.html +2 -0
- package/docs-src/components/combobox.page.html +358 -0
- package/docs-src/index.page.html +6 -0
- package/docs-src/nav.fragment.html +2 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/src/components/Combobox.js +338 -0
- package/src/components/Option.js +19 -0
- package/tests/components/Combobox.browser-test.js +790 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import Combobox from '../../src/components/Combobox.js';
|
|
2
|
+
|
|
3
|
+
const createCombobox = async (attrs = {}, children = '') => {
|
|
4
|
+
const container = document.createElement('div');
|
|
5
|
+
container.innerHTML = `
|
|
6
|
+
<k-combobox
|
|
7
|
+
${attrs.placeholder ? `placeholder="${attrs.placeholder}"` : ''}
|
|
8
|
+
${attrs.value ? `value="${attrs.value}"` : ''}
|
|
9
|
+
${attrs.searching ? 'searching' : ''}
|
|
10
|
+
${attrs.required ? 'required' : ''}
|
|
11
|
+
${attrs.requireMatch ? 'require-match' : ''}
|
|
12
|
+
${attrs.disabled ? 'disabled' : ''}
|
|
13
|
+
${attrs.name ? `name="${attrs.name}"` : ''}
|
|
14
|
+
${attrs.debounceMs ? `debounce-ms="${attrs.debounceMs}"` : ''}
|
|
15
|
+
${attrs.maxVisible ? `max-visible="${attrs.maxVisible}"` : ''}
|
|
16
|
+
>${children}</k-combobox>
|
|
17
|
+
`;
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
const el = container.querySelector('k-combobox');
|
|
20
|
+
await el.updateComplete;
|
|
21
|
+
return { container, el };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fruitOptions = `
|
|
25
|
+
<k-option value="apple">Apple</k-option>
|
|
26
|
+
<k-option value="banana">Banana</k-option>
|
|
27
|
+
<k-option value="cherry">Cherry</k-option>
|
|
28
|
+
<k-option value="date">Date</k-option>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const cleanup = container => {
|
|
32
|
+
container?.parentNode?.removeChild(container);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
/*
|
|
37
|
+
Element Creation
|
|
38
|
+
*/
|
|
39
|
+
'should create combobox element': async ({pass, fail}) => {
|
|
40
|
+
const { container, el } = await createCombobox();
|
|
41
|
+
if(!(el instanceof Combobox)) {
|
|
42
|
+
cleanup(container);
|
|
43
|
+
return fail('Element should be instance of Combobox');
|
|
44
|
+
}
|
|
45
|
+
cleanup(container);
|
|
46
|
+
pass();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
'should have shadow root': async ({pass, fail}) => {
|
|
50
|
+
const { container, el } = await createCombobox();
|
|
51
|
+
if(!el.shadowRoot) {
|
|
52
|
+
cleanup(container);
|
|
53
|
+
return fail('Combobox should have shadow root');
|
|
54
|
+
}
|
|
55
|
+
cleanup(container);
|
|
56
|
+
pass();
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
Default Properties
|
|
61
|
+
*/
|
|
62
|
+
'should have empty value by default': async ({pass, fail}) => {
|
|
63
|
+
const { container, el } = await createCombobox();
|
|
64
|
+
if(el.value !== '') {
|
|
65
|
+
cleanup(container);
|
|
66
|
+
return fail(`Expected '', got '${el.value}'`);
|
|
67
|
+
}
|
|
68
|
+
cleanup(container);
|
|
69
|
+
pass();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
'should have empty placeholder by default': async ({pass, fail}) => {
|
|
73
|
+
const { container, el } = await createCombobox();
|
|
74
|
+
if(el.placeholder !== '') {
|
|
75
|
+
cleanup(container);
|
|
76
|
+
return fail(`Expected '', got '${el.placeholder}'`);
|
|
77
|
+
}
|
|
78
|
+
cleanup(container);
|
|
79
|
+
pass();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
'should be closed by default': async ({pass, fail}) => {
|
|
83
|
+
const { container, el } = await createCombobox();
|
|
84
|
+
if(el.opened !== false) {
|
|
85
|
+
cleanup(container);
|
|
86
|
+
return fail(`Expected false, got ${el.opened}`);
|
|
87
|
+
}
|
|
88
|
+
cleanup(container);
|
|
89
|
+
pass();
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
'should not be searching by default': async ({pass, fail}) => {
|
|
93
|
+
const { container, el } = await createCombobox();
|
|
94
|
+
if(el.searching !== false) {
|
|
95
|
+
cleanup(container);
|
|
96
|
+
return fail(`Expected false, got ${el.searching}`);
|
|
97
|
+
}
|
|
98
|
+
cleanup(container);
|
|
99
|
+
pass();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'should have default debounceMs of 300': async ({pass, fail}) => {
|
|
103
|
+
const { container, el } = await createCombobox();
|
|
104
|
+
if(el.debounceMs !== 300) {
|
|
105
|
+
cleanup(container);
|
|
106
|
+
return fail(`Expected 300, got ${el.debounceMs}`);
|
|
107
|
+
}
|
|
108
|
+
cleanup(container);
|
|
109
|
+
pass();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
'should have default maxVisible of 8': async ({pass, fail}) => {
|
|
113
|
+
const { container, el } = await createCombobox();
|
|
114
|
+
if(el.maxVisible !== 8) {
|
|
115
|
+
cleanup(container);
|
|
116
|
+
return fail(`Expected 8, got ${el.maxVisible}`);
|
|
117
|
+
}
|
|
118
|
+
cleanup(container);
|
|
119
|
+
pass();
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
'should not be required by default': async ({pass, fail}) => {
|
|
123
|
+
const { container, el } = await createCombobox();
|
|
124
|
+
if(el.required !== false) {
|
|
125
|
+
cleanup(container);
|
|
126
|
+
return fail(`Expected false, got ${el.required}`);
|
|
127
|
+
}
|
|
128
|
+
cleanup(container);
|
|
129
|
+
pass();
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
'should not require match by default': async ({pass, fail}) => {
|
|
133
|
+
const { container, el } = await createCombobox();
|
|
134
|
+
if(el.requireMatch !== false) {
|
|
135
|
+
cleanup(container);
|
|
136
|
+
return fail(`Expected false, got ${el.requireMatch}`);
|
|
137
|
+
}
|
|
138
|
+
cleanup(container);
|
|
139
|
+
pass();
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
'should not be disabled by default': async ({pass, fail}) => {
|
|
143
|
+
const { container, el } = await createCombobox();
|
|
144
|
+
if(el.disabled !== false) {
|
|
145
|
+
cleanup(container);
|
|
146
|
+
return fail(`Expected false, got ${el.disabled}`);
|
|
147
|
+
}
|
|
148
|
+
cleanup(container);
|
|
149
|
+
pass();
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
Attributes
|
|
154
|
+
*/
|
|
155
|
+
'should reflect placeholder attribute': async ({pass, fail}) => {
|
|
156
|
+
const { container, el } = await createCombobox({ placeholder: 'Search...' });
|
|
157
|
+
if(el.placeholder !== 'Search...') {
|
|
158
|
+
cleanup(container);
|
|
159
|
+
return fail(`Expected 'Search...', got '${el.placeholder}'`);
|
|
160
|
+
}
|
|
161
|
+
cleanup(container);
|
|
162
|
+
pass();
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
'should reflect debounce-ms attribute': async ({pass, fail}) => {
|
|
166
|
+
const { container, el } = await createCombobox({ debounceMs: 500 });
|
|
167
|
+
if(el.debounceMs !== 500) {
|
|
168
|
+
cleanup(container);
|
|
169
|
+
return fail(`Expected 500, got ${el.debounceMs}`);
|
|
170
|
+
}
|
|
171
|
+
cleanup(container);
|
|
172
|
+
pass();
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
'should reflect searching attribute': async ({pass, fail}) => {
|
|
176
|
+
const { container, el } = await createCombobox({ searching: true });
|
|
177
|
+
if(el.searching !== true) {
|
|
178
|
+
cleanup(container);
|
|
179
|
+
return fail(`Expected true, got ${el.searching}`);
|
|
180
|
+
}
|
|
181
|
+
cleanup(container);
|
|
182
|
+
pass();
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
'should reflect required attribute': async ({pass, fail}) => {
|
|
186
|
+
const { container, el } = await createCombobox({ required: true });
|
|
187
|
+
if(el.required !== true) {
|
|
188
|
+
cleanup(container);
|
|
189
|
+
return fail(`Expected true, got ${el.required}`);
|
|
190
|
+
}
|
|
191
|
+
cleanup(container);
|
|
192
|
+
pass();
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
'should reflect require-match attribute': async ({pass, fail}) => {
|
|
196
|
+
const { container, el } = await createCombobox({ requireMatch: true });
|
|
197
|
+
if(el.requireMatch !== true) {
|
|
198
|
+
cleanup(container);
|
|
199
|
+
return fail(`Expected true, got ${el.requireMatch}`);
|
|
200
|
+
}
|
|
201
|
+
cleanup(container);
|
|
202
|
+
pass();
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
'should reflect disabled attribute': async ({pass, fail}) => {
|
|
206
|
+
const { container, el } = await createCombobox({ disabled: true });
|
|
207
|
+
if(el.disabled !== true) {
|
|
208
|
+
cleanup(container);
|
|
209
|
+
return fail(`Expected true, got ${el.disabled}`);
|
|
210
|
+
}
|
|
211
|
+
cleanup(container);
|
|
212
|
+
pass();
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
'should reflect max-visible attribute': async ({pass, fail}) => {
|
|
216
|
+
const { container, el } = await createCombobox({ maxVisible: 4 });
|
|
217
|
+
if(el.maxVisible !== 4) {
|
|
218
|
+
cleanup(container);
|
|
219
|
+
return fail(`Expected 4, got ${el.maxVisible}`);
|
|
220
|
+
}
|
|
221
|
+
cleanup(container);
|
|
222
|
+
pass();
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
Children (k-option)
|
|
227
|
+
*/
|
|
228
|
+
'should read k-option children': async ({pass, fail}) => {
|
|
229
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
230
|
+
el.opened = true;
|
|
231
|
+
await el.updateComplete;
|
|
232
|
+
const items = el.shadowRoot.querySelectorAll('.option');
|
|
233
|
+
if(items.length !== 4) {
|
|
234
|
+
cleanup(container);
|
|
235
|
+
return fail(`Expected 4 options, got ${items.length}`);
|
|
236
|
+
}
|
|
237
|
+
cleanup(container);
|
|
238
|
+
pass();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
'should use k-option text as label': async ({pass, fail}) => {
|
|
242
|
+
const { container, el } = await createCombobox({}, '<k-option value="a">Alpha</k-option>');
|
|
243
|
+
el.opened = true;
|
|
244
|
+
await el.updateComplete;
|
|
245
|
+
const item = el.shadowRoot.querySelector('.option');
|
|
246
|
+
if(!item || item.textContent.trim() !== 'Alpha') {
|
|
247
|
+
cleanup(container);
|
|
248
|
+
return fail(`Expected 'Alpha', got '${item?.textContent}'`);
|
|
249
|
+
}
|
|
250
|
+
cleanup(container);
|
|
251
|
+
pass();
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
Rendering
|
|
256
|
+
*/
|
|
257
|
+
'should render an input element': async ({pass, fail}) => {
|
|
258
|
+
const { container, el } = await createCombobox();
|
|
259
|
+
if(!el.shadowRoot.querySelector('input')) {
|
|
260
|
+
cleanup(container);
|
|
261
|
+
return fail('Input element not found');
|
|
262
|
+
}
|
|
263
|
+
cleanup(container);
|
|
264
|
+
pass();
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
'should not render menu when closed': async ({pass, fail}) => {
|
|
268
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
269
|
+
if(el.shadowRoot.querySelector('#menu')) {
|
|
270
|
+
cleanup(container);
|
|
271
|
+
return fail('Menu should not be rendered when closed');
|
|
272
|
+
}
|
|
273
|
+
cleanup(container);
|
|
274
|
+
pass();
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
'should render menu when opened': async ({pass, fail}) => {
|
|
278
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
279
|
+
el.opened = true;
|
|
280
|
+
await el.updateComplete;
|
|
281
|
+
if(!el.shadowRoot.querySelector('#menu')) {
|
|
282
|
+
cleanup(container);
|
|
283
|
+
return fail('Menu should be rendered when opened');
|
|
284
|
+
}
|
|
285
|
+
cleanup(container);
|
|
286
|
+
pass();
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
setOptions Method
|
|
291
|
+
*/
|
|
292
|
+
'should create k-option children via setOptions with strings': async ({pass, fail}) => {
|
|
293
|
+
const { container, el } = await createCombobox();
|
|
294
|
+
el.setOptions(['Apple', 'Banana', 'Cherry']);
|
|
295
|
+
await el.updateComplete;
|
|
296
|
+
const opts = el.querySelectorAll('k-option');
|
|
297
|
+
if(opts.length !== 3) {
|
|
298
|
+
cleanup(container);
|
|
299
|
+
return fail(`Expected 3 k-option children, got ${opts.length}`);
|
|
300
|
+
}
|
|
301
|
+
if(opts[0].textContent.trim() !== 'Apple') {
|
|
302
|
+
cleanup(container);
|
|
303
|
+
return fail(`Expected 'Apple', got '${opts[0].textContent}'`);
|
|
304
|
+
}
|
|
305
|
+
cleanup(container);
|
|
306
|
+
pass();
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
'should create k-option children via setOptions with objects': async ({pass, fail}) => {
|
|
310
|
+
const { container, el } = await createCombobox();
|
|
311
|
+
el.setOptions([{ label: 'Alice', value: 'a1' }, { label: 'Bob', value: 'b2' }]);
|
|
312
|
+
await el.updateComplete;
|
|
313
|
+
const opts = el.querySelectorAll('k-option');
|
|
314
|
+
if(opts.length !== 2) {
|
|
315
|
+
cleanup(container);
|
|
316
|
+
return fail(`Expected 2, got ${opts.length}`);
|
|
317
|
+
}
|
|
318
|
+
if(opts[0].value !== 'a1') {
|
|
319
|
+
cleanup(container);
|
|
320
|
+
return fail(`Expected value 'a1', got '${opts[0].value}'`);
|
|
321
|
+
}
|
|
322
|
+
if(opts[0].textContent.trim() !== 'Alice') {
|
|
323
|
+
cleanup(container);
|
|
324
|
+
return fail(`Expected 'Alice', got '${opts[0].textContent}'`);
|
|
325
|
+
}
|
|
326
|
+
cleanup(container);
|
|
327
|
+
pass();
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
'setOptions should replace existing children': async ({pass, fail}) => {
|
|
331
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
332
|
+
el.setOptions(['X', 'Y']);
|
|
333
|
+
await el.updateComplete;
|
|
334
|
+
const opts = el.querySelectorAll('k-option');
|
|
335
|
+
if(opts.length !== 2) {
|
|
336
|
+
cleanup(container);
|
|
337
|
+
return fail(`Expected 2 after replacement, got ${opts.length}`);
|
|
338
|
+
}
|
|
339
|
+
cleanup(container);
|
|
340
|
+
pass();
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
'setOptions should return this for chaining': async ({pass, fail}) => {
|
|
344
|
+
const { container, el } = await createCombobox();
|
|
345
|
+
if(el.setOptions(['A']) !== el) {
|
|
346
|
+
cleanup(container);
|
|
347
|
+
return fail('setOptions should return this');
|
|
348
|
+
}
|
|
349
|
+
cleanup(container);
|
|
350
|
+
pass();
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/*
|
|
354
|
+
clear Method
|
|
355
|
+
*/
|
|
356
|
+
'should clear value and close': async ({pass, fail}) => {
|
|
357
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
358
|
+
el.value = 'test';
|
|
359
|
+
el.opened = true;
|
|
360
|
+
el.clear();
|
|
361
|
+
if(el.value !== '' || el.opened !== false) {
|
|
362
|
+
cleanup(container);
|
|
363
|
+
return fail(`clear() did not reset: value='${el.value}', opened=${el.opened}`);
|
|
364
|
+
}
|
|
365
|
+
cleanup(container);
|
|
366
|
+
pass();
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
'clear should return this for chaining': async ({pass, fail}) => {
|
|
370
|
+
const { container, el } = await createCombobox();
|
|
371
|
+
if(el.clear() !== el) {
|
|
372
|
+
cleanup(container);
|
|
373
|
+
return fail('clear should return this');
|
|
374
|
+
}
|
|
375
|
+
cleanup(container);
|
|
376
|
+
pass();
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/*
|
|
380
|
+
Filtering
|
|
381
|
+
*/
|
|
382
|
+
'should filter options by input value': async ({pass, fail}) => {
|
|
383
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
384
|
+
el.value = 'an';
|
|
385
|
+
el.opened = true;
|
|
386
|
+
await el.updateComplete;
|
|
387
|
+
const items = el.shadowRoot.querySelectorAll('.option');
|
|
388
|
+
if(items.length !== 1) {
|
|
389
|
+
cleanup(container);
|
|
390
|
+
return fail(`Expected 1 filtered item (Banana), got ${items.length}`);
|
|
391
|
+
}
|
|
392
|
+
if(items[0].textContent.trim() !== 'Banana') {
|
|
393
|
+
cleanup(container);
|
|
394
|
+
return fail(`Expected 'Banana', got '${items[0].textContent}'`);
|
|
395
|
+
}
|
|
396
|
+
cleanup(container);
|
|
397
|
+
pass();
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
'should filter object options set via setOptions': async ({pass, fail}) => {
|
|
401
|
+
const { container, el } = await createCombobox();
|
|
402
|
+
el.setOptions([
|
|
403
|
+
{ label: 'alice@test.com', value: 'a1' },
|
|
404
|
+
{ label: 'bob@test.com', value: 'b2' },
|
|
405
|
+
{ label: 'alicia@test.com', value: 'a3' }
|
|
406
|
+
]);
|
|
407
|
+
el.value = 'ali';
|
|
408
|
+
el.opened = true;
|
|
409
|
+
await el.updateComplete;
|
|
410
|
+
const items = el.shadowRoot.querySelectorAll('.option');
|
|
411
|
+
if(items.length !== 2) {
|
|
412
|
+
cleanup(container);
|
|
413
|
+
return fail(`Expected 2 filtered items, got ${items.length}`);
|
|
414
|
+
}
|
|
415
|
+
cleanup(container);
|
|
416
|
+
pass();
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
'should show all options when value is empty': async ({pass, fail}) => {
|
|
420
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
421
|
+
el.value = '';
|
|
422
|
+
el.opened = true;
|
|
423
|
+
await el.updateComplete;
|
|
424
|
+
const items = el.shadowRoot.querySelectorAll('.option');
|
|
425
|
+
if(items.length !== 4) {
|
|
426
|
+
cleanup(container);
|
|
427
|
+
return fail(`Expected 4, got ${items.length}`);
|
|
428
|
+
}
|
|
429
|
+
cleanup(container);
|
|
430
|
+
pass();
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
'should show no-results when nothing matches': async ({pass, fail}) => {
|
|
434
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
435
|
+
el.value = 'zzz';
|
|
436
|
+
el.opened = true;
|
|
437
|
+
await el.updateComplete;
|
|
438
|
+
if(!el.shadowRoot.querySelector('.no-results')) {
|
|
439
|
+
cleanup(container);
|
|
440
|
+
return fail('No-results message should be displayed');
|
|
441
|
+
}
|
|
442
|
+
cleanup(container);
|
|
443
|
+
pass();
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/*
|
|
447
|
+
Max Visible
|
|
448
|
+
*/
|
|
449
|
+
'should limit visible options to maxVisible': async ({pass, fail}) => {
|
|
450
|
+
const { container, el } = await createCombobox({ maxVisible: 2 }, fruitOptions);
|
|
451
|
+
el.opened = true;
|
|
452
|
+
await el.updateComplete;
|
|
453
|
+
const items = el.shadowRoot.querySelectorAll('.option');
|
|
454
|
+
if(items.length !== 2) {
|
|
455
|
+
cleanup(container);
|
|
456
|
+
return fail(`Expected 2 visible, got ${items.length}`);
|
|
457
|
+
}
|
|
458
|
+
cleanup(container);
|
|
459
|
+
pass();
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
'should show more indicator when options exceed maxVisible': async ({pass, fail}) => {
|
|
463
|
+
const { container, el } = await createCombobox({ maxVisible: 2 }, fruitOptions);
|
|
464
|
+
el.opened = true;
|
|
465
|
+
await el.updateComplete;
|
|
466
|
+
const more = el.shadowRoot.querySelector('.more');
|
|
467
|
+
if(!more) {
|
|
468
|
+
cleanup(container);
|
|
469
|
+
return fail('More indicator should be shown');
|
|
470
|
+
}
|
|
471
|
+
if(!more.textContent.includes('2 more')) {
|
|
472
|
+
cleanup(container);
|
|
473
|
+
return fail(`Expected '2 more...', got '${more.textContent}'`);
|
|
474
|
+
}
|
|
475
|
+
cleanup(container);
|
|
476
|
+
pass();
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
/*
|
|
480
|
+
Events
|
|
481
|
+
*/
|
|
482
|
+
'should fire select event when option is clicked': async ({pass, fail}) => {
|
|
483
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
484
|
+
el.opened = true;
|
|
485
|
+
await el.updateComplete;
|
|
486
|
+
let selected = null;
|
|
487
|
+
el.addEventListener('select', e => { selected = e.detail; });
|
|
488
|
+
el.shadowRoot.querySelector('.option').click();
|
|
489
|
+
await el.updateComplete;
|
|
490
|
+
if(!selected || selected.value !== 'apple') {
|
|
491
|
+
cleanup(container);
|
|
492
|
+
return fail(`Expected value 'apple', got ${JSON.stringify(selected)}`);
|
|
493
|
+
}
|
|
494
|
+
if(selected.label !== 'Apple') {
|
|
495
|
+
cleanup(container);
|
|
496
|
+
return fail(`Expected label 'Apple', got '${selected.label}'`);
|
|
497
|
+
}
|
|
498
|
+
cleanup(container);
|
|
499
|
+
pass();
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
'should fire change event after selecting': async ({pass, fail}) => {
|
|
503
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
504
|
+
el.opened = true;
|
|
505
|
+
await el.updateComplete;
|
|
506
|
+
let changed = false;
|
|
507
|
+
el.addEventListener('change', () => { changed = true; });
|
|
508
|
+
el.shadowRoot.querySelector('.option').click();
|
|
509
|
+
await el.updateComplete;
|
|
510
|
+
if(!changed) {
|
|
511
|
+
cleanup(container);
|
|
512
|
+
return fail('change event should have fired');
|
|
513
|
+
}
|
|
514
|
+
cleanup(container);
|
|
515
|
+
pass();
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
'should close dropdown after selecting an option': async ({pass, fail}) => {
|
|
519
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
520
|
+
el.opened = true;
|
|
521
|
+
await el.updateComplete;
|
|
522
|
+
el.shadowRoot.querySelector('.option').click();
|
|
523
|
+
await el.updateComplete;
|
|
524
|
+
if(el.opened) {
|
|
525
|
+
cleanup(container);
|
|
526
|
+
return fail('Dropdown should close after selection');
|
|
527
|
+
}
|
|
528
|
+
cleanup(container);
|
|
529
|
+
pass();
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
'should set value to selected option label': async ({pass, fail}) => {
|
|
533
|
+
const { container, el } = await createCombobox({}, '<k-option value="a1">Alpha</k-option>');
|
|
534
|
+
el.opened = true;
|
|
535
|
+
await el.updateComplete;
|
|
536
|
+
el.shadowRoot.querySelector('.option').click();
|
|
537
|
+
await el.updateComplete;
|
|
538
|
+
if(el.value !== 'Alpha') {
|
|
539
|
+
cleanup(container);
|
|
540
|
+
return fail(`Expected 'Alpha', got '${el.value}'`);
|
|
541
|
+
}
|
|
542
|
+
cleanup(container);
|
|
543
|
+
pass();
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
'should fire search event after debounce': async ({pass, fail}) => {
|
|
547
|
+
const { container, el } = await createCombobox({ debounceMs: 50 });
|
|
548
|
+
let searchValue = null;
|
|
549
|
+
el.addEventListener('search', e => { searchValue = e.detail.value; });
|
|
550
|
+
const input = el.shadowRoot.querySelector('input');
|
|
551
|
+
input.value = 'test';
|
|
552
|
+
input.dispatchEvent(new Event('input'));
|
|
553
|
+
await new Promise(r => setTimeout(r, 100));
|
|
554
|
+
if(searchValue !== 'test') {
|
|
555
|
+
cleanup(container);
|
|
556
|
+
return fail(`Expected 'test', got '${searchValue}'`);
|
|
557
|
+
}
|
|
558
|
+
cleanup(container);
|
|
559
|
+
pass();
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/*
|
|
563
|
+
Keyboard Navigation
|
|
564
|
+
*/
|
|
565
|
+
'should open dropdown on ArrowDown': async ({pass, fail}) => {
|
|
566
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
567
|
+
const input = el.shadowRoot.querySelector('input');
|
|
568
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
569
|
+
await el.updateComplete;
|
|
570
|
+
if(!el.opened) {
|
|
571
|
+
cleanup(container);
|
|
572
|
+
return fail('Dropdown should open on ArrowDown');
|
|
573
|
+
}
|
|
574
|
+
cleanup(container);
|
|
575
|
+
pass();
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
'should close dropdown on Escape': async ({pass, fail}) => {
|
|
579
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
580
|
+
el.opened = true;
|
|
581
|
+
await el.updateComplete;
|
|
582
|
+
const input = el.shadowRoot.querySelector('input');
|
|
583
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
584
|
+
await el.updateComplete;
|
|
585
|
+
if(el.opened) {
|
|
586
|
+
cleanup(container);
|
|
587
|
+
return fail('Dropdown should close on Escape');
|
|
588
|
+
}
|
|
589
|
+
cleanup(container);
|
|
590
|
+
pass();
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
'should select focused option on Enter': async ({pass, fail}) => {
|
|
594
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
595
|
+
el.opened = true;
|
|
596
|
+
await el.updateComplete;
|
|
597
|
+
const input = el.shadowRoot.querySelector('input');
|
|
598
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
599
|
+
await el.updateComplete;
|
|
600
|
+
let selected = null;
|
|
601
|
+
el.addEventListener('select', e => { selected = e.detail; });
|
|
602
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
603
|
+
await el.updateComplete;
|
|
604
|
+
if(!selected || selected.value !== 'apple') {
|
|
605
|
+
cleanup(container);
|
|
606
|
+
return fail(`Expected 'apple', got ${JSON.stringify(selected)}`);
|
|
607
|
+
}
|
|
608
|
+
cleanup(container);
|
|
609
|
+
pass();
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
/*
|
|
613
|
+
Searching Indicator
|
|
614
|
+
*/
|
|
615
|
+
'should show spinner when searching and opened': async ({pass, fail}) => {
|
|
616
|
+
const { container, el } = await createCombobox({ searching: true }, fruitOptions);
|
|
617
|
+
el.opened = true;
|
|
618
|
+
await el.updateComplete;
|
|
619
|
+
if(!el.shadowRoot.querySelector('.searching k-spinner')) {
|
|
620
|
+
cleanup(container);
|
|
621
|
+
return fail('Spinner should be visible');
|
|
622
|
+
}
|
|
623
|
+
cleanup(container);
|
|
624
|
+
pass();
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
'should not show spinner when not searching': async ({pass, fail}) => {
|
|
628
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
629
|
+
el.opened = true;
|
|
630
|
+
await el.updateComplete;
|
|
631
|
+
if(el.shadowRoot.querySelector('.searching')) {
|
|
632
|
+
cleanup(container);
|
|
633
|
+
return fail('Searching indicator should not be visible');
|
|
634
|
+
}
|
|
635
|
+
cleanup(container);
|
|
636
|
+
pass();
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
/*
|
|
640
|
+
Document Click
|
|
641
|
+
*/
|
|
642
|
+
'should close on outside click': async ({pass, fail}) => {
|
|
643
|
+
const { container, el } = await createCombobox({}, fruitOptions);
|
|
644
|
+
el.opened = true;
|
|
645
|
+
await el.updateComplete;
|
|
646
|
+
document.body.click();
|
|
647
|
+
await el.updateComplete;
|
|
648
|
+
if(el.opened) {
|
|
649
|
+
cleanup(container);
|
|
650
|
+
return fail('Dropdown should close on outside click');
|
|
651
|
+
}
|
|
652
|
+
cleanup(container);
|
|
653
|
+
pass();
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
/*
|
|
657
|
+
Form Association
|
|
658
|
+
*/
|
|
659
|
+
'should be form-associated': async ({pass, fail}) => {
|
|
660
|
+
if(Combobox.formAssociated !== true) return fail('formAssociated should be true');
|
|
661
|
+
pass();
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
'should have internals': async ({pass, fail}) => {
|
|
665
|
+
const { container, el } = await createCombobox();
|
|
666
|
+
if(!el.internals) {
|
|
667
|
+
cleanup(container);
|
|
668
|
+
return fail('internals should exist');
|
|
669
|
+
}
|
|
670
|
+
cleanup(container);
|
|
671
|
+
pass();
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
'should submit matched option value in form': async ({pass, fail}) => {
|
|
675
|
+
const form = document.createElement('form');
|
|
676
|
+
form.innerHTML = `
|
|
677
|
+
<k-combobox name="fruit">
|
|
678
|
+
<k-option value="a1">Apple</k-option>
|
|
679
|
+
</k-combobox>
|
|
680
|
+
`;
|
|
681
|
+
document.body.appendChild(form);
|
|
682
|
+
const el = form.querySelector('k-combobox');
|
|
683
|
+
await el.updateComplete;
|
|
684
|
+
el.value = 'Apple';
|
|
685
|
+
await el.updateComplete;
|
|
686
|
+
const data = new FormData(form);
|
|
687
|
+
document.body.removeChild(form);
|
|
688
|
+
if(data.get('fruit') !== 'a1') return fail(`Expected 'a1', got '${data.get('fruit')}'`);
|
|
689
|
+
pass();
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
/*
|
|
693
|
+
Required Validation
|
|
694
|
+
*/
|
|
695
|
+
'required: invalid when empty': async ({pass, fail}) => {
|
|
696
|
+
const { container, el } = await createCombobox({ required: true }, fruitOptions);
|
|
697
|
+
await el.updateComplete;
|
|
698
|
+
if(el.internals.checkValidity()) {
|
|
699
|
+
cleanup(container);
|
|
700
|
+
return fail('Should be invalid when required and empty');
|
|
701
|
+
}
|
|
702
|
+
cleanup(container);
|
|
703
|
+
pass();
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
'required: valid when has value': async ({pass, fail}) => {
|
|
707
|
+
const { container, el } = await createCombobox({ required: true }, fruitOptions);
|
|
708
|
+
el.value = 'anything';
|
|
709
|
+
await el.updateComplete;
|
|
710
|
+
if(!el.internals.checkValidity()) {
|
|
711
|
+
cleanup(container);
|
|
712
|
+
return fail('Should be valid when required and has value');
|
|
713
|
+
}
|
|
714
|
+
cleanup(container);
|
|
715
|
+
pass();
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
/*
|
|
719
|
+
Require-Match Validation
|
|
720
|
+
*/
|
|
721
|
+
'require-match: valid when empty': async ({pass, fail}) => {
|
|
722
|
+
const { container, el } = await createCombobox({ requireMatch: true }, fruitOptions);
|
|
723
|
+
await el.updateComplete;
|
|
724
|
+
if(!el.internals.checkValidity()) {
|
|
725
|
+
cleanup(container);
|
|
726
|
+
return fail('Should be valid when empty (not required)');
|
|
727
|
+
}
|
|
728
|
+
cleanup(container);
|
|
729
|
+
pass();
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
'require-match: invalid when value does not match': async ({pass, fail}) => {
|
|
733
|
+
const { container, el } = await createCombobox({ requireMatch: true }, fruitOptions);
|
|
734
|
+
el.value = 'not a fruit';
|
|
735
|
+
await el.updateComplete;
|
|
736
|
+
if(el.internals.checkValidity()) {
|
|
737
|
+
cleanup(container);
|
|
738
|
+
return fail('Should be invalid when value does not match any option');
|
|
739
|
+
}
|
|
740
|
+
cleanup(container);
|
|
741
|
+
pass();
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
'require-match: valid when value matches option label': async ({pass, fail}) => {
|
|
745
|
+
const { container, el } = await createCombobox({ requireMatch: true }, fruitOptions);
|
|
746
|
+
el.value = 'Apple';
|
|
747
|
+
await el.updateComplete;
|
|
748
|
+
if(!el.internals.checkValidity()) {
|
|
749
|
+
cleanup(container);
|
|
750
|
+
return fail('Should be valid when value matches an option label');
|
|
751
|
+
}
|
|
752
|
+
cleanup(container);
|
|
753
|
+
pass();
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
/*
|
|
757
|
+
Form Reset
|
|
758
|
+
*/
|
|
759
|
+
'should clear value on form reset': async ({pass, fail}) => {
|
|
760
|
+
const form = document.createElement('form');
|
|
761
|
+
form.innerHTML = `
|
|
762
|
+
<k-combobox name="fruit">
|
|
763
|
+
<k-option value="a1">Apple</k-option>
|
|
764
|
+
</k-combobox>
|
|
765
|
+
`;
|
|
766
|
+
document.body.appendChild(form);
|
|
767
|
+
const el = form.querySelector('k-combobox');
|
|
768
|
+
await el.updateComplete;
|
|
769
|
+
el.value = 'Apple';
|
|
770
|
+
await el.updateComplete;
|
|
771
|
+
form.reset();
|
|
772
|
+
await el.updateComplete;
|
|
773
|
+
document.body.removeChild(form);
|
|
774
|
+
if(el.value !== '') return fail(`Expected '' after reset, got '${el.value}'`);
|
|
775
|
+
pass();
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
/*
|
|
779
|
+
Disabled
|
|
780
|
+
*/
|
|
781
|
+
'should render disabled input when disabled': async ({pass, fail}) => {
|
|
782
|
+
const { container, el } = await createCombobox({ disabled: true });
|
|
783
|
+
if(!el.shadowRoot.querySelector('input').disabled) {
|
|
784
|
+
cleanup(container);
|
|
785
|
+
return fail('Input should be disabled');
|
|
786
|
+
}
|
|
787
|
+
cleanup(container);
|
|
788
|
+
pass();
|
|
789
|
+
},
|
|
790
|
+
};
|