@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.
- package/dist/ktui.js +674 -225
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +13 -1
- package/lib/cjs/components/component.js +22 -0
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +7 -1
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/drawer/drawer.js +255 -9
- package/lib/cjs/components/drawer/drawer.js.map +1 -1
- package/lib/cjs/components/dropdown/dropdown.js +55 -8
- package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
- package/lib/cjs/components/select/combobox.js +0 -2
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/config.js +4 -1
- package/lib/cjs/components/select/config.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js +0 -16
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/remote.js +0 -40
- package/lib/cjs/components/select/remote.js.map +1 -1
- package/lib/cjs/components/select/search.js +93 -22
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +180 -114
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js +0 -2
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +44 -5
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/cjs/helpers/data.js +8 -0
- package/lib/cjs/helpers/data.js.map +1 -1
- package/lib/cjs/helpers/event-handler.js +6 -5
- package/lib/cjs/helpers/event-handler.js.map +1 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/component.js +22 -0
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +7 -1
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/drawer/drawer.js +255 -9
- package/lib/esm/components/drawer/drawer.js.map +1 -1
- package/lib/esm/components/dropdown/dropdown.js +55 -8
- package/lib/esm/components/dropdown/dropdown.js.map +1 -1
- package/lib/esm/components/select/combobox.js +0 -2
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/config.js +4 -1
- package/lib/esm/components/select/config.js.map +1 -1
- package/lib/esm/components/select/dropdown.js +0 -16
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/remote.js +0 -40
- package/lib/esm/components/select/remote.js.map +1 -1
- package/lib/esm/components/select/search.js +93 -22
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +180 -114
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js +0 -2
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/sticky/sticky.js +44 -5
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/lib/esm/helpers/data.js +8 -0
- package/lib/esm/helpers/data.js.map +1 -1
- package/lib/esm/helpers/event-handler.js +6 -5
- package/lib/esm/helpers/event-handler.js.map +1 -1
- package/lib/esm/index.js.map +1 -1
- package/package.json +6 -4
- package/src/components/component.ts +26 -0
- package/src/components/datatable/__tests__/race-conditions.test.ts +7 -7
- package/src/components/datatable/datatable.ts +8 -1
- package/src/components/drawer/drawer.ts +266 -10
- package/src/components/dropdown/dropdown.ts +63 -8
- package/src/components/select/__tests__/ux-behaviors.test.ts +997 -0
- package/src/components/select/combobox.ts +0 -1
- package/src/components/select/config.ts +7 -1
- package/src/components/select/dropdown.ts +0 -24
- package/src/components/select/remote.ts +0 -49
- package/src/components/select/search.ts +97 -24
- package/src/components/select/select.css +5 -1
- package/src/components/select/select.ts +211 -153
- package/src/components/select/tags.ts +0 -1
- package/src/components/sticky/sticky.ts +55 -5
- package/src/helpers/data.ts +10 -0
- package/src/helpers/event-handler.ts +7 -6
- 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
|
+
|