@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.
Files changed (101) hide show
  1. package/dist/ktui.js +11061 -10799
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +33 -27
  5. package/lib/cjs/components/collapse/collapse.js +0 -2
  6. package/lib/cjs/components/collapse/collapse.js.map +1 -1
  7. package/lib/cjs/components/component.js +11 -0
  8. package/lib/cjs/components/component.js.map +1 -1
  9. package/lib/cjs/components/datatable/datatable-sort.js +80 -11
  10. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  11. package/lib/cjs/components/datatable/datatable.js +77 -24
  12. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  13. package/lib/cjs/components/drawer/drawer.js +63 -42
  14. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  15. package/lib/cjs/components/dropdown/dropdown.js +6 -0
  16. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  17. package/lib/cjs/components/scrollto/scrollto.js +0 -2
  18. package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
  19. package/lib/cjs/components/select/combobox.js.map +1 -1
  20. package/lib/cjs/components/select/dropdown.js.map +1 -1
  21. package/lib/cjs/components/select/remote.js.map +1 -1
  22. package/lib/cjs/components/select/search.js +9 -5
  23. package/lib/cjs/components/select/search.js.map +1 -1
  24. package/lib/cjs/components/select/select.js +29 -9
  25. package/lib/cjs/components/select/select.js.map +1 -1
  26. package/lib/cjs/components/select/tags.js.map +1 -1
  27. package/lib/cjs/components/select/templates.js.map +1 -1
  28. package/lib/cjs/components/select/utils.js +10 -0
  29. package/lib/cjs/components/select/utils.js.map +1 -1
  30. package/lib/cjs/components/sticky/sticky.js +104 -24
  31. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  32. package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
  33. package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
  34. package/lib/cjs/components/toast/toast.js +1 -2
  35. package/lib/cjs/components/toast/toast.js.map +1 -1
  36. package/lib/cjs/helpers/dom.js +0 -2
  37. package/lib/cjs/helpers/dom.js.map +1 -1
  38. package/lib/esm/components/collapse/collapse.js +0 -2
  39. package/lib/esm/components/collapse/collapse.js.map +1 -1
  40. package/lib/esm/components/component.js +11 -0
  41. package/lib/esm/components/component.js.map +1 -1
  42. package/lib/esm/components/datatable/datatable-sort.js +80 -11
  43. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  44. package/lib/esm/components/datatable/datatable.js +77 -24
  45. package/lib/esm/components/datatable/datatable.js.map +1 -1
  46. package/lib/esm/components/drawer/drawer.js +63 -42
  47. package/lib/esm/components/drawer/drawer.js.map +1 -1
  48. package/lib/esm/components/dropdown/dropdown.js +6 -0
  49. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  50. package/lib/esm/components/scrollto/scrollto.js +0 -2
  51. package/lib/esm/components/scrollto/scrollto.js.map +1 -1
  52. package/lib/esm/components/select/combobox.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js.map +1 -1
  54. package/lib/esm/components/select/remote.js.map +1 -1
  55. package/lib/esm/components/select/search.js +9 -5
  56. package/lib/esm/components/select/search.js.map +1 -1
  57. package/lib/esm/components/select/select.js +29 -9
  58. package/lib/esm/components/select/select.js.map +1 -1
  59. package/lib/esm/components/select/tags.js.map +1 -1
  60. package/lib/esm/components/select/templates.js.map +1 -1
  61. package/lib/esm/components/select/utils.js +10 -0
  62. package/lib/esm/components/select/utils.js.map +1 -1
  63. package/lib/esm/components/sticky/sticky.js +104 -24
  64. package/lib/esm/components/sticky/sticky.js.map +1 -1
  65. package/lib/esm/components/theme-switch/theme-switch.js +0 -2
  66. package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
  67. package/lib/esm/components/toast/toast.js +1 -2
  68. package/lib/esm/components/toast/toast.js.map +1 -1
  69. package/lib/esm/helpers/dom.js +0 -2
  70. package/lib/esm/helpers/dom.js.map +1 -1
  71. package/package.json +14 -7
  72. package/src/components/collapse/collapse.ts +0 -3
  73. package/src/components/component.ts +14 -4
  74. package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
  75. package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
  76. package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
  77. package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
  78. package/src/components/datatable/__tests__/setup.ts +9 -4
  79. package/src/components/datatable/datatable-sort.ts +88 -10
  80. package/src/components/datatable/datatable.css +4 -4
  81. package/src/components/datatable/datatable.ts +91 -30
  82. package/src/components/datatable/types.ts +16 -0
  83. package/src/components/drawer/drawer.ts +97 -57
  84. package/src/components/drawer/types.ts +4 -2
  85. package/src/components/dropdown/dropdown.ts +8 -1
  86. package/src/components/scrollto/scrollto.ts +0 -3
  87. package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
  88. package/src/components/select/combobox.ts +0 -1
  89. package/src/components/select/dropdown.ts +0 -2
  90. package/src/components/select/remote.ts +1 -6
  91. package/src/components/select/search.ts +14 -7
  92. package/src/components/select/select.ts +29 -29
  93. package/src/components/select/tags.ts +0 -1
  94. package/src/components/select/templates.ts +8 -8
  95. package/src/components/select/utils.ts +15 -2
  96. package/src/components/sticky/__tests__/sticky.test.ts +205 -0
  97. package/src/components/sticky/sticky.ts +119 -21
  98. package/src/components/sticky/types.ts +3 -0
  99. package/src/components/theme-switch/theme-switch.ts +0 -3
  100. package/src/components/toast/toast.ts +3 -2
  101. 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
- // If drawer needs to be in front of backdrop, ensure it's in body (for proper z-index stacking)
95
- // This ensures the drawer and backdrop are in the same stacking context
96
- if (this._getOption('container') === 'body' && this._element.parentElement !== document.body) {
97
- // Store original parent for restoration when hiding
98
- if (!this._element.hasAttribute('data-kt-drawer-original-parent-id')) {
99
- const originalParent = this._element.parentElement;
100
- if (originalParent && originalParent !== document.body) {
101
- this._element.setAttribute('data-kt-drawer-original-parent-id', originalParent.id || '');
102
- // Store a reference to find the parent later (using closest to find Livewire component or header)
103
- const livewireComponent = originalParent.closest('[wire\\:id]');
104
- const header = originalParent.closest('header#header');
105
- if (livewireComponent) {
106
- this._element.setAttribute('data-kt-drawer-original-wire-id', (livewireComponent as HTMLElement).getAttribute('wire:id') || '');
107
- }
108
- if (header) {
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
- // Check if drawer is in a persisted Livewire component (like header with @persist)
213
- // If so, don't move it to body - keep it in place so Livewire can preserve it
214
- // This follows the same pattern as dropdowns/menus which work with wire:navigate
215
- const originalParent = this._element.parentNode;
216
- const isInPersistedComponent = originalParent &&
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
- document.body.append(this._backdropElement);
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('[data-kt-drawer]') as HTMLElement;
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(selector: string, timeout: number = 2000): Promise<HTMLElement | null> {
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 = document.querySelector(selector) || document.body.querySelector(selector);
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 = document.querySelector(selector) || document.body.querySelector(selector);
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 = document.querySelector(selector) || document.body.querySelector(selector);
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('click', (rawEvent: MouseEvent) => {
488
- const target = rawEvent.target as HTMLElement;
489
- if (target && target.hasAttribute('data-kt-drawer-toggle')) {
490
- }
491
- }, true); // Use capture phase to catch before any stopPropagation
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 = document.querySelector('[wire\\:id]')?.closest('[wire\\:id]') || document.querySelector('header#header');
512
- const drawerInPersisted = persistedHeader ? persistedHeader.querySelector(selector) : null;
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 = document.querySelector(selector) || document.body.querySelector(selector);
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(drawerAfterReinit as HTMLElement);
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([...Array.from(elementsInDoc), ...Array.from(elementsInBody)]);
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([...Array.from(elementsInDoc), ...Array.from(elementsInBody)]);
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('[data-kt-dropdown]') as HTMLElement;
394
+ const dropdownContainer = reference.closest(
395
+ '[data-kt-dropdown]',
396
+ ) as HTMLElement;
390
397
  if (dropdownContainer) return dropdownContainer;
391
398
  }
392
399
 
@@ -3,9 +3,6 @@
3
3
  * Copyright 2025 by Keenthemes Inc
4
4
  */
5
5
 
6
- /* eslint-disable max-len */
7
- /* eslint-disable require-jsdoc */
8
-
9
6
  import KTData from '../../helpers/data';
10
7
  import KTDom from '../../helpers/dom';
11
8
  import KTComponent from '../component';
@@ -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('[data-kt-select-option][data-value="1"]') as HTMLElement;
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
+ });
@@ -53,7 +53,6 @@ export class KTSelectCombobox {
53
53
  this._toggleClearButtonVisibility(this._searchInputElement.value);
54
54
  // this._select.showAllOptions(); // showAllOptions might be too broad, filtering is managed by typing.
55
55
  });
56
-
57
56
  }
58
57
 
59
58
  /**