@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,382 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { CollectionService } from '../../services/collection-service.js'
6
+ import { DataGridRow } from './data-grid-row.js'
7
+
8
+ type TestEntry = { id: number; name: string }
9
+
10
+ describe('DataGridRow', () => {
11
+ beforeEach(() => {
12
+ document.body.innerHTML = '<div id="root"></div>'
13
+ // Mock scrollTo for jsdom
14
+ Element.prototype.scrollTo = vi.fn()
15
+ })
16
+
17
+ afterEach(() => {
18
+ document.body.innerHTML = ''
19
+ })
20
+
21
+ const renderRow = async (props: {
22
+ entry: TestEntry
23
+ service: CollectionService<TestEntry>
24
+ columns?: Array<'id' | 'name'>
25
+ onRowClick?: (row: TestEntry, event: MouseEvent) => void
26
+ onRowDoubleClick?: (row: TestEntry, event: MouseEvent) => void
27
+ focusedRowStyle?: Partial<CSSStyleDeclaration>
28
+ selectedRowStyle?: Partial<CSSStyleDeclaration>
29
+ unfocusedRowStyle?: Partial<CSSStyleDeclaration>
30
+ unselectedRowStyle?: Partial<CSSStyleDeclaration>
31
+ rowComponents?: Record<string, (entry: TestEntry) => JSX.Element>
32
+ }) => {
33
+ const injector = new Injector()
34
+ const root = document.getElementById('root')!
35
+
36
+ initializeShadeRoot({
37
+ injector,
38
+ rootElement: root,
39
+ jsxElement: (
40
+ <div className="shade-grid-wrapper" style={{ overflow: 'auto', height: '200px' }}>
41
+ <table>
42
+ <thead>
43
+ <tr>
44
+ <th>ID</th>
45
+ <th>Name</th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <DataGridRow<TestEntry, 'id' | 'name'>
50
+ entry={props.entry}
51
+ service={props.service}
52
+ columns={props.columns ?? ['id', 'name']}
53
+ onRowClick={props.onRowClick}
54
+ onRowDoubleClick={props.onRowDoubleClick}
55
+ focusedRowStyle={props.focusedRowStyle}
56
+ selectedRowStyle={props.selectedRowStyle}
57
+ unfocusedRowStyle={props.unfocusedRowStyle}
58
+ unselectedRowStyle={props.unselectedRowStyle}
59
+ rowComponents={props.rowComponents}
60
+ />
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ ),
65
+ })
66
+ await sleepAsync(50)
67
+
68
+ return {
69
+ injector,
70
+ getRow: () => root.querySelector('shades-data-grid-row'),
71
+ getCells: () => root.querySelectorAll('td'),
72
+ }
73
+ }
74
+
75
+ describe('rendering', () => {
76
+ it('should render as a table row element', async () => {
77
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
78
+ const entry = { id: 1, name: 'Test' }
79
+ const { getRow } = await renderRow({ entry, service })
80
+
81
+ const row = getRow()
82
+ expect(row).toBeTruthy()
83
+ expect(row?.tagName.toLowerCase()).toBe('shades-data-grid-row')
84
+ })
85
+ })
86
+
87
+ it('should render a cell for each column', async () => {
88
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
89
+ const entry = { id: 1, name: 'Test' }
90
+ const { getCells } = await renderRow({ entry, service, columns: ['id', 'name'] })
91
+
92
+ expect(getCells().length).toBe(2)
93
+ })
94
+ })
95
+
96
+ it('should render entry property values in cells', async () => {
97
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
98
+ const entry = { id: 42, name: 'Test Entry' }
99
+ const { getCells } = await renderRow({ entry, service })
100
+
101
+ const cells = getCells()
102
+ expect(cells[0]?.textContent).toBe('42')
103
+ expect(cells[1]?.textContent).toBe('Test Entry')
104
+ })
105
+ })
106
+
107
+ it('should use custom row components when provided', async () => {
108
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
109
+ const entry = { id: 1, name: 'Custom' }
110
+ const { getCells } = await renderRow({
111
+ entry,
112
+ service,
113
+ rowComponents: {
114
+ id: (e: TestEntry) => <span data-testid="custom-id">ID: {e.id}</span>,
115
+ name: (e: TestEntry) => <strong data-testid="custom-name">{e.name}</strong>,
116
+ },
117
+ })
118
+
119
+ const cells = getCells()
120
+ expect(cells[0]?.querySelector('[data-testid="custom-id"]')?.textContent).toContain('ID: 1')
121
+ expect(cells[1]?.querySelector('[data-testid="custom-name"]')?.textContent).toBe('Custom')
122
+ })
123
+ })
124
+ })
125
+
126
+ describe('selection state', () => {
127
+ it('should not have selected class when entry is not selected', async () => {
128
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
129
+ const entry = { id: 1, name: 'Test' }
130
+ const { getRow } = await renderRow({ entry, service })
131
+
132
+ const row = getRow()
133
+ expect(row?.classList.contains('selected')).toBe(false)
134
+ })
135
+ })
136
+
137
+ it('should have selected class when entry is in selection', async () => {
138
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
139
+ const entry = { id: 1, name: 'Test' }
140
+ service.selection.setValue([entry])
141
+ const { getRow } = await renderRow({ entry, service })
142
+
143
+ const row = getRow()
144
+ expect(row?.classList.contains('selected')).toBe(true)
145
+ })
146
+ })
147
+
148
+ it('should update selected class when selection changes', async () => {
149
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
150
+ const entry = { id: 1, name: 'Test' }
151
+ const { getRow } = await renderRow({ entry, service })
152
+
153
+ const row = getRow()
154
+ expect(row?.classList.contains('selected')).toBe(false)
155
+
156
+ service.selection.setValue([entry])
157
+ await sleepAsync(50)
158
+ expect(row?.classList.contains('selected')).toBe(true)
159
+
160
+ service.selection.setValue([])
161
+ await sleepAsync(50)
162
+ expect(row?.classList.contains('selected')).toBe(false)
163
+ })
164
+ })
165
+
166
+ it('should set aria-selected attribute based on selection', async () => {
167
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
168
+ const entry = { id: 1, name: 'Test' }
169
+ const { getRow } = await renderRow({ entry, service })
170
+
171
+ const row = getRow()
172
+ expect(row?.getAttribute('aria-selected')).toBe('false')
173
+
174
+ service.selection.setValue([entry])
175
+ await sleepAsync(50)
176
+ expect(row?.getAttribute('aria-selected')).toBe('true')
177
+ })
178
+ })
179
+
180
+ it('should apply selectedRowStyle when entry is selected', async () => {
181
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
182
+ const entry = { id: 1, name: 'Test' }
183
+ service.selection.setValue([entry])
184
+ const { getRow } = await renderRow({
185
+ entry,
186
+ service,
187
+ selectedRowStyle: { backgroundColor: 'rgb(255, 0, 0)' },
188
+ })
189
+
190
+ const row = getRow() as HTMLElement | null
191
+ expect(row?.style.backgroundColor).toBe('rgb(255, 0, 0)')
192
+ })
193
+ })
194
+
195
+ it('should apply unselectedRowStyle when entry is not selected', async () => {
196
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
197
+ const entry = { id: 1, name: 'Test' }
198
+ const { getRow } = await renderRow({
199
+ entry,
200
+ service,
201
+ unselectedRowStyle: { backgroundColor: 'rgb(0, 255, 0)' },
202
+ })
203
+
204
+ const row = getRow() as HTMLElement | null
205
+ expect(row?.style.backgroundColor).toBe('rgb(0, 255, 0)')
206
+ })
207
+ })
208
+ })
209
+
210
+ describe('focus state', () => {
211
+ it('should not have focused class when entry is not focused', async () => {
212
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
213
+ const entry = { id: 1, name: 'Test' }
214
+ const { getRow } = await renderRow({ entry, service })
215
+
216
+ const row = getRow()
217
+ expect(row?.classList.contains('focused')).toBe(false)
218
+ })
219
+ })
220
+
221
+ it('should have focused class when entry is focused', async () => {
222
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
223
+ const entry = { id: 1, name: 'Test' }
224
+ service.focusedEntry.setValue(entry)
225
+ const { getRow } = await renderRow({ entry, service })
226
+
227
+ const row = getRow()
228
+ expect(row?.classList.contains('focused')).toBe(true)
229
+ })
230
+ })
231
+
232
+ it('should update focused class when focus changes', async () => {
233
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
234
+ const entry = { id: 1, name: 'Test' }
235
+ const { getRow } = await renderRow({ entry, service })
236
+
237
+ const row = getRow()
238
+ expect(row?.classList.contains('focused')).toBe(false)
239
+
240
+ service.focusedEntry.setValue(entry)
241
+ await sleepAsync(50)
242
+ expect(row?.classList.contains('focused')).toBe(true)
243
+
244
+ service.focusedEntry.setValue(undefined)
245
+ await sleepAsync(50)
246
+ expect(row?.classList.contains('focused')).toBe(false)
247
+ })
248
+ })
249
+
250
+ it('should apply focusedRowStyle when entry is focused', async () => {
251
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
252
+ const entry = { id: 1, name: 'Test' }
253
+ service.focusedEntry.setValue(entry)
254
+ const { getRow } = await renderRow({
255
+ entry,
256
+ service,
257
+ focusedRowStyle: { fontWeight: 'bold' },
258
+ })
259
+
260
+ const row = getRow() as HTMLElement | null
261
+ expect(row?.style.fontWeight).toBe('bold')
262
+ })
263
+ })
264
+
265
+ it('should apply unfocusedRowStyle when entry is not focused', async () => {
266
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
267
+ const entry = { id: 1, name: 'Test' }
268
+ const { getRow } = await renderRow({
269
+ entry,
270
+ service,
271
+ unfocusedRowStyle: { opacity: '0.8' },
272
+ })
273
+
274
+ const row = getRow() as HTMLElement | null
275
+ expect(row?.style.opacity).toBe('0.8')
276
+ })
277
+ })
278
+
279
+ it('should not have focused class when different entry is focused', async () => {
280
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
281
+ const entry = { id: 1, name: 'Test' }
282
+ const otherEntry = { id: 2, name: 'Other' }
283
+ service.focusedEntry.setValue(otherEntry)
284
+ const { getRow } = await renderRow({ entry, service })
285
+
286
+ const row = getRow()
287
+ expect(row?.classList.contains('focused')).toBe(false)
288
+ })
289
+ })
290
+ })
291
+
292
+ describe('click handlers', () => {
293
+ it('should call onRowClick when cell is clicked', async () => {
294
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
295
+ const entry = { id: 1, name: 'Test' }
296
+ const onRowClick = vi.fn()
297
+ const { getCells } = await renderRow({ entry, service, onRowClick })
298
+
299
+ const cell = getCells()[0]
300
+ cell.click()
301
+
302
+ expect(onRowClick).toHaveBeenCalledWith(entry, expect.any(MouseEvent))
303
+ })
304
+ })
305
+
306
+ it('should call onRowDoubleClick when cell is double-clicked', async () => {
307
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
308
+ const entry = { id: 1, name: 'Test' }
309
+ const onRowDoubleClick = vi.fn()
310
+ const { getCells } = await renderRow({ entry, service, onRowDoubleClick })
311
+
312
+ const cell = getCells()[0]
313
+ const dblClickEvent = new MouseEvent('dblclick', { bubbles: true })
314
+ cell.dispatchEvent(dblClickEvent)
315
+
316
+ expect(onRowDoubleClick).toHaveBeenCalledWith(entry, expect.any(MouseEvent))
317
+ })
318
+ })
319
+
320
+ it('should not throw when onRowClick is not provided', async () => {
321
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
322
+ const entry = { id: 1, name: 'Test' }
323
+ const { getCells } = await renderRow({ entry, service })
324
+
325
+ const cell = getCells()[0]
326
+ expect(() => cell.click()).not.toThrow()
327
+ })
328
+ })
329
+
330
+ it('should not throw when onRowDoubleClick is not provided', async () => {
331
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
332
+ const entry = { id: 1, name: 'Test' }
333
+ const { getCells } = await renderRow({ entry, service })
334
+
335
+ const cell = getCells()[0]
336
+ const dblClickEvent = new MouseEvent('dblclick', { bubbles: true })
337
+ expect(() => cell.dispatchEvent(dblClickEvent)).not.toThrow()
338
+ })
339
+ })
340
+ })
341
+
342
+ describe('combined selection and focus states', () => {
343
+ it('should have both selected and focused classes when applicable', async () => {
344
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
345
+ const entry = { id: 1, name: 'Test' }
346
+ service.selection.setValue([entry])
347
+ service.focusedEntry.setValue(entry)
348
+ const { getRow } = await renderRow({ entry, service })
349
+
350
+ const row = getRow()
351
+ expect(row?.classList.contains('selected')).toBe(true)
352
+ expect(row?.classList.contains('focused')).toBe(true)
353
+ })
354
+ })
355
+
356
+ it('should be selected but not focused', async () => {
357
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
358
+ const entry = { id: 1, name: 'Test' }
359
+ const otherEntry = { id: 2, name: 'Other' }
360
+ service.selection.setValue([entry])
361
+ service.focusedEntry.setValue(otherEntry)
362
+ const { getRow } = await renderRow({ entry, service })
363
+
364
+ const row = getRow()
365
+ expect(row?.classList.contains('selected')).toBe(true)
366
+ expect(row?.classList.contains('focused')).toBe(false)
367
+ })
368
+ })
369
+
370
+ it('should be focused but not selected', async () => {
371
+ await usingAsync(new CollectionService<TestEntry>(), async (service) => {
372
+ const entry = { id: 1, name: 'Test' }
373
+ service.focusedEntry.setValue(entry)
374
+ const { getRow } = await renderRow({ entry, service })
375
+
376
+ const row = getRow()
377
+ expect(row?.classList.contains('selected')).toBe(false)
378
+ expect(row?.classList.contains('focused')).toBe(true)
379
+ })
380
+ })
381
+ })
382
+ })
@@ -1,6 +1,7 @@
1
1
  import type { ChildrenList } from '@furystack/shades'
2
2
  import { attachStyles, createComponent, Shade } from '@furystack/shades'
3
3
  import type { CollectionService } from '../../services/collection-service.js'
4
+ import { cssVariableTheme } from '../../services/css-variable-theme.js'
4
5
  import type { DataRowCells } from './data-grid.js'
5
6
 
6
7
  export interface DataGridRowProps<T, Column extends string> {
@@ -21,44 +22,55 @@ export const DataGridRow: <T, Column extends string>(
21
22
  children: ChildrenList,
22
23
  ) => JSX.Element<any> = Shade({
23
24
  shadowDomName: 'shades-data-grid-row',
24
-
25
+ css: {
26
+ display: 'table-row',
27
+ cursor: 'default',
28
+ userSelect: 'none',
29
+ transition: 'background-color 0.15s ease, box-shadow 0.15s ease-in-out, transform 0.15s ease-in-out',
30
+ borderLeft: '3px solid transparent',
31
+ '&:not(.selected):hover': {
32
+ backgroundColor: 'rgba(128, 128, 128, 0.08)',
33
+ },
34
+ '&.selected': {
35
+ backgroundColor: 'rgba(128, 128, 128, 0.15)',
36
+ borderLeft: `3px solid ${cssVariableTheme.palette.primary.main}`,
37
+ },
38
+ '&.focused': {
39
+ boxShadow: `0 0 0 2px ${cssVariableTheme.palette.primary.main} inset, 0 2px 8px 0px rgba(0,0,0,0.15)`,
40
+ fontWeight: '500',
41
+ transform: 'scale(1.002)',
42
+ },
43
+ '& td': {
44
+ padding: '0.75em 1.2em',
45
+ borderBottom: '1px solid rgba(128, 128, 128, 0.1)',
46
+ verticalAlign: 'middle',
47
+ fontSize: '0.875rem',
48
+ lineHeight: '1.5',
49
+ },
50
+ },
25
51
  render: ({ props, element, useObservable }) => {
26
52
  const { entry, rowComponents, columns, service } = props
27
53
 
28
- const attachSelectedStyles = (selection: any[]) => {
29
- if (selection.includes(entry)) {
30
- element.classList.add('selected')
31
- attachStyles(element, {
32
- style: props.selectedRowStyle || {
33
- backgroundColor: 'rgba(128, 128, 128, 0.15)',
34
- borderLeft: `3px solid var(--shades-theme-palette-primary-main)`,
35
- },
36
- })
37
- element.setAttribute('aria-selected', 'true')
38
- } else {
39
- element.classList.remove('selected')
40
- attachStyles(element, {
41
- style: props.unselectedRowStyle || {
42
- backgroundColor: 'transparent',
43
- borderLeft: '3px solid transparent',
44
- },
45
- })
46
- element.setAttribute('aria-selected', 'false')
54
+ const updateSelectionState = (selection: unknown[]) => {
55
+ const isSelected = selection.includes(entry)
56
+ element.classList.toggle('selected', isSelected)
57
+ element.setAttribute('aria-selected', isSelected.toString())
58
+
59
+ if (props.selectedRowStyle && isSelected) {
60
+ attachStyles(element, { style: props.selectedRowStyle })
61
+ } else if (props.unselectedRowStyle && !isSelected) {
62
+ attachStyles(element, { style: props.unselectedRowStyle })
47
63
  }
48
64
  }
49
65
 
50
- const attachFocusedStyle = (newEntry?: any) => {
51
- if (newEntry === props.entry) {
52
- attachStyles(element, {
53
- style: props.focusedRowStyle || {
54
- boxShadow: `0 0 0 2px var(--shades-theme-palette-primary-main) inset, 0 2px 8px 0px rgba(0,0,0,0.15)`,
55
- transition: 'box-shadow 0.15s ease-in-out, transform 0.15s ease-in-out',
56
- fontWeight: '500',
57
- transform: 'scale(1.002)',
58
- },
59
- })
66
+ const updateFocusState = (focusedEntry?: unknown) => {
67
+ const isFocused = focusedEntry === entry
68
+ element.classList.toggle('focused', isFocused)
60
69
 
61
- element.classList.add('focused')
70
+ if (isFocused) {
71
+ if (props.focusedRowStyle) {
72
+ attachStyles(element, { style: props.focusedRowStyle })
73
+ }
62
74
 
63
75
  const headerHeight = element.closest('table')?.querySelector('th')?.getBoundingClientRect().height || 42
64
76
 
@@ -77,69 +89,25 @@ export const DataGridRow: <T, Column extends string>(
77
89
  if (desiredMaxTop > visibleMaxTop) {
78
90
  parent.scrollTo({ top: desiredMaxTop - visibleMaxTop, behavior: 'smooth' })
79
91
  }
80
- } else {
81
- element.classList.remove('focused')
82
- attachStyles(element, {
83
- style: props.unfocusedRowStyle || {
84
- boxShadow: 'none',
85
- fontWeight: 'inherit',
86
- transform: 'scale(1)',
87
- },
88
- })
92
+ } else if (props.unfocusedRowStyle) {
93
+ attachStyles(element, { style: props.unfocusedRowStyle })
89
94
  }
90
95
  }
91
96
 
92
97
  const [selection] = useObservable('isSelected', service.selection, {
93
- onChange: attachSelectedStyles,
98
+ onChange: updateSelectionState,
94
99
  })
95
- attachSelectedStyles(selection)
100
+ updateSelectionState(selection)
96
101
 
97
102
  const [focus] = useObservable('focus', service.focusedEntry, {
98
- onChange: attachFocusedStyle,
103
+ onChange: updateFocusState,
99
104
  })
100
- attachFocusedStyle(focus)
101
-
102
- element.style.display = 'table-row'
103
- element.style.cursor = 'default'
104
- element.style.userSelect = 'none'
105
- element.style.transition = 'background-color 0.15s ease'
106
-
107
- if (selection?.includes(entry)) {
108
- element.setAttribute('aria-selected', 'true')
109
- element.classList.add('selected')
110
- }
111
-
112
- if (focus === entry) {
113
- element.classList.add('focused')
114
- }
115
- element.setAttribute('aria-selected', selection?.includes(entry).toString() || 'false')
116
-
117
- // Add hover effect
118
- element.onmouseenter = () => {
119
- if (!selection?.includes(entry)) {
120
- element.style.backgroundColor = 'rgba(128, 128, 128, 0.08)'
121
- }
122
- }
123
- element.onmouseleave = () => {
124
- if (!selection?.includes(entry)) {
125
- element.style.backgroundColor = 'transparent'
126
- }
127
- }
105
+ updateFocusState(focus)
128
106
 
129
107
  return (
130
108
  <>
131
109
  {columns.map((column) => (
132
- <td
133
- style={{
134
- padding: '0.75em 1.2em',
135
- borderBottom: '1px solid rgba(128, 128, 128, 0.1)',
136
- verticalAlign: 'middle',
137
- fontSize: '0.875rem',
138
- lineHeight: '1.5',
139
- }}
140
- onclick={(ev) => props.onRowClick?.(entry, ev)}
141
- ondblclick={(ev) => props.onRowDoubleClick?.(entry, ev)}
142
- >
110
+ <td onclick={(ev) => props.onRowClick?.(entry, ev)} ondblclick={(ev) => props.onRowDoubleClick?.(entry, ev)}>
143
111
  {rowComponents?.[column]?.(entry, { selection, focus }) ||
144
112
  rowComponents?.default?.(entry, { selection, focus }) || (
145
113
  <span>