@keenthemes/ktui 1.1.5 → 1.1.6
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 +11232 -11095
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +33 -27
- package/lib/cjs/components/collapse/collapse.js +0 -2
- package/lib/cjs/components/collapse/collapse.js.map +1 -1
- package/lib/cjs/components/component.js +3 -1
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +1 -2
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +45 -23
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/drawer/drawer.js +21 -9
- package/lib/cjs/components/drawer/drawer.js.map +1 -1
- package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
- package/lib/cjs/components/scrollto/scrollto.js +0 -2
- package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/remote.js.map +1 -1
- package/lib/cjs/components/select/search.js +9 -5
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +22 -5
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/select/templates.js.map +1 -1
- package/lib/cjs/components/select/utils.js +10 -0
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +104 -24
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
- package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/cjs/components/toast/toast.js +1 -2
- package/lib/cjs/components/toast/toast.js.map +1 -1
- package/lib/cjs/helpers/dom.js +0 -2
- package/lib/cjs/helpers/dom.js.map +1 -1
- package/lib/esm/components/collapse/collapse.js +0 -2
- package/lib/esm/components/collapse/collapse.js.map +1 -1
- package/lib/esm/components/component.js +3 -1
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +1 -2
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +45 -23
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/drawer/drawer.js +21 -9
- package/lib/esm/components/drawer/drawer.js.map +1 -1
- package/lib/esm/components/dropdown/dropdown.js.map +1 -1
- package/lib/esm/components/scrollto/scrollto.js +0 -2
- package/lib/esm/components/scrollto/scrollto.js.map +1 -1
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/remote.js.map +1 -1
- package/lib/esm/components/select/search.js +9 -5
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +22 -5
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/select/templates.js.map +1 -1
- package/lib/esm/components/select/utils.js +10 -0
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/components/sticky/sticky.js +104 -24
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/lib/esm/components/theme-switch/theme-switch.js +0 -2
- package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/esm/components/toast/toast.js +1 -2
- package/lib/esm/components/toast/toast.js.map +1 -1
- package/lib/esm/helpers/dom.js +0 -2
- package/lib/esm/helpers/dom.js.map +1 -1
- package/package.json +14 -7
- package/src/components/collapse/collapse.ts +0 -3
- package/src/components/component.ts +5 -5
- package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
- package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
- package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
- package/src/components/datatable/__tests__/setup.ts +9 -4
- package/src/components/datatable/datatable-sort.ts +12 -16
- package/src/components/datatable/datatable.css +4 -4
- package/src/components/datatable/datatable.ts +56 -26
- package/src/components/datatable/types.ts +3 -1
- package/src/components/drawer/drawer.ts +61 -24
- package/src/components/dropdown/dropdown.ts +3 -1
- package/src/components/scrollto/scrollto.ts +0 -3
- package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
- package/src/components/select/combobox.ts +0 -1
- package/src/components/select/dropdown.ts +0 -2
- package/src/components/select/remote.ts +1 -6
- package/src/components/select/search.ts +14 -7
- package/src/components/select/select.ts +29 -29
- package/src/components/select/tags.ts +0 -1
- package/src/components/select/templates.ts +8 -8
- package/src/components/select/utils.ts +15 -2
- package/src/components/sticky/__tests__/sticky.test.ts +205 -0
- package/src/components/sticky/sticky.ts +119 -21
- package/src/components/sticky/types.ts +3 -0
- package/src/components/theme-switch/theme-switch.ts +0 -3
- package/src/components/toast/toast.ts +3 -2
- package/src/helpers/dom.ts +0 -3
|
@@ -53,7 +53,6 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
53
53
|
this._handleClose();
|
|
54
54
|
this._update();
|
|
55
55
|
this._handleContainer();
|
|
56
|
-
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
protected _handleClose(): void {
|
|
@@ -94,13 +93,19 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
94
93
|
|
|
95
94
|
// When container="body", move drawer to body only if NOT inside an element matching keepInPlaceWithin.
|
|
96
95
|
// When keepInPlaceWithin is set (e.g. for SPA/persisted layouts), keeping the drawer in place lets the host preserve it across navigations.
|
|
97
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
this._getOption('container') === 'body' &&
|
|
98
|
+
this._element.parentElement !== document.body
|
|
99
|
+
) {
|
|
98
100
|
const keepInPlace = this._isKeepInPlace();
|
|
99
101
|
if (!keepInPlace) {
|
|
100
102
|
if (!this._element.hasAttribute('data-kt-drawer-original-parent-id')) {
|
|
101
103
|
const originalParent = this._element.parentElement;
|
|
102
104
|
if (originalParent && originalParent !== document.body) {
|
|
103
|
-
this._element.setAttribute(
|
|
105
|
+
this._element.setAttribute(
|
|
106
|
+
'data-kt-drawer-original-parent-id',
|
|
107
|
+
originalParent.id || '',
|
|
108
|
+
);
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
document.body.appendChild(this._element);
|
|
@@ -204,7 +209,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
204
209
|
if (this._getOption('container')) {
|
|
205
210
|
if (this._getOption('container') === 'body') {
|
|
206
211
|
if (this._isKeepInPlace()) {
|
|
207
|
-
if (
|
|
212
|
+
if (
|
|
213
|
+
!this._element.style.position ||
|
|
214
|
+
this._element.style.position === 'static'
|
|
215
|
+
) {
|
|
208
216
|
this._element.style.position = 'fixed';
|
|
209
217
|
}
|
|
210
218
|
} else {
|
|
@@ -223,7 +231,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
223
231
|
const selector = (this._getOption('keepInPlaceWithin') as string)?.trim();
|
|
224
232
|
if (!selector || !this._element?.parentElement) return false;
|
|
225
233
|
const parent = this._element.parentElement;
|
|
226
|
-
const selectors = selector
|
|
234
|
+
const selectors = selector
|
|
235
|
+
.split(',')
|
|
236
|
+
.map((s) => s.trim())
|
|
237
|
+
.filter(Boolean);
|
|
227
238
|
for (const sel of selectors) {
|
|
228
239
|
try {
|
|
229
240
|
if (parent.closest(sel) !== null) return true;
|
|
@@ -326,7 +337,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
326
337
|
|
|
327
338
|
// Fallback: look for parent with data-kt-drawer attribute
|
|
328
339
|
if (reference) {
|
|
329
|
-
const drawerContainer = reference.closest(
|
|
340
|
+
const drawerContainer = reference.closest(
|
|
341
|
+
'[data-kt-drawer]',
|
|
342
|
+
) as HTMLElement;
|
|
330
343
|
if (drawerContainer) return drawerContainer;
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -350,7 +363,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
350
363
|
* Wait for an element to appear in the DOM using polling with MutationObserver fallback
|
|
351
364
|
* Useful for persisted Livewire components that may not be in DOM immediately
|
|
352
365
|
*/
|
|
353
|
-
public static waitForElement(
|
|
366
|
+
public static waitForElement(
|
|
367
|
+
selector: string,
|
|
368
|
+
timeout: number = 2000,
|
|
369
|
+
): Promise<HTMLElement | null> {
|
|
354
370
|
return new Promise((resolve) => {
|
|
355
371
|
let resolved = false;
|
|
356
372
|
|
|
@@ -362,7 +378,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
362
378
|
};
|
|
363
379
|
|
|
364
380
|
// Check if element already exists
|
|
365
|
-
const existing =
|
|
381
|
+
const existing =
|
|
382
|
+
document.querySelector(selector) ||
|
|
383
|
+
document.body.querySelector(selector);
|
|
366
384
|
if (existing) {
|
|
367
385
|
doResolve(existing as HTMLElement);
|
|
368
386
|
return;
|
|
@@ -377,7 +395,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
377
395
|
return;
|
|
378
396
|
}
|
|
379
397
|
attempts++;
|
|
380
|
-
const element =
|
|
398
|
+
const element =
|
|
399
|
+
document.querySelector(selector) ||
|
|
400
|
+
document.body.querySelector(selector);
|
|
381
401
|
if (element) {
|
|
382
402
|
clearInterval(pollInterval);
|
|
383
403
|
doResolve(element as HTMLElement);
|
|
@@ -395,7 +415,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
395
415
|
observer.disconnect();
|
|
396
416
|
return;
|
|
397
417
|
}
|
|
398
|
-
const element =
|
|
418
|
+
const element =
|
|
419
|
+
document.querySelector(selector) ||
|
|
420
|
+
document.body.querySelector(selector);
|
|
399
421
|
if (element) {
|
|
400
422
|
clearInterval(pollInterval);
|
|
401
423
|
observer.disconnect();
|
|
@@ -485,13 +507,16 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
485
507
|
}
|
|
486
508
|
|
|
487
509
|
public static handleToggle(): void {
|
|
488
|
-
|
|
489
510
|
// Add raw click listener to document.body to track all clicks
|
|
490
|
-
document.body.addEventListener(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
511
|
+
document.body.addEventListener(
|
|
512
|
+
'click',
|
|
513
|
+
(rawEvent: MouseEvent) => {
|
|
514
|
+
const target = rawEvent.target as HTMLElement;
|
|
515
|
+
if (target && target.hasAttribute('data-kt-drawer-toggle')) {
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
true,
|
|
519
|
+
); // Use capture phase to catch before any stopPropagation
|
|
495
520
|
|
|
496
521
|
KTEventHandler.on(
|
|
497
522
|
document.body,
|
|
@@ -511,8 +536,12 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
511
536
|
} else {
|
|
512
537
|
// Drawer element not found - wait for it to appear (handles persisted Livewire components)
|
|
513
538
|
// Check if drawer exists in persisted components (might be in header that's persisted)
|
|
514
|
-
const persistedHeader =
|
|
515
|
-
|
|
539
|
+
const persistedHeader =
|
|
540
|
+
document.querySelector('[wire\\:id]')?.closest('[wire\\:id]') ||
|
|
541
|
+
document.querySelector('header#header');
|
|
542
|
+
const drawerInPersisted = persistedHeader
|
|
543
|
+
? persistedHeader.querySelector(selector)
|
|
544
|
+
: null;
|
|
516
545
|
|
|
517
546
|
// Wait longer for persisted components that may take time to render
|
|
518
547
|
// Also check if drawer exists in persisted header component
|
|
@@ -533,12 +562,16 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
533
562
|
setTimeout(() => {
|
|
534
563
|
KTDrawer.reinit();
|
|
535
564
|
// Try one more time after reinit
|
|
536
|
-
const drawerAfterReinit =
|
|
565
|
+
const drawerAfterReinit =
|
|
566
|
+
document.querySelector(selector) ||
|
|
567
|
+
document.body.querySelector(selector);
|
|
537
568
|
if (drawerAfterReinit) {
|
|
538
569
|
if (!KTData.has(drawerAfterReinit as HTMLElement, 'drawer')) {
|
|
539
570
|
new KTDrawer(drawerAfterReinit as HTMLElement);
|
|
540
571
|
}
|
|
541
|
-
const drawerInstance = KTDrawer.getInstance(
|
|
572
|
+
const drawerInstance = KTDrawer.getInstance(
|
|
573
|
+
drawerAfterReinit as HTMLElement,
|
|
574
|
+
);
|
|
542
575
|
if (drawerInstance) {
|
|
543
576
|
drawerInstance.toggle(target);
|
|
544
577
|
}
|
|
@@ -625,7 +658,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
625
658
|
const elementsInDoc = document.querySelectorAll('[data-kt-drawer]');
|
|
626
659
|
const elementsInBody = document.body.querySelectorAll('[data-kt-drawer]');
|
|
627
660
|
// Combine and deduplicate
|
|
628
|
-
const allElements = new Set([
|
|
661
|
+
const allElements = new Set([
|
|
662
|
+
...Array.from(elementsInDoc),
|
|
663
|
+
...Array.from(elementsInBody),
|
|
664
|
+
]);
|
|
629
665
|
const elements = Array.from(allElements);
|
|
630
666
|
elements.forEach((element) => {
|
|
631
667
|
new KTDrawer(element as HTMLElement);
|
|
@@ -655,10 +691,12 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
655
691
|
const elementsInDoc = document.querySelectorAll('[data-kt-drawer]');
|
|
656
692
|
const elementsInBody = document.body.querySelectorAll('[data-kt-drawer]');
|
|
657
693
|
// Combine and deduplicate
|
|
658
|
-
const allElements = new Set([
|
|
694
|
+
const allElements = new Set([
|
|
695
|
+
...Array.from(elementsInDoc),
|
|
696
|
+
...Array.from(elementsInBody),
|
|
697
|
+
]);
|
|
659
698
|
const elements = Array.from(allElements);
|
|
660
699
|
|
|
661
|
-
|
|
662
700
|
// Clean up existing instances
|
|
663
701
|
elements.forEach((element) => {
|
|
664
702
|
try {
|
|
@@ -687,7 +725,6 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
687
725
|
KTDrawer.handleResize();
|
|
688
726
|
KTDrawer.handleClickAway();
|
|
689
727
|
KTDrawer.handleKeyword();
|
|
690
|
-
|
|
691
728
|
}
|
|
692
729
|
}
|
|
693
730
|
|
|
@@ -391,7 +391,9 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
|
|
|
391
391
|
|
|
392
392
|
// Fallback: look for parent with data-kt-dropdown attribute
|
|
393
393
|
if (reference) {
|
|
394
|
-
const dropdownContainer = reference.closest(
|
|
394
|
+
const dropdownContainer = reference.closest(
|
|
395
|
+
'[data-kt-dropdown]',
|
|
396
|
+
) as HTMLElement;
|
|
395
397
|
if (dropdownContainer) return dropdownContainer;
|
|
396
398
|
}
|
|
397
399
|
|
|
@@ -66,6 +66,24 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
66
66
|
vi.clearAllMocks();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
describe('refresh() before init (issue #109)', () => {
|
|
70
|
+
it('should not throw when refresh() is called immediately after getOrCreateInstance()', () => {
|
|
71
|
+
const selectEl = createSelectElement();
|
|
72
|
+
container.appendChild(selectEl);
|
|
73
|
+
|
|
74
|
+
// Simulate framework (e.g. Angular) setting default value by code before KTSelect init
|
|
75
|
+
(selectEl as HTMLSelectElement).value = '2';
|
|
76
|
+
|
|
77
|
+
// getOrCreateInstance returns synchronously; _setupComponent runs in a later microtask.
|
|
78
|
+
// Calling refresh() here used to throw because _dropdownContentElement was undefined.
|
|
79
|
+
const instance = KTSelect.getOrCreateInstance(selectEl, { height: 250 });
|
|
80
|
+
|
|
81
|
+
expect(() => {
|
|
82
|
+
instance.refresh();
|
|
83
|
+
}).not.toThrow();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
69
87
|
describe('Search Autofocus Enhancement', () => {
|
|
70
88
|
it('should focus search input when dropdown opens with searchAutofocus enabled', async () => {
|
|
71
89
|
const selectEl = createSelectElement();
|
|
@@ -250,6 +268,98 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
250
268
|
expect(select.getSelectedOptions()).toContain('1');
|
|
251
269
|
});
|
|
252
270
|
|
|
271
|
+
it('should select focused option (not first) when Enter is pressed after ArrowDown with search enabled (issue #108)', async () => {
|
|
272
|
+
const selectEl = createSelectElement([
|
|
273
|
+
{ value: 'apple', text: 'Apple' },
|
|
274
|
+
{ value: 'google', text: 'Google' },
|
|
275
|
+
{ value: 'amazon', text: 'Amazon' },
|
|
276
|
+
]);
|
|
277
|
+
container.appendChild(selectEl);
|
|
278
|
+
|
|
279
|
+
const select = new KTSelect(selectEl, {
|
|
280
|
+
enableSearch: true,
|
|
281
|
+
closeOnEnter: true,
|
|
282
|
+
height: 250,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await waitForInit(select);
|
|
286
|
+
|
|
287
|
+
// Open dropdown
|
|
288
|
+
select.openDropdown();
|
|
289
|
+
await waitFor(200);
|
|
290
|
+
|
|
291
|
+
const searchInput = select.getSearchInput();
|
|
292
|
+
expect(searchInput).toBeTruthy();
|
|
293
|
+
|
|
294
|
+
// Focus search input (user has not used arrow keys yet)
|
|
295
|
+
searchInput.focus();
|
|
296
|
+
await waitFor(50);
|
|
297
|
+
|
|
298
|
+
// Simulate user pressing ArrowDown twice: first moves to first option, second to second (Google).
|
|
299
|
+
// Enter must select the currently focused option (Google), not always the first (Apple).
|
|
300
|
+
const arrowDownEvent = (opts?: Partial<KeyboardEvent>) =>
|
|
301
|
+
new KeyboardEvent('keydown', {
|
|
302
|
+
key: 'ArrowDown',
|
|
303
|
+
bubbles: true,
|
|
304
|
+
cancelable: true,
|
|
305
|
+
...opts,
|
|
306
|
+
});
|
|
307
|
+
searchInput.dispatchEvent(arrowDownEvent());
|
|
308
|
+
await waitFor(20);
|
|
309
|
+
searchInput.dispatchEvent(arrowDownEvent());
|
|
310
|
+
await waitFor(20);
|
|
311
|
+
|
|
312
|
+
// Press Enter - should select the focused option (Google), not the first (Apple)
|
|
313
|
+
const enterEvent = new KeyboardEvent('keydown', {
|
|
314
|
+
key: 'Enter',
|
|
315
|
+
bubbles: true,
|
|
316
|
+
cancelable: true,
|
|
317
|
+
});
|
|
318
|
+
searchInput.dispatchEvent(enterEvent);
|
|
319
|
+
|
|
320
|
+
await waitFor(150);
|
|
321
|
+
|
|
322
|
+
// The highlighted option (google) must be selected, not the first (apple)
|
|
323
|
+
expect(select.getSelectedOptions()).toContain('google');
|
|
324
|
+
expect(select.getSelectedOptions()).not.toContain('apple');
|
|
325
|
+
expect(select.isDropdownOpen()).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should select focused option when Enter is pressed after ArrowDown then ArrowUp (last option focused)', async () => {
|
|
329
|
+
const selectEl = createSelectElement([
|
|
330
|
+
{ value: '1', text: 'Option 1' },
|
|
331
|
+
{ value: '2', text: 'Option 2' },
|
|
332
|
+
{ value: '3', text: 'Option 3' },
|
|
333
|
+
]);
|
|
334
|
+
container.appendChild(selectEl);
|
|
335
|
+
|
|
336
|
+
const select = new KTSelect(selectEl, {
|
|
337
|
+
enableSearch: true,
|
|
338
|
+
closeOnEnter: true,
|
|
339
|
+
height: 250,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await waitForInit(select);
|
|
343
|
+
|
|
344
|
+
select.openDropdown();
|
|
345
|
+
await waitFor(200);
|
|
346
|
+
|
|
347
|
+
const searchInput = select.getSearchInput();
|
|
348
|
+
searchInput!.focus();
|
|
349
|
+
await waitFor(50);
|
|
350
|
+
|
|
351
|
+
// ArrowDown focuses first option; ArrowUp from first wraps to last (3). Enter selects focused.
|
|
352
|
+
searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }));
|
|
353
|
+
await waitFor(20);
|
|
354
|
+
searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true }));
|
|
355
|
+
await waitFor(20);
|
|
356
|
+
|
|
357
|
+
searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
|
358
|
+
await waitFor(150);
|
|
359
|
+
|
|
360
|
+
expect(select.getSelectedOptions()).toContain('3');
|
|
361
|
+
});
|
|
362
|
+
|
|
253
363
|
it('should close dropdown and trigger selection when Enter is pressed after typing search query', async () => {
|
|
254
364
|
const selectEl = createSelectElement([
|
|
255
365
|
{ value: '1', text: 'Apple' },
|
|
@@ -671,9 +781,7 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
671
781
|
|
|
672
782
|
// Registry should be empty (we can't directly access private static, but we can verify behavior)
|
|
673
783
|
// Opening another dropdown should work without issues
|
|
674
|
-
const selectEl2 = createSelectElement([
|
|
675
|
-
{ value: 'a', text: 'Option A' },
|
|
676
|
-
]);
|
|
784
|
+
const selectEl2 = createSelectElement([{ value: 'a', text: 'Option A' }]);
|
|
677
785
|
container.appendChild(selectEl2);
|
|
678
786
|
const select2 = new KTSelect(selectEl2, { height: 250 });
|
|
679
787
|
await waitForInit(select2);
|
|
@@ -700,9 +808,7 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
700
808
|
await waitFor(100);
|
|
701
809
|
|
|
702
810
|
// Creating a new select should work without issues
|
|
703
|
-
const selectEl2 = createSelectElement([
|
|
704
|
-
{ value: 'a', text: 'Option A' },
|
|
705
|
-
]);
|
|
811
|
+
const selectEl2 = createSelectElement([{ value: 'a', text: 'Option A' }]);
|
|
706
812
|
container.appendChild(selectEl2);
|
|
707
813
|
const select2 = new KTSelect(selectEl2, { height: 250 });
|
|
708
814
|
await waitForInit(select2);
|
|
@@ -931,7 +1037,9 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
931
1037
|
|
|
932
1038
|
const option = select
|
|
933
1039
|
.getDropdownElement()
|
|
934
|
-
?.querySelector(
|
|
1040
|
+
?.querySelector(
|
|
1041
|
+
'[data-kt-select-option][data-value="1"]',
|
|
1042
|
+
) as HTMLElement;
|
|
935
1043
|
|
|
936
1044
|
expect(option).toBeTruthy();
|
|
937
1045
|
option.click();
|
|
@@ -993,5 +1101,163 @@ describe('KTSelect UX Behaviors', () => {
|
|
|
993
1101
|
document.removeEventListener('kt-select:show', showHandler);
|
|
994
1102
|
});
|
|
995
1103
|
});
|
|
996
|
-
});
|
|
997
1104
|
|
|
1105
|
+
describe('setSelectedOptions sync', () => {
|
|
1106
|
+
it('should sync native select, trigger display, and dropdown option when setSelectedOptions([option]) is called (single-select)', async () => {
|
|
1107
|
+
const selectEl = createSelectElement([
|
|
1108
|
+
{ value: '1', text: 'Option 1' },
|
|
1109
|
+
{ value: '2', text: 'Option 2' },
|
|
1110
|
+
{ value: '3', text: 'Option 3' },
|
|
1111
|
+
]);
|
|
1112
|
+
container.appendChild(selectEl);
|
|
1113
|
+
|
|
1114
|
+
const select = new KTSelect(selectEl, { height: 250 });
|
|
1115
|
+
await waitForInit(select);
|
|
1116
|
+
|
|
1117
|
+
const option2 = selectEl.querySelector(
|
|
1118
|
+
'option[value="2"]',
|
|
1119
|
+
) as HTMLOptionElement;
|
|
1120
|
+
expect(option2).toBeTruthy();
|
|
1121
|
+
|
|
1122
|
+
select.setSelectedOptions([option2]);
|
|
1123
|
+
await waitFor(50);
|
|
1124
|
+
|
|
1125
|
+
expect(select.getSelectedOptions()).toEqual(['2']);
|
|
1126
|
+
expect((selectEl as HTMLSelectElement).value).toBe('2');
|
|
1127
|
+
const displayEl = select.getValueDisplayElement();
|
|
1128
|
+
expect(displayEl?.textContent?.trim()).toBe('Option 2');
|
|
1129
|
+
|
|
1130
|
+
select.openDropdown();
|
|
1131
|
+
await waitFor(100);
|
|
1132
|
+
const dropdownOption = select
|
|
1133
|
+
.getDropdownElement()
|
|
1134
|
+
?.querySelector('[data-kt-select-option][data-value="2"]');
|
|
1135
|
+
expect(dropdownOption?.classList.contains('selected')).toBe(true);
|
|
1136
|
+
expect(dropdownOption?.getAttribute('aria-selected')).toBe('true');
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('should sync native options, trigger/tags, and dropdown when setSelectedOptions([optionA, optionB]) is called (multi-select)', async () => {
|
|
1140
|
+
const selectEl = createSelectElement([
|
|
1141
|
+
{ value: 'a', text: 'A' },
|
|
1142
|
+
{ value: 'b', text: 'B' },
|
|
1143
|
+
{ value: 'c', text: 'C' },
|
|
1144
|
+
]);
|
|
1145
|
+
selectEl.setAttribute('multiple', 'multiple');
|
|
1146
|
+
container.appendChild(selectEl);
|
|
1147
|
+
|
|
1148
|
+
const select = new KTSelect(selectEl, {
|
|
1149
|
+
multiple: true,
|
|
1150
|
+
height: 250,
|
|
1151
|
+
});
|
|
1152
|
+
await waitForInit(select);
|
|
1153
|
+
|
|
1154
|
+
const optionA = selectEl.querySelector(
|
|
1155
|
+
'option[value="a"]',
|
|
1156
|
+
) as HTMLOptionElement;
|
|
1157
|
+
const optionB = selectEl.querySelector(
|
|
1158
|
+
'option[value="b"]',
|
|
1159
|
+
) as HTMLOptionElement;
|
|
1160
|
+
expect(optionA).toBeTruthy();
|
|
1161
|
+
expect(optionB).toBeTruthy();
|
|
1162
|
+
|
|
1163
|
+
select.setSelectedOptions([optionA, optionB]);
|
|
1164
|
+
await waitFor(50);
|
|
1165
|
+
|
|
1166
|
+
expect(select.getSelectedOptions()).toContain('a');
|
|
1167
|
+
expect(select.getSelectedOptions()).toContain('b');
|
|
1168
|
+
expect(select.getSelectedOptions().length).toBe(2);
|
|
1169
|
+
expect(optionA.selected).toBe(true);
|
|
1170
|
+
expect(optionB.selected).toBe(true);
|
|
1171
|
+
const optionC = selectEl.querySelector(
|
|
1172
|
+
'option[value="c"]',
|
|
1173
|
+
) as HTMLOptionElement;
|
|
1174
|
+
expect(optionC.selected).toBe(false);
|
|
1175
|
+
|
|
1176
|
+
select.openDropdown();
|
|
1177
|
+
await waitFor(100);
|
|
1178
|
+
const dropdownA = select
|
|
1179
|
+
.getDropdownElement()
|
|
1180
|
+
?.querySelector('[data-kt-select-option][data-value="a"]');
|
|
1181
|
+
const dropdownB = select
|
|
1182
|
+
.getDropdownElement()
|
|
1183
|
+
?.querySelector('[data-kt-select-option][data-value="b"]');
|
|
1184
|
+
expect(dropdownA?.classList.contains('selected')).toBe(true);
|
|
1185
|
+
expect(dropdownB?.classList.contains('selected')).toBe(true);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('should clear selection, native select, show placeholder, and remove selected state when setSelectedOptions([]) is called', async () => {
|
|
1189
|
+
const selectEl = createSelectElement([
|
|
1190
|
+
{ value: '1', text: 'Option 1' },
|
|
1191
|
+
{ value: '2', text: 'Option 2' },
|
|
1192
|
+
]);
|
|
1193
|
+
container.appendChild(selectEl);
|
|
1194
|
+
|
|
1195
|
+
const select = new KTSelect(selectEl, {
|
|
1196
|
+
placeholder: 'Choose...',
|
|
1197
|
+
height: 250,
|
|
1198
|
+
});
|
|
1199
|
+
await waitForInit(select);
|
|
1200
|
+
|
|
1201
|
+
const option1 = selectEl.querySelector(
|
|
1202
|
+
'option[value="1"]',
|
|
1203
|
+
) as HTMLOptionElement;
|
|
1204
|
+
select.setSelectedOptions([option1]);
|
|
1205
|
+
await waitFor(50);
|
|
1206
|
+
expect(select.getSelectedOptions()).toEqual(['1']);
|
|
1207
|
+
|
|
1208
|
+
select.setSelectedOptions([]);
|
|
1209
|
+
await waitFor(50);
|
|
1210
|
+
|
|
1211
|
+
expect(select.getSelectedOptions()).toEqual([]);
|
|
1212
|
+
expect((selectEl as HTMLSelectElement).value).toBe('');
|
|
1213
|
+
Array.from(selectEl.options).forEach((opt) => {
|
|
1214
|
+
expect(opt.selected).toBe(false);
|
|
1215
|
+
});
|
|
1216
|
+
const displayEl = select.getValueDisplayElement();
|
|
1217
|
+
const placeholderEl = displayEl?.querySelector(
|
|
1218
|
+
'[data-kt-select-placeholder]',
|
|
1219
|
+
);
|
|
1220
|
+
expect(placeholderEl).toBeTruthy();
|
|
1221
|
+
|
|
1222
|
+
select.openDropdown();
|
|
1223
|
+
await waitFor(100);
|
|
1224
|
+
const options = select
|
|
1225
|
+
.getDropdownElement()
|
|
1226
|
+
?.querySelectorAll('[data-kt-select-option]');
|
|
1227
|
+
options?.forEach((opt) => {
|
|
1228
|
+
expect(opt.classList.contains('selected')).toBe(false);
|
|
1229
|
+
expect(opt.getAttribute('aria-selected')).toBe('false');
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it('should update trigger text immediately when setSelectedOptions([option]) is called with dropdown closed, and show option selected on next open', async () => {
|
|
1234
|
+
const selectEl = createSelectElement([
|
|
1235
|
+
{ value: '1', text: 'First' },
|
|
1236
|
+
{ value: '2', text: 'Second' },
|
|
1237
|
+
{ value: '3', text: 'Third' },
|
|
1238
|
+
]);
|
|
1239
|
+
container.appendChild(selectEl);
|
|
1240
|
+
|
|
1241
|
+
const select = new KTSelect(selectEl, { height: 250 });
|
|
1242
|
+
await waitForInit(select);
|
|
1243
|
+
expect(select.isDropdownOpen()).toBe(false);
|
|
1244
|
+
|
|
1245
|
+
const option3 = selectEl.querySelector(
|
|
1246
|
+
'option[value="3"]',
|
|
1247
|
+
) as HTMLOptionElement;
|
|
1248
|
+
select.setSelectedOptions([option3]);
|
|
1249
|
+
await waitFor(50);
|
|
1250
|
+
|
|
1251
|
+
const displayEl = select.getValueDisplayElement();
|
|
1252
|
+
expect(displayEl?.textContent?.trim()).toBe('Third');
|
|
1253
|
+
|
|
1254
|
+
select.openDropdown();
|
|
1255
|
+
await waitFor(100);
|
|
1256
|
+
const dropdownOption = select
|
|
1257
|
+
.getDropdownElement()
|
|
1258
|
+
?.querySelector('[data-kt-select-option][data-value="3"]');
|
|
1259
|
+
expect(dropdownOption?.classList.contains('selected')).toBe(true);
|
|
1260
|
+
expect(dropdownOption?.getAttribute('aria-selected')).toBe('true');
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
@@ -408,7 +408,6 @@ export class KTSelectDropdown extends KTComponent {
|
|
|
408
408
|
transitionComplete = true;
|
|
409
409
|
clearTimeout(fallbackTimer);
|
|
410
410
|
|
|
411
|
-
|
|
412
411
|
this._dropdownElement.classList.add('hidden');
|
|
413
412
|
this._dropdownElement.classList.remove('open');
|
|
414
413
|
this._toggleElement.classList.remove('active');
|
|
@@ -420,7 +419,6 @@ export class KTSelectDropdown extends KTComponent {
|
|
|
420
419
|
this._isOpen = false;
|
|
421
420
|
|
|
422
421
|
// Events will be handled by KTSelect
|
|
423
|
-
|
|
424
422
|
};
|
|
425
423
|
|
|
426
424
|
KTDom.transitionEnd(this._dropdownElement, completeTransition);
|
|
@@ -48,7 +48,7 @@ export class KTSelectRemote {
|
|
|
48
48
|
this._lastQuery = query || '';
|
|
49
49
|
this._currentPage = page;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
const url = this._buildUrl(query, page);
|
|
52
52
|
|
|
53
53
|
// Dispatch search start event
|
|
54
54
|
this._dispatchEvent('remoteSearchStart');
|
|
@@ -146,12 +146,10 @@ export class KTSelectRemote {
|
|
|
146
146
|
*/
|
|
147
147
|
private _processData(data: any): KTSelectOptionData[] {
|
|
148
148
|
try {
|
|
149
|
-
|
|
150
149
|
let processedData = data;
|
|
151
150
|
|
|
152
151
|
// Extract data from the API property if specified
|
|
153
152
|
if (this._config.apiDataProperty && data[this._config.apiDataProperty]) {
|
|
154
|
-
|
|
155
153
|
// If pagination metadata is available, extract it
|
|
156
154
|
if (this._config.pagination) {
|
|
157
155
|
if (data.total_pages) {
|
|
@@ -173,7 +171,6 @@ export class KTSelectRemote {
|
|
|
173
171
|
return [];
|
|
174
172
|
}
|
|
175
173
|
|
|
176
|
-
|
|
177
174
|
// Map data to KTSelectOptionData format
|
|
178
175
|
const mappedData = processedData.map((item: any): KTSelectOptionData => {
|
|
179
176
|
const mappedItem = this._mapItemToOption(item);
|
|
@@ -235,7 +232,6 @@ export class KTSelectRemote {
|
|
|
235
232
|
const valueField = this._config.dataValueField || 'id';
|
|
236
233
|
const labelField = this._config.dataFieldText || 'title';
|
|
237
234
|
|
|
238
|
-
|
|
239
235
|
// Extract values using improved getValue function
|
|
240
236
|
const getValue = (obj: any, path: string): any => {
|
|
241
237
|
if (!path || !obj) return null;
|
|
@@ -256,7 +252,6 @@ export class KTSelectRemote {
|
|
|
256
252
|
result = result[part];
|
|
257
253
|
}
|
|
258
254
|
|
|
259
|
-
|
|
260
255
|
return result;
|
|
261
256
|
} catch (error) {
|
|
262
257
|
console.error(`Error extracting path ${path}:`, error);
|
|
@@ -39,7 +39,6 @@ export class KTSelectSearch {
|
|
|
39
39
|
this._searchInput = this._select.getSearchInput();
|
|
40
40
|
|
|
41
41
|
if (this._searchInput) {
|
|
42
|
-
|
|
43
42
|
// First remove any existing listeners to prevent duplicates
|
|
44
43
|
this._removeEventListeners();
|
|
45
44
|
|
|
@@ -195,7 +194,9 @@ export class KTSelectSearch {
|
|
|
195
194
|
try {
|
|
196
195
|
this._searchInput?.focus();
|
|
197
196
|
// Check if focus was successful
|
|
198
|
-
const isFocused =
|
|
197
|
+
const isFocused =
|
|
198
|
+
document.activeElement === this._searchInput ||
|
|
199
|
+
this._searchInput === document.activeElement;
|
|
199
200
|
if (isFocused) {
|
|
200
201
|
// Focus successful
|
|
201
202
|
return;
|
|
@@ -241,14 +242,20 @@ export class KTSelectSearch {
|
|
|
241
242
|
break;
|
|
242
243
|
case 'Enter':
|
|
243
244
|
event.preventDefault();
|
|
244
|
-
|
|
245
|
+
// Use currently focused option (from arrow keys); only fall back to first if none focused
|
|
246
|
+
const optionToSelect =
|
|
247
|
+
this._focusManager.getFocusedOption() ??
|
|
248
|
+
this._focusManager.focusFirst();
|
|
245
249
|
|
|
246
|
-
if (
|
|
247
|
-
const optionValue =
|
|
250
|
+
if (optionToSelect) {
|
|
251
|
+
const optionValue = optionToSelect.getAttribute('data-value');
|
|
248
252
|
if (optionValue) {
|
|
249
253
|
const config = this._select.getConfig();
|
|
250
|
-
const isAlreadySelected =
|
|
251
|
-
|
|
254
|
+
const isAlreadySelected =
|
|
255
|
+
!config.multiple &&
|
|
256
|
+
this._select.getSelectedOptions().includes(optionValue);
|
|
257
|
+
const shouldClose =
|
|
258
|
+
!config.multiple && config.closeOnEnter !== false;
|
|
252
259
|
|
|
253
260
|
if (isAlreadySelected && shouldClose) {
|
|
254
261
|
this._select.closeDropdown();
|