@keenthemes/ktui 1.2.5 → 1.2.7

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 (196) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +1538 -786
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +85 -5
  6. package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
  7. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
  9. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
  11. package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
  13. package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
  15. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
  17. package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
  18. package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
  19. package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
  20. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js +338 -0
  23. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  24. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  25. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-local-provider.js +85 -27
  27. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +13 -13
  30. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  31. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  32. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  34. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  35. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  37. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  38. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  41. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  42. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  43. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  45. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  46. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  49. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  53. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  54. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  55. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  57. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  58. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  59. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  60. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  61. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  62. package/lib/cjs/components/datatable/datatable.d.ts +35 -34
  63. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  64. package/lib/cjs/components/datatable/datatable.js +233 -497
  65. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  66. package/lib/cjs/components/datatable/index.d.ts +1 -1
  67. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  68. package/lib/cjs/components/datatable/types.d.ts +127 -11
  69. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  70. package/lib/cjs/index.d.ts +1 -1
  71. package/lib/cjs/index.d.ts.map +1 -1
  72. package/lib/cjs/index.js +6 -0
  73. package/lib/cjs/index.js.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  75. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  76. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  77. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  78. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  80. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  81. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  82. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  83. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  84. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  85. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  86. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  87. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  88. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  89. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  90. package/lib/esm/components/datatable/datatable-layout-plugin.js +334 -0
  91. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  92. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  93. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  94. package/lib/esm/components/datatable/datatable-local-provider.js +85 -27
  95. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  96. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  97. package/lib/esm/components/datatable/datatable-pagination-renderer.js +13 -13
  98. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  99. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  100. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  101. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  102. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  103. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  104. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  105. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  106. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  107. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  108. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  109. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  110. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  111. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  112. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  113. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  114. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  115. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  117. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  119. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  120. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  121. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  122. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  123. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  124. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  125. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  126. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  127. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  128. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  129. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  130. package/lib/esm/components/datatable/datatable.d.ts +35 -34
  131. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/datatable.js +235 -499
  133. package/lib/esm/components/datatable/datatable.js.map +1 -1
  134. package/lib/esm/components/datatable/index.d.ts +1 -1
  135. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  136. package/lib/esm/components/datatable/types.d.ts +127 -11
  137. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  138. package/lib/esm/index.d.ts +1 -1
  139. package/lib/esm/index.d.ts.map +1 -1
  140. package/lib/esm/index.js +6 -0
  141. package/lib/esm/index.js.map +1 -1
  142. package/package.json +5 -1
  143. package/skills/ktui/SKILL.md +711 -0
  144. package/skills/ktui-datatable/SKILL.md +302 -0
  145. package/skills/ktui-install/SKILL.md +150 -0
  146. package/skills/ktui-select/SKILL.md +271 -0
  147. package/src/components/__tests__/component.test.ts +347 -0
  148. package/src/components/collapse/collapse.css +2 -2
  149. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  150. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  151. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  152. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  153. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  154. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  155. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  156. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  157. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  158. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  159. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  160. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  161. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  162. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  163. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  164. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  165. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  166. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  167. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  168. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  169. package/src/components/datatable/__tests__/pagination-reset.test.ts +147 -6
  170. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  171. package/src/components/datatable/__tests__/setup.ts +12 -4
  172. package/src/components/datatable/datatable-checkbox.ts +139 -143
  173. package/src/components/datatable/datatable-column-utils.ts +63 -0
  174. package/src/components/datatable/datatable-contracts.ts +2 -3
  175. package/src/components/datatable/datatable-defaults.ts +204 -0
  176. package/src/components/datatable/datatable-layout-plugin.ts +459 -0
  177. package/src/components/datatable/datatable-local-provider.ts +106 -35
  178. package/src/components/datatable/datatable-pagination-renderer.ts +13 -15
  179. package/src/components/datatable/datatable-registry.ts +89 -0
  180. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  181. package/src/components/datatable/datatable-search-handler.ts +97 -0
  182. package/src/components/datatable/datatable-sort.ts +111 -66
  183. package/src/components/datatable/datatable-spinner.ts +103 -0
  184. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  185. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  186. package/src/components/datatable/datatable-utils.ts +12 -0
  187. package/src/components/datatable/datatable.css +98 -0
  188. package/src/components/datatable/datatable.ts +288 -583
  189. package/src/components/datatable/index.ts +8 -0
  190. package/src/components/datatable/types.ts +157 -23
  191. package/src/helpers/__tests__/dom.test.ts +776 -0
  192. package/src/helpers/__tests__/utils.test.ts +332 -0
  193. package/src/index.ts +15 -0
  194. package/skills/ktui-components/SKILL.md +0 -41
  195. package/skills/ktui-theming/SKILL.md +0 -50
  196. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -0,0 +1,257 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { KTDataTable } from '../datatable';
3
+
4
+ type DataRow = {
5
+ id: string;
6
+ name: string;
7
+ status: string;
8
+ };
9
+
10
+ const rows: DataRow[] = [
11
+ { id: '1', name: 'Alpha', status: 'Active' },
12
+ { id: '2', name: 'Beta', status: 'Pending' },
13
+ { id: '3', name: 'Gamma', status: 'Disabled' },
14
+ ];
15
+
16
+ const createDatatableFixture = () => {
17
+ const wrapper = document.createElement('div');
18
+ wrapper.className = 'kt-table-wrapper';
19
+
20
+ const container = document.createElement('div');
21
+ container.id = 'kt_datatable_locked_layout';
22
+ container.setAttribute('data-kt-datatable', 'true');
23
+
24
+ const table = document.createElement('table');
25
+ table.setAttribute('data-kt-datatable-table', 'true');
26
+
27
+ const thead = document.createElement('thead');
28
+ thead.innerHTML = `
29
+ <tr>
30
+ <th data-kt-datatable-column="id">ID</th>
31
+ <th data-kt-datatable-column="name">Name</th>
32
+ <th data-kt-datatable-column="status">Status</th>
33
+ </tr>
34
+ `;
35
+ table.appendChild(thead);
36
+
37
+ const tbody = document.createElement('tbody');
38
+ rows.forEach((row) => {
39
+ const tr = document.createElement('tr');
40
+ tr.innerHTML = `<td>${row.id}</td><td>${row.name}</td><td>${row.status}</td>`;
41
+ tbody.appendChild(tr);
42
+ });
43
+ table.appendChild(tbody);
44
+
45
+ wrapper.appendChild(table);
46
+ container.appendChild(wrapper);
47
+
48
+ const info = document.createElement('span');
49
+ info.setAttribute('data-kt-datatable-info', 'true');
50
+ const size = document.createElement('select');
51
+ size.setAttribute('data-kt-datatable-size', 'true');
52
+ const pagination = document.createElement('div');
53
+ pagination.setAttribute('data-kt-datatable-pagination', 'true');
54
+
55
+ container.appendChild(info);
56
+ container.appendChild(size);
57
+ container.appendChild(pagination);
58
+ document.body.appendChild(container);
59
+
60
+ const sizedCells = table.querySelectorAll<HTMLTableCellElement>('th, td');
61
+ sizedCells.forEach((cell) => {
62
+ Object.defineProperty(cell, 'getBoundingClientRect', {
63
+ value: () =>
64
+ ({
65
+ width: 120,
66
+ height: 40,
67
+ top: 0,
68
+ left: 0,
69
+ right: 0,
70
+ bottom: 0,
71
+ x: 0,
72
+ y: 0,
73
+ toJSON: () => ({}),
74
+ }) as DOMRect,
75
+ });
76
+ });
77
+
78
+ return { container, table };
79
+ };
80
+
81
+ describe('KTDataTable locked layout plugin', () => {
82
+ beforeEach(() => {
83
+ vi.useFakeTimers();
84
+ });
85
+
86
+ it('applies locked header, rows and columns in local mode and keeps them after sorting', async () => {
87
+ const { container, table } = createDatatableFixture();
88
+
89
+ const datatable = new KTDataTable<DataRow>(container, {
90
+ stateSave: false,
91
+ columns: {
92
+ id: { title: 'ID' },
93
+ name: { title: 'Name' },
94
+ status: { title: 'Status' },
95
+ },
96
+ lockedLayout: {
97
+ stickyHeader: true,
98
+ stickyRows: { top: 1, bottom: 1 },
99
+ stickyColumns: { left: ['id'], right: ['status'] },
100
+ },
101
+ });
102
+
103
+ await vi.runAllTimersAsync();
104
+
105
+ expect(
106
+ table.querySelectorAll('.kt-datatable-locked-header').length,
107
+ ).toBeGreaterThan(0);
108
+ expect(
109
+ table.querySelectorAll('.kt-datatable-locked-top-row').length,
110
+ ).toBeGreaterThan(0);
111
+ expect(
112
+ table.querySelectorAll('.kt-datatable-locked-bottom-row').length,
113
+ ).toBeGreaterThan(0);
114
+ expect(
115
+ table.querySelectorAll('.kt-datatable-locked-left').length,
116
+ ).toBeGreaterThan(0);
117
+ expect(
118
+ table.querySelectorAll('.kt-datatable-locked-right').length,
119
+ ).toBeGreaterThan(0);
120
+
121
+ datatable.sort('name');
122
+ await vi.runAllTimersAsync();
123
+
124
+ expect(
125
+ table.querySelectorAll('.kt-datatable-locked-cell').length,
126
+ ).toBeGreaterThan(0);
127
+ });
128
+
129
+ it('reapplies locked layout after remote fetch redraw', async () => {
130
+ const { container, table } = createDatatableFixture();
131
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
132
+ new Response(JSON.stringify({ data: rows, totalCount: rows.length }), {
133
+ status: 200,
134
+ headers: { 'Content-Type': 'application/json' },
135
+ }),
136
+ );
137
+
138
+ new KTDataTable<DataRow>(container, {
139
+ stateSave: false,
140
+ apiEndpoint: 'https://example.test/datatable',
141
+ requestMethod: 'GET',
142
+ mapResponse: (data) => data,
143
+ lockedLayout: {
144
+ stickyHeader: true,
145
+ stickyColumns: { left: ['id'] },
146
+ },
147
+ });
148
+
149
+ await vi.runAllTimersAsync();
150
+
151
+ expect(fetchMock).toHaveBeenCalled();
152
+ expect(
153
+ table.querySelectorAll('.kt-datatable-locked-header').length,
154
+ ).toBeGreaterThan(0);
155
+ expect(
156
+ table.querySelectorAll('.kt-datatable-locked-left').length,
157
+ ).toBeGreaterThan(0);
158
+ });
159
+
160
+ it('keeps pagination state when using locked columns in local mode', async () => {
161
+ const { container } = createDatatableFixture();
162
+
163
+ const datatable = new KTDataTable<DataRow>(container, {
164
+ stateSave: false,
165
+ pageSize: 2,
166
+ columns: {
167
+ id: { title: 'ID' },
168
+ name: { title: 'Name' },
169
+ status: { title: 'Status' },
170
+ },
171
+ lockedLayout: {
172
+ stickyHeader: true,
173
+ stickyColumns: { left: ['id'] },
174
+ },
175
+ });
176
+
177
+ await vi.runAllTimersAsync();
178
+ expect(datatable.getState().totalPages).toBe(2);
179
+
180
+ datatable.goPage(2);
181
+ await vi.runAllTimersAsync();
182
+ expect(datatable.getState().page).toBe(2);
183
+ expect(datatable.getState().totalPages).toBe(2);
184
+ expect(datatable.getState().totalItems).toBe(rows.length);
185
+ });
186
+
187
+ it('uses collapsed table borders for row-only locked layout', async () => {
188
+ const { container, table } = createDatatableFixture();
189
+
190
+ new KTDataTable<DataRow>(container, {
191
+ stateSave: false,
192
+ lockedLayout: {
193
+ stickyHeader: true,
194
+ stickyRows: { top: 1, bottom: 1 },
195
+ },
196
+ });
197
+
198
+ await vi.runAllTimersAsync();
199
+
200
+ expect(table.classList.contains('kt-datatable-locked-layout')).toBe(true);
201
+ expect(
202
+ table.classList.contains('kt-datatable-locked-layout-separate'),
203
+ ).toBe(false);
204
+ expect(table.style.borderCollapse).not.toBe('separate');
205
+ expect(
206
+ table.tHead?.classList.contains('kt-datatable-locked-header-section'),
207
+ ).toBe(true);
208
+ const stickyHeaderCell = table.querySelector(
209
+ 'th.kt-datatable-locked-header',
210
+ ) as HTMLTableCellElement | null;
211
+ expect(stickyHeaderCell?.style.position).not.toBe('sticky');
212
+ });
213
+
214
+ it('uses separate table borders when sticky columns are enabled', async () => {
215
+ const { container, table } = createDatatableFixture();
216
+
217
+ new KTDataTable<DataRow>(container, {
218
+ stateSave: false,
219
+ lockedLayout: {
220
+ stickyHeader: true,
221
+ stickyColumns: { left: ['id'] },
222
+ },
223
+ });
224
+
225
+ await vi.runAllTimersAsync();
226
+
227
+ expect(
228
+ table.classList.contains('kt-datatable-locked-layout-separate'),
229
+ ).toBe(true);
230
+ expect(table.style.borderCollapse).toBe('separate');
231
+ });
232
+
233
+ it('does not set inline background color for locked header columns', async () => {
234
+ const { container, table } = createDatatableFixture();
235
+
236
+ new KTDataTable<DataRow>(container, {
237
+ stateSave: false,
238
+ columns: {
239
+ id: { title: 'ID' },
240
+ name: { title: 'Name' },
241
+ status: { title: 'Status' },
242
+ },
243
+ lockedLayout: {
244
+ stickyHeader: true,
245
+ stickyColumns: { left: ['id'] },
246
+ },
247
+ });
248
+
249
+ await vi.runAllTimersAsync();
250
+
251
+ const lockedHeaderCell = table.querySelector(
252
+ 'th.kt-datatable-locked-left',
253
+ ) as HTMLTableCellElement | null;
254
+ expect(lockedHeaderCell).not.toBeNull();
255
+ expect(lockedHeaderCell?.style.backgroundColor).toBe('');
256
+ });
257
+ });
@@ -30,23 +30,23 @@ describe('KTDataTable - Multi-row header column count', () => {
30
30
  const thead = document.createElement('thead');
31
31
  const row1 = document.createElement('tr');
32
32
  row1.innerHTML = `
33
- <th rowspan="2">Person</th>
33
+ <th rowspan="2" data-kt-datatable-column="person">Person</th>
34
34
  <th colspan="3">Backlog</th>
35
35
  <th colspan="3">Floater (2) 2025</th>
36
36
  <th colspan="3">Floater (1) 2025</th>
37
37
  <th colspan="3">CL2025</th>
38
38
  <th colspan="3">LWP</th>
39
- <th rowspan="2">Action</th>
39
+ <th rowspan="2" data-kt-datatable-column="action">Action</th>
40
40
  `;
41
41
  thead.appendChild(row1);
42
42
 
43
43
  const row2 = document.createElement('tr');
44
44
  row2.innerHTML = `
45
- <th>Assigned</th><th>Used</th><th>Balance</th>
46
- <th>Assigned</th><th>Used</th><th>Balance</th>
47
- <th>Assigned</th><th>Used</th><th>Balance</th>
48
- <th>Assigned</th><th>Used</th><th>Balance</th>
49
- <th>Assigned</th><th>Used</th><th>Balance</th>
45
+ <th data-kt-datatable-column="assigned1">Assigned</th><th data-kt-datatable-column="used1">Used</th><th data-kt-datatable-column="balance1">Balance</th>
46
+ <th data-kt-datatable-column="assigned2">Assigned</th><th data-kt-datatable-column="used2">Used</th><th data-kt-datatable-column="balance2">Balance</th>
47
+ <th data-kt-datatable-column="assigned3">Assigned</th><th data-kt-datatable-column="used3">Used</th><th data-kt-datatable-column="balance3">Balance</th>
48
+ <th data-kt-datatable-column="assigned4">Assigned</th><th data-kt-datatable-column="used4">Used</th><th data-kt-datatable-column="balance4">Balance</th>
49
+ <th data-kt-datatable-column="assigned5">Assigned</th><th data-kt-datatable-column="used5">Used</th><th data-kt-datatable-column="balance5">Balance</th>
50
50
  `;
51
51
  thead.appendChild(row2);
52
52
  tableElement.appendChild(thead);
@@ -98,6 +98,10 @@ describe('KTDataTable - Pagination Reset', () => {
98
98
  };
99
99
 
100
100
  beforeEach(() => {
101
+ // Dispose previous test's datatable to cancel in-flight async operations
102
+ if (datatable) {
103
+ try { datatable.dispose(); } catch { /* already disposed */ }
104
+ }
101
105
  // Clear any existing elements
102
106
  document.body.innerHTML = '';
103
107
  vi.clearAllMocks();
@@ -313,7 +317,7 @@ describe('KTDataTable - Pagination Reset', () => {
313
317
  expect(datatable.getState().page).toBe(1);
314
318
 
315
319
  // Apply third filter (page should stay at 1)
316
- datatable.setFilter({ column: 'id', type: 'numeric', value: '10' });
320
+ datatable.setFilter({ column: 'id', type: 'numeric', value: 10 });
317
321
  expect(datatable.getState().page).toBe(1);
318
322
  });
319
323
 
@@ -438,7 +442,17 @@ describe('KTDataTable - Pagination Reset', () => {
438
442
  });
439
443
 
440
444
  describe('Scenario: State persistence respects pagination reset', () => {
445
+ const isLocalStorageAvailable = (): boolean => {
446
+ try {
447
+ localStorage.getItem('test');
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ };
453
+
441
454
  it('should save page 1 to state when search resets pagination', async () => {
455
+ if (!isLocalStorageAvailable()) return;
442
456
  const { container } = createMockDataTable(25);
443
457
 
444
458
  // Enable state saving with unique namespace
@@ -476,6 +490,7 @@ describe('KTDataTable - Pagination Reset', () => {
476
490
  });
477
491
 
478
492
  it('should save page 1 to state when filter resets pagination', () => {
493
+ if (!isLocalStorageAvailable()) return;
479
494
  const { container } = createMockDataTable(25);
480
495
 
481
496
  datatable = new KTDataTable(container, {
@@ -503,6 +518,7 @@ describe('KTDataTable - Pagination Reset', () => {
503
518
  });
504
519
 
505
520
  it('should restore to page 1 with active search on reload', async () => {
521
+ if (!isLocalStorageAvailable()) return;
506
522
  const { container } = createMockDataTable(25);
507
523
  const namespace = 'test-datatable-restore';
508
524
 
@@ -552,6 +568,131 @@ describe('KTDataTable - Pagination Reset', () => {
552
568
  });
553
569
 
554
570
  describe('Edge Cases', () => {
571
+ it('keeps total pages stable when navigating to page 2 in local mode', () => {
572
+ const { container } = createMockDataTable(6);
573
+ datatable = new KTDataTable(container, {
574
+ pageSize: 5,
575
+ stateSave: false,
576
+ });
577
+
578
+ expect(datatable.getState().totalPages).toBe(2);
579
+ datatable.goPage(2);
580
+ expect(datatable.getState().page).toBe(2);
581
+ expect(datatable.getState().totalPages).toBe(2);
582
+
583
+ // Trigger another redraw cycle to ensure local mode does not shrink original data.
584
+ datatable.reload();
585
+ expect(datatable.getState().page).toBe(2);
586
+ expect(datatable.getState().totalPages).toBe(2);
587
+ });
588
+
589
+ it('does not shrink originalData when tbody checksum mismatches after pagination', async () => {
590
+ const { container } = createMockDataTable(18);
591
+ datatable = new KTDataTable(container, {
592
+ pageSize: 5,
593
+ stateSave: false,
594
+ });
595
+
596
+ await new Promise((resolve) => setTimeout(resolve, 50));
597
+ expect(datatable.getState().totalPages).toBe(4);
598
+
599
+ // Simulate a fetch before _contentChecksum was aligned with the paginated tbody.
600
+ datatable.getState()._contentChecksum = 'stale-checksum';
601
+ datatable.reload();
602
+
603
+ await new Promise((resolve) => setTimeout(resolve, 50));
604
+ expect(datatable.getState().totalPages).toBe(4);
605
+ });
606
+
607
+ it('keeps 4 pages when thead has checkbox and actions columns (bulk-actions demo)', async () => {
608
+ const container = document.createElement('div');
609
+ container.id = 'test-bulk-actions-datatable';
610
+
611
+ const table = document.createElement('table');
612
+ table.setAttribute('data-kt-datatable-table', 'true');
613
+
614
+ const thead = document.createElement('thead');
615
+ thead.innerHTML = `
616
+ <tr>
617
+ <th><input type="checkbox" data-kt-datatable-check="true" /></th>
618
+ <th data-kt-datatable-column="label">Label</th>
619
+ <th data-kt-datatable-column="method">Method</th>
620
+ <th data-kt-datatable-column="status">Status</th>
621
+ <th data-kt-datatable-column="lastSession">Last Session</th>
622
+ <th data-kt-datatable-column="actions"></th>
623
+ </tr>
624
+ `;
625
+
626
+ const tbody = document.createElement('tbody');
627
+ for (let i = 1; i <= 18; i++) {
628
+ const row = document.createElement('tr');
629
+ row.innerHTML = `
630
+ <td><input type="checkbox" data-kt-datatable-row-check="true" value="${i - 1}" /></td>
631
+ <td>User ${i}</td>
632
+ <td>Web</td>
633
+ <td>active</td>
634
+ <td>22 Jul 2024</td>
635
+ <td><button type="button">Edit</button></td>
636
+ `;
637
+ tbody.appendChild(row);
638
+ }
639
+
640
+ table.appendChild(thead);
641
+ table.appendChild(tbody);
642
+
643
+ const infoElement = document.createElement('div');
644
+ infoElement.setAttribute('data-kt-datatable-info', 'true');
645
+ const sizeElement = document.createElement('select');
646
+ sizeElement.setAttribute('data-kt-datatable-size', 'true');
647
+ const paginationElement = document.createElement('div');
648
+ paginationElement.setAttribute('data-kt-datatable-pagination', 'true');
649
+
650
+ container.appendChild(table);
651
+ container.appendChild(infoElement);
652
+ container.appendChild(sizeElement);
653
+ container.appendChild(paginationElement);
654
+ document.body.appendChild(container);
655
+
656
+ datatable = new KTDataTable(container, {
657
+ pageSize: 5,
658
+ stateSave: false,
659
+ });
660
+
661
+ await new Promise((resolve) => setTimeout(resolve, 50));
662
+
663
+ expect(datatable.getState().totalPages).toBe(4);
664
+ datatable.goPage(2);
665
+ await new Promise((resolve) => setTimeout(resolve, 50));
666
+ expect(datatable.getState().page).toBe(2);
667
+ expect(datatable.getState().totalPages).toBe(4);
668
+ });
669
+
670
+ it('shows page 2 rows with tableLayout fixed and columns config (docs column-widths demo)', async () => {
671
+ const { container } = createMockDataTable(18);
672
+ datatable = new KTDataTable(container, {
673
+ pageSize: 5,
674
+ stateSave: false,
675
+ tableLayout: 'fixed',
676
+ columns: {
677
+ id: { width: '60px' },
678
+ name: { width: '140px' },
679
+ status: { width: '100px' },
680
+ },
681
+ });
682
+
683
+ await new Promise((resolve) => setTimeout(resolve, 50));
684
+
685
+ datatable.goPage(2);
686
+ await new Promise((resolve) => setTimeout(resolve, 50));
687
+
688
+ expect(datatable.getState().page).toBe(2);
689
+
690
+ const rows = container.querySelectorAll('tbody tr');
691
+ expect(rows.length).toBe(5);
692
+ expect(rows[0].cells[0].textContent).toBe('6');
693
+ expect(datatable.getState().totalPages).toBe(4);
694
+ });
695
+
555
696
  it('should handle search reset on page 1 (no-op)', () => {
556
697
  const { container } = createMockDataTable(25);
557
698
  datatable = new KTDataTable(container, {
@@ -651,9 +792,9 @@ describe('KTDataTable - Pagination Reset', () => {
651
792
  stateSave: false,
652
793
  });
653
794
 
654
- const reloadSpy = vi.fn();
655
- // Listen for 'reload' event directly (CustomEvent)
656
- container.addEventListener('reload', reloadSpy);
795
+ const updateSpy = vi.fn();
796
+ // Listen for 'kt.datatable.update' event directly (CustomEvent)
797
+ container.addEventListener('kt.datatable.update', updateSpy);
657
798
 
658
799
  datatable.goPage(2);
659
800
  datatable.search('test');
@@ -661,8 +802,8 @@ describe('KTDataTable - Pagination Reset', () => {
661
802
  // Wait for async reload to complete
662
803
  await new Promise((resolve) => setTimeout(resolve, 50));
663
804
 
664
- // reload event should still fire
665
- expect(reloadSpy).toHaveBeenCalled();
805
+ // update event should still fire
806
+ expect(updateSpy).toHaveBeenCalled();
666
807
  });
667
808
  });
668
809
  });
@@ -407,13 +407,13 @@ describe('KTDataTable Race Condition Fixes', () => {
407
407
 
408
408
  describe('Event Handling During Race Conditions', () => {
409
409
  it('should fire fetch event for successful requests', async () => {
410
- const fetchEvents: Event[] = [];
410
+ const updateEvents: Event[] = [];
411
411
 
412
412
  const element = container.querySelector(
413
413
  '[data-kt-datatable="true"]',
414
414
  ) as HTMLElement;
415
- element.addEventListener('fetch', (e) => {
416
- fetchEvents.push(e);
415
+ element.addEventListener('kt.datatable.update', (e) => {
416
+ updateEvents.push(e);
417
417
  });
418
418
 
419
419
  const datatable = new KTDataTable(element, {
@@ -425,18 +425,18 @@ describe('KTDataTable Race Condition Fixes', () => {
425
425
  datatable.search('test');
426
426
  await waitFor(150); // Complete search
427
427
 
428
- // Should fire fetch for initial and search
429
- expect(fetchEvents.length).toBeGreaterThanOrEqual(2);
428
+ // Should fire update for initial and search
429
+ expect(updateEvents.length).toBeGreaterThanOrEqual(2);
430
430
  });
431
431
 
432
432
  it('should fire fetched event after successful data load', async () => {
433
- const fetchedEvents: Event[] = [];
433
+ const updateEvents: Event[] = [];
434
434
 
435
435
  const element = container.querySelector(
436
436
  '[data-kt-datatable="true"]',
437
437
  ) as HTMLElement;
438
- element.addEventListener('fetched', (e) => {
439
- fetchedEvents.push(e);
438
+ element.addEventListener('kt.datatable.update', (e) => {
439
+ updateEvents.push(e);
440
440
  });
441
441
 
442
442
  new KTDataTable(element, {
@@ -445,8 +445,8 @@ describe('KTDataTable Race Condition Fixes', () => {
445
445
 
446
446
  await waitFor(150);
447
447
 
448
- // Should fire fetched for initial request
449
- expect(fetchedEvents.length).toBeGreaterThanOrEqual(1);
448
+ // Should fire update for initial request
449
+ expect(updateEvents.length).toBeGreaterThanOrEqual(1);
450
450
  });
451
451
 
452
452
  it('should not fire error events for AbortError', async () => {
@@ -455,7 +455,7 @@ describe('KTDataTable Race Condition Fixes', () => {
455
455
  const element = container.querySelector(
456
456
  '[data-kt-datatable="true"]',
457
457
  ) as HTMLElement;
458
- element.addEventListener('error.kt.datatable', (e) => {
458
+ element.addEventListener('kt.datatable.error', (e) => {
459
459
  errorEvents.push(e);
460
460
  });
461
461
 
@@ -17,8 +17,12 @@ vi.mock('../../../index', () => ({
17
17
 
18
18
  // Setup DOM environment before each test
19
19
  beforeEach(() => {
20
- // Clear localStorage
21
- localStorage.clear();
20
+ // Clear localStorage (may be unavailable in Node.js without --localstorage-file)
21
+ try {
22
+ localStorage.clear();
23
+ } catch {
24
+ // localStorage not available
25
+ }
22
26
 
23
27
  // Reset document body
24
28
  document.body.innerHTML = '';
@@ -29,8 +33,12 @@ beforeEach(() => {
29
33
 
30
34
  // Cleanup after each test
31
35
  afterEach(() => {
32
- // Clear localStorage
33
- localStorage.clear();
36
+ // Clear localStorage (may be unavailable in Node.js without --localstorage-file)
37
+ try {
38
+ localStorage.clear();
39
+ } catch {
40
+ // localStorage not available
41
+ }
34
42
 
35
43
  // Reset document body
36
44
  document.body.innerHTML = '';