@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.
Files changed (98) hide show
  1. package/dist/ktui.js +11232 -11095
  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 +3 -1
  8. package/lib/cjs/components/component.js.map +1 -1
  9. package/lib/cjs/components/datatable/datatable-sort.js +1 -2
  10. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  11. package/lib/cjs/components/datatable/datatable.js +45 -23
  12. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  13. package/lib/cjs/components/drawer/drawer.js +21 -9
  14. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  15. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  16. package/lib/cjs/components/scrollto/scrollto.js +0 -2
  17. package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
  18. package/lib/cjs/components/select/combobox.js.map +1 -1
  19. package/lib/cjs/components/select/dropdown.js.map +1 -1
  20. package/lib/cjs/components/select/remote.js.map +1 -1
  21. package/lib/cjs/components/select/search.js +9 -5
  22. package/lib/cjs/components/select/search.js.map +1 -1
  23. package/lib/cjs/components/select/select.js +22 -5
  24. package/lib/cjs/components/select/select.js.map +1 -1
  25. package/lib/cjs/components/select/tags.js.map +1 -1
  26. package/lib/cjs/components/select/templates.js.map +1 -1
  27. package/lib/cjs/components/select/utils.js +10 -0
  28. package/lib/cjs/components/select/utils.js.map +1 -1
  29. package/lib/cjs/components/sticky/sticky.js +104 -24
  30. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  31. package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
  32. package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
  33. package/lib/cjs/components/toast/toast.js +1 -2
  34. package/lib/cjs/components/toast/toast.js.map +1 -1
  35. package/lib/cjs/helpers/dom.js +0 -2
  36. package/lib/cjs/helpers/dom.js.map +1 -1
  37. package/lib/esm/components/collapse/collapse.js +0 -2
  38. package/lib/esm/components/collapse/collapse.js.map +1 -1
  39. package/lib/esm/components/component.js +3 -1
  40. package/lib/esm/components/component.js.map +1 -1
  41. package/lib/esm/components/datatable/datatable-sort.js +1 -2
  42. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  43. package/lib/esm/components/datatable/datatable.js +45 -23
  44. package/lib/esm/components/datatable/datatable.js.map +1 -1
  45. package/lib/esm/components/drawer/drawer.js +21 -9
  46. package/lib/esm/components/drawer/drawer.js.map +1 -1
  47. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  48. package/lib/esm/components/scrollto/scrollto.js +0 -2
  49. package/lib/esm/components/scrollto/scrollto.js.map +1 -1
  50. package/lib/esm/components/select/combobox.js.map +1 -1
  51. package/lib/esm/components/select/dropdown.js.map +1 -1
  52. package/lib/esm/components/select/remote.js.map +1 -1
  53. package/lib/esm/components/select/search.js +9 -5
  54. package/lib/esm/components/select/search.js.map +1 -1
  55. package/lib/esm/components/select/select.js +22 -5
  56. package/lib/esm/components/select/select.js.map +1 -1
  57. package/lib/esm/components/select/tags.js.map +1 -1
  58. package/lib/esm/components/select/templates.js.map +1 -1
  59. package/lib/esm/components/select/utils.js +10 -0
  60. package/lib/esm/components/select/utils.js.map +1 -1
  61. package/lib/esm/components/sticky/sticky.js +104 -24
  62. package/lib/esm/components/sticky/sticky.js.map +1 -1
  63. package/lib/esm/components/theme-switch/theme-switch.js +0 -2
  64. package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
  65. package/lib/esm/components/toast/toast.js +1 -2
  66. package/lib/esm/components/toast/toast.js.map +1 -1
  67. package/lib/esm/helpers/dom.js +0 -2
  68. package/lib/esm/helpers/dom.js.map +1 -1
  69. package/package.json +14 -7
  70. package/src/components/collapse/collapse.ts +0 -3
  71. package/src/components/component.ts +5 -5
  72. package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
  73. package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
  74. package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
  75. package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
  76. package/src/components/datatable/__tests__/setup.ts +9 -4
  77. package/src/components/datatable/datatable-sort.ts +12 -16
  78. package/src/components/datatable/datatable.css +4 -4
  79. package/src/components/datatable/datatable.ts +56 -26
  80. package/src/components/datatable/types.ts +3 -1
  81. package/src/components/drawer/drawer.ts +61 -24
  82. package/src/components/dropdown/dropdown.ts +3 -1
  83. package/src/components/scrollto/scrollto.ts +0 -3
  84. package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
  85. package/src/components/select/combobox.ts +0 -1
  86. package/src/components/select/dropdown.ts +0 -2
  87. package/src/components/select/remote.ts +1 -6
  88. package/src/components/select/search.ts +14 -7
  89. package/src/components/select/select.ts +29 -29
  90. package/src/components/select/tags.ts +0 -1
  91. package/src/components/select/templates.ts +8 -8
  92. package/src/components/select/utils.ts +15 -2
  93. package/src/components/sticky/__tests__/sticky.test.ts +205 -0
  94. package/src/components/sticky/sticky.ts +119 -21
  95. package/src/components/sticky/types.ts +3 -0
  96. package/src/components/theme-switch/theme-switch.ts +0 -3
  97. package/src/components/toast/toast.ts +3 -2
  98. 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 (this._getOption('container') === 'body' && this._element.parentElement !== document.body) {
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('data-kt-drawer-original-parent-id', originalParent.id || '');
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 (!this._element.style.position || this._element.style.position === 'static') {
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.split(',').map((s) => s.trim()).filter(Boolean);
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('[data-kt-drawer]') as HTMLElement;
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(selector: string, timeout: number = 2000): Promise<HTMLElement | null> {
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 = document.querySelector(selector) || document.body.querySelector(selector);
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 = document.querySelector(selector) || document.body.querySelector(selector);
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 = document.querySelector(selector) || document.body.querySelector(selector);
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('click', (rawEvent: MouseEvent) => {
491
- const target = rawEvent.target as HTMLElement;
492
- if (target && target.hasAttribute('data-kt-drawer-toggle')) {
493
- }
494
- }, 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
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 = document.querySelector('[wire\\:id]')?.closest('[wire\\:id]') || document.querySelector('header#header');
515
- 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;
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 = document.querySelector(selector) || document.body.querySelector(selector);
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(drawerAfterReinit as HTMLElement);
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([...Array.from(elementsInDoc), ...Array.from(elementsInBody)]);
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([...Array.from(elementsInDoc), ...Array.from(elementsInBody)]);
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('[data-kt-dropdown]') as HTMLElement;
394
+ const dropdownContainer = reference.closest(
395
+ '[data-kt-dropdown]',
396
+ ) as HTMLElement;
395
397
  if (dropdownContainer) return dropdownContainer;
396
398
  }
397
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
  /**
@@ -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
- let url = this._buildUrl(query, page);
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 = document.activeElement === this._searchInput || this._searchInput === document.activeElement;
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
- const firstAvailableOption = this._focusManager.focusFirst();
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 (firstAvailableOption) {
247
- const optionValue = firstAvailableOption.getAttribute('data-value');
250
+ if (optionToSelect) {
251
+ const optionValue = optionToSelect.getAttribute('data-value');
248
252
  if (optionValue) {
249
253
  const config = this._select.getConfig();
250
- const isAlreadySelected = !config.multiple && this._select.getSelectedOptions().includes(optionValue);
251
- const shouldClose = !config.multiple && config.closeOnEnter !== false;
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();