@keenthemes/ktui 1.1.1 → 1.1.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.
Files changed (81) hide show
  1. package/dist/ktui.js +674 -225
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +13 -1
  5. package/lib/cjs/components/component.js +22 -0
  6. package/lib/cjs/components/component.js.map +1 -1
  7. package/lib/cjs/components/datatable/datatable.js +7 -1
  8. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  9. package/lib/cjs/components/drawer/drawer.js +255 -9
  10. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  11. package/lib/cjs/components/dropdown/dropdown.js +55 -8
  12. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  13. package/lib/cjs/components/select/combobox.js +0 -2
  14. package/lib/cjs/components/select/combobox.js.map +1 -1
  15. package/lib/cjs/components/select/config.js +4 -1
  16. package/lib/cjs/components/select/config.js.map +1 -1
  17. package/lib/cjs/components/select/dropdown.js +0 -16
  18. package/lib/cjs/components/select/dropdown.js.map +1 -1
  19. package/lib/cjs/components/select/remote.js +0 -40
  20. package/lib/cjs/components/select/remote.js.map +1 -1
  21. package/lib/cjs/components/select/search.js +93 -22
  22. package/lib/cjs/components/select/search.js.map +1 -1
  23. package/lib/cjs/components/select/select.js +180 -114
  24. package/lib/cjs/components/select/select.js.map +1 -1
  25. package/lib/cjs/components/select/tags.js +0 -2
  26. package/lib/cjs/components/select/tags.js.map +1 -1
  27. package/lib/cjs/components/sticky/sticky.js +44 -5
  28. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  29. package/lib/cjs/helpers/data.js +8 -0
  30. package/lib/cjs/helpers/data.js.map +1 -1
  31. package/lib/cjs/helpers/event-handler.js +6 -5
  32. package/lib/cjs/helpers/event-handler.js.map +1 -1
  33. package/lib/cjs/index.js.map +1 -1
  34. package/lib/esm/components/component.js +22 -0
  35. package/lib/esm/components/component.js.map +1 -1
  36. package/lib/esm/components/datatable/datatable.js +7 -1
  37. package/lib/esm/components/datatable/datatable.js.map +1 -1
  38. package/lib/esm/components/drawer/drawer.js +255 -9
  39. package/lib/esm/components/drawer/drawer.js.map +1 -1
  40. package/lib/esm/components/dropdown/dropdown.js +55 -8
  41. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  42. package/lib/esm/components/select/combobox.js +0 -2
  43. package/lib/esm/components/select/combobox.js.map +1 -1
  44. package/lib/esm/components/select/config.js +4 -1
  45. package/lib/esm/components/select/config.js.map +1 -1
  46. package/lib/esm/components/select/dropdown.js +0 -16
  47. package/lib/esm/components/select/dropdown.js.map +1 -1
  48. package/lib/esm/components/select/remote.js +0 -40
  49. package/lib/esm/components/select/remote.js.map +1 -1
  50. package/lib/esm/components/select/search.js +93 -22
  51. package/lib/esm/components/select/search.js.map +1 -1
  52. package/lib/esm/components/select/select.js +180 -114
  53. package/lib/esm/components/select/select.js.map +1 -1
  54. package/lib/esm/components/select/tags.js +0 -2
  55. package/lib/esm/components/select/tags.js.map +1 -1
  56. package/lib/esm/components/sticky/sticky.js +44 -5
  57. package/lib/esm/components/sticky/sticky.js.map +1 -1
  58. package/lib/esm/helpers/data.js +8 -0
  59. package/lib/esm/helpers/data.js.map +1 -1
  60. package/lib/esm/helpers/event-handler.js +6 -5
  61. package/lib/esm/helpers/event-handler.js.map +1 -1
  62. package/lib/esm/index.js.map +1 -1
  63. package/package.json +6 -4
  64. package/src/components/component.ts +26 -0
  65. package/src/components/datatable/__tests__/race-conditions.test.ts +7 -7
  66. package/src/components/datatable/datatable.ts +8 -1
  67. package/src/components/drawer/drawer.ts +266 -10
  68. package/src/components/dropdown/dropdown.ts +63 -8
  69. package/src/components/select/__tests__/ux-behaviors.test.ts +997 -0
  70. package/src/components/select/combobox.ts +0 -1
  71. package/src/components/select/config.ts +7 -1
  72. package/src/components/select/dropdown.ts +0 -24
  73. package/src/components/select/remote.ts +0 -49
  74. package/src/components/select/search.ts +97 -24
  75. package/src/components/select/select.css +5 -1
  76. package/src/components/select/select.ts +211 -153
  77. package/src/components/select/tags.ts +0 -1
  78. package/src/components/sticky/sticky.ts +55 -5
  79. package/src/helpers/data.ts +10 -0
  80. package/src/helpers/event-handler.ts +7 -6
  81. package/src/index.ts +2 -0
@@ -0,0 +1,997 @@
1
+ /**
2
+ * UX Behaviors Tests for KTSelect
3
+ * Tests the enhancements: search autofocus, Enter key behavior, global dropdown management, and global event dispatch
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
+ import { KTSelect } from '../select';
8
+ import { waitFor } from '../../datatable/__tests__/setup';
9
+
10
+ describe('KTSelect UX Behaviors', () => {
11
+ let container: HTMLElement;
12
+
13
+ /**
14
+ * Helper to create a select element with options
15
+ */
16
+ const createSelectElement = (
17
+ options: Array<{ value: string; text: string }> = [
18
+ { value: '1', text: 'Option 1' },
19
+ { value: '2', text: 'Option 2' },
20
+ { value: '3', text: 'Option 3' },
21
+ ],
22
+ ): HTMLSelectElement => {
23
+ const select = document.createElement('select');
24
+ select.className = 'kt-select';
25
+ options.forEach((opt) => {
26
+ const option = document.createElement('option');
27
+ option.value = opt.value;
28
+ option.textContent = opt.text;
29
+ select.appendChild(option);
30
+ });
31
+ return select;
32
+ };
33
+
34
+ /**
35
+ * Helper to wait for KTSelect to fully initialize
36
+ */
37
+ const waitForInit = async (select: KTSelect): Promise<void> => {
38
+ // Wait for async initialization - KTSelect uses promises for setup
39
+ await waitFor(200);
40
+ // Wait for next tick to ensure all modules are initialized
41
+ await new Promise((resolve) => setTimeout(resolve, 0));
42
+ // Additional wait for DOM to be ready
43
+ await waitFor(50);
44
+ };
45
+
46
+ beforeEach(() => {
47
+ container = document.createElement('div');
48
+ document.body.appendChild(container);
49
+ });
50
+
51
+ afterEach(() => {
52
+ // Clean up all KTSelect instances
53
+ const selects = document.querySelectorAll('.kt-select');
54
+ selects.forEach((select) => {
55
+ const instance = (select as any).instance;
56
+ if (instance && typeof instance.destroy === 'function') {
57
+ instance.destroy();
58
+ }
59
+ });
60
+
61
+ // Clear document body
62
+ document.body.innerHTML = '';
63
+ container = null as any;
64
+
65
+ // Clear all event listeners
66
+ vi.clearAllMocks();
67
+ });
68
+
69
+ describe('Search Autofocus Enhancement', () => {
70
+ it('should focus search input when dropdown opens with searchAutofocus enabled', async () => {
71
+ const selectEl = createSelectElement();
72
+ container.appendChild(selectEl);
73
+
74
+ const select = new KTSelect(selectEl, {
75
+ enableSearch: true,
76
+ searchAutofocus: true,
77
+ height: 250,
78
+ });
79
+
80
+ await waitForInit(select);
81
+
82
+ // Open dropdown
83
+ select.openDropdown();
84
+ await waitFor(200); // Wait for autofocus retry mechanism
85
+
86
+ const searchInput = select.getSearchInput();
87
+ expect(searchInput).toBeTruthy();
88
+ expect(document.activeElement).toBe(searchInput);
89
+ });
90
+
91
+ it('should not focus search input when searchAutofocus is disabled', async () => {
92
+ const selectEl = createSelectElement();
93
+ container.appendChild(selectEl);
94
+
95
+ const select = new KTSelect(selectEl, {
96
+ enableSearch: true,
97
+ searchAutofocus: false,
98
+ height: 250,
99
+ });
100
+
101
+ await waitForInit(select);
102
+
103
+ // Open dropdown
104
+ select.openDropdown();
105
+ await waitFor(200);
106
+
107
+ const searchInput = select.getSearchInput();
108
+ expect(searchInput).toBeTruthy();
109
+ expect(document.activeElement).not.toBe(searchInput);
110
+ });
111
+
112
+ it('should retry focus if initial focus fails', async () => {
113
+ const selectEl = createSelectElement();
114
+ container.appendChild(selectEl);
115
+
116
+ const select = new KTSelect(selectEl, {
117
+ enableSearch: true,
118
+ searchAutofocus: true,
119
+ height: 250,
120
+ });
121
+
122
+ await waitForInit(select);
123
+
124
+ // Get search input and set up spy before opening dropdown
125
+ const searchInput = select.getSearchInput();
126
+ expect(searchInput).toBeTruthy();
127
+
128
+ // Spy on focus method
129
+ const focusSpy = vi.spyOn(searchInput, 'focus');
130
+
131
+ // Open dropdown - this will trigger autofocus
132
+ select.openDropdown();
133
+ await waitFor(350); // Wait for retry mechanism (0ms, 50ms, 100ms, 200ms)
134
+
135
+ // Focus should have been called at least once (initial attempt + retries)
136
+ expect(focusSpy).toHaveBeenCalled();
137
+ focusSpy.mockRestore();
138
+ });
139
+ });
140
+
141
+ describe('Enter Key Behavior', () => {
142
+ it('should close dropdown when Enter is pressed with closeOnEnter enabled (default)', async () => {
143
+ const selectEl = createSelectElement();
144
+ container.appendChild(selectEl);
145
+
146
+ const select = new KTSelect(selectEl, {
147
+ enableSearch: true,
148
+ closeOnEnter: true,
149
+ height: 250,
150
+ });
151
+
152
+ await waitForInit(select);
153
+
154
+ // Open dropdown
155
+ select.openDropdown();
156
+ await waitFor(200);
157
+
158
+ const searchInput = select.getSearchInput();
159
+ expect(searchInput).toBeTruthy();
160
+
161
+ // Focus search input
162
+ searchInput.focus();
163
+ await waitFor(50);
164
+
165
+ // Press Enter
166
+ const enterEvent = new KeyboardEvent('keydown', {
167
+ key: 'Enter',
168
+ bubbles: true,
169
+ cancelable: true,
170
+ });
171
+ searchInput.dispatchEvent(enterEvent);
172
+
173
+ await waitFor(150);
174
+
175
+ // Dropdown should be closed
176
+ expect(select.isDropdownOpen()).toBe(false);
177
+ });
178
+
179
+ it('should keep dropdown open when Enter is pressed with closeOnEnter disabled', async () => {
180
+ const selectEl = createSelectElement();
181
+ container.appendChild(selectEl);
182
+
183
+ const select = new KTSelect(selectEl, {
184
+ enableSearch: true,
185
+ closeOnEnter: false,
186
+ height: 250,
187
+ });
188
+
189
+ await waitForInit(select);
190
+
191
+ // Open dropdown
192
+ select.openDropdown();
193
+ await waitFor(200);
194
+
195
+ const searchInput = select.getSearchInput();
196
+ expect(searchInput).toBeTruthy();
197
+
198
+ // Focus search input
199
+ searchInput.focus();
200
+ await waitFor(50);
201
+
202
+ // Press Enter
203
+ const enterEvent = new KeyboardEvent('keydown', {
204
+ key: 'Enter',
205
+ bubbles: true,
206
+ cancelable: true,
207
+ });
208
+ searchInput.dispatchEvent(enterEvent);
209
+
210
+ await waitFor(150);
211
+
212
+ // Dropdown should remain open
213
+ expect(select.isDropdownOpen()).toBe(true);
214
+ });
215
+
216
+ it('should select first option when Enter is pressed', async () => {
217
+ const selectEl = createSelectElement();
218
+ container.appendChild(selectEl);
219
+
220
+ const select = new KTSelect(selectEl, {
221
+ enableSearch: true,
222
+ closeOnEnter: true,
223
+ height: 250,
224
+ });
225
+
226
+ await waitForInit(select);
227
+
228
+ // Open dropdown
229
+ select.openDropdown();
230
+ await waitFor(200);
231
+
232
+ const searchInput = select.getSearchInput();
233
+ expect(searchInput).toBeTruthy();
234
+
235
+ // Focus search input
236
+ searchInput.focus();
237
+ await waitFor(50);
238
+
239
+ // Press Enter
240
+ const enterEvent = new KeyboardEvent('keydown', {
241
+ key: 'Enter',
242
+ bubbles: true,
243
+ cancelable: true,
244
+ });
245
+ searchInput.dispatchEvent(enterEvent);
246
+
247
+ await waitFor(150);
248
+
249
+ // First option should be selected
250
+ expect(select.getSelectedOptions()).toContain('1');
251
+ });
252
+
253
+ it('should close dropdown and trigger selection when Enter is pressed after typing search query', async () => {
254
+ const selectEl = createSelectElement([
255
+ { value: '1', text: 'Apple' },
256
+ { value: '2', text: 'Banana' },
257
+ { value: '3', text: 'Cherry' },
258
+ ]);
259
+ container.appendChild(selectEl);
260
+
261
+ const select = new KTSelect(selectEl, {
262
+ enableSearch: true,
263
+ closeOnEnter: true,
264
+ height: 250,
265
+ });
266
+
267
+ await waitForInit(select);
268
+
269
+ // Set up change event listener to verify selection-complete lifecycle
270
+ const changeHandler = vi.fn();
271
+ selectEl.addEventListener('change', changeHandler);
272
+
273
+ // Open dropdown
274
+ select.openDropdown();
275
+ await waitFor(200);
276
+
277
+ const searchInput = select.getSearchInput();
278
+ expect(searchInput).toBeTruthy();
279
+
280
+ // Focus search input
281
+ searchInput.focus();
282
+ await waitFor(50);
283
+
284
+ // Type search query
285
+ searchInput.value = 'App';
286
+ const inputEvent = new Event('input', { bubbles: true });
287
+ searchInput.dispatchEvent(inputEvent);
288
+ await waitFor(100); // Wait for filtering
289
+
290
+ // Verify dropdown is still open
291
+ expect(select.isDropdownOpen()).toBe(true);
292
+
293
+ // Press Enter
294
+ const enterEvent = new KeyboardEvent('keydown', {
295
+ key: 'Enter',
296
+ bubbles: true,
297
+ cancelable: true,
298
+ });
299
+ searchInput.dispatchEvent(enterEvent);
300
+
301
+ await waitFor(200);
302
+
303
+ // Dropdown should be closed
304
+ expect(select.isDropdownOpen()).toBe(false);
305
+
306
+ // Selection should be made
307
+ expect(select.getSelectedOptions()).toContain('1');
308
+
309
+ // Change event should be dispatched (selection-complete lifecycle)
310
+ expect(changeHandler).toHaveBeenCalledTimes(1);
311
+
312
+ // Cleanup
313
+ selectEl.removeEventListener('change', changeHandler);
314
+ });
315
+
316
+ it('should close dropdown and trigger selection when Enter is pressed with filtered results', async () => {
317
+ const selectEl = createSelectElement([
318
+ { value: '1', text: 'Red Apple' },
319
+ { value: '2', text: 'Green Apple' },
320
+ { value: '3', text: 'Banana' },
321
+ { value: '4', text: 'Cherry' },
322
+ ]);
323
+ container.appendChild(selectEl);
324
+
325
+ const select = new KTSelect(selectEl, {
326
+ enableSearch: true,
327
+ closeOnEnter: true,
328
+ height: 250,
329
+ });
330
+
331
+ await waitForInit(select);
332
+
333
+ // Set up change event listener
334
+ const changeHandler = vi.fn();
335
+ selectEl.addEventListener('change', changeHandler);
336
+
337
+ // Open dropdown
338
+ select.openDropdown();
339
+ await waitFor(200);
340
+
341
+ const searchInput = select.getSearchInput();
342
+ expect(searchInput).toBeTruthy();
343
+
344
+ // Focus search input
345
+ searchInput.focus();
346
+ await waitFor(50);
347
+
348
+ // Type search query that filters to multiple results
349
+ searchInput.value = 'Apple';
350
+ const inputEvent = new Event('input', { bubbles: true });
351
+ searchInput.dispatchEvent(inputEvent);
352
+ await waitFor(150); // Wait for filtering
353
+
354
+ // Verify dropdown is still open and options are filtered
355
+ expect(select.isDropdownOpen()).toBe(true);
356
+
357
+ // Press Enter - should select first filtered option
358
+ const enterEvent = new KeyboardEvent('keydown', {
359
+ key: 'Enter',
360
+ bubbles: true,
361
+ cancelable: true,
362
+ });
363
+ searchInput.dispatchEvent(enterEvent);
364
+
365
+ await waitFor(200);
366
+
367
+ // Dropdown should be closed
368
+ expect(select.isDropdownOpen()).toBe(false);
369
+
370
+ // First filtered option should be selected
371
+ expect(select.getSelectedOptions()).toContain('1');
372
+
373
+ // Change event should be dispatched
374
+ expect(changeHandler).toHaveBeenCalledTimes(1);
375
+ const event = changeHandler.mock.calls[0][0] as CustomEvent;
376
+ expect(event.detail?.payload?.value).toBe('1');
377
+ expect(event.detail?.payload?.selected).toBe(true);
378
+
379
+ // Cleanup
380
+ selectEl.removeEventListener('change', changeHandler);
381
+ });
382
+
383
+ it('should trigger change event when Enter selects option (selection-complete lifecycle)', async () => {
384
+ const selectEl = createSelectElement([
385
+ { value: '1', text: 'Option 1' },
386
+ { value: '2', text: 'Option 2' },
387
+ { value: '3', text: 'Option 3' },
388
+ ]);
389
+ container.appendChild(selectEl);
390
+
391
+ const select = new KTSelect(selectEl, {
392
+ enableSearch: true,
393
+ closeOnEnter: true,
394
+ height: 250,
395
+ });
396
+
397
+ await waitForInit(select);
398
+
399
+ // Set up listeners for both element and document events
400
+ const elementChangeHandler = vi.fn();
401
+ const documentChangeHandler = vi.fn();
402
+ selectEl.addEventListener('change', elementChangeHandler);
403
+ document.addEventListener('kt-select:change', documentChangeHandler);
404
+
405
+ // Open dropdown
406
+ select.openDropdown();
407
+ await waitFor(200);
408
+
409
+ const searchInput = select.getSearchInput();
410
+ expect(searchInput).toBeTruthy();
411
+
412
+ // Focus search input
413
+ searchInput.focus();
414
+ await waitFor(50);
415
+
416
+ // Press Enter
417
+ const enterEvent = new KeyboardEvent('keydown', {
418
+ key: 'Enter',
419
+ bubbles: true,
420
+ cancelable: true,
421
+ });
422
+ searchInput.dispatchEvent(enterEvent);
423
+
424
+ await waitFor(200);
425
+
426
+ // Element change event should be dispatched
427
+ expect(elementChangeHandler).toHaveBeenCalledTimes(1);
428
+ const elementEvent = elementChangeHandler.mock.calls[0][0] as CustomEvent;
429
+ expect(elementEvent.detail?.payload?.value).toBe('1');
430
+ expect(elementEvent.detail?.payload?.selected).toBe(true);
431
+
432
+ // Document change event should be dispatched (global events enabled by default)
433
+ expect(documentChangeHandler).toHaveBeenCalledTimes(1);
434
+ const docEvent = documentChangeHandler.mock.calls[0][0] as CustomEvent;
435
+ expect(docEvent.detail?.instance).toBe(select);
436
+ expect(docEvent.detail?.element).toBe(selectEl);
437
+ expect(docEvent.detail?.payload?.value).toBe('1');
438
+
439
+ // Cleanup
440
+ selectEl.removeEventListener('change', elementChangeHandler);
441
+ document.removeEventListener('kt-select:change', documentChangeHandler);
442
+ });
443
+
444
+ it('should close dropdown when Enter is pressed even if first option is already selected', async () => {
445
+ const selectEl = createSelectElement([
446
+ { value: '1', text: 'Option 1' },
447
+ { value: '2', text: 'Option 2' },
448
+ { value: '3', text: 'Option 3' },
449
+ ]);
450
+ container.appendChild(selectEl);
451
+
452
+ const select = new KTSelect(selectEl, {
453
+ enableSearch: true,
454
+ closeOnEnter: true,
455
+ height: 250,
456
+ });
457
+
458
+ await waitForInit(select);
459
+
460
+ // Select the first option first
461
+ select.toggleSelection('1');
462
+ await waitFor(100);
463
+ expect(select.getSelectedOptions()).toContain('1');
464
+
465
+ // Open dropdown again
466
+ select.openDropdown();
467
+ await waitFor(200);
468
+
469
+ const searchInput = select.getSearchInput();
470
+ expect(searchInput).toBeTruthy();
471
+
472
+ // Focus search input
473
+ searchInput.focus();
474
+ await waitFor(50);
475
+
476
+ // Press Enter - even though first option is already selected, dropdown should close
477
+ const enterEvent = new KeyboardEvent('keydown', {
478
+ key: 'Enter',
479
+ bubbles: true,
480
+ cancelable: true,
481
+ });
482
+ searchInput.dispatchEvent(enterEvent);
483
+
484
+ await waitFor(200);
485
+
486
+ // Dropdown should be closed even though option was already selected
487
+ expect(select.isDropdownOpen()).toBe(false);
488
+ });
489
+
490
+ it('should not close dropdown when Enter is pressed with no available options', async () => {
491
+ const selectEl = createSelectElement([
492
+ { value: '1', text: 'Apple' },
493
+ { value: '2', text: 'Banana' },
494
+ ]);
495
+ container.appendChild(selectEl);
496
+
497
+ const select = new KTSelect(selectEl, {
498
+ enableSearch: true,
499
+ closeOnEnter: true,
500
+ height: 250,
501
+ });
502
+
503
+ await waitForInit(select);
504
+
505
+ // Open dropdown
506
+ select.openDropdown();
507
+ await waitFor(200);
508
+
509
+ const searchInput = select.getSearchInput();
510
+ expect(searchInput).toBeTruthy();
511
+
512
+ // Focus search input
513
+ searchInput.focus();
514
+ await waitFor(50);
515
+
516
+ // Type search query that matches no results
517
+ searchInput.value = 'XYZ123NoMatch';
518
+ const inputEvent = new Event('input', { bubbles: true });
519
+ searchInput.dispatchEvent(inputEvent);
520
+ await waitFor(150); // Wait for filtering
521
+
522
+ // Verify dropdown is still open
523
+ expect(select.isDropdownOpen()).toBe(true);
524
+
525
+ // Press Enter - should not close dropdown since no options available
526
+ const enterEvent = new KeyboardEvent('keydown', {
527
+ key: 'Enter',
528
+ bubbles: true,
529
+ cancelable: true,
530
+ });
531
+ searchInput.dispatchEvent(enterEvent);
532
+
533
+ await waitFor(150);
534
+
535
+ // Dropdown should remain open (no option to select)
536
+ expect(select.isDropdownOpen()).toBe(true);
537
+
538
+ // No selection should be made
539
+ expect(select.getSelectedOptions().length).toBe(0);
540
+ });
541
+
542
+ it('should focus display element (button) after closing dropdown with Enter key', async () => {
543
+ const selectEl = createSelectElement([
544
+ { value: '1', text: 'Option 1' },
545
+ { value: '2', text: 'Option 2' },
546
+ ]);
547
+ container.appendChild(selectEl);
548
+
549
+ const select = new KTSelect(selectEl, {
550
+ enableSearch: true,
551
+ closeOnEnter: true,
552
+ height: 250,
553
+ });
554
+
555
+ await waitForInit(select);
556
+
557
+ // Open dropdown
558
+ select.openDropdown();
559
+ await waitFor(200);
560
+
561
+ const searchInput = select.getSearchInput();
562
+ expect(searchInput).toBeTruthy();
563
+
564
+ // Focus search input
565
+ searchInput.focus();
566
+ await waitFor(50);
567
+ expect(document.activeElement).toBe(searchInput);
568
+
569
+ // Press Enter to select and close
570
+ const enterEvent = new KeyboardEvent('keydown', {
571
+ key: 'Enter',
572
+ bubbles: true,
573
+ cancelable: true,
574
+ });
575
+ searchInput.dispatchEvent(enterEvent);
576
+
577
+ // Wait for dropdown to close and focus to move
578
+ await waitFor(200);
579
+
580
+ // Dropdown should be closed
581
+ expect(select.isDropdownOpen()).toBe(false);
582
+
583
+ // Display element (button) should be focused so user can press Enter again
584
+ const displayElement = select.getDisplayElement();
585
+ expect(displayElement).toBeTruthy();
586
+ expect(document.activeElement).toBe(displayElement);
587
+ });
588
+ });
589
+
590
+ describe('Global Dropdown Management', () => {
591
+ it('should close other open dropdowns when opening a new one (default behavior)', async () => {
592
+ const selectEl1 = createSelectElement();
593
+ const selectEl2 = createSelectElement([
594
+ { value: 'a', text: 'Option A' },
595
+ { value: 'b', text: 'Option B' },
596
+ ]);
597
+ container.appendChild(selectEl1);
598
+ container.appendChild(selectEl2);
599
+
600
+ const select1 = new KTSelect(selectEl1, { height: 250 });
601
+ const select2 = new KTSelect(selectEl2, { height: 250 });
602
+
603
+ await waitForInit(select1);
604
+ await waitForInit(select2);
605
+
606
+ // Open first dropdown
607
+ select1.openDropdown();
608
+ await waitFor(200);
609
+ expect(select1.isDropdownOpen()).toBe(true);
610
+
611
+ // Open second dropdown - should close first
612
+ select2.openDropdown();
613
+ await waitFor(200);
614
+
615
+ // First dropdown should be closed
616
+ expect(select1.isDropdownOpen()).toBe(false);
617
+
618
+ // Second dropdown should be open
619
+ expect(select2.isDropdownOpen()).toBe(true);
620
+ });
621
+
622
+ it('should allow multiple dropdowns when closeOnOtherOpen is disabled', async () => {
623
+ const selectEl1 = createSelectElement();
624
+ const selectEl2 = createSelectElement([
625
+ { value: 'a', text: 'Option A' },
626
+ { value: 'b', text: 'Option B' },
627
+ ]);
628
+ container.appendChild(selectEl1);
629
+ container.appendChild(selectEl2);
630
+
631
+ const select1 = new KTSelect(selectEl1, {
632
+ closeOnOtherOpen: false,
633
+ height: 250,
634
+ });
635
+ const select2 = new KTSelect(selectEl2, {
636
+ closeOnOtherOpen: false,
637
+ height: 250,
638
+ });
639
+
640
+ await waitForInit(select1);
641
+ await waitForInit(select2);
642
+
643
+ // Open first dropdown
644
+ select1.openDropdown();
645
+ await waitFor(200);
646
+ expect(select1.isDropdownOpen()).toBe(true);
647
+
648
+ // Open second dropdown - first should remain open
649
+ select2.openDropdown();
650
+ await waitFor(200);
651
+
652
+ // Both dropdowns should be open
653
+ expect(select1.isDropdownOpen()).toBe(true);
654
+ expect(select2.isDropdownOpen()).toBe(true);
655
+ });
656
+
657
+ it('should remove instance from registry when dropdown closes', async () => {
658
+ const selectEl = createSelectElement();
659
+ container.appendChild(selectEl);
660
+
661
+ const select = new KTSelect(selectEl, { height: 250 });
662
+ await waitForInit(select);
663
+
664
+ // Open dropdown
665
+ select.openDropdown();
666
+ await waitFor(100);
667
+
668
+ // Close dropdown
669
+ select.closeDropdown();
670
+ await waitFor(100);
671
+
672
+ // Registry should be empty (we can't directly access private static, but we can verify behavior)
673
+ // Opening another dropdown should work without issues
674
+ const selectEl2 = createSelectElement([
675
+ { value: 'a', text: 'Option A' },
676
+ ]);
677
+ container.appendChild(selectEl2);
678
+ const select2 = new KTSelect(selectEl2, { height: 250 });
679
+ await waitForInit(select2);
680
+ select2.openDropdown();
681
+ await waitFor(100);
682
+
683
+ // Should work without errors
684
+ expect(select2).toBeTruthy();
685
+ });
686
+
687
+ it('should clean up registry when instance is destroyed', async () => {
688
+ const selectEl = createSelectElement();
689
+ container.appendChild(selectEl);
690
+
691
+ const select = new KTSelect(selectEl, { height: 250 });
692
+ await waitForInit(select);
693
+
694
+ // Open dropdown
695
+ select.openDropdown();
696
+ await waitFor(100);
697
+
698
+ // Destroy instance
699
+ select.destroy();
700
+ await waitFor(100);
701
+
702
+ // Creating a new select should work without issues
703
+ const selectEl2 = createSelectElement([
704
+ { value: 'a', text: 'Option A' },
705
+ ]);
706
+ container.appendChild(selectEl2);
707
+ const select2 = new KTSelect(selectEl2, { height: 250 });
708
+ await waitForInit(select2);
709
+ select2.openDropdown();
710
+ await waitFor(100);
711
+
712
+ // Should work without errors
713
+ expect(select2).toBeTruthy();
714
+ });
715
+ });
716
+
717
+ describe('Global Event Dispatch', () => {
718
+ it('should dispatch events on document when dispatchGlobalEvents is enabled (default)', async () => {
719
+ const selectEl = createSelectElement();
720
+ container.appendChild(selectEl);
721
+
722
+ const select = new KTSelect(selectEl, {
723
+ dispatchGlobalEvents: true,
724
+ height: 250,
725
+ });
726
+
727
+ await waitForInit(select);
728
+
729
+ // Set up document listener
730
+ const showHandler = vi.fn();
731
+ document.addEventListener('kt-select:show', showHandler);
732
+
733
+ // Open dropdown
734
+ select.openDropdown();
735
+ await waitFor(100);
736
+
737
+ // Event should be dispatched on document
738
+ expect(showHandler).toHaveBeenCalledTimes(1);
739
+ const event = showHandler.mock.calls[0][0] as CustomEvent;
740
+ expect(event.detail.instance).toBe(select);
741
+ expect(event.detail.element).toBe(selectEl);
742
+
743
+ // Cleanup
744
+ document.removeEventListener('kt-select:show', showHandler);
745
+ });
746
+
747
+ it('should not dispatch events on document when dispatchGlobalEvents is disabled', async () => {
748
+ const selectEl = createSelectElement();
749
+ container.appendChild(selectEl);
750
+
751
+ const select = new KTSelect(selectEl, {
752
+ dispatchGlobalEvents: false,
753
+ height: 250,
754
+ });
755
+
756
+ await waitForInit(select);
757
+
758
+ // Set up document listener
759
+ const showHandler = vi.fn();
760
+ document.addEventListener('kt-select:show', showHandler);
761
+
762
+ // Open dropdown
763
+ select.openDropdown();
764
+ await waitFor(100);
765
+
766
+ // Event should NOT be dispatched on document
767
+ expect(showHandler).not.toHaveBeenCalled();
768
+
769
+ // Cleanup
770
+ document.removeEventListener('kt-select:show', showHandler);
771
+ });
772
+
773
+ it('should dispatch events on element regardless of dispatchGlobalEvents setting', async () => {
774
+ const selectEl = createSelectElement();
775
+ container.appendChild(selectEl);
776
+
777
+ const select = new KTSelect(selectEl, {
778
+ dispatchGlobalEvents: false,
779
+ height: 250,
780
+ });
781
+
782
+ await waitForInit(select);
783
+
784
+ // Set up element listener
785
+ const showHandler = vi.fn();
786
+ selectEl.addEventListener('show', showHandler);
787
+
788
+ // Open dropdown
789
+ select.openDropdown();
790
+ await waitFor(100);
791
+
792
+ // Event should be dispatched on element
793
+ expect(showHandler).toHaveBeenCalledTimes(1);
794
+
795
+ // Cleanup
796
+ selectEl.removeEventListener('show', showHandler);
797
+ });
798
+
799
+ it('should dispatch both namespaced and non-namespaced events on document', async () => {
800
+ const selectEl = createSelectElement();
801
+ container.appendChild(selectEl);
802
+
803
+ const select = new KTSelect(selectEl, {
804
+ dispatchGlobalEvents: true,
805
+ height: 250,
806
+ });
807
+
808
+ await waitForInit(select);
809
+
810
+ // Set up listeners for both namespaced and non-namespaced
811
+ const namespacedHandler = vi.fn();
812
+ const nonNamespacedHandler = vi.fn();
813
+
814
+ // Use capture phase to catch events before they bubble
815
+ document.addEventListener('kt-select:show', namespacedHandler, true);
816
+ document.addEventListener('show', nonNamespacedHandler, true);
817
+
818
+ // Open dropdown
819
+ select.openDropdown();
820
+ await waitFor(200);
821
+
822
+ // Both events should fire on document
823
+ expect(namespacedHandler).toHaveBeenCalledTimes(1);
824
+ // Non-namespaced event should also be dispatched on document (for jQuery compatibility)
825
+ const nonNamespacedCalls = nonNamespacedHandler.mock.calls.filter(
826
+ (call) => call[0].type === 'show' && call[0].target === document,
827
+ );
828
+ expect(nonNamespacedCalls.length).toBe(1);
829
+
830
+ // Verify event detail structure is consistent
831
+ const namespacedEvent = namespacedHandler.mock.calls[0][0] as CustomEvent;
832
+ const nonNamespacedEvent = nonNamespacedCalls[0][0] as CustomEvent;
833
+ expect(nonNamespacedEvent.detail.instance).toBe(select);
834
+ expect(nonNamespacedEvent.detail.element).toBe(selectEl);
835
+ expect(nonNamespacedEvent.detail).toEqual(namespacedEvent.detail);
836
+
837
+ // Cleanup
838
+ document.removeEventListener('kt-select:show', namespacedHandler, true);
839
+ document.removeEventListener('show', nonNamespacedHandler, true);
840
+ });
841
+
842
+ it('should support jQuery-style non-namespaced event listeners on document', async () => {
843
+ const selectEl = createSelectElement();
844
+ container.appendChild(selectEl);
845
+
846
+ const select = new KTSelect(selectEl, {
847
+ dispatchGlobalEvents: true,
848
+ height: 250,
849
+ });
850
+
851
+ await waitForInit(select);
852
+
853
+ // Simulate jQuery-style listener: $(document).on('show', ...)
854
+ const showHandler = vi.fn();
855
+ document.addEventListener('show', showHandler);
856
+
857
+ // Open dropdown
858
+ select.openDropdown();
859
+ await waitFor(200);
860
+
861
+ // Event should be dispatched on document and handler should be called
862
+ // Filter to only count events dispatched directly on document (not bubbled from element)
863
+ const documentEvents = showHandler.mock.calls.filter(
864
+ (call) => call[0].target === document,
865
+ );
866
+ expect(documentEvents.length).toBe(1);
867
+ const event = documentEvents[0][0] as CustomEvent;
868
+ expect(event.type).toBe('show');
869
+ expect(event.target).toBe(document);
870
+ expect(event.detail.instance).toBe(select);
871
+ expect(event.detail.element).toBe(selectEl);
872
+
873
+ // Cleanup
874
+ document.removeEventListener('show', showHandler);
875
+ });
876
+
877
+ it('should include component instance and element in event detail', async () => {
878
+ const selectEl = createSelectElement();
879
+ container.appendChild(selectEl);
880
+
881
+ const select = new KTSelect(selectEl, {
882
+ dispatchGlobalEvents: true,
883
+ height: 250,
884
+ });
885
+
886
+ await waitForInit(select);
887
+
888
+ // Set up document listener
889
+ const closeHandler = vi.fn();
890
+ document.addEventListener('kt-select:close', closeHandler);
891
+
892
+ // Open dropdown first
893
+ select.openDropdown();
894
+ await waitFor(200);
895
+
896
+ // Clear handler calls from open event
897
+ closeHandler.mockClear();
898
+
899
+ // Close dropdown
900
+ select.closeDropdown();
901
+ await waitFor(200);
902
+
903
+ // Event should include instance and element
904
+ expect(closeHandler).toHaveBeenCalledTimes(1);
905
+ const event = closeHandler.mock.calls[0][0] as CustomEvent;
906
+ expect(event.detail.instance).toBe(select);
907
+ expect(event.detail.element).toBe(selectEl);
908
+
909
+ // Cleanup
910
+ document.removeEventListener('kt-select:close', closeHandler);
911
+ });
912
+
913
+ it('should dispatch change events on document when configured', async () => {
914
+ const selectEl = createSelectElement();
915
+ container.appendChild(selectEl);
916
+
917
+ const select = new KTSelect(selectEl, {
918
+ dispatchGlobalEvents: true,
919
+ height: 250,
920
+ });
921
+
922
+ await waitForInit(select);
923
+
924
+ // Set up document listener
925
+ const changeHandler = vi.fn();
926
+ document.addEventListener('kt-select:change', changeHandler);
927
+
928
+ // Select an option by clicking on it
929
+ select.openDropdown();
930
+ await waitFor(200);
931
+
932
+ const option = select
933
+ .getDropdownElement()
934
+ ?.querySelector('[data-kt-select-option][data-value="1"]') as HTMLElement;
935
+
936
+ expect(option).toBeTruthy();
937
+ option.click();
938
+ await waitFor(200);
939
+
940
+ // Event should be dispatched on document
941
+ expect(changeHandler).toHaveBeenCalled();
942
+
943
+ // Cleanup
944
+ document.removeEventListener('kt-select:change', changeHandler);
945
+ });
946
+ });
947
+
948
+ describe('Integration Tests', () => {
949
+ it('should work correctly with all features enabled', async () => {
950
+ const selectEl = createSelectElement();
951
+ container.appendChild(selectEl);
952
+
953
+ const select = new KTSelect(selectEl, {
954
+ enableSearch: true,
955
+ searchAutofocus: true,
956
+ closeOnEnter: true,
957
+ closeOnOtherOpen: true,
958
+ dispatchGlobalEvents: true,
959
+ height: 250,
960
+ });
961
+
962
+ await waitForInit(select);
963
+
964
+ // Set up document listener
965
+ const showHandler = vi.fn();
966
+ document.addEventListener('kt-select:show', showHandler);
967
+
968
+ // Open dropdown
969
+ select.openDropdown();
970
+ await waitFor(200);
971
+
972
+ // Verify autofocus
973
+ const searchInput = select.getSearchInput();
974
+ expect(searchInput).toBeTruthy();
975
+ expect(document.activeElement).toBe(searchInput);
976
+
977
+ // Verify global event dispatch
978
+ expect(showHandler).toHaveBeenCalledTimes(1);
979
+
980
+ // Press Enter
981
+ const enterEvent = new KeyboardEvent('keydown', {
982
+ key: 'Enter',
983
+ bubbles: true,
984
+ cancelable: true,
985
+ });
986
+ searchInput.dispatchEvent(enterEvent);
987
+ await waitFor(200);
988
+
989
+ // Verify dropdown closed
990
+ expect(select.isDropdownOpen()).toBe(false);
991
+
992
+ // Cleanup
993
+ document.removeEventListener('kt-select:show', showHandler);
994
+ });
995
+ });
996
+ });
997
+