@keenthemes/ktui 1.1.4 → 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 +11061 -10799
- 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 +11 -0
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +80 -11
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +77 -24
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/drawer/drawer.js +63 -42
- package/lib/cjs/components/drawer/drawer.js.map +1 -1
- package/lib/cjs/components/dropdown/dropdown.js +6 -0
- 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 +29 -9
- 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 +11 -0
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +80 -11
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +77 -24
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/drawer/drawer.js +63 -42
- package/lib/esm/components/drawer/drawer.js.map +1 -1
- package/lib/esm/components/dropdown/dropdown.js +6 -0
- 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 +29 -9
- 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 +14 -4
- 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 +88 -10
- package/src/components/datatable/datatable.css +4 -4
- package/src/components/datatable/datatable.ts +91 -30
- package/src/components/datatable/types.ts +16 -0
- package/src/components/drawer/drawer.ts +97 -57
- package/src/components/drawer/types.ts +4 -2
- package/src/components/dropdown/dropdown.ts +8 -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
|
@@ -33,6 +33,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
33
33
|
persistent: false,
|
|
34
34
|
container: '',
|
|
35
35
|
focus: true,
|
|
36
|
+
keepInPlaceWithin: '',
|
|
36
37
|
};
|
|
37
38
|
protected override _config: KTDrawerConfigInterface = this._defaultConfig;
|
|
38
39
|
protected _isOpen: boolean = false;
|
|
@@ -52,7 +53,6 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
52
53
|
this._handleClose();
|
|
53
54
|
this._update();
|
|
54
55
|
this._handleContainer();
|
|
55
|
-
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
protected _handleClose(): void {
|
|
@@ -91,26 +91,25 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
91
91
|
|
|
92
92
|
KTDrawer.hide();
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this._element.setAttribute('data-kt-drawer-original-in-header', 'true');
|
|
94
|
+
// When container="body", move drawer to body only if NOT inside an element matching keepInPlaceWithin.
|
|
95
|
+
// When keepInPlaceWithin is set (e.g. for SPA/persisted layouts), keeping the drawer in place lets the host preserve it across navigations.
|
|
96
|
+
if (
|
|
97
|
+
this._getOption('container') === 'body' &&
|
|
98
|
+
this._element.parentElement !== document.body
|
|
99
|
+
) {
|
|
100
|
+
const keepInPlace = this._isKeepInPlace();
|
|
101
|
+
if (!keepInPlace) {
|
|
102
|
+
if (!this._element.hasAttribute('data-kt-drawer-original-parent-id')) {
|
|
103
|
+
const originalParent = this._element.parentElement;
|
|
104
|
+
if (originalParent && originalParent !== document.body) {
|
|
105
|
+
this._element.setAttribute(
|
|
106
|
+
'data-kt-drawer-original-parent-id',
|
|
107
|
+
originalParent.id || '',
|
|
108
|
+
);
|
|
110
109
|
}
|
|
111
110
|
}
|
|
111
|
+
document.body.appendChild(this._element);
|
|
112
112
|
}
|
|
113
|
-
document.body.appendChild(this._element);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
if (this._getOption('backdrop') === true) this._createBackdrop();
|
|
@@ -209,23 +208,14 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
209
208
|
protected _handleContainer(): void {
|
|
210
209
|
if (this._getOption('container')) {
|
|
211
210
|
if (this._getOption('container') === 'body') {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
((originalParent as HTMLElement).closest('[wire\\:id]') !== null ||
|
|
218
|
-
(originalParent as HTMLElement).closest('header#header') !== null);
|
|
219
|
-
|
|
220
|
-
if (isInPersistedComponent) {
|
|
221
|
-
// Don't move to body - keep in original location for Livewire persistence
|
|
222
|
-
// Use fixed positioning to achieve the same visual effect
|
|
223
|
-
// Ensure drawer has fixed positioning to work from its current location
|
|
224
|
-
if (!this._element.style.position || this._element.style.position === 'static') {
|
|
211
|
+
if (this._isKeepInPlace()) {
|
|
212
|
+
if (
|
|
213
|
+
!this._element.style.position ||
|
|
214
|
+
this._element.style.position === 'static'
|
|
215
|
+
) {
|
|
225
216
|
this._element.style.position = 'fixed';
|
|
226
217
|
}
|
|
227
218
|
} else {
|
|
228
|
-
// Not in persisted component - safe to move to body (follows original behavior)
|
|
229
219
|
document.body.appendChild(this._element);
|
|
230
220
|
}
|
|
231
221
|
} else {
|
|
@@ -236,6 +226,25 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
236
226
|
}
|
|
237
227
|
}
|
|
238
228
|
|
|
229
|
+
/** True when drawer is inside an element matching keepInPlaceWithin (so we keep it in place instead of moving to body). */
|
|
230
|
+
protected _isKeepInPlace(): boolean {
|
|
231
|
+
const selector = (this._getOption('keepInPlaceWithin') as string)?.trim();
|
|
232
|
+
if (!selector || !this._element?.parentElement) return false;
|
|
233
|
+
const parent = this._element.parentElement;
|
|
234
|
+
const selectors = selector
|
|
235
|
+
.split(',')
|
|
236
|
+
.map((s) => s.trim())
|
|
237
|
+
.filter(Boolean);
|
|
238
|
+
for (const sel of selectors) {
|
|
239
|
+
try {
|
|
240
|
+
if (parent.closest(sel) !== null) return true;
|
|
241
|
+
} catch {
|
|
242
|
+
// invalid selector, skip
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
239
248
|
protected _autoFocus(): void {
|
|
240
249
|
if (!this._element) return;
|
|
241
250
|
const input: HTMLInputElement | null = this._element.querySelector(
|
|
@@ -253,7 +262,12 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
253
262
|
this._backdropElement = document.createElement('DIV');
|
|
254
263
|
this._backdropElement.style.zIndex = (zindex - 1).toString();
|
|
255
264
|
this._backdropElement.setAttribute('data-kt-drawer-backdrop', 'true');
|
|
256
|
-
|
|
265
|
+
const parent = this._element.parentElement;
|
|
266
|
+
if (parent) {
|
|
267
|
+
parent.insertBefore(this._backdropElement, this._element);
|
|
268
|
+
} else {
|
|
269
|
+
document.body.append(this._backdropElement);
|
|
270
|
+
}
|
|
257
271
|
KTDom.reflow(this._backdropElement);
|
|
258
272
|
KTDom.addClass(
|
|
259
273
|
this._backdropElement,
|
|
@@ -284,8 +298,8 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
284
298
|
return KTUtils.stringToBoolean(this._getOption('enable'));
|
|
285
299
|
}
|
|
286
300
|
|
|
287
|
-
public toggle(): void {
|
|
288
|
-
return this._toggle();
|
|
301
|
+
public toggle(relatedTarget?: HTMLElement): void {
|
|
302
|
+
return this._toggle(relatedTarget);
|
|
289
303
|
}
|
|
290
304
|
|
|
291
305
|
public show(relatedTarget?: HTMLElement): void {
|
|
@@ -323,7 +337,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
323
337
|
|
|
324
338
|
// Fallback: look for parent with data-kt-drawer attribute
|
|
325
339
|
if (reference) {
|
|
326
|
-
const drawerContainer = reference.closest(
|
|
340
|
+
const drawerContainer = reference.closest(
|
|
341
|
+
'[data-kt-drawer]',
|
|
342
|
+
) as HTMLElement;
|
|
327
343
|
if (drawerContainer) return drawerContainer;
|
|
328
344
|
}
|
|
329
345
|
|
|
@@ -347,7 +363,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
347
363
|
* Wait for an element to appear in the DOM using polling with MutationObserver fallback
|
|
348
364
|
* Useful for persisted Livewire components that may not be in DOM immediately
|
|
349
365
|
*/
|
|
350
|
-
public static waitForElement(
|
|
366
|
+
public static waitForElement(
|
|
367
|
+
selector: string,
|
|
368
|
+
timeout: number = 2000,
|
|
369
|
+
): Promise<HTMLElement | null> {
|
|
351
370
|
return new Promise((resolve) => {
|
|
352
371
|
let resolved = false;
|
|
353
372
|
|
|
@@ -359,7 +378,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
359
378
|
};
|
|
360
379
|
|
|
361
380
|
// Check if element already exists
|
|
362
|
-
const existing =
|
|
381
|
+
const existing =
|
|
382
|
+
document.querySelector(selector) ||
|
|
383
|
+
document.body.querySelector(selector);
|
|
363
384
|
if (existing) {
|
|
364
385
|
doResolve(existing as HTMLElement);
|
|
365
386
|
return;
|
|
@@ -374,7 +395,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
374
395
|
return;
|
|
375
396
|
}
|
|
376
397
|
attempts++;
|
|
377
|
-
const element =
|
|
398
|
+
const element =
|
|
399
|
+
document.querySelector(selector) ||
|
|
400
|
+
document.body.querySelector(selector);
|
|
378
401
|
if (element) {
|
|
379
402
|
clearInterval(pollInterval);
|
|
380
403
|
doResolve(element as HTMLElement);
|
|
@@ -392,7 +415,9 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
392
415
|
observer.disconnect();
|
|
393
416
|
return;
|
|
394
417
|
}
|
|
395
|
-
const element =
|
|
418
|
+
const element =
|
|
419
|
+
document.querySelector(selector) ||
|
|
420
|
+
document.body.querySelector(selector);
|
|
396
421
|
if (element) {
|
|
397
422
|
clearInterval(pollInterval);
|
|
398
423
|
observer.disconnect();
|
|
@@ -482,13 +507,16 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
482
507
|
}
|
|
483
508
|
|
|
484
509
|
public static handleToggle(): void {
|
|
485
|
-
|
|
486
510
|
// Add raw click listener to document.body to track all clicks
|
|
487
|
-
document.body.addEventListener(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
492
520
|
|
|
493
521
|
KTEventHandler.on(
|
|
494
522
|
document.body,
|
|
@@ -504,12 +532,16 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
504
532
|
const drawer = KTDrawer.getInstance(target);
|
|
505
533
|
|
|
506
534
|
if (drawer) {
|
|
507
|
-
drawer.toggle();
|
|
535
|
+
drawer.toggle(target);
|
|
508
536
|
} else {
|
|
509
537
|
// Drawer element not found - wait for it to appear (handles persisted Livewire components)
|
|
510
538
|
// Check if drawer exists in persisted components (might be in header that's persisted)
|
|
511
|
-
const persistedHeader =
|
|
512
|
-
|
|
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;
|
|
513
545
|
|
|
514
546
|
// Wait longer for persisted components that may take time to render
|
|
515
547
|
// Also check if drawer exists in persisted header component
|
|
@@ -522,7 +554,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
522
554
|
// Get instance and toggle
|
|
523
555
|
const drawerInstance = KTDrawer.getInstance(drawerElement);
|
|
524
556
|
if (drawerInstance) {
|
|
525
|
-
drawerInstance.toggle();
|
|
557
|
+
drawerInstance.toggle(target);
|
|
526
558
|
}
|
|
527
559
|
} else {
|
|
528
560
|
// Drawer never appeared - trigger a reinit to see if it helps
|
|
@@ -530,14 +562,18 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
530
562
|
setTimeout(() => {
|
|
531
563
|
KTDrawer.reinit();
|
|
532
564
|
// Try one more time after reinit
|
|
533
|
-
const drawerAfterReinit =
|
|
565
|
+
const drawerAfterReinit =
|
|
566
|
+
document.querySelector(selector) ||
|
|
567
|
+
document.body.querySelector(selector);
|
|
534
568
|
if (drawerAfterReinit) {
|
|
535
569
|
if (!KTData.has(drawerAfterReinit as HTMLElement, 'drawer')) {
|
|
536
570
|
new KTDrawer(drawerAfterReinit as HTMLElement);
|
|
537
571
|
}
|
|
538
|
-
const drawerInstance = KTDrawer.getInstance(
|
|
572
|
+
const drawerInstance = KTDrawer.getInstance(
|
|
573
|
+
drawerAfterReinit as HTMLElement,
|
|
574
|
+
);
|
|
539
575
|
if (drawerInstance) {
|
|
540
|
-
drawerInstance.toggle();
|
|
576
|
+
drawerInstance.toggle(target);
|
|
541
577
|
}
|
|
542
578
|
}
|
|
543
579
|
}, 500);
|
|
@@ -622,7 +658,10 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
622
658
|
const elementsInDoc = document.querySelectorAll('[data-kt-drawer]');
|
|
623
659
|
const elementsInBody = document.body.querySelectorAll('[data-kt-drawer]');
|
|
624
660
|
// Combine and deduplicate
|
|
625
|
-
const allElements = new Set([
|
|
661
|
+
const allElements = new Set([
|
|
662
|
+
...Array.from(elementsInDoc),
|
|
663
|
+
...Array.from(elementsInBody),
|
|
664
|
+
]);
|
|
626
665
|
const elements = Array.from(allElements);
|
|
627
666
|
elements.forEach((element) => {
|
|
628
667
|
new KTDrawer(element as HTMLElement);
|
|
@@ -652,10 +691,12 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
652
691
|
const elementsInDoc = document.querySelectorAll('[data-kt-drawer]');
|
|
653
692
|
const elementsInBody = document.body.querySelectorAll('[data-kt-drawer]');
|
|
654
693
|
// Combine and deduplicate
|
|
655
|
-
const allElements = new Set([
|
|
694
|
+
const allElements = new Set([
|
|
695
|
+
...Array.from(elementsInDoc),
|
|
696
|
+
...Array.from(elementsInBody),
|
|
697
|
+
]);
|
|
656
698
|
const elements = Array.from(allElements);
|
|
657
699
|
|
|
658
|
-
|
|
659
700
|
// Clean up existing instances
|
|
660
701
|
elements.forEach((element) => {
|
|
661
702
|
try {
|
|
@@ -684,7 +725,6 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
|
|
|
684
725
|
KTDrawer.handleResize();
|
|
685
726
|
KTDrawer.handleClickAway();
|
|
686
727
|
KTDrawer.handleKeyword();
|
|
687
|
-
|
|
688
728
|
}
|
|
689
729
|
}
|
|
690
730
|
|
|
@@ -17,9 +17,11 @@ export interface KTDrawerConfigInterface {
|
|
|
17
17
|
persistent: boolean;
|
|
18
18
|
focus: boolean;
|
|
19
19
|
container: string;
|
|
20
|
+
/** When set, drawer is not moved to body when inside an element matching this selector (e.g. for SPA/persisted layouts). Comma-separated for multiple selectors. */
|
|
21
|
+
keepInPlaceWithin?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface KTDrawerInterface {
|
|
22
|
-
show(): void;
|
|
24
|
+
show(relatedTarget?: HTMLElement): void;
|
|
23
25
|
hide(): void;
|
|
24
|
-
toggle(): void;
|
|
26
|
+
toggle(relatedTarget?: HTMLElement): void;
|
|
25
27
|
}
|
|
@@ -45,6 +45,8 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
|
|
|
45
45
|
protected _menuElement: HTMLElement;
|
|
46
46
|
protected _isTransitioning: boolean = false;
|
|
47
47
|
protected _isOpen: boolean = false;
|
|
48
|
+
/** Timestamp when _show() was last called; used to ignore duplicate _hide() from double handlers */
|
|
49
|
+
protected _shownAt: number = 0;
|
|
48
50
|
|
|
49
51
|
constructor(element: HTMLElement, config?: KTDropdownConfigInterface) {
|
|
50
52
|
super();
|
|
@@ -206,10 +208,13 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
|
|
|
206
208
|
this._fireEvent('shown');
|
|
207
209
|
this._dispatchEvent('shown');
|
|
208
210
|
});
|
|
211
|
+
this._shownAt = Date.now();
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
protected _hide(): void {
|
|
212
215
|
if (!this._isOpen || this._isTransitioning) return;
|
|
216
|
+
// If another handler fired _hide() right after _show() (e.g. double initHandlers), ignore
|
|
217
|
+
if (this._shownAt && Date.now() - this._shownAt < 150) return;
|
|
213
218
|
|
|
214
219
|
const payload = { cancel: false };
|
|
215
220
|
this._fireEvent('hide', payload);
|
|
@@ -386,7 +391,9 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
|
|
|
386
391
|
|
|
387
392
|
// Fallback: look for parent with data-kt-dropdown attribute
|
|
388
393
|
if (reference) {
|
|
389
|
-
const dropdownContainer = reference.closest(
|
|
394
|
+
const dropdownContainer = reference.closest(
|
|
395
|
+
'[data-kt-dropdown]',
|
|
396
|
+
) as HTMLElement;
|
|
390
397
|
if (dropdownContainer) return dropdownContainer;
|
|
391
398
|
}
|
|
392
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
|
+
});
|