@keenthemes/ktui 1.2.6 → 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 (190) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +3775 -2298
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +25 -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.map +1 -1
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.js +11 -1
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -1
  23. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  24. package/lib/cjs/components/datatable/datatable-local-provider.js +80 -24
  25. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  27. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +3 -2
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  30. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  31. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  32. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  34. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  35. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  37. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  38. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  41. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  42. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  43. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  45. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  46. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  49. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  53. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  54. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  55. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  57. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  58. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  59. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  60. package/lib/cjs/components/datatable/datatable.d.ts +26 -34
  61. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  62. package/lib/cjs/components/datatable/datatable.js +155 -492
  63. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  64. package/lib/cjs/components/datatable/index.d.ts +1 -1
  65. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  66. package/lib/cjs/components/datatable/types.d.ts +100 -11
  67. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  68. package/lib/cjs/index.d.ts +1 -1
  69. package/lib/cjs/index.d.ts.map +1 -1
  70. package/lib/cjs/index.js +6 -0
  71. package/lib/cjs/index.js.map +1 -1
  72. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  73. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  75. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  76. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  77. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  78. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  80. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  81. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  82. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  83. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  84. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  85. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  86. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
  87. package/lib/esm/components/datatable/datatable-layout-plugin.js +11 -1
  88. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -1
  89. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  90. package/lib/esm/components/datatable/datatable-local-provider.js +80 -24
  91. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  92. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  93. package/lib/esm/components/datatable/datatable-pagination-renderer.js +3 -2
  94. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  95. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  96. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  97. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  98. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  99. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  100. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  101. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  102. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  103. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  104. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  105. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  106. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  107. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  108. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  109. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  110. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  111. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  112. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  113. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  114. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  115. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  117. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  119. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  120. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  121. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  122. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  123. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  124. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  125. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  126. package/lib/esm/components/datatable/datatable.d.ts +26 -34
  127. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  128. package/lib/esm/components/datatable/datatable.js +157 -494
  129. package/lib/esm/components/datatable/datatable.js.map +1 -1
  130. package/lib/esm/components/datatable/index.d.ts +1 -1
  131. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/types.d.ts +100 -11
  133. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  134. package/lib/esm/index.d.ts +1 -1
  135. package/lib/esm/index.d.ts.map +1 -1
  136. package/lib/esm/index.js +6 -0
  137. package/lib/esm/index.js.map +1 -1
  138. package/package.json +5 -1
  139. package/skills/ktui/SKILL.md +711 -0
  140. package/skills/ktui-datatable/SKILL.md +302 -0
  141. package/skills/ktui-install/SKILL.md +150 -0
  142. package/skills/ktui-select/SKILL.md +271 -0
  143. package/src/components/__tests__/component.test.ts +347 -0
  144. package/src/components/collapse/collapse.css +2 -2
  145. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  146. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  147. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  148. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  149. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  150. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  151. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  152. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  153. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  154. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  155. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  156. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  157. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  158. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  159. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  160. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  161. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  162. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  163. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  164. package/src/components/datatable/__tests__/pagination-reset.test.ts +129 -6
  165. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  166. package/src/components/datatable/__tests__/setup.ts +12 -4
  167. package/src/components/datatable/datatable-checkbox.ts +144 -145
  168. package/src/components/datatable/datatable-column-utils.ts +63 -0
  169. package/src/components/datatable/datatable-contracts.ts +2 -3
  170. package/src/components/datatable/datatable-defaults.ts +204 -0
  171. package/src/components/datatable/datatable-layout-plugin.ts +11 -1
  172. package/src/components/datatable/datatable-local-provider.ts +91 -28
  173. package/src/components/datatable/datatable-pagination-renderer.ts +3 -2
  174. package/src/components/datatable/datatable-registry.ts +89 -0
  175. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  176. package/src/components/datatable/datatable-search-handler.ts +97 -0
  177. package/src/components/datatable/datatable-sort.ts +111 -66
  178. package/src/components/datatable/datatable-spinner.ts +103 -0
  179. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  180. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  181. package/src/components/datatable/datatable-utils.ts +12 -0
  182. package/src/components/datatable/datatable.ts +191 -580
  183. package/src/components/datatable/index.ts +3 -0
  184. package/src/components/datatable/types.ts +124 -23
  185. package/src/helpers/__tests__/dom.test.ts +776 -0
  186. package/src/helpers/__tests__/utils.test.ts +332 -0
  187. package/src/index.ts +10 -0
  188. package/skills/ktui-components/SKILL.md +0 -41
  189. package/skills/ktui-theming/SKILL.md +0 -50
  190. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -0,0 +1,417 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { KTDataTableSortHandler } from '../datatable-sort';
3
+ import type {
4
+ KTDataTableConfigInterface,
5
+ KTDataTableDataInterface,
6
+ } from '../types';
7
+
8
+ function createSortHandler(
9
+ config: Partial<KTDataTableConfigInterface> = {},
10
+ state: { sortField: string | number; sortOrder: string } = {
11
+ sortField: '',
12
+ sortOrder: '',
13
+ },
14
+ ) {
15
+ const thead = document.createElement('thead');
16
+ const tr = document.createElement('tr');
17
+
18
+ const th1 = document.createElement('th');
19
+ th1.setAttribute('data-kt-datatable-column', 'name');
20
+ th1.innerHTML = '<span class="sort-icon"></span>';
21
+
22
+ const th2 = document.createElement('th');
23
+ th2.setAttribute('data-kt-datatable-column', 'price');
24
+ th2.innerHTML = '<span class="sort-icon"></span>';
25
+
26
+ const th3 = document.createElement('th');
27
+ th3.setAttribute('data-kt-datatable-column', 'date');
28
+ th3.innerHTML = '<span class="sort-icon"></span>';
29
+
30
+ tr.appendChild(th1);
31
+ tr.appendChild(th2);
32
+ tr.appendChild(th3);
33
+ thead.appendChild(tr);
34
+
35
+ const updateData = vi.fn();
36
+ const setState = vi.fn();
37
+ const emit = vi.fn();
38
+
39
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
40
+ config: {
41
+ sort: { classes: { base: 'sort-icon', asc: 'asc', desc: 'desc' } },
42
+ ...config,
43
+ } as any,
44
+ theadElement: thead,
45
+ getState: () => state as any,
46
+ setState: setState as any,
47
+ emit,
48
+ updateData,
49
+ });
50
+
51
+ return { handler, thead, updateData, setState, emit };
52
+ }
53
+
54
+ describe('Sort handler extended coverage', () => {
55
+ describe('Sort direction cycling', () => {
56
+ it('clicking unsorted column → asc', () => {
57
+ const { handler, thead, setState, updateData } = createSortHandler();
58
+ handler.initSort();
59
+
60
+ const th = thead.querySelector('th')!;
61
+ th.click();
62
+
63
+ expect(setState).toHaveBeenCalledWith('name', 'asc');
64
+ expect(updateData).toHaveBeenCalled();
65
+
66
+ handler.dispose();
67
+ });
68
+
69
+ it('clicking asc column → desc', () => {
70
+ const { handler, thead, setState, updateData } = createSortHandler(
71
+ {},
72
+ { sortField: 'name', sortOrder: 'asc' },
73
+ );
74
+ handler.initSort();
75
+
76
+ const th = thead.querySelector('th')!;
77
+ th.click();
78
+
79
+ expect(setState).toHaveBeenCalledWith('name', 'desc');
80
+ expect(updateData).toHaveBeenCalled();
81
+
82
+ handler.dispose();
83
+ });
84
+
85
+ it('clicking desc column → removes sort (clears field)', () => {
86
+ const { handler, thead, setState, updateData } = createSortHandler(
87
+ {},
88
+ { sortField: 'name', sortOrder: 'desc' },
89
+ );
90
+ handler.initSort();
91
+
92
+ const th = thead.querySelector('th')!;
93
+ th.click();
94
+
95
+ expect(setState).toHaveBeenCalledWith('name', '');
96
+ expect(updateData).toHaveBeenCalled();
97
+
98
+ handler.dispose();
99
+ });
100
+
101
+ it('clicking with data-kt-datatable-column-sort attribute uses that as sort field', () => {
102
+ const thead = document.createElement('thead');
103
+ const tr = document.createElement('tr');
104
+ const th = document.createElement('th');
105
+ th.setAttribute('data-kt-datatable-column', 'name');
106
+ th.setAttribute('data-kt-datatable-column-sort', 'customField');
107
+ th.innerHTML = '<span class="sort-icon"></span>';
108
+ tr.appendChild(th);
109
+ thead.appendChild(tr);
110
+
111
+ const setState = vi.fn();
112
+ const updateData = vi.fn();
113
+
114
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
115
+ config: {
116
+ sort: { classes: { base: 'sort-icon' } },
117
+ } as any,
118
+ theadElement: thead,
119
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
120
+ setState: setState as any,
121
+ emit: vi.fn(),
122
+ updateData,
123
+ });
124
+
125
+ handler.initSort();
126
+ th.click();
127
+
128
+ expect(setState).toHaveBeenCalledWith('customField', 'asc');
129
+
130
+ handler.dispose();
131
+ });
132
+ });
133
+
134
+ describe('Custom sort comparator', () => {
135
+ it('column with customComparator uses provided function instead of default string compare', () => {
136
+ const sortValueFn = vi.fn(
137
+ (cellValue: string | number, rowData: KTDataTableDataInterface) => {
138
+ return typeof cellValue === 'string'
139
+ ? cellValue.length
140
+ : Number(cellValue);
141
+ },
142
+ );
143
+
144
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
145
+ config: {
146
+ columns: {
147
+ name: { sortValue: sortValueFn },
148
+ },
149
+ } as any,
150
+ theadElement: document.createElement('thead'),
151
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
152
+ setState: vi.fn(),
153
+ emit: vi.fn(),
154
+ updateData: vi.fn(),
155
+ });
156
+
157
+ const data = [
158
+ { name: 'Bob' },
159
+ { name: 'Alice' },
160
+ { name: 'Charlie' },
161
+ ];
162
+
163
+ const sorted = handler.sortData(data, 'name', 'asc');
164
+ expect(sortValueFn).toHaveBeenCalled();
165
+ // Bob(3) < Alice(5) < Charlie(7) by length
166
+ expect(sorted.map((r) => r.name)).toEqual(['Bob', 'Alice', 'Charlie']);
167
+ });
168
+
169
+ it('customComparator receives (cellValue, rowData) arguments', () => {
170
+ const receivedArgs: unknown[] = [];
171
+ const sortValueFn = vi.fn(
172
+ (cellValue: string | number, rowData: KTDataTableDataInterface) => {
173
+ receivedArgs.push({ cellValue, rowData });
174
+ return String(cellValue);
175
+ },
176
+ );
177
+
178
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
179
+ config: {
180
+ columns: {
181
+ name: { sortValue: sortValueFn },
182
+ },
183
+ } as any,
184
+ theadElement: document.createElement('thead'),
185
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
186
+ setState: vi.fn(),
187
+ emit: vi.fn(),
188
+ updateData: vi.fn(),
189
+ });
190
+
191
+ const data = [{ name: 'Alice' }, { name: 'Bob' }];
192
+ handler.sortData(data, 'name', 'asc');
193
+
194
+ expect(receivedArgs.length).toBeGreaterThan(0);
195
+ });
196
+ });
197
+
198
+ describe('HTML stripping in sort', () => {
199
+ it('sort column with HTML tags strips tags before comparing', () => {
200
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
201
+ config: {} as any,
202
+ theadElement: document.createElement('thead'),
203
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
204
+ setState: vi.fn(),
205
+ emit: vi.fn(),
206
+ updateData: vi.fn(),
207
+ });
208
+
209
+ const data = [
210
+ { name: '<b>Zoe</b>' },
211
+ { name: '<span>Alice</span>' },
212
+ { name: '<i>Bob</i>' },
213
+ ];
214
+
215
+ const sorted = handler.sortData(data, 'name', 'asc');
216
+ expect(sorted.map((r) => r.name)).toEqual([
217
+ '<span>Alice</span>',
218
+ '<i>Bob</i>',
219
+ '<b>Zoe</b>',
220
+ ]);
221
+ });
222
+
223
+ it('sort column with &nbsp; entities strips them before comparing', () => {
224
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
225
+ config: {} as any,
226
+ theadElement: document.createElement('thead'),
227
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
228
+ setState: vi.fn(),
229
+ emit: vi.fn(),
230
+ updateData: vi.fn(),
231
+ });
232
+
233
+ const data = [
234
+ { name: 'Charlie' },
235
+ { name: '&nbsp;Alice&nbsp;' },
236
+ { name: 'Bob' },
237
+ ];
238
+
239
+ const sorted = handler.sortData(data, 'name', 'asc');
240
+ // stripHtml removes &nbsp; → "Alice" sorts first
241
+ expect(sorted[0].name).toBe('&nbsp;Alice&nbsp;');
242
+ });
243
+ });
244
+
245
+ describe('Dispose', () => {
246
+ it('dispose() aborts AbortController', () => {
247
+ const { handler, thead, updateData } = createSortHandler();
248
+ handler.initSort();
249
+
250
+ handler.dispose();
251
+
252
+ // After dispose, clicking should not trigger updateData
253
+ updateData.mockClear();
254
+ const th = thead.querySelector('th')!;
255
+ th.click();
256
+ expect(updateData).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('dispose() called twice does not throw', () => {
260
+ const { handler } = createSortHandler();
261
+ handler.initSort();
262
+ handler.dispose();
263
+ expect(() => handler.dispose()).not.toThrow();
264
+ });
265
+ });
266
+
267
+ describe('initSort()', () => {
268
+ it('initSort() with no sortable columns does not throw', () => {
269
+ const thead = document.createElement('thead');
270
+ const tr = document.createElement('tr');
271
+ // No sort-icon class
272
+ const th = document.createElement('th');
273
+ th.setAttribute('data-kt-datatable-column', 'name');
274
+ th.textContent = 'Name';
275
+ tr.appendChild(th);
276
+ thead.appendChild(tr);
277
+
278
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
279
+ config: {
280
+ sort: { classes: { base: 'sort-icon' } },
281
+ } as any,
282
+ theadElement: thead,
283
+ getState: () => ({ sortField: '', sortOrder: '' }) as any,
284
+ setState: vi.fn(),
285
+ emit: vi.fn(),
286
+ updateData: vi.fn(),
287
+ });
288
+
289
+ expect(() => handler.initSort()).not.toThrow();
290
+ handler.dispose();
291
+ });
292
+
293
+ it('initSort() aborts previous AbortController before creating new one', () => {
294
+ const { handler, thead, updateData } = createSortHandler();
295
+ handler.initSort();
296
+
297
+ // Click on first th
298
+ const th = thead.querySelector('th')!;
299
+ th.click();
300
+ expect(updateData).toHaveBeenCalledTimes(1);
301
+
302
+ // Re-init
303
+ handler.initSort();
304
+ updateData.mockClear();
305
+
306
+ // Click again - should only fire once (new listener, old one aborted)
307
+ th.click();
308
+ expect(updateData).toHaveBeenCalledTimes(1);
309
+
310
+ handler.dispose();
311
+ });
312
+
313
+ it('initSort() adds sort indicator classes to active column', () => {
314
+ const { handler, thead } = createSortHandler(
315
+ {},
316
+ { sortField: 'name', sortOrder: 'asc' },
317
+ );
318
+ handler.initSort();
319
+
320
+ const th = thead.querySelector('th')!;
321
+ const sortElement = th.querySelector('.sort-icon') as HTMLElement;
322
+ expect(sortElement.className).toContain('asc');
323
+
324
+ handler.dispose();
325
+ });
326
+
327
+ it('initSort() with desc order adds desc class', () => {
328
+ const { handler, thead } = createSortHandler(
329
+ {},
330
+ { sortField: 'price', sortOrder: 'desc' },
331
+ );
332
+ handler.initSort();
333
+
334
+ const allTh = thead.querySelectorAll('th');
335
+ const priceTh = allTh[1];
336
+ const sortElement = priceTh.querySelector('.sort-icon') as HTMLElement;
337
+ expect(sortElement.className).toContain('desc');
338
+
339
+ handler.dispose();
340
+ });
341
+
342
+ it('initSort() with empty sort order resets classes', () => {
343
+ const { handler, thead } = createSortHandler(
344
+ {},
345
+ { sortField: '', sortOrder: '' },
346
+ );
347
+ handler.initSort();
348
+
349
+ const th = thead.querySelector('th')!;
350
+ const sortElement = th.querySelector('.sort-icon') as HTMLElement;
351
+ expect(sortElement.className).toBe('sort-icon');
352
+
353
+ handler.dispose();
354
+ });
355
+ });
356
+
357
+ describe('toggleSortOrder', () => {
358
+ it('returns asc when current field differs from new field', () => {
359
+ const { handler } = createSortHandler();
360
+ const result = handler.toggleSortOrder('name', 'asc', 'price');
361
+ expect(result).toBe('asc');
362
+ });
363
+
364
+ it('returns desc when current field is same and order is asc', () => {
365
+ const { handler } = createSortHandler();
366
+ const result = handler.toggleSortOrder('name', 'asc', 'name');
367
+ expect(result).toBe('desc');
368
+ });
369
+
370
+ it('returns empty string when current field is same and order is desc', () => {
371
+ const { handler } = createSortHandler();
372
+ const result = handler.toggleSortOrder('name', 'desc', 'name');
373
+ expect(result).toBe('');
374
+ });
375
+
376
+ it('returns asc when current field is same and order is empty', () => {
377
+ const { handler } = createSortHandler();
378
+ const result = handler.toggleSortOrder('name', '', 'name');
379
+ expect(result).toBe('asc');
380
+ });
381
+ });
382
+
383
+ describe('setSortIcon', () => {
384
+ it('sets aria-sort attribute on active column', () => {
385
+ const { handler, thead } = createSortHandler();
386
+ handler.setSortIcon('name', 'asc');
387
+
388
+ const th = thead.querySelector('th')!;
389
+ expect(th.getAttribute('aria-sort')).toBe('asc');
390
+ });
391
+
392
+ it('sets aria-sort to none on inactive columns', () => {
393
+ const { handler, thead } = createSortHandler();
394
+ handler.setSortIcon('name', 'asc');
395
+
396
+ const allTh = thead.querySelectorAll('th');
397
+ const priceTh = allTh[1];
398
+ expect(priceTh.getAttribute('aria-sort')).toBe('none');
399
+ });
400
+
401
+ it('sets aria-sort to none when sort order is empty', () => {
402
+ const { handler, thead } = createSortHandler();
403
+ handler.setSortIcon('name', '');
404
+
405
+ const th = thead.querySelector('th')!;
406
+ expect(th.getAttribute('aria-sort')).toBe('none');
407
+ });
408
+
409
+ it('sort by numeric index works correctly', () => {
410
+ const { handler, thead } = createSortHandler();
411
+ handler.setSortIcon(0 as any, 'asc');
412
+
413
+ const th = thead.querySelector('th')!;
414
+ expect(th.getAttribute('aria-sort')).toBe('asc');
415
+ });
416
+ });
417
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createSpinner } from '../datatable-spinner';
3
+
4
+ describe('createSpinner', () => {
5
+ let root: HTMLDivElement;
6
+ let table: HTMLTableElement;
7
+
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '';
10
+ root = document.createElement('div');
11
+ table = document.createElement('table');
12
+ root.appendChild(table);
13
+ document.body.appendChild(root);
14
+ });
15
+
16
+ it('should return an object with show, hide, and remove methods', () => {
17
+ const spinner = createSpinner();
18
+ expect(spinner).toHaveProperty('show');
19
+ expect(spinner).toHaveProperty('hide');
20
+ expect(spinner).toHaveProperty('remove');
21
+ });
22
+
23
+ it('should add loading class to root on show', () => {
24
+ const spinner = createSpinner();
25
+ spinner.show(root, { loadingClass: 'loading' }, table);
26
+ expect(root.classList.contains('loading')).toBe(true);
27
+ });
28
+
29
+ it('should remove loading class from root on hide', () => {
30
+ const spinner = createSpinner();
31
+ spinner.show(root, { loadingClass: 'loading' }, table);
32
+ spinner.hide(root, { loadingClass: 'loading' });
33
+ expect(root.classList.contains('loading')).toBe(false);
34
+ });
35
+
36
+ it('should create spinner element from template on show', () => {
37
+ const spinner = createSpinner();
38
+ const config = {
39
+ loading: {
40
+ template: '<div class="spinner">{content}</div>',
41
+ content: 'Loading...',
42
+ },
43
+ };
44
+ spinner.show(root, config, table);
45
+
46
+ const spinnerEl = table.querySelector('[data-kt-datatable-spinner]');
47
+ expect(spinnerEl).not.toBeNull();
48
+ expect(spinnerEl?.classList.contains('spinner')).toBe(true);
49
+ });
50
+
51
+ it('should show existing spinner from DOM', () => {
52
+ const existingSpinner = document.createElement('div');
53
+ existingSpinner.setAttribute('data-kt-datatable-spinner', 'true');
54
+ existingSpinner.style.display = 'none';
55
+ root.appendChild(existingSpinner);
56
+
57
+ const spinner = createSpinner();
58
+ spinner.show(root, { attributes: { spinner: '[data-kt-datatable-spinner]' } }, table);
59
+ expect(existingSpinner.style.display).toBe('block');
60
+ });
61
+
62
+ it('should hide spinner on hide', () => {
63
+ const existingSpinner = document.createElement('div');
64
+ existingSpinner.setAttribute('data-kt-datatable-spinner', 'true');
65
+ existingSpinner.style.display = 'block';
66
+ root.appendChild(existingSpinner);
67
+
68
+ const spinner = createSpinner();
69
+ spinner.hide(root, { attributes: { spinner: '[data-kt-datatable-spinner]' } });
70
+ expect(existingSpinner.style.display).toBe('none');
71
+ });
72
+
73
+ it('should remove spinner element from DOM', () => {
74
+ const spinnerEl = document.createElement('div');
75
+ spinnerEl.setAttribute('data-kt-datatable-spinner', 'true');
76
+ table.appendChild(spinnerEl);
77
+
78
+ const spinner = createSpinner();
79
+ spinner.remove(root, { attributes: { spinner: '[data-kt-datatable-spinner]' } });
80
+ expect(table.querySelector('[data-kt-datatable-spinner]')).toBeNull();
81
+ });
82
+
83
+ it('should handle null root gracefully', () => {
84
+ const spinner = createSpinner();
85
+ expect(() => spinner.show(null, {}, table)).not.toThrow();
86
+ expect(() => spinner.hide(null, {})).not.toThrow();
87
+ expect(() => spinner.remove(null, {})).not.toThrow();
88
+ });
89
+
90
+ it('should handle missing spinner element gracefully', () => {
91
+ const spinner = createSpinner();
92
+ expect(() => spinner.hide(root, {})).not.toThrow();
93
+ expect(() => spinner.remove(root, {})).not.toThrow();
94
+ });
95
+ });