@furystack/shades-common-components 12.3.0 → 12.5.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 (268) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/esm/components/app-bar-link.spec.js +16 -19
  3. package/esm/components/app-bar-link.spec.js.map +1 -1
  4. package/esm/components/app-bar.spec.js +6 -4
  5. package/esm/components/app-bar.spec.js.map +1 -1
  6. package/esm/components/avatar.spec.js +9 -9
  7. package/esm/components/avatar.spec.js.map +1 -1
  8. package/esm/components/breadcrumb.spec.js +2 -2
  9. package/esm/components/breadcrumb.spec.js.map +1 -1
  10. package/esm/components/button-group.d.ts +32 -0
  11. package/esm/components/button-group.d.ts.map +1 -1
  12. package/esm/components/button-group.js +26 -2
  13. package/esm/components/button-group.js.map +1 -1
  14. package/esm/components/button-group.spec.js +127 -11
  15. package/esm/components/button-group.spec.js.map +1 -1
  16. package/esm/components/button.spec.js +4 -4
  17. package/esm/components/button.spec.js.map +1 -1
  18. package/esm/components/cache-view.spec.js +2 -3
  19. package/esm/components/cache-view.spec.js.map +1 -1
  20. package/esm/components/carousel.spec.js +47 -47
  21. package/esm/components/carousel.spec.js.map +1 -1
  22. package/esm/components/circular-progress.spec.js +2 -2
  23. package/esm/components/command-palette/command-palette-input.spec.js +23 -19
  24. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
  25. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +27 -27
  26. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -1
  27. package/esm/components/command-palette/index.spec.js +64 -51
  28. package/esm/components/command-palette/index.spec.js.map +1 -1
  29. package/esm/components/context-menu/context-menu.spec.js +33 -33
  30. package/esm/components/context-menu/context-menu.spec.js.map +1 -1
  31. package/esm/components/data-grid/body.spec.js +13 -13
  32. package/esm/components/data-grid/body.spec.js.map +1 -1
  33. package/esm/components/data-grid/data-grid-row.spec.js +8 -8
  34. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -1
  35. package/esm/components/data-grid/data-grid.d.ts +40 -2
  36. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/esm/components/data-grid/data-grid.js +7 -10
  38. package/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/esm/components/data-grid/data-grid.spec.js +71 -28
  40. package/esm/components/data-grid/data-grid.spec.js.map +1 -1
  41. package/esm/components/data-grid/filters/boolean-filter.d.ts +12 -0
  42. package/esm/components/data-grid/filters/boolean-filter.d.ts.map +1 -0
  43. package/esm/components/data-grid/filters/boolean-filter.js +27 -0
  44. package/esm/components/data-grid/filters/boolean-filter.js.map +1 -0
  45. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts +2 -0
  46. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts.map +1 -0
  47. package/esm/components/data-grid/filters/boolean-filter.spec.js +114 -0
  48. package/esm/components/data-grid/filters/boolean-filter.spec.js.map +1 -0
  49. package/esm/components/data-grid/filters/date-filter.d.ts +12 -0
  50. package/esm/components/data-grid/filters/date-filter.d.ts.map +1 -0
  51. package/esm/components/data-grid/filters/date-filter.js +109 -0
  52. package/esm/components/data-grid/filters/date-filter.js.map +1 -0
  53. package/esm/components/data-grid/filters/date-filter.spec.d.ts +2 -0
  54. package/esm/components/data-grid/filters/date-filter.spec.d.ts.map +1 -0
  55. package/esm/components/data-grid/filters/date-filter.spec.js +145 -0
  56. package/esm/components/data-grid/filters/date-filter.spec.js.map +1 -0
  57. package/esm/components/data-grid/filters/enum-filter.d.ts +16 -0
  58. package/esm/components/data-grid/filters/enum-filter.d.ts.map +1 -0
  59. package/esm/components/data-grid/filters/enum-filter.js +72 -0
  60. package/esm/components/data-grid/filters/enum-filter.js.map +1 -0
  61. package/esm/components/data-grid/filters/enum-filter.spec.d.ts +2 -0
  62. package/esm/components/data-grid/filters/enum-filter.spec.d.ts.map +1 -0
  63. package/esm/components/data-grid/filters/enum-filter.spec.js +136 -0
  64. package/esm/components/data-grid/filters/enum-filter.spec.js.map +1 -0
  65. package/esm/components/data-grid/filters/filter-dropdown.d.ts +6 -0
  66. package/esm/components/data-grid/filters/filter-dropdown.d.ts.map +1 -0
  67. package/esm/components/data-grid/filters/filter-dropdown.js +41 -0
  68. package/esm/components/data-grid/filters/filter-dropdown.js.map +1 -0
  69. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts +2 -0
  70. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts.map +1 -0
  71. package/esm/components/data-grid/filters/filter-dropdown.spec.js +69 -0
  72. package/esm/components/data-grid/filters/filter-dropdown.spec.js.map +1 -0
  73. package/esm/components/data-grid/filters/filter-styles.d.ts +24 -0
  74. package/esm/components/data-grid/filters/filter-styles.d.ts.map +1 -0
  75. package/esm/components/data-grid/filters/filter-styles.js +25 -0
  76. package/esm/components/data-grid/filters/filter-styles.js.map +1 -0
  77. package/esm/components/data-grid/filters/index.d.ts +7 -0
  78. package/esm/components/data-grid/filters/index.d.ts.map +1 -0
  79. package/esm/components/data-grid/filters/index.js +7 -0
  80. package/esm/components/data-grid/filters/index.js.map +1 -0
  81. package/esm/components/data-grid/filters/number-filter.d.ts +12 -0
  82. package/esm/components/data-grid/filters/number-filter.d.ts.map +1 -0
  83. package/esm/components/data-grid/filters/number-filter.js +65 -0
  84. package/esm/components/data-grid/filters/number-filter.js.map +1 -0
  85. package/esm/components/data-grid/filters/number-filter.spec.d.ts +2 -0
  86. package/esm/components/data-grid/filters/number-filter.spec.d.ts.map +1 -0
  87. package/esm/components/data-grid/filters/number-filter.spec.js +142 -0
  88. package/esm/components/data-grid/filters/number-filter.spec.js.map +1 -0
  89. package/esm/components/data-grid/filters/string-filter.d.ts +12 -0
  90. package/esm/components/data-grid/filters/string-filter.d.ts.map +1 -0
  91. package/esm/components/data-grid/filters/string-filter.js +63 -0
  92. package/esm/components/data-grid/filters/string-filter.js.map +1 -0
  93. package/esm/components/data-grid/filters/string-filter.spec.d.ts +2 -0
  94. package/esm/components/data-grid/filters/string-filter.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/filters/string-filter.spec.js +128 -0
  96. package/esm/components/data-grid/filters/string-filter.spec.js.map +1 -0
  97. package/esm/components/data-grid/footer.d.ts.map +1 -1
  98. package/esm/components/data-grid/footer.js +24 -9
  99. package/esm/components/data-grid/footer.js.map +1 -1
  100. package/esm/components/data-grid/footer.spec.js +38 -36
  101. package/esm/components/data-grid/footer.spec.js.map +1 -1
  102. package/esm/components/data-grid/header.d.ts +6 -9
  103. package/esm/components/data-grid/header.d.ts.map +1 -1
  104. package/esm/components/data-grid/header.js +51 -117
  105. package/esm/components/data-grid/header.js.map +1 -1
  106. package/esm/components/data-grid/header.spec.js +116 -187
  107. package/esm/components/data-grid/header.spec.js.map +1 -1
  108. package/esm/components/data-grid/index.d.ts +1 -0
  109. package/esm/components/data-grid/index.d.ts.map +1 -1
  110. package/esm/components/data-grid/index.js +1 -0
  111. package/esm/components/data-grid/index.js.map +1 -1
  112. package/esm/components/data-grid/selection-cell.spec.js +8 -8
  113. package/esm/components/data-grid/selection-cell.spec.js.map +1 -1
  114. package/esm/components/drawer/drawer-toggle-button.spec.js +22 -22
  115. package/esm/components/drawer/drawer-toggle-button.spec.js.map +1 -1
  116. package/esm/components/drawer/index.spec.js +36 -36
  117. package/esm/components/drawer/index.spec.js.map +1 -1
  118. package/esm/components/dropdown.spec.js +38 -30
  119. package/esm/components/dropdown.spec.js.map +1 -1
  120. package/esm/components/fab.spec.js +4 -4
  121. package/esm/components/fab.spec.js.map +1 -1
  122. package/esm/components/form.d.ts +5 -2
  123. package/esm/components/form.d.ts.map +1 -1
  124. package/esm/components/form.js +28 -6
  125. package/esm/components/form.js.map +1 -1
  126. package/esm/components/form.spec.js +227 -20
  127. package/esm/components/form.spec.js.map +1 -1
  128. package/esm/components/grid.spec.js +3 -3
  129. package/esm/components/grid.spec.js.map +1 -1
  130. package/esm/components/image.spec.js +55 -52
  131. package/esm/components/image.spec.js.map +1 -1
  132. package/esm/components/inputs/autocomplete.spec.js +7 -14
  133. package/esm/components/inputs/autocomplete.spec.js.map +1 -1
  134. package/esm/components/inputs/checkbox.spec.js +22 -22
  135. package/esm/components/inputs/checkbox.spec.js.map +1 -1
  136. package/esm/components/inputs/input-number.spec.js +47 -47
  137. package/esm/components/inputs/input-number.spec.js.map +1 -1
  138. package/esm/components/inputs/input.spec.js +53 -53
  139. package/esm/components/inputs/input.spec.js.map +1 -1
  140. package/esm/components/inputs/radio-group.spec.js +14 -14
  141. package/esm/components/inputs/radio-group.spec.js.map +1 -1
  142. package/esm/components/inputs/radio.spec.js +16 -16
  143. package/esm/components/inputs/radio.spec.js.map +1 -1
  144. package/esm/components/inputs/select.spec.js +74 -74
  145. package/esm/components/inputs/select.spec.js.map +1 -1
  146. package/esm/components/inputs/slider.spec.js +16 -16
  147. package/esm/components/inputs/slider.spec.js.map +1 -1
  148. package/esm/components/inputs/switch.spec.js +24 -24
  149. package/esm/components/inputs/switch.spec.js.map +1 -1
  150. package/esm/components/inputs/text-area.spec.js +17 -17
  151. package/esm/components/inputs/text-area.spec.js.map +1 -1
  152. package/esm/components/linear-progress.spec.js +2 -2
  153. package/esm/components/list/list.spec.js +36 -36
  154. package/esm/components/list/list.spec.js.map +1 -1
  155. package/esm/components/markdown/markdown-display.spec.js +15 -15
  156. package/esm/components/markdown/markdown-display.spec.js.map +1 -1
  157. package/esm/components/markdown/markdown-editor.spec.js +8 -8
  158. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  159. package/esm/components/markdown/markdown-input.spec.js +17 -17
  160. package/esm/components/markdown/markdown-input.spec.js.map +1 -1
  161. package/esm/components/menu/menu.spec.js +28 -28
  162. package/esm/components/menu/menu.spec.js.map +1 -1
  163. package/esm/components/modal.spec.js +15 -18
  164. package/esm/components/modal.spec.js.map +1 -1
  165. package/esm/components/noty-list.spec.js +25 -23
  166. package/esm/components/noty-list.spec.js.map +1 -1
  167. package/esm/components/page-container/index.spec.js +16 -16
  168. package/esm/components/page-container/index.spec.js.map +1 -1
  169. package/esm/components/page-container/page-header.spec.js +16 -16
  170. package/esm/components/page-container/page-header.spec.js.map +1 -1
  171. package/esm/components/page-layout/index.spec.js +29 -29
  172. package/esm/components/page-layout/index.spec.js.map +1 -1
  173. package/esm/components/paper.spec.js +3 -3
  174. package/esm/components/paper.spec.js.map +1 -1
  175. package/esm/components/rating.spec.js +61 -61
  176. package/esm/components/rating.spec.js.map +1 -1
  177. package/esm/components/skeleton.spec.js +10 -6
  178. package/esm/components/skeleton.spec.js.map +1 -1
  179. package/esm/components/suggest/suggest-input.spec.js +4 -10
  180. package/esm/components/suggest/suggest-input.spec.js.map +1 -1
  181. package/esm/components/tabs.spec.js +30 -30
  182. package/esm/components/tabs.spec.js.map +1 -1
  183. package/esm/components/tree/tree.spec.js +27 -27
  184. package/esm/components/tree/tree.spec.js.map +1 -1
  185. package/esm/components/typography.spec.js +3 -3
  186. package/esm/components/typography.spec.js.map +1 -1
  187. package/esm/components/wizard/index.spec.js +5 -5
  188. package/esm/components/wizard/index.spec.js.map +1 -1
  189. package/esm/utils/promisify-animation.d.ts.map +1 -1
  190. package/esm/utils/promisify-animation.js +3 -0
  191. package/esm/utils/promisify-animation.js.map +1 -1
  192. package/package.json +2 -2
  193. package/src/components/app-bar-link.spec.tsx +16 -19
  194. package/src/components/app-bar.spec.tsx +6 -4
  195. package/src/components/avatar.spec.tsx +9 -9
  196. package/src/components/breadcrumb.spec.tsx +2 -2
  197. package/src/components/button-group.spec.tsx +155 -11
  198. package/src/components/button-group.tsx +49 -2
  199. package/src/components/button.spec.tsx +4 -4
  200. package/src/components/cache-view.spec.tsx +3 -3
  201. package/src/components/carousel.spec.tsx +47 -47
  202. package/src/components/circular-progress.spec.tsx +2 -2
  203. package/src/components/command-palette/command-palette-input.spec.tsx +23 -19
  204. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +27 -27
  205. package/src/components/command-palette/index.spec.tsx +64 -51
  206. package/src/components/context-menu/context-menu.spec.tsx +33 -33
  207. package/src/components/data-grid/body.spec.tsx +13 -13
  208. package/src/components/data-grid/data-grid-row.spec.tsx +8 -8
  209. package/src/components/data-grid/data-grid.spec.tsx +106 -28
  210. package/src/components/data-grid/data-grid.tsx +44 -11
  211. package/src/components/data-grid/filters/boolean-filter.spec.tsx +142 -0
  212. package/src/components/data-grid/filters/boolean-filter.tsx +45 -0
  213. package/src/components/data-grid/filters/date-filter.spec.tsx +181 -0
  214. package/src/components/data-grid/filters/date-filter.tsx +162 -0
  215. package/src/components/data-grid/filters/enum-filter.spec.tsx +168 -0
  216. package/src/components/data-grid/filters/enum-filter.tsx +119 -0
  217. package/src/components/data-grid/filters/filter-dropdown.spec.tsx +89 -0
  218. package/src/components/data-grid/filters/filter-dropdown.tsx +60 -0
  219. package/src/components/data-grid/filters/filter-styles.ts +26 -0
  220. package/src/components/data-grid/filters/index.ts +6 -0
  221. package/src/components/data-grid/filters/number-filter.spec.tsx +174 -0
  222. package/src/components/data-grid/filters/number-filter.tsx +115 -0
  223. package/src/components/data-grid/filters/string-filter.spec.tsx +157 -0
  224. package/src/components/data-grid/filters/string-filter.tsx +112 -0
  225. package/src/components/data-grid/footer.spec.tsx +38 -36
  226. package/src/components/data-grid/footer.tsx +21 -8
  227. package/src/components/data-grid/header.spec.tsx +128 -212
  228. package/src/components/data-grid/header.tsx +95 -183
  229. package/src/components/data-grid/index.tsx +1 -0
  230. package/src/components/data-grid/selection-cell.spec.tsx +8 -8
  231. package/src/components/drawer/drawer-toggle-button.spec.tsx +22 -22
  232. package/src/components/drawer/index.spec.tsx +36 -36
  233. package/src/components/dropdown.spec.tsx +38 -30
  234. package/src/components/fab.spec.tsx +4 -4
  235. package/src/components/form.spec.tsx +329 -20
  236. package/src/components/form.tsx +31 -8
  237. package/src/components/grid.spec.tsx +3 -3
  238. package/src/components/image.spec.tsx +55 -52
  239. package/src/components/inputs/autocomplete.spec.tsx +7 -14
  240. package/src/components/inputs/checkbox.spec.tsx +22 -22
  241. package/src/components/inputs/input-number.spec.tsx +47 -47
  242. package/src/components/inputs/input.spec.tsx +53 -53
  243. package/src/components/inputs/radio-group.spec.tsx +14 -14
  244. package/src/components/inputs/radio.spec.tsx +16 -16
  245. package/src/components/inputs/select.spec.tsx +74 -74
  246. package/src/components/inputs/slider.spec.tsx +16 -16
  247. package/src/components/inputs/switch.spec.tsx +24 -24
  248. package/src/components/inputs/text-area.spec.tsx +17 -17
  249. package/src/components/linear-progress.spec.tsx +2 -2
  250. package/src/components/list/list.spec.tsx +36 -36
  251. package/src/components/markdown/markdown-display.spec.tsx +15 -15
  252. package/src/components/markdown/markdown-editor.spec.tsx +8 -8
  253. package/src/components/markdown/markdown-input.spec.tsx +17 -17
  254. package/src/components/menu/menu.spec.tsx +28 -28
  255. package/src/components/modal.spec.tsx +15 -18
  256. package/src/components/noty-list.spec.tsx +25 -23
  257. package/src/components/page-container/index.spec.tsx +16 -16
  258. package/src/components/page-container/page-header.spec.tsx +16 -16
  259. package/src/components/page-layout/index.spec.tsx +29 -29
  260. package/src/components/paper.spec.tsx +3 -3
  261. package/src/components/rating.spec.tsx +61 -61
  262. package/src/components/skeleton.spec.tsx +10 -6
  263. package/src/components/suggest/suggest-input.spec.tsx +4 -10
  264. package/src/components/tabs.spec.tsx +30 -30
  265. package/src/components/tree/tree.spec.tsx +27 -27
  266. package/src/components/typography.spec.tsx +3 -3
  267. package/src/components/wizard/index.spec.tsx +5 -5
  268. package/src/utils/promisify-animation.ts +3 -0
@@ -1,7 +1,7 @@
1
1
  import type { FindOptions } from '@furystack/core'
2
2
  import { Injector } from '@furystack/inject'
3
- import { createComponent, initializeShadeRoot } from '@furystack/shades'
4
- import { ObservableValue, sleepAsync, usingAsync } from '@furystack/utils'
3
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
4
+ import { ObservableValue, usingAsync } from '@furystack/utils'
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
6
  import { CollectionService } from '../../services/collection-service.js'
7
7
  import { DataGrid } from './data-grid.js'
@@ -70,7 +70,7 @@ describe('DataGrid', () => {
70
70
  ),
71
71
  })
72
72
 
73
- await sleepAsync(50)
73
+ await flushUpdates()
74
74
 
75
75
  const grid = document.querySelector('shade-data-grid')
76
76
  expect(grid).not.toBeNull()
@@ -99,7 +99,7 @@ describe('DataGrid', () => {
99
99
  ),
100
100
  })
101
101
 
102
- await sleepAsync(50)
102
+ await flushUpdates()
103
103
 
104
104
  const grid = document.querySelector('shade-data-grid')
105
105
  const table = grid?.querySelector('table')
@@ -133,7 +133,7 @@ describe('DataGrid', () => {
133
133
  ),
134
134
  })
135
135
 
136
- await sleepAsync(50)
136
+ await flushUpdates()
137
137
 
138
138
  const grid = document.querySelector('shade-data-grid')
139
139
  const customHeader = grid?.querySelector('[data-testid="custom-header-id"]')
@@ -163,7 +163,7 @@ describe('DataGrid', () => {
163
163
  ),
164
164
  })
165
165
 
166
- await sleepAsync(50)
166
+ await flushUpdates()
167
167
 
168
168
  const grid = document.querySelector('shade-data-grid')
169
169
  const defaultHeaderId = grid?.querySelector('[data-testid="default-header-id"]')
@@ -193,13 +193,91 @@ describe('DataGrid', () => {
193
193
  ),
194
194
  })
195
195
 
196
- await sleepAsync(50)
196
+ await flushUpdates()
197
197
 
198
198
  const grid = document.querySelector('shade-data-grid')
199
199
  const defaultHeaders = grid?.querySelectorAll('data-grid-header')
200
200
  expect(defaultHeaders?.length).toBe(2)
201
201
  })
202
202
  })
203
+
204
+ it('should render filter buttons when columnFilters are provided', async () => {
205
+ await withTestGrid(async ({ injector, service, findOptions }) => {
206
+ const rootElement = document.getElementById('root') as HTMLDivElement
207
+
208
+ initializeShadeRoot({
209
+ injector,
210
+ rootElement,
211
+ jsxElement: (
212
+ <DataGrid<TestEntry, 'id' | 'name'>
213
+ columns={['id', 'name']}
214
+ collectionService={service}
215
+ findOptions={findOptions}
216
+ styles={{}}
217
+ columnFilters={{ name: { type: 'string' } }}
218
+ />
219
+ ),
220
+ })
221
+
222
+ await flushUpdates()
223
+
224
+ const grid = document.querySelector('shade-data-grid')
225
+ const filterButtons = grid?.querySelectorAll('data-grid-filter-button')
226
+ expect(filterButtons?.length).toBe(1)
227
+ })
228
+ })
229
+
230
+ it('should not render filter buttons when columnFilters is not provided', async () => {
231
+ await withTestGrid(async ({ injector, service, findOptions }) => {
232
+ const rootElement = document.getElementById('root') as HTMLDivElement
233
+
234
+ initializeShadeRoot({
235
+ injector,
236
+ rootElement,
237
+ jsxElement: (
238
+ <DataGrid<TestEntry, 'id' | 'name'>
239
+ columns={['id', 'name']}
240
+ collectionService={service}
241
+ findOptions={findOptions}
242
+ styles={{}}
243
+ />
244
+ ),
245
+ })
246
+
247
+ await flushUpdates()
248
+
249
+ const grid = document.querySelector('shade-data-grid')
250
+ const filterButtons = grid?.querySelectorAll('data-grid-filter-button')
251
+ expect(filterButtons?.length).toBe(0)
252
+ })
253
+ })
254
+
255
+ it('should render without headerComponents and rowComponents', async () => {
256
+ await withTestGrid(async ({ injector, service, findOptions }) => {
257
+ const rootElement = document.getElementById('root') as HTMLDivElement
258
+
259
+ initializeShadeRoot({
260
+ injector,
261
+ rootElement,
262
+ jsxElement: (
263
+ <DataGrid<TestEntry, 'id' | 'name'>
264
+ columns={['id', 'name']}
265
+ collectionService={service}
266
+ findOptions={findOptions}
267
+ styles={{}}
268
+ />
269
+ ),
270
+ })
271
+
272
+ await flushUpdates()
273
+
274
+ const grid = document.querySelector('shade-data-grid')
275
+ expect(grid).not.toBeNull()
276
+
277
+ const headers = grid?.querySelectorAll('data-grid-header')
278
+ expect(headers?.length).toBe(2)
279
+ })
280
+ })
203
281
  })
204
282
 
205
283
  describe('focus management', () => {
@@ -224,7 +302,7 @@ describe('DataGrid', () => {
224
302
  ),
225
303
  })
226
304
 
227
- await sleepAsync(50)
305
+ await flushUpdates()
228
306
 
229
307
  const grid = document.querySelector('shade-data-grid')
230
308
  const wrapper = grid?.querySelector('.shade-grid-wrapper') as HTMLElement
@@ -257,7 +335,7 @@ describe('DataGrid', () => {
257
335
  ),
258
336
  })
259
337
 
260
- await sleepAsync(50)
338
+ await flushUpdates()
261
339
 
262
340
  const grid = document.querySelector('shade-data-grid')
263
341
  const wrapper = grid?.querySelector('.shade-grid-wrapper') as HTMLElement
@@ -296,7 +374,7 @@ describe('DataGrid', () => {
296
374
  ),
297
375
  })
298
376
 
299
- await sleepAsync(50)
377
+ await flushUpdates()
300
378
 
301
379
  const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
302
380
  window.dispatchEvent(keydownEvent)
@@ -327,7 +405,7 @@ describe('DataGrid', () => {
327
405
  ),
328
406
  })
329
407
 
330
- await sleepAsync(50)
408
+ await flushUpdates()
331
409
 
332
410
  const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
333
411
  window.dispatchEvent(keydownEvent)
@@ -358,7 +436,7 @@ describe('DataGrid', () => {
358
436
  ),
359
437
  })
360
438
 
361
- await sleepAsync(50)
439
+ await flushUpdates()
362
440
 
363
441
  const keydownEvent = new KeyboardEvent('keydown', { key: 'Home', bubbles: true })
364
442
  window.dispatchEvent(keydownEvent)
@@ -389,7 +467,7 @@ describe('DataGrid', () => {
389
467
  ),
390
468
  })
391
469
 
392
- await sleepAsync(50)
470
+ await flushUpdates()
393
471
 
394
472
  const keydownEvent = new KeyboardEvent('keydown', { key: 'End', bubbles: true })
395
473
  window.dispatchEvent(keydownEvent)
@@ -419,7 +497,7 @@ describe('DataGrid', () => {
419
497
  ),
420
498
  })
421
499
 
422
- await sleepAsync(50)
500
+ await flushUpdates()
423
501
 
424
502
  const keydownEvent = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
425
503
  window.dispatchEvent(keydownEvent)
@@ -452,7 +530,7 @@ describe('DataGrid', () => {
452
530
  ),
453
531
  })
454
532
 
455
- await sleepAsync(50)
533
+ await flushUpdates()
456
534
 
457
535
  const keydownEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
458
536
  window.dispatchEvent(keydownEvent)
@@ -485,7 +563,7 @@ describe('DataGrid', () => {
485
563
  ),
486
564
  })
487
565
 
488
- await sleepAsync(50)
566
+ await flushUpdates()
489
567
 
490
568
  const keydownEvent = new KeyboardEvent('keydown', { key: ' ', bubbles: true })
491
569
  window.dispatchEvent(keydownEvent)
@@ -518,7 +596,7 @@ describe('DataGrid', () => {
518
596
  ),
519
597
  })
520
598
 
521
- await sleepAsync(50)
599
+ await flushUpdates()
522
600
 
523
601
  const keydownEvent = new KeyboardEvent('keydown', { key: '+', bubbles: true })
524
602
  window.dispatchEvent(keydownEvent)
@@ -550,7 +628,7 @@ describe('DataGrid', () => {
550
628
  ),
551
629
  })
552
630
 
553
- await sleepAsync(50)
631
+ await flushUpdates()
554
632
 
555
633
  const keydownEvent = new KeyboardEvent('keydown', { key: '-', bubbles: true })
556
634
  window.dispatchEvent(keydownEvent)
@@ -582,7 +660,7 @@ describe('DataGrid', () => {
582
660
  ),
583
661
  })
584
662
 
585
- await sleepAsync(50)
663
+ await flushUpdates()
586
664
 
587
665
  const keydownEvent = new KeyboardEvent('keydown', { key: '*', bubbles: true })
588
666
  window.dispatchEvent(keydownEvent)
@@ -616,7 +694,7 @@ describe('DataGrid', () => {
616
694
  ),
617
695
  })
618
696
 
619
- await sleepAsync(50)
697
+ await flushUpdates()
620
698
 
621
699
  const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
622
700
  window.dispatchEvent(keydownEvent)
@@ -648,7 +726,7 @@ describe('DataGrid', () => {
648
726
  ),
649
727
  })
650
728
 
651
- await sleepAsync(50)
729
+ await flushUpdates()
652
730
 
653
731
  const keydownEvent = new KeyboardEvent('keydown', { key: 'Insert', bubbles: true })
654
732
  window.dispatchEvent(keydownEvent)
@@ -681,7 +759,7 @@ describe('DataGrid', () => {
681
759
  ),
682
760
  })
683
761
 
684
- await sleepAsync(50)
762
+ await flushUpdates()
685
763
 
686
764
  const grid = document.querySelector('shade-data-grid') as HTMLElement
687
765
  expect(grid?.style.backgroundColor).toBe('red')
@@ -709,7 +787,7 @@ describe('DataGrid', () => {
709
787
  ),
710
788
  })
711
789
 
712
- await sleepAsync(50)
790
+ await flushUpdates()
713
791
 
714
792
  const grid = document.querySelector('shade-data-grid')
715
793
  const headers = grid?.querySelectorAll('th') as NodeListOf<HTMLElement>
@@ -740,7 +818,7 @@ describe('DataGrid', () => {
740
818
  ),
741
819
  })
742
820
 
743
- await sleepAsync(50)
821
+ await flushUpdates()
744
822
 
745
823
  const grid = document.querySelector('shade-data-grid')
746
824
  const emptyState = grid?.querySelector('[data-testid="empty-state"]')
@@ -779,7 +857,7 @@ describe('DataGrid', () => {
779
857
  ),
780
858
  })
781
859
 
782
- await sleepAsync(50)
860
+ await flushUpdates()
783
861
 
784
862
  const grid = document.querySelector('shade-data-grid')
785
863
  const cell = grid?.querySelector('td') as HTMLTableCellElement
@@ -817,7 +895,7 @@ describe('DataGrid', () => {
817
895
  ),
818
896
  })
819
897
 
820
- await sleepAsync(50)
898
+ await flushUpdates()
821
899
 
822
900
  const grid = document.querySelector('shade-data-grid')
823
901
  const cell = grid?.querySelector('td') as HTMLTableCellElement
@@ -854,12 +932,12 @@ describe('DataGrid', () => {
854
932
  ),
855
933
  })
856
934
 
857
- await sleepAsync(50)
935
+ await flushUpdates()
858
936
 
859
937
  const grid = document.querySelector('shade-data-grid') as HTMLElement
860
938
  grid.remove()
861
939
 
862
- await sleepAsync(10)
940
+ await flushUpdates()
863
941
 
864
942
  window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
865
943
  expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' })
@@ -10,6 +10,35 @@ import { DataGridBody } from './body.js'
10
10
  import { DataGridFooter } from './footer.js'
11
11
  import { DataGridHeader } from './header.js'
12
12
 
13
+ export type StringFilterConfig = { type: 'string' }
14
+ export type NumberFilterConfig = { type: 'number' }
15
+ export type BooleanFilterConfig = { type: 'boolean' }
16
+ export type EnumFilterConfig = {
17
+ type: 'enum'
18
+ values: Array<{ label: string; value: string }>
19
+ }
20
+ export type DateFilterConfig = { type: 'date' }
21
+
22
+ export type ColumnFilterConfig =
23
+ | StringFilterConfig
24
+ | NumberFilterConfig
25
+ | BooleanFilterConfig
26
+ | EnumFilterConfig
27
+ | DateFilterConfig
28
+
29
+ /**
30
+ * Loosely typed find options used internally by filter components.
31
+ * Avoids generic entity types while supporting dynamic field access
32
+ * with explicit casts at each use site.
33
+ */
34
+ export type FilterableFindOptions = {
35
+ top?: number
36
+ skip?: number
37
+ order?: Record<string, 'ASC' | 'DESC'>
38
+ filter?: Record<string, unknown>
39
+ select?: string[]
40
+ }
41
+
13
42
  export type DataHeaderCells<Column extends string> = {
14
43
  [TKey in Column | 'default']?: (name: Column) => JSX.Element
15
44
  }
@@ -38,11 +67,17 @@ export interface DataGridProps<T, Column extends string> {
38
67
  /**
39
68
  * A list of custom header components to use
40
69
  */
41
- headerComponents: DataHeaderCells<Column>
70
+ headerComponents?: DataHeaderCells<Column>
42
71
  /**
43
72
  * A list of custom row components to use
44
73
  */
45
- rowComponents: DataRowCells<T, Column>
74
+ rowComponents?: DataRowCells<T, Column>
75
+
76
+ /**
77
+ * Filter configuration per column. Only columns with a config will show a filter button.
78
+ */
79
+ columnFilters?: { [K in Column]?: ColumnFilterConfig }
80
+
46
81
  /**
47
82
  * Optional autoFocus property to set the grid as focused
48
83
  */
@@ -96,20 +131,17 @@ export const DataGrid: <T, Column extends string>(
96
131
  backdropFilter: 'blur(12px) saturate(180%)',
97
132
  background: cssVariableTheme.action.activeBackground,
98
133
  color: cssVariableTheme.text.secondary,
99
- height: '48px',
100
- padding: '0 1.2em',
134
+ height: '36px',
135
+ padding: '0 0.6em',
101
136
  alignItems: 'center',
102
- borderRadius: cssVariableTheme.shape.borderRadius.xs,
103
- top: '2px',
137
+ top: '0',
104
138
  position: 'sticky',
105
- fontVariant: 'all-petite-caps',
106
- fontSize: '0.875rem',
139
+ fontSize: '0.75rem',
107
140
  fontWeight: cssVariableTheme.typography.fontWeight.semibold,
108
- letterSpacing: '0.05em',
141
+ letterSpacing: '0.03em',
109
142
  textAlign: 'left',
110
143
  zIndex: '1',
111
- boxShadow: cssVariableTheme.shadows.sm,
112
- borderBottom: `2px solid ${cssVariableTheme.action.subtleBorder}`,
144
+ borderBottom: `1px solid ${cssVariableTheme.action.subtleBorder}`,
113
145
  borderRight: `1px solid ${cssVariableTheme.action.subtleBorder}`,
114
146
  },
115
147
  },
@@ -156,6 +188,7 @@ export const DataGrid: <T, Column extends string>(
156
188
  >
157
189
  field={column}
158
190
  findOptions={props.findOptions}
191
+ filterConfig={props.columnFilters?.[column]}
159
192
  />
160
193
  )}
161
194
  </th>
@@ -0,0 +1,142 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { ObservableValue, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import type { FilterableFindOptions } from '../data-grid.js'
6
+ import { BooleanFilter } from './boolean-filter.js'
7
+
8
+ describe('BooleanFilter', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '<div id="root"></div>'
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ })
16
+
17
+ const createFindOptions = (options: Partial<FilterableFindOptions> = {}): ObservableValue<FilterableFindOptions> => {
18
+ return new ObservableValue<FilterableFindOptions>(options)
19
+ }
20
+
21
+ const renderBooleanFilter = async (
22
+ findOptions: ObservableValue<FilterableFindOptions>,
23
+ field = 'isActive',
24
+ onClose = vi.fn(),
25
+ ) => {
26
+ const injector = new Injector()
27
+ const rootElement = document.getElementById('root')!
28
+ initializeShadeRoot({
29
+ injector,
30
+ rootElement,
31
+ jsxElement: <BooleanFilter field={field} findOptions={findOptions} onClose={onClose} />,
32
+ })
33
+ await flushUpdates()
34
+ return { injector, onClose }
35
+ }
36
+
37
+ it('should render three options: True, False, Any', async () => {
38
+ const findOptions = createFindOptions()
39
+ await usingAsync((await renderBooleanFilter(findOptions)).injector, async () => {
40
+ const buttons = document.querySelectorAll('shade-segmented-control button[data-value]')
41
+ expect(buttons.length).toBe(3)
42
+ const values = Array.from(buttons).map((b) => b.getAttribute('data-value'))
43
+ expect(values).toEqual(['true', 'false', 'any'])
44
+ })
45
+ })
46
+
47
+ it('should select "any" when no filter is set', async () => {
48
+ const findOptions = createFindOptions()
49
+ await usingAsync((await renderBooleanFilter(findOptions)).injector, async () => {
50
+ const selected = document.querySelector('shade-segmented-control button[data-selected]')
51
+ expect(selected?.getAttribute('data-value')).toBe('any')
52
+ })
53
+ })
54
+
55
+ it('should select "true" when filter is $eq: true', async () => {
56
+ const findOptions = createFindOptions({ filter: { isActive: { $eq: true } } })
57
+ await usingAsync((await renderBooleanFilter(findOptions)).injector, async () => {
58
+ const selected = document.querySelector('shade-segmented-control button[data-selected]')
59
+ expect(selected?.getAttribute('data-value')).toBe('true')
60
+ })
61
+ })
62
+
63
+ it('should select "false" when filter is $eq: false', async () => {
64
+ const findOptions = createFindOptions({ filter: { isActive: { $eq: false } } })
65
+ await usingAsync((await renderBooleanFilter(findOptions)).injector, async () => {
66
+ const selected = document.querySelector('shade-segmented-control button[data-selected]')
67
+ expect(selected?.getAttribute('data-value')).toBe('false')
68
+ })
69
+ })
70
+
71
+ it('should set filter to $eq: true when "True" is clicked', async () => {
72
+ const findOptions = createFindOptions()
73
+ const { injector, onClose } = await renderBooleanFilter(findOptions)
74
+ await usingAsync(injector, async () => {
75
+ const trueButton = document.querySelector(
76
+ 'shade-segmented-control button[data-value="true"]',
77
+ ) as HTMLButtonElement
78
+ trueButton?.click()
79
+ await flushUpdates()
80
+
81
+ expect(findOptions.getValue().filter).toEqual({ isActive: { $eq: true } })
82
+ expect(onClose).toHaveBeenCalled()
83
+ })
84
+ })
85
+
86
+ it('should set filter to $eq: false when "False" is clicked', async () => {
87
+ const findOptions = createFindOptions()
88
+ const { injector, onClose } = await renderBooleanFilter(findOptions)
89
+ await usingAsync(injector, async () => {
90
+ const falseButton = document.querySelector(
91
+ 'shade-segmented-control button[data-value="false"]',
92
+ ) as HTMLButtonElement
93
+ falseButton?.click()
94
+ await flushUpdates()
95
+
96
+ expect(findOptions.getValue().filter).toEqual({ isActive: { $eq: false } })
97
+ expect(onClose).toHaveBeenCalled()
98
+ })
99
+ })
100
+
101
+ it('should remove filter when "Any" is clicked', async () => {
102
+ const findOptions = createFindOptions({ filter: { isActive: { $eq: true } } })
103
+ const { injector, onClose } = await renderBooleanFilter(findOptions)
104
+ await usingAsync(injector, async () => {
105
+ const anyButton = document.querySelector('shade-segmented-control button[data-value="any"]') as HTMLButtonElement
106
+ anyButton?.click()
107
+ await flushUpdates()
108
+
109
+ expect(findOptions.getValue().filter?.isActive).toBeUndefined()
110
+ expect(onClose).toHaveBeenCalled()
111
+ })
112
+ })
113
+
114
+ it('should preserve filters on other fields', async () => {
115
+ const findOptions = createFindOptions({ filter: { isActive: { $eq: true }, name: { $regex: 'test' } } })
116
+ const { injector, onClose } = await renderBooleanFilter(findOptions)
117
+ await usingAsync(injector, async () => {
118
+ const anyButton = document.querySelector('shade-segmented-control button[data-value="any"]') as HTMLButtonElement
119
+ anyButton?.click()
120
+ await flushUpdates()
121
+
122
+ const updatedFilter = findOptions.getValue().filter
123
+ expect(updatedFilter?.isActive).toBeUndefined()
124
+ expect(updatedFilter?.name).toEqual({ $regex: 'test' })
125
+ expect(onClose).toHaveBeenCalled()
126
+ })
127
+ })
128
+
129
+ it('should reset skip to 0 when applying filter', async () => {
130
+ const findOptions = createFindOptions({ skip: 20 })
131
+ const { injector } = await renderBooleanFilter(findOptions)
132
+ await usingAsync(injector, async () => {
133
+ const trueButton = document.querySelector(
134
+ 'shade-segmented-control button[data-value="true"]',
135
+ ) as HTMLButtonElement
136
+ trueButton?.click()
137
+ await flushUpdates()
138
+
139
+ expect(findOptions.getValue().skip).toBe(0)
140
+ })
141
+ })
142
+ })
@@ -0,0 +1,45 @@
1
+ import { createComponent, Shade } from '@furystack/shades'
2
+ import type { ObservableValue } from '@furystack/utils'
3
+ import { SegmentedControl } from '../../button-group.js'
4
+ import type { FilterableFindOptions } from '../data-grid.js'
5
+
6
+ type BooleanFilterValue = 'true' | 'false' | 'any'
7
+
8
+ export const BooleanFilter = Shade<{
9
+ field: string
10
+ findOptions: ObservableValue<FilterableFindOptions>
11
+ onClose: () => void
12
+ }>({
13
+ shadowDomName: 'data-grid-boolean-filter',
14
+ render: ({ props, useObservable }) => {
15
+ const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions)
16
+
17
+ const currentFilter = findOptions.filter?.[props.field] as { $eq?: boolean } | undefined
18
+ const currentValue: BooleanFilterValue =
19
+ currentFilter?.$eq === true ? 'true' : currentFilter?.$eq === false ? 'false' : 'any'
20
+
21
+ const applyFilter = (value: BooleanFilterValue) => {
22
+ const filter = { ...findOptions.filter }
23
+ if (value === 'any') {
24
+ delete filter[props.field]
25
+ } else {
26
+ filter[props.field] = { $eq: value === 'true' }
27
+ }
28
+ setFindOptions({ ...findOptions, filter, skip: 0 })
29
+ props.onClose()
30
+ }
31
+
32
+ return (
33
+ <SegmentedControl
34
+ size="small"
35
+ value={currentValue}
36
+ onValueChange={(v) => applyFilter(v as BooleanFilterValue)}
37
+ options={[
38
+ { value: 'true', label: 'True' },
39
+ { value: 'false', label: 'False' },
40
+ { value: 'any', label: 'Any' },
41
+ ]}
42
+ />
43
+ )
44
+ },
45
+ })