@furystack/shades-common-components 10.0.35 → 11.0.0

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 (295) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/esm/components/animations.spec.d.ts +2 -0
  3. package/esm/components/animations.spec.d.ts.map +1 -0
  4. package/esm/components/animations.spec.js +201 -0
  5. package/esm/components/animations.spec.js.map +1 -0
  6. package/esm/components/app-bar-link.js +21 -20
  7. package/esm/components/app-bar-link.js.map +1 -1
  8. package/esm/components/app-bar-link.spec.d.ts +2 -0
  9. package/esm/components/app-bar-link.spec.d.ts.map +1 -0
  10. package/esm/components/app-bar-link.spec.js +252 -0
  11. package/esm/components/app-bar-link.spec.js.map +1 -0
  12. package/esm/components/app-bar.js +21 -21
  13. package/esm/components/app-bar.js.map +1 -1
  14. package/esm/components/app-bar.spec.d.ts +2 -0
  15. package/esm/components/app-bar.spec.d.ts.map +1 -0
  16. package/esm/components/app-bar.spec.js +117 -0
  17. package/esm/components/app-bar.spec.js.map +1 -0
  18. package/esm/components/avatar.d.ts.map +1 -1
  19. package/esm/components/avatar.js +15 -19
  20. package/esm/components/avatar.js.map +1 -1
  21. package/esm/components/avatar.spec.d.ts +2 -0
  22. package/esm/components/avatar.spec.d.ts.map +1 -0
  23. package/esm/components/avatar.spec.js +114 -0
  24. package/esm/components/avatar.spec.js.map +1 -0
  25. package/esm/components/button.d.ts.map +1 -1
  26. package/esm/components/button.js +145 -156
  27. package/esm/components/button.js.map +1 -1
  28. package/esm/components/button.spec.d.ts +2 -0
  29. package/esm/components/button.spec.d.ts.map +1 -0
  30. package/esm/components/button.spec.js +155 -0
  31. package/esm/components/button.spec.js.map +1 -0
  32. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  33. package/esm/components/command-palette/command-palette-input.js +18 -16
  34. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  35. package/esm/components/command-palette/command-palette-input.spec.d.ts +2 -0
  36. package/esm/components/command-palette/command-palette-input.spec.d.ts.map +1 -0
  37. package/esm/components/command-palette/command-palette-input.spec.js +233 -0
  38. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -0
  39. package/esm/components/command-palette/command-palette-manager.spec.d.ts +2 -0
  40. package/esm/components/command-palette/command-palette-manager.spec.d.ts.map +1 -0
  41. package/esm/components/command-palette/command-palette-manager.spec.js +362 -0
  42. package/esm/components/command-palette/command-palette-manager.spec.js.map +1 -0
  43. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  44. package/esm/components/command-palette/command-palette-suggestion-list.js +42 -46
  45. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  46. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts +2 -0
  47. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts.map +1 -0
  48. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +376 -0
  49. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -0
  50. package/esm/components/command-palette/index.d.ts.map +1 -1
  51. package/esm/components/command-palette/index.js +100 -110
  52. package/esm/components/command-palette/index.js.map +1 -1
  53. package/esm/components/command-palette/index.spec.d.ts +2 -0
  54. package/esm/components/command-palette/index.spec.d.ts.map +1 -0
  55. package/esm/components/command-palette/index.spec.js +509 -0
  56. package/esm/components/command-palette/index.spec.js.map +1 -0
  57. package/esm/components/data-grid/body.js +1 -1
  58. package/esm/components/data-grid/body.js.map +1 -1
  59. package/esm/components/data-grid/body.spec.d.ts +2 -0
  60. package/esm/components/data-grid/body.spec.d.ts.map +1 -0
  61. package/esm/components/data-grid/body.spec.js +228 -0
  62. package/esm/components/data-grid/body.spec.js.map +1 -0
  63. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  64. package/esm/components/data-grid/data-grid-row.js +49 -73
  65. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  66. package/esm/components/data-grid/data-grid-row.spec.d.ts +2 -0
  67. package/esm/components/data-grid/data-grid-row.spec.d.ts.map +1 -0
  68. package/esm/components/data-grid/data-grid-row.spec.js +296 -0
  69. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -0
  70. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  71. package/esm/components/data-grid/data-grid.js +35 -28
  72. package/esm/components/data-grid/data-grid.js.map +1 -1
  73. package/esm/components/data-grid/data-grid.spec.d.ts +2 -0
  74. package/esm/components/data-grid/data-grid.spec.d.ts.map +1 -0
  75. package/esm/components/data-grid/data-grid.spec.js +544 -0
  76. package/esm/components/data-grid/data-grid.spec.js.map +1 -0
  77. package/esm/components/data-grid/footer.js +21 -15
  78. package/esm/components/data-grid/footer.js.map +1 -1
  79. package/esm/components/data-grid/footer.spec.d.ts +2 -0
  80. package/esm/components/data-grid/footer.spec.d.ts.map +1 -0
  81. package/esm/components/data-grid/footer.spec.js +264 -0
  82. package/esm/components/data-grid/footer.spec.js.map +1 -0
  83. package/esm/components/data-grid/header.d.ts.map +1 -1
  84. package/esm/components/data-grid/header.js +55 -33
  85. package/esm/components/data-grid/header.js.map +1 -1
  86. package/esm/components/data-grid/header.spec.d.ts +2 -0
  87. package/esm/components/data-grid/header.spec.d.ts.map +1 -0
  88. package/esm/components/data-grid/header.spec.js +421 -0
  89. package/esm/components/data-grid/header.spec.js.map +1 -0
  90. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  91. package/esm/components/data-grid/selection-cell.js +13 -6
  92. package/esm/components/data-grid/selection-cell.js.map +1 -1
  93. package/esm/components/data-grid/selection-cell.spec.d.ts +2 -0
  94. package/esm/components/data-grid/selection-cell.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/selection-cell.spec.js +118 -0
  96. package/esm/components/data-grid/selection-cell.spec.js.map +1 -0
  97. package/esm/components/fab.d.ts.map +1 -1
  98. package/esm/components/fab.js +10 -1
  99. package/esm/components/fab.js.map +1 -1
  100. package/esm/components/fab.spec.d.ts +2 -0
  101. package/esm/components/fab.spec.d.ts.map +1 -0
  102. package/esm/components/fab.spec.js +95 -0
  103. package/esm/components/fab.spec.js.map +1 -0
  104. package/esm/components/form.spec.d.ts +2 -0
  105. package/esm/components/form.spec.d.ts.map +1 -0
  106. package/esm/components/form.spec.js +314 -0
  107. package/esm/components/form.spec.js.map +1 -0
  108. package/esm/components/grid.d.ts.map +1 -1
  109. package/esm/components/grid.js +40 -37
  110. package/esm/components/grid.js.map +1 -1
  111. package/esm/components/grid.spec.d.ts +2 -0
  112. package/esm/components/grid.spec.d.ts.map +1 -0
  113. package/esm/components/grid.spec.js +316 -0
  114. package/esm/components/grid.spec.js.map +1 -0
  115. package/esm/components/inputs/autocomplete.spec.d.ts +2 -0
  116. package/esm/components/inputs/autocomplete.spec.d.ts.map +1 -0
  117. package/esm/components/inputs/autocomplete.spec.js +194 -0
  118. package/esm/components/inputs/autocomplete.spec.js.map +1 -0
  119. package/esm/components/inputs/input.d.ts.map +1 -1
  120. package/esm/components/inputs/input.js +141 -109
  121. package/esm/components/inputs/input.js.map +1 -1
  122. package/esm/components/inputs/input.spec.d.ts +2 -0
  123. package/esm/components/inputs/input.spec.d.ts.map +1 -0
  124. package/esm/components/inputs/input.spec.js +577 -0
  125. package/esm/components/inputs/input.spec.js.map +1 -0
  126. package/esm/components/inputs/text-area.d.ts.map +1 -1
  127. package/esm/components/inputs/text-area.js +54 -58
  128. package/esm/components/inputs/text-area.js.map +1 -1
  129. package/esm/components/inputs/text-area.spec.d.ts +2 -0
  130. package/esm/components/inputs/text-area.spec.d.ts.map +1 -0
  131. package/esm/components/inputs/text-area.spec.js +214 -0
  132. package/esm/components/inputs/text-area.spec.js.map +1 -0
  133. package/esm/components/loader.js +1 -1
  134. package/esm/components/loader.js.map +1 -1
  135. package/esm/components/loader.spec.d.ts +2 -0
  136. package/esm/components/loader.spec.d.ts.map +1 -0
  137. package/esm/components/loader.spec.js +251 -0
  138. package/esm/components/loader.spec.js.map +1 -0
  139. package/esm/components/modal.d.ts.map +1 -1
  140. package/esm/components/modal.js +11 -9
  141. package/esm/components/modal.js.map +1 -1
  142. package/esm/components/modal.spec.d.ts +2 -0
  143. package/esm/components/modal.spec.d.ts.map +1 -0
  144. package/esm/components/modal.spec.js +227 -0
  145. package/esm/components/modal.spec.js.map +1 -0
  146. package/esm/components/noty-list.d.ts.map +1 -1
  147. package/esm/components/noty-list.js +39 -40
  148. package/esm/components/noty-list.js.map +1 -1
  149. package/esm/components/noty-list.spec.d.ts +2 -0
  150. package/esm/components/noty-list.spec.d.ts.map +1 -0
  151. package/esm/components/noty-list.spec.js +486 -0
  152. package/esm/components/noty-list.spec.js.map +1 -0
  153. package/esm/components/paper.d.ts.map +1 -1
  154. package/esm/components/paper.js +15 -12
  155. package/esm/components/paper.js.map +1 -1
  156. package/esm/components/paper.spec.d.ts +2 -0
  157. package/esm/components/paper.spec.d.ts.map +1 -0
  158. package/esm/components/paper.spec.js +63 -0
  159. package/esm/components/paper.spec.js.map +1 -0
  160. package/esm/components/skeleton.js +1 -1
  161. package/esm/components/skeleton.js.map +1 -1
  162. package/esm/components/skeleton.spec.d.ts +2 -0
  163. package/esm/components/skeleton.spec.d.ts.map +1 -0
  164. package/esm/components/skeleton.spec.js +159 -0
  165. package/esm/components/skeleton.spec.js.map +1 -0
  166. package/esm/components/styles.spec.d.ts +2 -0
  167. package/esm/components/styles.spec.d.ts.map +1 -0
  168. package/esm/components/styles.spec.js +56 -0
  169. package/esm/components/styles.spec.js.map +1 -0
  170. package/esm/components/suggest/index.d.ts.map +1 -1
  171. package/esm/components/suggest/index.js +74 -83
  172. package/esm/components/suggest/index.js.map +1 -1
  173. package/esm/components/suggest/index.spec.d.ts +2 -0
  174. package/esm/components/suggest/index.spec.d.ts.map +1 -0
  175. package/esm/components/suggest/index.spec.js +515 -0
  176. package/esm/components/suggest/index.spec.js.map +1 -0
  177. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  178. package/esm/components/suggest/suggest-input.js +16 -17
  179. package/esm/components/suggest/suggest-input.js.map +1 -1
  180. package/esm/components/suggest/suggest-input.spec.d.ts +2 -0
  181. package/esm/components/suggest/suggest-input.spec.d.ts.map +1 -0
  182. package/esm/components/suggest/suggest-input.spec.js +138 -0
  183. package/esm/components/suggest/suggest-input.spec.js.map +1 -0
  184. package/esm/components/suggest/suggest-manager.spec.d.ts +2 -0
  185. package/esm/components/suggest/suggest-manager.spec.d.ts.map +1 -0
  186. package/esm/components/suggest/suggest-manager.spec.js +308 -0
  187. package/esm/components/suggest/suggest-manager.spec.js.map +1 -0
  188. package/esm/components/suggest/suggestion-list.d.ts.map +1 -1
  189. package/esm/components/suggest/suggestion-list.js +43 -48
  190. package/esm/components/suggest/suggestion-list.js.map +1 -1
  191. package/esm/components/suggest/suggestion-list.spec.d.ts +2 -0
  192. package/esm/components/suggest/suggestion-list.spec.d.ts.map +1 -0
  193. package/esm/components/suggest/suggestion-list.spec.js +252 -0
  194. package/esm/components/suggest/suggestion-list.spec.js.map +1 -0
  195. package/esm/components/tabs.d.ts.map +1 -1
  196. package/esm/components/tabs.js +32 -18
  197. package/esm/components/tabs.js.map +1 -1
  198. package/esm/components/tabs.spec.d.ts +2 -0
  199. package/esm/components/tabs.spec.d.ts.map +1 -0
  200. package/esm/components/tabs.spec.js +187 -0
  201. package/esm/components/tabs.spec.js.map +1 -0
  202. package/esm/components/wizard/index.d.ts.map +1 -1
  203. package/esm/components/wizard/index.js +10 -7
  204. package/esm/components/wizard/index.js.map +1 -1
  205. package/esm/components/wizard/index.spec.d.ts +2 -0
  206. package/esm/components/wizard/index.spec.d.ts.map +1 -0
  207. package/esm/components/wizard/index.spec.js +171 -0
  208. package/esm/components/wizard/index.spec.js.map +1 -0
  209. package/esm/services/collection-service.spec.js +391 -2
  210. package/esm/services/collection-service.spec.js.map +1 -1
  211. package/esm/services/css-variable-theme.d.ts.map +1 -1
  212. package/esm/services/css-variable-theme.js +21 -1
  213. package/esm/services/css-variable-theme.js.map +1 -1
  214. package/esm/services/css-variable-theme.spec.d.ts +2 -0
  215. package/esm/services/css-variable-theme.spec.d.ts.map +1 -0
  216. package/esm/services/css-variable-theme.spec.js +169 -0
  217. package/esm/services/css-variable-theme.spec.js.map +1 -0
  218. package/esm/services/default-palette.d.ts +4 -0
  219. package/esm/services/default-palette.d.ts.map +1 -1
  220. package/esm/services/default-palette.js +22 -0
  221. package/esm/services/default-palette.js.map +1 -1
  222. package/esm/services/theme-provider-service.d.ts +59 -1
  223. package/esm/services/theme-provider-service.d.ts.map +1 -1
  224. package/esm/services/theme-provider-service.js.map +1 -1
  225. package/esm/services/theme-provider-service.spec.d.ts +2 -0
  226. package/esm/services/theme-provider-service.spec.d.ts.map +1 -0
  227. package/esm/services/theme-provider-service.spec.js +166 -0
  228. package/esm/services/theme-provider-service.spec.js.map +1 -0
  229. package/package.json +2 -2
  230. package/src/components/animations.spec.ts +299 -0
  231. package/src/components/app-bar-link.spec.tsx +341 -0
  232. package/src/components/app-bar-link.tsx +21 -21
  233. package/src/components/app-bar.spec.tsx +142 -0
  234. package/src/components/app-bar.tsx +22 -22
  235. package/src/components/avatar.spec.tsx +146 -0
  236. package/src/components/avatar.tsx +17 -20
  237. package/src/components/button.spec.tsx +193 -0
  238. package/src/components/button.tsx +162 -197
  239. package/src/components/command-palette/command-palette-input.spec.tsx +320 -0
  240. package/src/components/command-palette/command-palette-input.tsx +19 -22
  241. package/src/components/command-palette/command-palette-manager.spec.ts +470 -0
  242. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +499 -0
  243. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -46
  244. package/src/components/command-palette/index.spec.tsx +684 -0
  245. package/src/components/command-palette/index.tsx +107 -136
  246. package/src/components/data-grid/body.spec.tsx +340 -0
  247. package/src/components/data-grid/body.tsx +1 -1
  248. package/src/components/data-grid/data-grid-row.spec.tsx +382 -0
  249. package/src/components/data-grid/data-grid-row.tsx +50 -82
  250. package/src/components/data-grid/data-grid.spec.tsx +939 -0
  251. package/src/components/data-grid/data-grid.tsx +38 -35
  252. package/src/components/data-grid/footer.spec.tsx +344 -0
  253. package/src/components/data-grid/footer.tsx +19 -19
  254. package/src/components/data-grid/header.spec.tsx +563 -0
  255. package/src/components/data-grid/header.tsx +53 -44
  256. package/src/components/data-grid/selection-cell.spec.tsx +150 -0
  257. package/src/components/data-grid/selection-cell.tsx +12 -6
  258. package/src/components/fab.spec.tsx +108 -0
  259. package/src/components/fab.tsx +10 -1
  260. package/src/components/form.spec.tsx +481 -0
  261. package/src/components/grid.spec.tsx +334 -0
  262. package/src/components/grid.tsx +57 -63
  263. package/src/components/inputs/autocomplete.spec.tsx +258 -0
  264. package/src/components/inputs/input.spec.tsx +808 -0
  265. package/src/components/inputs/input.tsx +153 -139
  266. package/src/components/inputs/text-area.spec.tsx +285 -0
  267. package/src/components/inputs/text-area.tsx +53 -79
  268. package/src/components/loader.spec.tsx +346 -0
  269. package/src/components/loader.tsx +1 -1
  270. package/src/components/modal.spec.tsx +304 -0
  271. package/src/components/modal.tsx +11 -9
  272. package/src/components/noty-list.spec.tsx +631 -0
  273. package/src/components/noty-list.tsx +39 -50
  274. package/src/components/paper.spec.tsx +72 -0
  275. package/src/components/paper.tsx +15 -13
  276. package/src/components/skeleton.spec.tsx +219 -0
  277. package/src/components/skeleton.tsx +1 -1
  278. package/src/components/styles.spec.ts +70 -0
  279. package/src/components/suggest/index.spec.tsx +861 -0
  280. package/src/components/suggest/index.tsx +74 -101
  281. package/src/components/suggest/suggest-input.spec.tsx +181 -0
  282. package/src/components/suggest/suggest-input.tsx +16 -24
  283. package/src/components/suggest/suggest-manager.spec.ts +409 -0
  284. package/src/components/suggest/suggestion-list.spec.tsx +334 -0
  285. package/src/components/suggest/suggestion-list.tsx +43 -48
  286. package/src/components/tabs.spec.tsx +236 -0
  287. package/src/components/tabs.tsx +33 -21
  288. package/src/components/wizard/index.spec.tsx +224 -0
  289. package/src/components/wizard/index.tsx +10 -9
  290. package/src/services/collection-service.spec.ts +492 -3
  291. package/src/services/css-variable-theme.spec.ts +204 -0
  292. package/src/services/css-variable-theme.ts +21 -1
  293. package/src/services/default-palette.ts +22 -0
  294. package/src/services/theme-provider-service.spec.ts +195 -0
  295. package/src/services/theme-provider-service.ts +60 -2
@@ -0,0 +1,939 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { ObservableValue, sleepAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { CollectionService } from '../../services/collection-service.js'
6
+ import { DataGrid } from './data-grid.js'
7
+
8
+ type TestEntry = { id: number; name: string }
9
+
10
+ describe('DataGrid', () => {
11
+ beforeEach(() => {
12
+ document.body.innerHTML = '<div id="root"></div>'
13
+ })
14
+
15
+ afterEach(() => {
16
+ document.body.innerHTML = ''
17
+ })
18
+
19
+ const createTestService = () => {
20
+ const service = new CollectionService<TestEntry>()
21
+ service.data.setValue({
22
+ count: 3,
23
+ entries: [
24
+ { id: 1, name: 'First' },
25
+ { id: 2, name: 'Second' },
26
+ { id: 3, name: 'Third' },
27
+ ],
28
+ })
29
+ return service
30
+ }
31
+
32
+ describe('rendering', () => {
33
+ it('should render with columns', async () => {
34
+ const injector = new Injector()
35
+ const rootElement = document.getElementById('root') as HTMLDivElement
36
+ const service = createTestService()
37
+ const findOptions = new ObservableValue<any>({})
38
+
39
+ initializeShadeRoot({
40
+ injector,
41
+ rootElement,
42
+ jsxElement: (
43
+ <DataGrid<TestEntry, 'id' | 'name'>
44
+ columns={['id', 'name']}
45
+ collectionService={service}
46
+ findOptions={findOptions}
47
+ styles={{}}
48
+ headerComponents={{}}
49
+ rowComponents={{}}
50
+ />
51
+ ),
52
+ })
53
+
54
+ await sleepAsync(50)
55
+
56
+ const grid = document.querySelector('shade-data-grid')
57
+ expect(grid).not.toBeNull()
58
+
59
+ const headers = grid?.querySelectorAll('th')
60
+ expect(headers?.length).toBe(2)
61
+
62
+ service[Symbol.dispose]()
63
+ findOptions[Symbol.dispose]()
64
+ })
65
+
66
+ it('should render table structure', async () => {
67
+ const injector = new Injector()
68
+ const rootElement = document.getElementById('root') as HTMLDivElement
69
+ const service = createTestService()
70
+ const findOptions = new ObservableValue<any>({})
71
+
72
+ initializeShadeRoot({
73
+ injector,
74
+ rootElement,
75
+ jsxElement: (
76
+ <DataGrid<TestEntry, 'id' | 'name'>
77
+ columns={['id', 'name']}
78
+ collectionService={service}
79
+ findOptions={findOptions}
80
+ styles={{}}
81
+ headerComponents={{}}
82
+ rowComponents={{}}
83
+ />
84
+ ),
85
+ })
86
+
87
+ await sleepAsync(50)
88
+
89
+ const grid = document.querySelector('shade-data-grid')
90
+ const table = grid?.querySelector('table')
91
+ const thead = grid?.querySelector('thead')
92
+ const tbody = grid?.querySelector('tbody')
93
+
94
+ expect(table).not.toBeNull()
95
+ expect(thead).not.toBeNull()
96
+ expect(tbody).not.toBeNull()
97
+
98
+ service[Symbol.dispose]()
99
+ findOptions[Symbol.dispose]()
100
+ })
101
+
102
+ it('should render custom header components when provided', async () => {
103
+ const injector = new Injector()
104
+ const rootElement = document.getElementById('root') as HTMLDivElement
105
+ const service = createTestService()
106
+ const findOptions = new ObservableValue<any>({})
107
+
108
+ initializeShadeRoot({
109
+ injector,
110
+ rootElement,
111
+ jsxElement: (
112
+ <DataGrid<TestEntry, 'id' | 'name'>
113
+ columns={['id', 'name']}
114
+ collectionService={service}
115
+ findOptions={findOptions}
116
+ styles={{}}
117
+ headerComponents={{
118
+ id: () => <span data-testid="custom-header-id">Custom ID Header</span>,
119
+ }}
120
+ rowComponents={{}}
121
+ />
122
+ ),
123
+ })
124
+
125
+ await sleepAsync(50)
126
+
127
+ const grid = document.querySelector('shade-data-grid')
128
+ const customHeader = grid?.querySelector('[data-testid="custom-header-id"]')
129
+ expect(customHeader).not.toBeNull()
130
+ expect(customHeader?.textContent).toBe('Custom ID Header')
131
+
132
+ service[Symbol.dispose]()
133
+ findOptions[Symbol.dispose]()
134
+ })
135
+
136
+ it('should render default header components from headerComponents.default', async () => {
137
+ const injector = new Injector()
138
+ const rootElement = document.getElementById('root') as HTMLDivElement
139
+ const service = createTestService()
140
+ const findOptions = new ObservableValue<any>({})
141
+
142
+ initializeShadeRoot({
143
+ injector,
144
+ rootElement,
145
+ jsxElement: (
146
+ <DataGrid<TestEntry, 'id' | 'name'>
147
+ columns={['id', 'name']}
148
+ collectionService={service}
149
+ findOptions={findOptions}
150
+ styles={{}}
151
+ headerComponents={{
152
+ default: (name) => <span data-testid={`default-header-${name}`}>Default: {name}</span>,
153
+ }}
154
+ rowComponents={{}}
155
+ />
156
+ ),
157
+ })
158
+
159
+ await sleepAsync(50)
160
+
161
+ const grid = document.querySelector('shade-data-grid')
162
+ const defaultHeaderId = grid?.querySelector('[data-testid="default-header-id"]')
163
+ const defaultHeaderName = grid?.querySelector('[data-testid="default-header-name"]')
164
+
165
+ expect(defaultHeaderId?.textContent).toBe('Default: id')
166
+ expect(defaultHeaderName?.textContent).toBe('Default: name')
167
+
168
+ service[Symbol.dispose]()
169
+ findOptions[Symbol.dispose]()
170
+ })
171
+
172
+ it('should render DataGridHeader when no custom header is provided', async () => {
173
+ const injector = new Injector()
174
+ const rootElement = document.getElementById('root') as HTMLDivElement
175
+ const service = createTestService()
176
+ const findOptions = new ObservableValue<any>({})
177
+
178
+ initializeShadeRoot({
179
+ injector,
180
+ rootElement,
181
+ jsxElement: (
182
+ <DataGrid<TestEntry, 'id' | 'name'>
183
+ columns={['id', 'name']}
184
+ collectionService={service}
185
+ findOptions={findOptions}
186
+ styles={{}}
187
+ headerComponents={{}}
188
+ rowComponents={{}}
189
+ />
190
+ ),
191
+ })
192
+
193
+ await sleepAsync(50)
194
+
195
+ const grid = document.querySelector('shade-data-grid')
196
+ const defaultHeaders = grid?.querySelectorAll('data-grid-header')
197
+ expect(defaultHeaders?.length).toBe(2)
198
+
199
+ service[Symbol.dispose]()
200
+ findOptions[Symbol.dispose]()
201
+ })
202
+ })
203
+
204
+ describe('focus management', () => {
205
+ it('should set focus on click', async () => {
206
+ const injector = new Injector()
207
+ const rootElement = document.getElementById('root') as HTMLDivElement
208
+ const service = createTestService()
209
+ const findOptions = new ObservableValue<any>({})
210
+
211
+ expect(service.hasFocus.getValue()).toBe(false)
212
+
213
+ initializeShadeRoot({
214
+ injector,
215
+ rootElement,
216
+ jsxElement: (
217
+ <DataGrid<TestEntry, 'id' | 'name'>
218
+ columns={['id', 'name']}
219
+ collectionService={service}
220
+ findOptions={findOptions}
221
+ styles={{}}
222
+ headerComponents={{}}
223
+ rowComponents={{}}
224
+ />
225
+ ),
226
+ })
227
+
228
+ await sleepAsync(50)
229
+
230
+ const grid = document.querySelector('shade-data-grid')
231
+ const wrapper = grid?.querySelector('.shade-grid-wrapper') as HTMLElement
232
+
233
+ wrapper?.click()
234
+
235
+ expect(service.hasFocus.getValue()).toBe(true)
236
+
237
+ service[Symbol.dispose]()
238
+ findOptions[Symbol.dispose]()
239
+ })
240
+
241
+ it('should lose focus on click outside', async () => {
242
+ const injector = new Injector()
243
+ const rootElement = document.getElementById('root') as HTMLDivElement
244
+ const service = createTestService()
245
+ const findOptions = new ObservableValue<any>({})
246
+
247
+ initializeShadeRoot({
248
+ injector,
249
+ rootElement,
250
+ jsxElement: (
251
+ <>
252
+ <div data-testid="outside">Outside</div>
253
+ <DataGrid<TestEntry, 'id' | 'name'>
254
+ columns={['id', 'name']}
255
+ collectionService={service}
256
+ findOptions={findOptions}
257
+ styles={{}}
258
+ headerComponents={{}}
259
+ rowComponents={{}}
260
+ />
261
+ </>
262
+ ),
263
+ })
264
+
265
+ await sleepAsync(50)
266
+
267
+ const grid = document.querySelector('shade-data-grid')
268
+ const wrapper = grid?.querySelector('.shade-grid-wrapper') as HTMLElement
269
+ wrapper?.click()
270
+
271
+ expect(service.hasFocus.getValue()).toBe(true)
272
+
273
+ const outside = document.querySelector('[data-testid="outside"]') as HTMLElement
274
+ outside?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
275
+
276
+ expect(service.hasFocus.getValue()).toBe(false)
277
+
278
+ service[Symbol.dispose]()
279
+ findOptions[Symbol.dispose]()
280
+ })
281
+ })
282
+
283
+ describe('keyboard navigation', () => {
284
+ it('should handle ArrowDown to move focus to next entry', async () => {
285
+ const injector = new Injector()
286
+ const rootElement = document.getElementById('root') as HTMLDivElement
287
+ const service = createTestService()
288
+ const findOptions = new ObservableValue<any>({})
289
+
290
+ service.hasFocus.setValue(true)
291
+ service.focusedEntry.setValue(service.data.getValue().entries[0])
292
+
293
+ initializeShadeRoot({
294
+ injector,
295
+ rootElement,
296
+ jsxElement: (
297
+ <DataGrid<TestEntry, 'id' | 'name'>
298
+ columns={['id', 'name']}
299
+ collectionService={service}
300
+ findOptions={findOptions}
301
+ styles={{}}
302
+ headerComponents={{}}
303
+ rowComponents={{}}
304
+ />
305
+ ),
306
+ })
307
+
308
+ await sleepAsync(50)
309
+
310
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
311
+ window.dispatchEvent(keydownEvent)
312
+
313
+ expect(service.focusedEntry.getValue()).toEqual({ id: 2, name: 'Second' })
314
+
315
+ service[Symbol.dispose]()
316
+ findOptions[Symbol.dispose]()
317
+ })
318
+
319
+ it('should handle ArrowUp to move focus to previous entry', async () => {
320
+ const injector = new Injector()
321
+ const rootElement = document.getElementById('root') as HTMLDivElement
322
+ const service = createTestService()
323
+ const findOptions = new ObservableValue<any>({})
324
+
325
+ service.hasFocus.setValue(true)
326
+ service.focusedEntry.setValue(service.data.getValue().entries[1])
327
+
328
+ initializeShadeRoot({
329
+ injector,
330
+ rootElement,
331
+ jsxElement: (
332
+ <DataGrid<TestEntry, 'id' | 'name'>
333
+ columns={['id', 'name']}
334
+ collectionService={service}
335
+ findOptions={findOptions}
336
+ styles={{}}
337
+ headerComponents={{}}
338
+ rowComponents={{}}
339
+ />
340
+ ),
341
+ })
342
+
343
+ await sleepAsync(50)
344
+
345
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
346
+ window.dispatchEvent(keydownEvent)
347
+
348
+ expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
349
+
350
+ service[Symbol.dispose]()
351
+ findOptions[Symbol.dispose]()
352
+ })
353
+
354
+ it('should handle Home to move focus to first entry', async () => {
355
+ const injector = new Injector()
356
+ const rootElement = document.getElementById('root') as HTMLDivElement
357
+ const service = createTestService()
358
+ const findOptions = new ObservableValue<any>({})
359
+
360
+ service.hasFocus.setValue(true)
361
+ service.focusedEntry.setValue(service.data.getValue().entries[2])
362
+
363
+ initializeShadeRoot({
364
+ injector,
365
+ rootElement,
366
+ jsxElement: (
367
+ <DataGrid<TestEntry, 'id' | 'name'>
368
+ columns={['id', 'name']}
369
+ collectionService={service}
370
+ findOptions={findOptions}
371
+ styles={{}}
372
+ headerComponents={{}}
373
+ rowComponents={{}}
374
+ />
375
+ ),
376
+ })
377
+
378
+ await sleepAsync(50)
379
+
380
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'Home', bubbles: true })
381
+ window.dispatchEvent(keydownEvent)
382
+
383
+ expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
384
+
385
+ service[Symbol.dispose]()
386
+ findOptions[Symbol.dispose]()
387
+ })
388
+
389
+ it('should handle End to move focus to last entry', async () => {
390
+ const injector = new Injector()
391
+ const rootElement = document.getElementById('root') as HTMLDivElement
392
+ const service = createTestService()
393
+ const findOptions = new ObservableValue<any>({})
394
+
395
+ service.hasFocus.setValue(true)
396
+ service.focusedEntry.setValue(service.data.getValue().entries[0])
397
+
398
+ initializeShadeRoot({
399
+ injector,
400
+ rootElement,
401
+ jsxElement: (
402
+ <DataGrid<TestEntry, 'id' | 'name'>
403
+ columns={['id', 'name']}
404
+ collectionService={service}
405
+ findOptions={findOptions}
406
+ styles={{}}
407
+ headerComponents={{}}
408
+ rowComponents={{}}
409
+ />
410
+ ),
411
+ })
412
+
413
+ await sleepAsync(50)
414
+
415
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'End', bubbles: true })
416
+ window.dispatchEvent(keydownEvent)
417
+
418
+ expect(service.focusedEntry.getValue()).toEqual({ id: 3, name: 'Third' })
419
+
420
+ service[Symbol.dispose]()
421
+ findOptions[Symbol.dispose]()
422
+ })
423
+
424
+ it('should handle Tab to toggle focus', async () => {
425
+ const injector = new Injector()
426
+ const rootElement = document.getElementById('root') as HTMLDivElement
427
+ const service = createTestService()
428
+ const findOptions = new ObservableValue<any>({})
429
+
430
+ service.hasFocus.setValue(true)
431
+
432
+ initializeShadeRoot({
433
+ injector,
434
+ rootElement,
435
+ jsxElement: (
436
+ <DataGrid<TestEntry, 'id' | 'name'>
437
+ columns={['id', 'name']}
438
+ collectionService={service}
439
+ findOptions={findOptions}
440
+ styles={{}}
441
+ headerComponents={{}}
442
+ rowComponents={{}}
443
+ />
444
+ ),
445
+ })
446
+
447
+ await sleepAsync(50)
448
+
449
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
450
+ window.dispatchEvent(keydownEvent)
451
+
452
+ expect(service.hasFocus.getValue()).toBe(false)
453
+
454
+ service[Symbol.dispose]()
455
+ findOptions[Symbol.dispose]()
456
+ })
457
+
458
+ it('should handle Escape to clear selection and search', async () => {
459
+ const injector = new Injector()
460
+ const rootElement = document.getElementById('root') as HTMLDivElement
461
+ const service = createTestService()
462
+ const findOptions = new ObservableValue<any>({})
463
+
464
+ const { entries } = service.data.getValue()
465
+ service.hasFocus.setValue(true)
466
+ service.selection.setValue([entries[0], entries[1]])
467
+ service.searchTerm.setValue('test')
468
+
469
+ initializeShadeRoot({
470
+ injector,
471
+ rootElement,
472
+ jsxElement: (
473
+ <DataGrid<TestEntry, 'id' | 'name'>
474
+ columns={['id', 'name']}
475
+ collectionService={service}
476
+ findOptions={findOptions}
477
+ styles={{}}
478
+ headerComponents={{}}
479
+ rowComponents={{}}
480
+ />
481
+ ),
482
+ })
483
+
484
+ await sleepAsync(50)
485
+
486
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
487
+ window.dispatchEvent(keydownEvent)
488
+
489
+ expect(service.selection.getValue()).toEqual([])
490
+ expect(service.searchTerm.getValue()).toBe('')
491
+
492
+ service[Symbol.dispose]()
493
+ findOptions[Symbol.dispose]()
494
+ })
495
+
496
+ it('should handle Space to toggle selection of focused entry', async () => {
497
+ const injector = new Injector()
498
+ const rootElement = document.getElementById('root') as HTMLDivElement
499
+ const service = createTestService()
500
+ const findOptions = new ObservableValue<any>({})
501
+
502
+ const { entries } = service.data.getValue()
503
+ service.hasFocus.setValue(true)
504
+ service.focusedEntry.setValue(entries[0])
505
+
506
+ initializeShadeRoot({
507
+ injector,
508
+ rootElement,
509
+ jsxElement: (
510
+ <DataGrid<TestEntry, 'id' | 'name'>
511
+ columns={['id', 'name']}
512
+ collectionService={service}
513
+ findOptions={findOptions}
514
+ styles={{}}
515
+ headerComponents={{}}
516
+ rowComponents={{}}
517
+ />
518
+ ),
519
+ })
520
+
521
+ await sleepAsync(50)
522
+
523
+ const keydownEvent = new KeyboardEvent('keydown', { key: ' ', bubbles: true })
524
+ window.dispatchEvent(keydownEvent)
525
+
526
+ expect(service.selection.getValue()).toContain(entries[0])
527
+
528
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
529
+ expect(service.selection.getValue()).not.toContain(entries[0])
530
+
531
+ service[Symbol.dispose]()
532
+ findOptions[Symbol.dispose]()
533
+ })
534
+
535
+ it('should handle + to select all entries', async () => {
536
+ const injector = new Injector()
537
+ const rootElement = document.getElementById('root') as HTMLDivElement
538
+ const service = createTestService()
539
+ const findOptions = new ObservableValue<any>({})
540
+
541
+ service.hasFocus.setValue(true)
542
+
543
+ initializeShadeRoot({
544
+ injector,
545
+ rootElement,
546
+ jsxElement: (
547
+ <DataGrid<TestEntry, 'id' | 'name'>
548
+ columns={['id', 'name']}
549
+ collectionService={service}
550
+ findOptions={findOptions}
551
+ styles={{}}
552
+ headerComponents={{}}
553
+ rowComponents={{}}
554
+ />
555
+ ),
556
+ })
557
+
558
+ await sleepAsync(50)
559
+
560
+ const keydownEvent = new KeyboardEvent('keydown', { key: '+', bubbles: true })
561
+ window.dispatchEvent(keydownEvent)
562
+
563
+ expect(service.selection.getValue().length).toBe(3)
564
+
565
+ service[Symbol.dispose]()
566
+ findOptions[Symbol.dispose]()
567
+ })
568
+
569
+ it('should handle - to deselect all entries', async () => {
570
+ const injector = new Injector()
571
+ const rootElement = document.getElementById('root') as HTMLDivElement
572
+ const service = createTestService()
573
+ const findOptions = new ObservableValue<any>({})
574
+
575
+ const { entries } = service.data.getValue()
576
+ service.hasFocus.setValue(true)
577
+ service.selection.setValue([...entries])
578
+
579
+ initializeShadeRoot({
580
+ injector,
581
+ rootElement,
582
+ jsxElement: (
583
+ <DataGrid<TestEntry, 'id' | 'name'>
584
+ columns={['id', 'name']}
585
+ collectionService={service}
586
+ findOptions={findOptions}
587
+ styles={{}}
588
+ headerComponents={{}}
589
+ rowComponents={{}}
590
+ />
591
+ ),
592
+ })
593
+
594
+ await sleepAsync(50)
595
+
596
+ const keydownEvent = new KeyboardEvent('keydown', { key: '-', bubbles: true })
597
+ window.dispatchEvent(keydownEvent)
598
+
599
+ expect(service.selection.getValue().length).toBe(0)
600
+
601
+ service[Symbol.dispose]()
602
+ findOptions[Symbol.dispose]()
603
+ })
604
+
605
+ it('should handle * to invert selection', async () => {
606
+ const injector = new Injector()
607
+ const rootElement = document.getElementById('root') as HTMLDivElement
608
+ const service = createTestService()
609
+ const findOptions = new ObservableValue<any>({})
610
+
611
+ const { entries } = service.data.getValue()
612
+ service.hasFocus.setValue(true)
613
+ service.selection.setValue([entries[0]])
614
+
615
+ initializeShadeRoot({
616
+ injector,
617
+ rootElement,
618
+ jsxElement: (
619
+ <DataGrid<TestEntry, 'id' | 'name'>
620
+ columns={['id', 'name']}
621
+ collectionService={service}
622
+ findOptions={findOptions}
623
+ styles={{}}
624
+ headerComponents={{}}
625
+ rowComponents={{}}
626
+ />
627
+ ),
628
+ })
629
+
630
+ await sleepAsync(50)
631
+
632
+ const keydownEvent = new KeyboardEvent('keydown', { key: '*', bubbles: true })
633
+ window.dispatchEvent(keydownEvent)
634
+
635
+ const selection = service.selection.getValue()
636
+ expect(selection).not.toContain(entries[0])
637
+ expect(selection).toContain(entries[1])
638
+ expect(selection).toContain(entries[2])
639
+
640
+ service[Symbol.dispose]()
641
+ findOptions[Symbol.dispose]()
642
+ })
643
+
644
+ it('should not handle keyboard when not focused', async () => {
645
+ const injector = new Injector()
646
+ const rootElement = document.getElementById('root') as HTMLDivElement
647
+ const service = createTestService()
648
+ const findOptions = new ObservableValue<any>({})
649
+
650
+ service.hasFocus.setValue(false)
651
+ service.focusedEntry.setValue(service.data.getValue().entries[0])
652
+
653
+ initializeShadeRoot({
654
+ injector,
655
+ rootElement,
656
+ jsxElement: (
657
+ <DataGrid<TestEntry, 'id' | 'name'>
658
+ columns={['id', 'name']}
659
+ collectionService={service}
660
+ findOptions={findOptions}
661
+ styles={{}}
662
+ headerComponents={{}}
663
+ rowComponents={{}}
664
+ />
665
+ ),
666
+ })
667
+
668
+ await sleepAsync(50)
669
+
670
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
671
+ window.dispatchEvent(keydownEvent)
672
+
673
+ expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
674
+
675
+ service[Symbol.dispose]()
676
+ findOptions[Symbol.dispose]()
677
+ })
678
+
679
+ it('should handle Insert to toggle selection and move to next', async () => {
680
+ const injector = new Injector()
681
+ const rootElement = document.getElementById('root') as HTMLDivElement
682
+ const service = createTestService()
683
+ const findOptions = new ObservableValue<any>({})
684
+
685
+ const { entries } = service.data.getValue()
686
+ service.hasFocus.setValue(true)
687
+ service.focusedEntry.setValue(entries[0])
688
+
689
+ initializeShadeRoot({
690
+ injector,
691
+ rootElement,
692
+ jsxElement: (
693
+ <DataGrid<TestEntry, 'id' | 'name'>
694
+ columns={['id', 'name']}
695
+ collectionService={service}
696
+ findOptions={findOptions}
697
+ styles={{}}
698
+ headerComponents={{}}
699
+ rowComponents={{}}
700
+ />
701
+ ),
702
+ })
703
+
704
+ await sleepAsync(50)
705
+
706
+ const keydownEvent = new KeyboardEvent('keydown', { key: 'Insert', bubbles: true })
707
+ window.dispatchEvent(keydownEvent)
708
+
709
+ expect(service.selection.getValue()).toContain(entries[0])
710
+ expect(service.focusedEntry.getValue()).toEqual(entries[1])
711
+
712
+ service[Symbol.dispose]()
713
+ findOptions[Symbol.dispose]()
714
+ })
715
+ })
716
+
717
+ describe('styles', () => {
718
+ it('should apply wrapper styles when provided', async () => {
719
+ const injector = new Injector()
720
+ const rootElement = document.getElementById('root') as HTMLDivElement
721
+ const service = createTestService()
722
+ const findOptions = new ObservableValue<any>({})
723
+
724
+ initializeShadeRoot({
725
+ injector,
726
+ rootElement,
727
+ jsxElement: (
728
+ <DataGrid<TestEntry, 'id' | 'name'>
729
+ columns={['id', 'name']}
730
+ collectionService={service}
731
+ findOptions={findOptions}
732
+ styles={{
733
+ wrapper: { backgroundColor: 'red' },
734
+ }}
735
+ headerComponents={{}}
736
+ rowComponents={{}}
737
+ />
738
+ ),
739
+ })
740
+
741
+ await sleepAsync(50)
742
+
743
+ const grid = document.querySelector('shade-data-grid') as HTMLElement
744
+ expect(grid?.style.backgroundColor).toBe('red')
745
+
746
+ service[Symbol.dispose]()
747
+ findOptions[Symbol.dispose]()
748
+ })
749
+
750
+ it('should apply header styles when provided', async () => {
751
+ const injector = new Injector()
752
+ const rootElement = document.getElementById('root') as HTMLDivElement
753
+ const service = createTestService()
754
+ const findOptions = new ObservableValue<any>({})
755
+
756
+ initializeShadeRoot({
757
+ injector,
758
+ rootElement,
759
+ jsxElement: (
760
+ <DataGrid<TestEntry, 'id' | 'name'>
761
+ columns={['id', 'name']}
762
+ collectionService={service}
763
+ findOptions={findOptions}
764
+ styles={{
765
+ header: { color: 'blue' },
766
+ }}
767
+ headerComponents={{}}
768
+ rowComponents={{}}
769
+ />
770
+ ),
771
+ })
772
+
773
+ await sleepAsync(50)
774
+
775
+ const grid = document.querySelector('shade-data-grid')
776
+ const headers = grid?.querySelectorAll('th') as NodeListOf<HTMLElement>
777
+ expect(headers?.[0]?.style.color).toBe('blue')
778
+
779
+ service[Symbol.dispose]()
780
+ findOptions[Symbol.dispose]()
781
+ })
782
+ })
783
+
784
+ describe('empty and loading states', () => {
785
+ it('should show empty component when no data', async () => {
786
+ const injector = new Injector()
787
+ const rootElement = document.getElementById('root') as HTMLDivElement
788
+ const service = new CollectionService<TestEntry>()
789
+ const findOptions = new ObservableValue<any>({})
790
+
791
+ initializeShadeRoot({
792
+ injector,
793
+ rootElement,
794
+ jsxElement: (
795
+ <DataGrid<TestEntry, 'id' | 'name'>
796
+ columns={['id', 'name']}
797
+ collectionService={service}
798
+ findOptions={findOptions}
799
+ styles={{}}
800
+ headerComponents={{}}
801
+ rowComponents={{}}
802
+ emptyComponent={<div data-testid="empty-state">No data available</div>}
803
+ />
804
+ ),
805
+ })
806
+
807
+ await sleepAsync(50)
808
+
809
+ const grid = document.querySelector('shade-data-grid')
810
+ const emptyState = grid?.querySelector('[data-testid="empty-state"]')
811
+ expect(emptyState).not.toBeNull()
812
+ expect(emptyState?.textContent).toBe('No data available')
813
+
814
+ service[Symbol.dispose]()
815
+ findOptions[Symbol.dispose]()
816
+ })
817
+ })
818
+
819
+ describe('row interactions', () => {
820
+ it('should pass row click to collectionService', async () => {
821
+ const injector = new Injector()
822
+ const rootElement = document.getElementById('root') as HTMLDivElement
823
+ const onRowClick = vi.fn()
824
+ const service = new CollectionService<TestEntry>({ onRowClick })
825
+ const findOptions = new ObservableValue<any>({})
826
+
827
+ service.data.setValue({
828
+ count: 1,
829
+ entries: [{ id: 1, name: 'Test' }],
830
+ })
831
+
832
+ initializeShadeRoot({
833
+ injector,
834
+ rootElement,
835
+ jsxElement: (
836
+ <DataGrid<TestEntry, 'id' | 'name'>
837
+ columns={['id', 'name']}
838
+ collectionService={service}
839
+ findOptions={findOptions}
840
+ styles={{}}
841
+ headerComponents={{}}
842
+ rowComponents={{}}
843
+ />
844
+ ),
845
+ })
846
+
847
+ await sleepAsync(50)
848
+
849
+ const grid = document.querySelector('shade-data-grid')
850
+ const cell = grid?.querySelector('td') as HTMLTableCellElement
851
+ cell?.click()
852
+
853
+ expect(onRowClick).toHaveBeenCalledWith({ id: 1, name: 'Test' })
854
+
855
+ service[Symbol.dispose]()
856
+ findOptions[Symbol.dispose]()
857
+ })
858
+
859
+ it('should pass row double click to collectionService', async () => {
860
+ const injector = new Injector()
861
+ const rootElement = document.getElementById('root') as HTMLDivElement
862
+ const onRowDoubleClick = vi.fn()
863
+ const service = new CollectionService<TestEntry>({ onRowDoubleClick })
864
+ const findOptions = new ObservableValue<any>({})
865
+
866
+ service.data.setValue({
867
+ count: 1,
868
+ entries: [{ id: 1, name: 'Test' }],
869
+ })
870
+
871
+ initializeShadeRoot({
872
+ injector,
873
+ rootElement,
874
+ jsxElement: (
875
+ <DataGrid<TestEntry, 'id' | 'name'>
876
+ columns={['id', 'name']}
877
+ collectionService={service}
878
+ findOptions={findOptions}
879
+ styles={{}}
880
+ headerComponents={{}}
881
+ rowComponents={{}}
882
+ />
883
+ ),
884
+ })
885
+
886
+ await sleepAsync(50)
887
+
888
+ const grid = document.querySelector('shade-data-grid')
889
+ const cell = grid?.querySelector('td') as HTMLTableCellElement
890
+ const dblClickEvent = new MouseEvent('dblclick', { bubbles: true })
891
+ cell?.dispatchEvent(dblClickEvent)
892
+
893
+ expect(onRowDoubleClick).toHaveBeenCalledWith({ id: 1, name: 'Test' })
894
+
895
+ service[Symbol.dispose]()
896
+ findOptions[Symbol.dispose]()
897
+ })
898
+ })
899
+
900
+ describe('keyboard listener cleanup', () => {
901
+ it('should remove keyboard listener when component is disconnected', async () => {
902
+ const injector = new Injector()
903
+ const rootElement = document.getElementById('root') as HTMLDivElement
904
+ const service = createTestService()
905
+ const findOptions = new ObservableValue<any>({})
906
+
907
+ service.hasFocus.setValue(true)
908
+ service.focusedEntry.setValue(service.data.getValue().entries[0])
909
+
910
+ initializeShadeRoot({
911
+ injector,
912
+ rootElement,
913
+ jsxElement: (
914
+ <DataGrid<TestEntry, 'id' | 'name'>
915
+ columns={['id', 'name']}
916
+ collectionService={service}
917
+ findOptions={findOptions}
918
+ styles={{}}
919
+ headerComponents={{}}
920
+ rowComponents={{}}
921
+ />
922
+ ),
923
+ })
924
+
925
+ await sleepAsync(50)
926
+
927
+ const grid = document.querySelector('shade-data-grid') as HTMLElement
928
+ grid.remove()
929
+
930
+ await sleepAsync(10)
931
+
932
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
933
+ expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
934
+
935
+ service[Symbol.dispose]()
936
+ findOptions[Symbol.dispose]()
937
+ })
938
+ })
939
+ })