@keenthemes/ktui 1.1.0 → 1.1.1

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 (222) hide show
  1. package/README.md +0 -27
  2. package/dist/ktui.js +6790 -14063
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +1132 -2705
  6. package/lib/cjs/components/datatable/__tests__/pagination-reset.test.js +596 -0
  7. package/lib/cjs/components/datatable/__tests__/pagination-reset.test.js.map +1 -0
  8. package/lib/cjs/components/datatable/__tests__/race-conditions.test.js +548 -0
  9. package/lib/cjs/components/datatable/__tests__/race-conditions.test.js.map +1 -0
  10. package/lib/cjs/components/datatable/__tests__/setup.js +63 -0
  11. package/lib/cjs/components/datatable/__tests__/setup.js.map +1 -0
  12. package/lib/cjs/components/datatable/datatable.js +92 -30
  13. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  14. package/lib/cjs/index.js +1 -10
  15. package/lib/cjs/index.js.map +1 -1
  16. package/lib/esm/components/datatable/__tests__/pagination-reset.test.js +594 -0
  17. package/lib/esm/components/datatable/__tests__/pagination-reset.test.js.map +1 -0
  18. package/lib/esm/components/datatable/__tests__/race-conditions.test.js +546 -0
  19. package/lib/esm/components/datatable/__tests__/race-conditions.test.js.map +1 -0
  20. package/lib/esm/components/datatable/__tests__/setup.js +58 -0
  21. package/lib/esm/components/datatable/__tests__/setup.js.map +1 -0
  22. package/lib/esm/components/datatable/datatable.js +92 -30
  23. package/lib/esm/components/datatable/datatable.js.map +1 -1
  24. package/lib/esm/index.js +0 -7
  25. package/lib/esm/index.js.map +1 -1
  26. package/package.json +7 -9
  27. package/src/components/alert/alert.css +188 -429
  28. package/src/components/datatable/__tests__/pagination-reset.test.ts +657 -0
  29. package/src/components/datatable/__tests__/race-conditions.test.ts +455 -0
  30. package/src/components/datatable/__tests__/setup.ts +67 -0
  31. package/src/components/datatable/datatable.ts +66 -11
  32. package/src/components/input/input.css +0 -1
  33. package/src/components/select/select.css +0 -1
  34. package/src/components/select/variants.css +4 -0
  35. package/src/components/textarea/textarea.css +0 -1
  36. package/src/index.ts +0 -10
  37. package/styles.css +0 -1
  38. package/lib/cjs/components/alert/alert.js +0 -1025
  39. package/lib/cjs/components/alert/alert.js.map +0 -1
  40. package/lib/cjs/components/alert/index.js +0 -20
  41. package/lib/cjs/components/alert/index.js.map +0 -1
  42. package/lib/cjs/components/alert/templates.js +0 -120
  43. package/lib/cjs/components/alert/templates.js.map +0 -1
  44. package/lib/cjs/components/alert/types.js +0 -7
  45. package/lib/cjs/components/alert/types.js.map +0 -1
  46. package/lib/cjs/components/datepicker/config/config.js +0 -42
  47. package/lib/cjs/components/datepicker/config/config.js.map +0 -1
  48. package/lib/cjs/components/datepicker/config/index.js +0 -24
  49. package/lib/cjs/components/datepicker/config/index.js.map +0 -1
  50. package/lib/cjs/components/datepicker/config/interfaces.js +0 -7
  51. package/lib/cjs/components/datepicker/config/interfaces.js.map +0 -1
  52. package/lib/cjs/components/datepicker/config/types.js +0 -7
  53. package/lib/cjs/components/datepicker/config/types.js.map +0 -1
  54. package/lib/cjs/components/datepicker/core/event-manager.js +0 -135
  55. package/lib/cjs/components/datepicker/core/event-manager.js.map +0 -1
  56. package/lib/cjs/components/datepicker/core/focus-manager.js +0 -167
  57. package/lib/cjs/components/datepicker/core/focus-manager.js.map +0 -1
  58. package/lib/cjs/components/datepicker/core/helpers.js +0 -219
  59. package/lib/cjs/components/datepicker/core/helpers.js.map +0 -1
  60. package/lib/cjs/components/datepicker/core/index.js +0 -25
  61. package/lib/cjs/components/datepicker/core/index.js.map +0 -1
  62. package/lib/cjs/components/datepicker/core/unified-state-manager.js +0 -394
  63. package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +0 -1
  64. package/lib/cjs/components/datepicker/datepicker.js +0 -2252
  65. package/lib/cjs/components/datepicker/datepicker.js.map +0 -1
  66. package/lib/cjs/components/datepicker/index.js +0 -24
  67. package/lib/cjs/components/datepicker/index.js.map +0 -1
  68. package/lib/cjs/components/datepicker/ui/index.js +0 -23
  69. package/lib/cjs/components/datepicker/ui/index.js.map +0 -1
  70. package/lib/cjs/components/datepicker/ui/input/dropdown.js +0 -489
  71. package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +0 -1
  72. package/lib/cjs/components/datepicker/ui/input/index.js +0 -23
  73. package/lib/cjs/components/datepicker/ui/input/index.js.map +0 -1
  74. package/lib/cjs/components/datepicker/ui/input/segmented-input.js +0 -640
  75. package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +0 -1
  76. package/lib/cjs/components/datepicker/ui/renderers/calendar.js +0 -446
  77. package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +0 -1
  78. package/lib/cjs/components/datepicker/ui/renderers/footer.js +0 -42
  79. package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +0 -1
  80. package/lib/cjs/components/datepicker/ui/renderers/header.js +0 -32
  81. package/lib/cjs/components/datepicker/ui/renderers/header.js.map +0 -1
  82. package/lib/cjs/components/datepicker/ui/renderers/index.js +0 -25
  83. package/lib/cjs/components/datepicker/ui/renderers/index.js.map +0 -1
  84. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +0 -384
  85. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +0 -1
  86. package/lib/cjs/components/datepicker/ui/templates/index.js +0 -22
  87. package/lib/cjs/components/datepicker/ui/templates/index.js.map +0 -1
  88. package/lib/cjs/components/datepicker/ui/templates/templates.js +0 -253
  89. package/lib/cjs/components/datepicker/ui/templates/templates.js.map +0 -1
  90. package/lib/cjs/components/datepicker/utils/date-formatters.js +0 -88
  91. package/lib/cjs/components/datepicker/utils/date-formatters.js.map +0 -1
  92. package/lib/cjs/components/datepicker/utils/date-utils.js +0 -194
  93. package/lib/cjs/components/datepicker/utils/date-utils.js.map +0 -1
  94. package/lib/cjs/components/datepicker/utils/index.js +0 -24
  95. package/lib/cjs/components/datepicker/utils/index.js.map +0 -1
  96. package/lib/cjs/components/datepicker/utils/time-utils.js +0 -213
  97. package/lib/cjs/components/datepicker/utils/time-utils.js.map +0 -1
  98. package/lib/esm/components/alert/alert.js +0 -1022
  99. package/lib/esm/components/alert/alert.js.map +0 -1
  100. package/lib/esm/components/alert/index.js +0 -4
  101. package/lib/esm/components/alert/index.js.map +0 -1
  102. package/lib/esm/components/alert/templates.js +0 -112
  103. package/lib/esm/components/alert/templates.js.map +0 -1
  104. package/lib/esm/components/alert/types.js +0 -6
  105. package/lib/esm/components/alert/types.js.map +0 -1
  106. package/lib/esm/components/datepicker/config/config.js +0 -39
  107. package/lib/esm/components/datepicker/config/config.js.map +0 -1
  108. package/lib/esm/components/datepicker/config/index.js +0 -8
  109. package/lib/esm/components/datepicker/config/index.js.map +0 -1
  110. package/lib/esm/components/datepicker/config/interfaces.js +0 -6
  111. package/lib/esm/components/datepicker/config/interfaces.js.map +0 -1
  112. package/lib/esm/components/datepicker/config/types.js +0 -6
  113. package/lib/esm/components/datepicker/config/types.js.map +0 -1
  114. package/lib/esm/components/datepicker/core/event-manager.js +0 -133
  115. package/lib/esm/components/datepicker/core/event-manager.js.map +0 -1
  116. package/lib/esm/components/datepicker/core/focus-manager.js +0 -164
  117. package/lib/esm/components/datepicker/core/focus-manager.js.map +0 -1
  118. package/lib/esm/components/datepicker/core/helpers.js +0 -211
  119. package/lib/esm/components/datepicker/core/helpers.js.map +0 -1
  120. package/lib/esm/components/datepicker/core/index.js +0 -9
  121. package/lib/esm/components/datepicker/core/index.js.map +0 -1
  122. package/lib/esm/components/datepicker/core/unified-state-manager.js +0 -391
  123. package/lib/esm/components/datepicker/core/unified-state-manager.js.map +0 -1
  124. package/lib/esm/components/datepicker/datepicker.js +0 -2248
  125. package/lib/esm/components/datepicker/datepicker.js.map +0 -1
  126. package/lib/esm/components/datepicker/index.js +0 -7
  127. package/lib/esm/components/datepicker/index.js.map +0 -1
  128. package/lib/esm/components/datepicker/ui/index.js +0 -7
  129. package/lib/esm/components/datepicker/ui/index.js.map +0 -1
  130. package/lib/esm/components/datepicker/ui/input/dropdown.js +0 -486
  131. package/lib/esm/components/datepicker/ui/input/dropdown.js.map +0 -1
  132. package/lib/esm/components/datepicker/ui/input/index.js +0 -7
  133. package/lib/esm/components/datepicker/ui/input/index.js.map +0 -1
  134. package/lib/esm/components/datepicker/ui/input/segmented-input.js +0 -637
  135. package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +0 -1
  136. package/lib/esm/components/datepicker/ui/renderers/calendar.js +0 -443
  137. package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +0 -1
  138. package/lib/esm/components/datepicker/ui/renderers/footer.js +0 -39
  139. package/lib/esm/components/datepicker/ui/renderers/footer.js.map +0 -1
  140. package/lib/esm/components/datepicker/ui/renderers/header.js +0 -29
  141. package/lib/esm/components/datepicker/ui/renderers/header.js.map +0 -1
  142. package/lib/esm/components/datepicker/ui/renderers/index.js +0 -9
  143. package/lib/esm/components/datepicker/ui/renderers/index.js.map +0 -1
  144. package/lib/esm/components/datepicker/ui/renderers/time-picker.js +0 -381
  145. package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +0 -1
  146. package/lib/esm/components/datepicker/ui/templates/index.js +0 -6
  147. package/lib/esm/components/datepicker/ui/templates/index.js.map +0 -1
  148. package/lib/esm/components/datepicker/ui/templates/templates.js +0 -242
  149. package/lib/esm/components/datepicker/ui/templates/templates.js.map +0 -1
  150. package/lib/esm/components/datepicker/utils/date-formatters.js +0 -83
  151. package/lib/esm/components/datepicker/utils/date-formatters.js.map +0 -1
  152. package/lib/esm/components/datepicker/utils/date-utils.js +0 -184
  153. package/lib/esm/components/datepicker/utils/date-utils.js.map +0 -1
  154. package/lib/esm/components/datepicker/utils/index.js +0 -8
  155. package/lib/esm/components/datepicker/utils/index.js.map +0 -1
  156. package/lib/esm/components/datepicker/utils/time-utils.js +0 -201
  157. package/lib/esm/components/datepicker/utils/time-utils.js.map +0 -1
  158. package/src/components/alert/alert.ts +0 -990
  159. package/src/components/alert/index.ts +0 -4
  160. package/src/components/alert/templates.ts +0 -110
  161. package/src/components/alert/tests/accessibility/aria-roles.test.ts +0 -19
  162. package/src/components/alert/tests/accessibility/focus-management.test.ts +0 -19
  163. package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +0 -22
  164. package/src/components/alert/tests/actions/confirm-cancel.test.ts +0 -122
  165. package/src/components/alert/tests/actions/input-field.test.ts +0 -180
  166. package/src/components/alert/tests/alert.basic.test.ts +0 -126
  167. package/src/components/alert/tests/alert.config.test.ts +0 -75
  168. package/src/components/alert/tests/alert.templates.test.ts +0 -17
  169. package/src/components/alert/tests/config/attribute-config.test.ts +0 -94
  170. package/src/components/alert/tests/config/json-config.test.ts +0 -119
  171. package/src/components/alert/tests/config/merging.test.ts +0 -89
  172. package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +0 -96
  173. package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +0 -105
  174. package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +0 -90
  175. package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +0 -91
  176. package/src/components/alert/tests/edge-cases/invalid-config.test.ts +0 -19
  177. package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +0 -19
  178. package/src/components/alert/tests/rendering/custom-content.test.ts +0 -81
  179. package/src/components/alert/tests/rendering/info-alert.test.ts +0 -84
  180. package/src/components/alert/tests/rendering/success-alert.test.ts +0 -100
  181. package/src/components/alert/tests/templates/default-templates.test.ts +0 -16
  182. package/src/components/alert/tests/templates/user-templates.test.ts +0 -16
  183. package/src/components/alert/types.ts +0 -145
  184. package/src/components/datepicker/__tests__/datepicker-events.test.ts +0 -356
  185. package/src/components/datepicker/__tests__/datepicker-init.test.ts +0 -343
  186. package/src/components/datepicker/__tests__/datepicker-integration.test.ts +0 -435
  187. package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +0 -220
  188. package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +0 -380
  189. package/src/components/datepicker/__tests__/selective-state-updates.test.ts +0 -400
  190. package/src/components/datepicker/__tests__/state-manager.test.ts +0 -421
  191. package/src/components/datepicker/__tests__/time-preservation.test.ts +0 -387
  192. package/src/components/datepicker/config/config.ts +0 -40
  193. package/src/components/datepicker/config/index.ts +0 -8
  194. package/src/components/datepicker/config/interfaces.ts +0 -82
  195. package/src/components/datepicker/config/types.ts +0 -188
  196. package/src/components/datepicker/core/event-manager.ts +0 -159
  197. package/src/components/datepicker/core/focus-manager.ts +0 -201
  198. package/src/components/datepicker/core/helpers.ts +0 -231
  199. package/src/components/datepicker/core/index.ts +0 -9
  200. package/src/components/datepicker/core/unified-state-manager.ts +0 -459
  201. package/src/components/datepicker/datepicker.css +0 -435
  202. package/src/components/datepicker/datepicker.ts +0 -2548
  203. package/src/components/datepicker/index.ts +0 -8
  204. package/src/components/datepicker/ui/index.ts +0 -7
  205. package/src/components/datepicker/ui/input/dropdown.ts +0 -552
  206. package/src/components/datepicker/ui/input/index.ts +0 -7
  207. package/src/components/datepicker/ui/input/segmented-input.ts +0 -638
  208. package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +0 -611
  209. package/src/components/datepicker/ui/renderers/calendar.ts +0 -530
  210. package/src/components/datepicker/ui/renderers/footer.ts +0 -43
  211. package/src/components/datepicker/ui/renderers/header.ts +0 -33
  212. package/src/components/datepicker/ui/renderers/index.ts +0 -9
  213. package/src/components/datepicker/ui/renderers/time-picker.ts +0 -438
  214. package/src/components/datepicker/ui/templates/index.ts +0 -6
  215. package/src/components/datepicker/ui/templates/templates.ts +0 -306
  216. package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +0 -160
  217. package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +0 -86
  218. package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +0 -215
  219. package/src/components/datepicker/utils/date-formatters.ts +0 -85
  220. package/src/components/datepicker/utils/date-utils.ts +0 -172
  221. package/src/components/datepicker/utils/index.ts +0 -8
  222. package/src/components/datepicker/utils/time-utils.ts +0 -221
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Race Condition Tests for KTDataTable
3
+ * Tests the fixes for concurrent request handling, request cancellation, and stale response detection
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
+ import { KTDataTable } from '../datatable';
8
+ import { waitFor } from './setup';
9
+
10
+ describe('KTDataTable Race Condition Fixes', () => {
11
+ let container: HTMLElement;
12
+ let mockFetch: ReturnType<typeof vi.fn>;
13
+ let abortSignals: AbortSignal[] = [];
14
+
15
+ beforeEach(() => {
16
+ // Setup DOM
17
+ container = document.createElement('div');
18
+ container.innerHTML = `
19
+ <div data-kt-datatable="true">
20
+ <table data-kt-datatable-table="true">
21
+ <thead>
22
+ <tr>
23
+ <th data-kt-datatable-column="id">ID</th>
24
+ <th data-kt-datatable-column="name">Name</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody></tbody>
28
+ </table>
29
+ <div data-kt-datatable-info="true"></div>
30
+ <select data-kt-datatable-size="true"></select>
31
+ <div data-kt-datatable-pagination="true"></div>
32
+ </div>
33
+ `;
34
+ document.body.appendChild(container);
35
+
36
+ // Mock fetch to track requests and signals
37
+ abortSignals = [];
38
+ mockFetch = vi.fn((url, options) => {
39
+ // Store abort signal for verification
40
+ if (options?.signal) {
41
+ abortSignals.push(options.signal);
42
+ }
43
+
44
+ // Simulate network delay
45
+ return new Promise((resolve, reject) => {
46
+ const timeout = setTimeout(() => {
47
+ if (options?.signal?.aborted) {
48
+ reject(new DOMException('The operation was aborted.', 'AbortError'));
49
+ } else {
50
+ resolve(
51
+ new Response(
52
+ JSON.stringify({
53
+ data: [
54
+ { id: 1, name: 'Item 1' },
55
+ { id: 2, name: 'Item 2' },
56
+ ],
57
+ totalCount: 2,
58
+ }),
59
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
60
+ ),
61
+ );
62
+ }
63
+ }, 100); // 100ms delay
64
+
65
+ // Handle abort
66
+ if (options?.signal) {
67
+ options.signal.addEventListener('abort', () => {
68
+ clearTimeout(timeout);
69
+ reject(new DOMException('The operation was aborted.', 'AbortError'));
70
+ });
71
+ }
72
+ });
73
+ });
74
+
75
+ global.fetch = mockFetch;
76
+ });
77
+
78
+ afterEach(() => {
79
+ document.body.removeChild(container);
80
+ vi.clearAllMocks();
81
+ abortSignals = [];
82
+ });
83
+
84
+ describe('AbortController Integration', () => {
85
+ it('should create AbortController for remote data requests', async () => {
86
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
87
+ apiEndpoint: '/api/data',
88
+ });
89
+
90
+ await waitFor(150);
91
+
92
+ expect(mockFetch).toHaveBeenCalledTimes(1);
93
+ expect(abortSignals.length).toBe(1);
94
+ expect(abortSignals[0]).toBeInstanceOf(AbortSignal);
95
+ });
96
+
97
+ it('should use _isFetching flag to prevent concurrent requests', async () => {
98
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
99
+ apiEndpoint: '/api/data',
100
+ });
101
+
102
+ // Try to trigger search during initial fetch
103
+ datatable.search('test'); // Should be blocked by _isFetching
104
+
105
+ await waitFor(150);
106
+
107
+ // Should only have 1 request (initial) because _isFetching blocked the second
108
+ expect(mockFetch).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ it('should allow new request after previous completes', async () => {
112
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
113
+ apiEndpoint: '/api/data',
114
+ });
115
+
116
+ // Wait for initial fetch to complete
117
+ await waitFor(150);
118
+
119
+ // Now trigger a search - should succeed
120
+ datatable.search('test');
121
+ await waitFor(150);
122
+
123
+ // Should have 2 requests total
124
+ expect(mockFetch).toHaveBeenCalledTimes(2);
125
+ expect(abortSignals.length).toBe(2);
126
+ });
127
+
128
+ it('should abort previous request when _performFetchRequest is called again', async () => {
129
+ // This tests the AbortController logic directly by making multiple sequential requests
130
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
131
+ apiEndpoint: '/api/data',
132
+ });
133
+
134
+ await waitFor(150); // Complete initial request
135
+
136
+ // Trigger first search
137
+ datatable.search('first');
138
+ await waitFor(150);
139
+
140
+ // Trigger second search (first should complete, second starts fresh)
141
+ datatable.search('second');
142
+ await waitFor(150);
143
+
144
+ // Should have 3 requests: initial + first search + second search
145
+ expect(mockFetch).toHaveBeenCalledTimes(3);
146
+ expect(abortSignals.length).toBe(3);
147
+
148
+ // Each gets its own AbortController
149
+ abortSignals.forEach((signal) => {
150
+ expect(signal).toBeInstanceOf(AbortSignal);
151
+ });
152
+ });
153
+ });
154
+
155
+ describe('Request ID Sequencing', () => {
156
+ it('should assign incremental request IDs for sequential requests', async () => {
157
+ let requestIds: number[] = [];
158
+ let callCount = 0;
159
+
160
+ // Mock to capture request sequence
161
+ mockFetch.mockImplementation((url, options) => {
162
+ callCount++;
163
+ const id = callCount;
164
+ requestIds.push(id);
165
+
166
+ return new Promise((resolve) => {
167
+ setTimeout(() => {
168
+ resolve(
169
+ new Response(
170
+ JSON.stringify({
171
+ data: [{ id: id, name: `Item ${id}` }],
172
+ totalCount: 1,
173
+ }),
174
+ { status: 200 },
175
+ ),
176
+ );
177
+ }, 50);
178
+ });
179
+ });
180
+
181
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
182
+ apiEndpoint: '/api/data',
183
+ });
184
+
185
+ await waitFor(100); // Complete initial request
186
+
187
+ datatable.search('a');
188
+ await waitFor(100); // Complete search
189
+
190
+ datatable.search('b');
191
+ await waitFor(100); // Complete second search
192
+
193
+ // Request IDs should be sequential: 1, 2, 3
194
+ expect(requestIds).toEqual([1, 2, 3]);
195
+ });
196
+
197
+ it('should have request ID validation logic in place', async () => {
198
+ // This tests that request IDs are tracked internally
199
+ // The actual stale response scenario is prevented by _isFetching flag
200
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
201
+ apiEndpoint: '/api/data',
202
+ });
203
+
204
+ await waitFor(150); // Complete initial
205
+
206
+ datatable.search('first');
207
+ await waitFor(150); // Complete first search
208
+
209
+ datatable.search('second');
210
+ await waitFor(150); // Complete second search
211
+
212
+ // All requests should complete successfully with incremental request IDs
213
+ expect(mockFetch).toHaveBeenCalledTimes(3);
214
+
215
+ // Table should show data from the last successful request
216
+ const tbody = container.querySelector('tbody');
217
+ expect(tbody?.querySelectorAll('tr').length).toBeGreaterThan(0);
218
+ });
219
+ });
220
+
221
+ describe('_isFetching Flag Management', () => {
222
+ it('should prevent concurrent fetch executions', async () => {
223
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
224
+ apiEndpoint: '/api/data',
225
+ });
226
+
227
+ // Try to trigger reload immediately (should be blocked by initial fetch)
228
+ datatable.reload(); // Blocked by _isFetching
229
+ datatable.reload(); // Blocked by _isFetching
230
+
231
+ await waitFor(150);
232
+
233
+ // Should only have 1 request: initial
234
+ // The reload calls are blocked by _isFetching
235
+ expect(mockFetch).toHaveBeenCalledTimes(1);
236
+ });
237
+
238
+ it('should reset _isFetching flag after fetch completes', async () => {
239
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
240
+ apiEndpoint: '/api/data',
241
+ });
242
+
243
+ await waitFor(150); // Wait for initial fetch
244
+
245
+ // Should be able to trigger new fetch after previous completes
246
+ datatable.reload();
247
+ await waitFor(150);
248
+
249
+ expect(mockFetch).toHaveBeenCalledTimes(2);
250
+ });
251
+
252
+ it('should reset _isFetching flag even after fetch error', async () => {
253
+ let callCount = 0;
254
+ mockFetch.mockImplementation(() => {
255
+ callCount++;
256
+ if (callCount === 1) {
257
+ // Return invalid JSON to trigger parse error
258
+ return Promise.resolve(
259
+ new Response('Not JSON', { status: 200 }),
260
+ );
261
+ }
262
+ return Promise.resolve(
263
+ new Response(
264
+ JSON.stringify({
265
+ data: [{ id: 1, name: 'Success' }],
266
+ totalCount: 1,
267
+ }),
268
+ { status: 200 },
269
+ ),
270
+ );
271
+ });
272
+
273
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
274
+ apiEndpoint: '/api/data',
275
+ });
276
+
277
+ await waitFor(150); // Initial request triggers parse error
278
+
279
+ // Should be able to retry after error
280
+ datatable.reload();
281
+ await waitFor(150);
282
+
283
+ expect(mockFetch).toHaveBeenCalledTimes(2);
284
+ });
285
+ });
286
+
287
+ describe('Loading Spinner Management', () => {
288
+ it('should show spinner during fetch', async () => {
289
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
290
+ const datatable = new KTDataTable(element, {
291
+ apiEndpoint: '/api/data',
292
+ });
293
+
294
+ await waitFor(10); // Spinner should be visible
295
+
296
+ expect(element.classList.contains('loading')).toBe(true);
297
+
298
+ await waitFor(150); // Wait for fetch to complete
299
+
300
+ expect(element.classList.contains('loading')).toBe(false);
301
+ });
302
+
303
+ it('should keep spinner visible during overlapping requests', async () => {
304
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
305
+ const datatable = new KTDataTable(element, {
306
+ apiEndpoint: '/api/data',
307
+ });
308
+
309
+ await waitFor(10);
310
+ expect(element.classList.contains('loading')).toBe(true);
311
+
312
+ // Trigger second request while first is in progress
313
+ datatable.search('test');
314
+ await waitFor(10);
315
+
316
+ // Spinner should still be visible
317
+ expect(element.classList.contains('loading')).toBe(true);
318
+
319
+ await waitFor(150);
320
+
321
+ // Spinner should hide only after last request completes
322
+ expect(element.classList.contains('loading')).toBe(false);
323
+ });
324
+
325
+ it('should not flicker spinner during rapid interactions', async () => {
326
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
327
+ const datatable = new KTDataTable(element, {
328
+ apiEndpoint: '/api/data',
329
+ });
330
+
331
+ const spinnerStates: boolean[] = [];
332
+ const checkInterval = setInterval(() => {
333
+ spinnerStates.push(element.classList.contains('loading'));
334
+ }, 20);
335
+
336
+ await waitFor(10);
337
+ datatable.search('a');
338
+ await waitFor(10);
339
+ datatable.search('ab');
340
+ await waitFor(10);
341
+ datatable.search('abc');
342
+
343
+ await waitFor(150);
344
+ clearInterval(checkInterval);
345
+
346
+ // Spinner should go from false -> true -> false
347
+ // No flickering (true -> false -> true)
348
+ const transitions = spinnerStates.reduce((acc, curr, idx) => {
349
+ if (idx > 0 && spinnerStates[idx - 1] !== curr) {
350
+ acc.push(curr);
351
+ }
352
+ return acc;
353
+ }, [] as boolean[]);
354
+
355
+ // Should have exactly 2 transitions: off->on, on->off
356
+ // No additional transitions that would indicate flickering
357
+ expect(transitions.length).toBeLessThanOrEqual(2);
358
+ });
359
+ });
360
+
361
+ describe('Event Handling During Race Conditions', () => {
362
+ it('should fire fetch event for successful requests', async () => {
363
+ const fetchEvents: any[] = [];
364
+
365
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
366
+ element.addEventListener('fetch', (e) => {
367
+ fetchEvents.push(e);
368
+ });
369
+
370
+ const datatable = new KTDataTable(element, {
371
+ apiEndpoint: '/api/data',
372
+ });
373
+
374
+ await waitFor(150); // Complete initial
375
+
376
+ datatable.search('test');
377
+ await waitFor(150); // Complete search
378
+
379
+ // Should fire fetch for initial and search
380
+ expect(fetchEvents.length).toBeGreaterThanOrEqual(2);
381
+ });
382
+
383
+ it('should fire fetched event after successful data load', async () => {
384
+ const fetchedEvents: any[] = [];
385
+
386
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
387
+ element.addEventListener('fetched', (e) => {
388
+ fetchedEvents.push(e);
389
+ });
390
+
391
+ const datatable = new KTDataTable(element, {
392
+ apiEndpoint: '/api/data',
393
+ });
394
+
395
+ await waitFor(150);
396
+
397
+ // Should fire fetched for initial request
398
+ expect(fetchedEvents.length).toBeGreaterThanOrEqual(1);
399
+ });
400
+
401
+ it('should not fire error events for AbortError', async () => {
402
+ const errorEvents: any[] = [];
403
+
404
+ const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
405
+ element.addEventListener('error.kt.datatable', (e) => {
406
+ errorEvents.push(e);
407
+ });
408
+
409
+ const datatable = new KTDataTable(element, {
410
+ apiEndpoint: '/api/data',
411
+ });
412
+
413
+ await waitFor(10);
414
+ datatable.search('test'); // Cancels previous
415
+
416
+ await waitFor(150);
417
+
418
+ // AbortError should not trigger error event
419
+ expect(errorEvents.length).toBe(0);
420
+ });
421
+ });
422
+
423
+ describe('Backward Compatibility', () => {
424
+ it('should work with local data mode (no AbortController needed)', async () => {
425
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!);
426
+
427
+ // Should not call fetch for local data
428
+ expect(mockFetch).not.toHaveBeenCalled();
429
+
430
+ // Should still work without errors
431
+ datatable.search('Item 1');
432
+ datatable.sort('name');
433
+ datatable.goPage(1);
434
+ });
435
+
436
+ it('should maintain existing API compatibility', async () => {
437
+ const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
438
+ apiEndpoint: '/api/data',
439
+ });
440
+
441
+ await waitFor(150);
442
+
443
+ // All existing methods should work
444
+ expect(() => {
445
+ datatable.reload();
446
+ datatable.search('test');
447
+ datatable.sort('name');
448
+ datatable.goPage(1);
449
+ datatable.setPageSize(20);
450
+ datatable.getState();
451
+ }).not.toThrow();
452
+ });
453
+ });
454
+ });
455
+
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Vitest setup file for datatable tests
3
+ * Provides DOM environment setup and utilities
4
+ */
5
+
6
+ import { beforeEach, afterEach, vi } from 'vitest';
7
+
8
+ // Mock KTComponents to prevent auto-initialization errors
9
+ vi.mock('../../../index', () => ({
10
+ default: {
11
+ init: vi.fn(),
12
+ },
13
+ KTComponents: {
14
+ init: vi.fn(),
15
+ },
16
+ }));
17
+
18
+ // Setup DOM environment before each test
19
+ beforeEach(() => {
20
+ // Clear localStorage
21
+ localStorage.clear();
22
+
23
+ // Reset document body
24
+ document.body.innerHTML = '';
25
+
26
+ // Clear any global state
27
+ // Add any other global setup here
28
+ });
29
+
30
+ // Cleanup after each test
31
+ afterEach(() => {
32
+ // Clear localStorage
33
+ localStorage.clear();
34
+
35
+ // Reset document body
36
+ document.body.innerHTML = '';
37
+
38
+ // Clear all pending timers to prevent "document is not defined" errors
39
+ vi.clearAllTimers();
40
+ });
41
+
42
+ // Mock window.matchMedia if needed
43
+ Object.defineProperty(window, 'matchMedia', {
44
+ writable: true,
45
+ value: (query: string) => ({
46
+ matches: false,
47
+ media: query,
48
+ onchange: null as ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null,
49
+ addListener: () => {}, // deprecated
50
+ removeListener: () => {}, // deprecated
51
+ addEventListener: () => {},
52
+ removeEventListener: () => {},
53
+ dispatchEvent: () => true,
54
+ }),
55
+ });
56
+
57
+ // Export utilities that tests can use
58
+ export const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
59
+
60
+ export const createMockElement = (tag: string, attributes: Record<string, string> = {}) => {
61
+ const el = document.createElement(tag);
62
+ Object.entries(attributes).forEach(([key, value]) => {
63
+ el.setAttribute(key, value);
64
+ });
65
+ return el;
66
+ };
67
+
@@ -58,6 +58,18 @@ export class KTDataTable<T extends KTDataTableDataInterface>
58
58
  private _data: T[] = [];
59
59
  private _isFetching: boolean = false;
60
60
 
61
+ /**
62
+ * AbortController for cancelling previous fetch requests
63
+ * Used to prevent race conditions when multiple requests are triggered rapidly
64
+ */
65
+ private _abortController: AbortController | null = null;
66
+
67
+ /**
68
+ * Request ID counter for tracking request sequence
69
+ * Used to detect and ignore stale responses from older requests
70
+ */
71
+ private _requestId: number = 0;
72
+
61
73
  constructor(element: HTMLElement, config?: KTDataTableConfigInterface) {
62
74
  super();
63
75
 
@@ -448,15 +460,16 @@ export class KTDataTable<T extends KTDataTableDataInterface>
448
460
  try {
449
461
  this._showSpinner(); // Show spinner before fetching data
450
462
 
451
- // Fetch data from the DOM and initialize the checkbox plugin
452
- return typeof this._config.apiEndpoint === 'undefined'
453
- ? this._fetchDataFromLocal().then(
454
- this._finalize.bind(this) as () => Promise<void>,
455
- )
456
- : this._fetchDataFromServer().then(
457
- this._finalize.bind(this) as () => Promise<void>,
458
- );
463
+ // Fetch data and finalize - properly await to ensure finally block runs after completion
464
+ if (typeof this._config.apiEndpoint === 'undefined') {
465
+ await this._fetchDataFromLocal();
466
+ await this._finalize();
467
+ } else {
468
+ await this._fetchDataFromServer();
469
+ await this._finalize();
470
+ }
459
471
  } finally {
472
+ // Finally block now correctly executes after promises resolve, not immediately
460
473
  this._isFetching = false;
461
474
  }
462
475
  }
@@ -703,11 +716,30 @@ export class KTDataTable<T extends KTDataTableDataInterface>
703
716
  * Fetch data from the server
704
717
  */
705
718
  private async _fetchDataFromServer(): Promise<void> {
719
+ // Increment request ID to track this specific request
720
+ const currentRequestId = ++this._requestId;
721
+
706
722
  this._fireEvent('fetch');
707
723
  this._dispatchEvent('fetch');
708
724
 
709
725
  const queryParams = this._getQueryParamsForFetchRequest();
710
- const response = await this._performFetchRequest(queryParams);
726
+
727
+ let response: Response;
728
+ try {
729
+ response = await this._performFetchRequest(queryParams);
730
+ } catch (error) {
731
+ // Silently ignore AbortError - request was cancelled
732
+ if ((error as Error).name === 'AbortError') {
733
+ return;
734
+ }
735
+ throw error;
736
+ }
737
+
738
+ // Check if this response is stale (a newer request has been initiated)
739
+ if (currentRequestId !== this._requestId) {
740
+ // Ignore stale response - a more recent request is in progress or has completed
741
+ return;
742
+ }
711
743
 
712
744
  let responseData = null;
713
745
 
@@ -730,6 +762,11 @@ export class KTDataTable<T extends KTDataTableDataInterface>
730
762
  return;
731
763
  }
732
764
 
765
+ // Double-check request ID after JSON parsing (additional safety)
766
+ if (currentRequestId !== this._requestId) {
767
+ return;
768
+ }
769
+
733
770
  this._fireEvent('fetched', { response: responseData });
734
771
  this._dispatchEvent('fetched', { response: responseData });
735
772
 
@@ -809,6 +846,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
809
846
  let requestMethod: RequestInit['method'] = this._config.requestMethod;
810
847
  let requestBody: RequestInit['body'] | undefined = undefined;
811
848
 
849
+ // Cancel previous request to prevent race conditions
850
+ if (this._abortController) {
851
+ this._abortController.abort();
852
+ }
853
+
854
+ // Create new AbortController for this request
855
+ this._abortController = new AbortController();
856
+
812
857
  // If the request method is POST, send the query params as the request body
813
858
  if (requestMethod === 'POST') {
814
859
  requestBody = queryParams;
@@ -828,8 +873,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
828
873
  ...(this._config.requestCredentials && {
829
874
  credentials: this._config.requestCredentials,
830
875
  }),
876
+ // Add abort signal if available
877
+ ...(this._abortController && { signal: this._abortController.signal }),
831
878
  }).catch((error) => {
832
- // Trigger an error event
879
+ // Silently ignore AbortError - this is expected when requests are cancelled
880
+ if (error.name === 'AbortError') {
881
+ return Promise.reject(error);
882
+ }
883
+
884
+ // Trigger an error event for non-abort errors
833
885
  this._fireEvent('error', { error });
834
886
  this._dispatchEvent('error', { error });
835
887
 
@@ -897,7 +949,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
897
949
  this._fireEvent('drew');
898
950
  this._dispatchEvent('drew');
899
951
 
900
- this._hideSpinner(); // Hide spinner after data is fetched
952
+ // Spinner is hidden in _finalize() to ensure it stays visible until the entire request completes
953
+ // Removed duplicate _hideSpinner() call here to prevent premature hiding
901
954
 
902
955
  if (this._config.stateSave) {
903
956
  this._saveState();
@@ -1668,6 +1721,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1668
1721
  ),
1669
1722
  filter,
1670
1723
  ];
1724
+ this._config._state.page = 1;
1671
1725
  return this;
1672
1726
  }
1673
1727
 
@@ -1677,6 +1731,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1677
1731
 
1678
1732
  public search(query: string | object): void {
1679
1733
  this._config._state.search = query;
1734
+ this._config._state.page = 1;
1680
1735
  this.reload();
1681
1736
  }
1682
1737
 
@@ -10,7 +10,6 @@
10
10
  @apply outline-0 block w-full bg-background border border-input shadow-xs shadow-[rgba(0,0,0,0.05)] transition-[color,box-shadow] text-foreground placeholder:text-muted-foreground;
11
11
  @apply focus-visible:ring-ring/30 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px];
12
12
  @apply disabled:cursor-not-allowed disabled:opacity-60;
13
- @apply [&[readonly]]:bg-muted/80 [&[readonly]]:cursor-not-allowed [&[readonly]]:text-secondary-foreground/80;
14
13
  @apply file:h-full [&[type=file]]:py-0;
15
14
  @apply file:border-solid file:border-input file:bg-transparent file:font-medium file:not-italic file:text-foreground file:p-0 file:border-0 file:border-e;
16
15
  @apply aria-invalid:border-destructive/60 aria-invalid:ring-destructive/10;
@@ -9,7 +9,6 @@
9
9
  @apply cursor-pointer py-0 appearance-none flex items-center gap-2 w-full bg-background border border-input shadow-xs shadow-[rgba(0,0,0,0.05)] transition-[color,box-shadow] text-foreground placeholder:text-muted-foreground/80;
10
10
  @apply focus-visible:ring-ring/30 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px];
11
11
  @apply disabled:cursor-not-allowed disabled:opacity-60;
12
- @apply [&[readonly]]:opacity-70;
13
12
  @apply aria-invalid:border-destructive/60 aria-invalid:ring-destructive/10;
14
13
 
15
14
  background-repeat: no-repeat;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
@@ -10,7 +10,6 @@
10
10
  @apply w-full bg-background border border-input text-foreground shadow-xs shadow-[rgba(0,0,0,0.05)] transition-[color,box-shadow] placeholder:text-muted-foreground/80;
11
11
  @apply focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/30;
12
12
  @apply disabled:cursor-not-allowed disabled:opacity-60;
13
- @apply [&[readonly]]:bg-muted/80 [&[readonly]]:cursor-not-allowed;
14
13
  @apply aria-invalid:border-destructive/60 aria-invalid:ring-destructive/10;
15
14
  }
16
15