@svgrid/grid 1.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 (213) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +39 -0
  3. package/dist/FlexRender.svelte +96 -0
  4. package/dist/FlexRender.svelte.d.ts +49 -0
  5. package/dist/SvGrid.svelte +8742 -0
  6. package/dist/SvGrid.svelte.d.ts +381 -0
  7. package/dist/SvGridChart.svelte +653 -0
  8. package/dist/SvGridChart.svelte.d.ts +25 -0
  9. package/dist/SvGridChart.test.d.ts +1 -0
  10. package/dist/SvGridChart.test.js +62 -0
  11. package/dist/SvGridDropdown.svelte +675 -0
  12. package/dist/SvGridDropdown.svelte.d.ts +40 -0
  13. package/dist/a11y.contract.test.d.ts +1 -0
  14. package/dist/a11y.contract.test.js +39 -0
  15. package/dist/a11y.d.ts +38 -0
  16. package/dist/a11y.js +37 -0
  17. package/dist/a11y.test.d.ts +1 -0
  18. package/dist/a11y.test.js +44 -0
  19. package/dist/cell-formatting.d.ts +17 -0
  20. package/dist/cell-formatting.js +141 -0
  21. package/dist/cell-formatting.test.d.ts +1 -0
  22. package/dist/cell-formatting.test.js +234 -0
  23. package/dist/chart-export.d.ts +14 -0
  24. package/dist/chart-export.js +112 -0
  25. package/dist/chart.d.ts +226 -0
  26. package/dist/chart.js +732 -0
  27. package/dist/chart.test.d.ts +1 -0
  28. package/dist/chart.test.js +289 -0
  29. package/dist/collaboration.d.ts +74 -0
  30. package/dist/collaboration.js +98 -0
  31. package/dist/collaboration.test.d.ts +1 -0
  32. package/dist/collaboration.test.js +65 -0
  33. package/dist/conditional-formatting.d.ts +91 -0
  34. package/dist/conditional-formatting.js +170 -0
  35. package/dist/conditional-formatting.test.d.ts +1 -0
  36. package/dist/conditional-formatting.test.js +87 -0
  37. package/dist/core.coverage.test.d.ts +1 -0
  38. package/dist/core.coverage.test.js +186 -0
  39. package/dist/core.d.ts +370 -0
  40. package/dist/core.js +568 -0
  41. package/dist/core.performance.test.d.ts +1 -0
  42. package/dist/core.performance.test.js +29 -0
  43. package/dist/createGrid.svelte.d.ts +6 -0
  44. package/dist/createGrid.svelte.js +17 -0
  45. package/dist/createGrid.test.d.ts +1 -0
  46. package/dist/createGrid.test.js +9 -0
  47. package/dist/createGridState.svelte.d.ts +3 -0
  48. package/dist/createGridState.svelte.js +13 -0
  49. package/dist/editors/cell-editors.d.ts +23 -0
  50. package/dist/editors/cell-editors.js +97 -0
  51. package/dist/editors/cell-editors.test.d.ts +1 -0
  52. package/dist/editors/cell-editors.test.js +75 -0
  53. package/dist/fill-patterns.d.ts +30 -0
  54. package/dist/fill-patterns.js +207 -0
  55. package/dist/fill-patterns.test.d.ts +1 -0
  56. package/dist/fill-patterns.test.js +81 -0
  57. package/dist/filtering/excel-filters.d.ts +26 -0
  58. package/dist/filtering/excel-filters.js +64 -0
  59. package/dist/filtering/excel-filters.test.d.ts +1 -0
  60. package/dist/filtering/excel-filters.test.js +116 -0
  61. package/dist/filtering/locale-filter.test.d.ts +13 -0
  62. package/dist/filtering/locale-filter.test.js +189 -0
  63. package/dist/flex-render.d.ts +1 -0
  64. package/dist/flex-render.js +2 -0
  65. package/dist/flex-render.test.d.ts +1 -0
  66. package/dist/flex-render.test.js +142 -0
  67. package/dist/group-aggregate.test.d.ts +1 -0
  68. package/dist/group-aggregate.test.js +32 -0
  69. package/dist/index.d.ts +29 -0
  70. package/dist/index.js +27 -0
  71. package/dist/keyboard.d.ts +8 -0
  72. package/dist/keyboard.js +91 -0
  73. package/dist/keyboard.test.d.ts +1 -0
  74. package/dist/keyboard.test.js +27 -0
  75. package/dist/merge-objects.d.ts +9 -0
  76. package/dist/merge-objects.js +28 -0
  77. package/dist/named-views.d.ts +42 -0
  78. package/dist/named-views.js +80 -0
  79. package/dist/named-views.test.d.ts +1 -0
  80. package/dist/named-views.test.js +57 -0
  81. package/dist/new-features.test.d.ts +1 -0
  82. package/dist/new-features.test.js +183 -0
  83. package/dist/render-component.d.ts +13 -0
  84. package/dist/render-component.js +14 -0
  85. package/dist/render-component.test.d.ts +1 -0
  86. package/dist/render-component.test.js +41 -0
  87. package/dist/server-data-source.d.ts +76 -0
  88. package/dist/server-data-source.js +83 -0
  89. package/dist/server-data-source.test.d.ts +1 -0
  90. package/dist/server-data-source.test.js +81 -0
  91. package/dist/sparkline.d.ts +56 -0
  92. package/dist/sparkline.js +97 -0
  93. package/dist/sparkline.test.d.ts +1 -0
  94. package/dist/sparkline.test.js +52 -0
  95. package/dist/static-functions.d.ts +1 -0
  96. package/dist/static-functions.js +1 -0
  97. package/dist/subscribe.d.ts +9 -0
  98. package/dist/subscribe.js +31 -0
  99. package/dist/subscribe.test.d.ts +1 -0
  100. package/dist/subscribe.test.js +93 -0
  101. package/dist/sv-grid-scrollbar.d.ts +1 -0
  102. package/dist/sv-grid-scrollbar.js +310 -0
  103. package/dist/svgrid-wrapper.types.d.ts +341 -0
  104. package/dist/svgrid-wrapper.types.js +1 -0
  105. package/dist/svgrid.api-extensions.test.d.ts +14 -0
  106. package/dist/svgrid.api-extensions.test.js +334 -0
  107. package/dist/svgrid.api.test.d.ts +15 -0
  108. package/dist/svgrid.api.test.js +169 -0
  109. package/dist/svgrid.behavior.test.d.ts +15 -0
  110. package/dist/svgrid.behavior.test.js +605 -0
  111. package/dist/svgrid.column-reorder.test.d.ts +15 -0
  112. package/dist/svgrid.column-reorder.test.js +224 -0
  113. package/dist/svgrid.features.test.d.ts +1 -0
  114. package/dist/svgrid.features.test.js +129 -0
  115. package/dist/svgrid.interaction.test.d.ts +9 -0
  116. package/dist/svgrid.interaction.test.js +318 -0
  117. package/dist/svgrid.locale-filtering.test.d.ts +15 -0
  118. package/dist/svgrid.locale-filtering.test.js +261 -0
  119. package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
  120. package/dist/svgrid.new-features.wrapper.test.js +136 -0
  121. package/dist/svgrid.row-pinning.test.d.ts +14 -0
  122. package/dist/svgrid.row-pinning.test.js +250 -0
  123. package/dist/svgrid.set-filter.test.d.ts +17 -0
  124. package/dist/svgrid.set-filter.test.js +268 -0
  125. package/dist/svgrid.wrapper.test.d.ts +1 -0
  126. package/dist/svgrid.wrapper.test.js +33 -0
  127. package/dist/test-setup.d.ts +1 -0
  128. package/dist/test-setup.js +29 -0
  129. package/dist/transaction.test.d.ts +1 -0
  130. package/dist/transaction.test.js +89 -0
  131. package/dist/virtualization/column-virtualizer.d.ts +20 -0
  132. package/dist/virtualization/column-virtualizer.js +19 -0
  133. package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
  134. package/dist/virtualization/column-virtualizer.test.js +25 -0
  135. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
  136. package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
  137. package/dist/virtualization/types.d.ts +28 -0
  138. package/dist/virtualization/types.js +1 -0
  139. package/dist/virtualization/virtualizer.d.ts +13 -0
  140. package/dist/virtualization/virtualizer.js +232 -0
  141. package/dist/virtualization/virtualizer.test.d.ts +1 -0
  142. package/dist/virtualization/virtualizer.test.js +41 -0
  143. package/package.json +69 -0
  144. package/src/FlexRender.svelte +96 -0
  145. package/src/SvGrid.svelte +8742 -0
  146. package/src/SvGridChart.svelte +653 -0
  147. package/src/SvGridChart.test.ts +69 -0
  148. package/src/SvGridDropdown.svelte +675 -0
  149. package/src/a11y.contract.test.ts +49 -0
  150. package/src/a11y.test.ts +59 -0
  151. package/src/a11y.ts +59 -0
  152. package/src/cell-formatting.test.ts +273 -0
  153. package/src/cell-formatting.ts +169 -0
  154. package/src/chart-export.ts +144 -0
  155. package/src/chart.test.ts +311 -0
  156. package/src/chart.ts +934 -0
  157. package/src/collaboration.test.ts +74 -0
  158. package/src/collaboration.ts +161 -0
  159. package/src/conditional-formatting.test.ts +107 -0
  160. package/src/conditional-formatting.ts +239 -0
  161. package/src/core.coverage.test.ts +230 -0
  162. package/src/core.performance.test.ts +30 -0
  163. package/src/core.ts +991 -0
  164. package/src/createGrid.svelte.ts +42 -0
  165. package/src/createGrid.test.ts +10 -0
  166. package/src/createGridState.svelte.ts +17 -0
  167. package/src/editors/cell-editors.test.ts +80 -0
  168. package/src/editors/cell-editors.ts +127 -0
  169. package/src/fill-patterns.test.ts +93 -0
  170. package/src/fill-patterns.ts +229 -0
  171. package/src/filtering/excel-filters.test.ts +131 -0
  172. package/src/filtering/excel-filters.ts +101 -0
  173. package/src/filtering/locale-filter.test.ts +220 -0
  174. package/src/flex-render.test.ts +143 -0
  175. package/src/flex-render.ts +3 -0
  176. package/src/group-aggregate.test.ts +48 -0
  177. package/src/index.ts +159 -0
  178. package/src/keyboard.test.ts +59 -0
  179. package/src/keyboard.ts +97 -0
  180. package/src/merge-objects.ts +48 -0
  181. package/src/named-views.test.ts +66 -0
  182. package/src/named-views.ts +120 -0
  183. package/src/new-features.test.ts +217 -0
  184. package/src/render-component.test.ts +51 -0
  185. package/src/render-component.ts +28 -0
  186. package/src/server-data-source.test.ts +88 -0
  187. package/src/server-data-source.ts +163 -0
  188. package/src/sparkline.test.ts +59 -0
  189. package/src/sparkline.ts +164 -0
  190. package/src/static-functions.ts +11 -0
  191. package/src/subscribe.test.ts +103 -0
  192. package/src/subscribe.ts +38 -0
  193. package/src/sv-grid-scrollbar.ts +347 -0
  194. package/src/svgrid-wrapper.types.ts +382 -0
  195. package/src/svgrid.api-extensions.test.ts +362 -0
  196. package/src/svgrid.api.test.ts +192 -0
  197. package/src/svgrid.behavior.test.ts +657 -0
  198. package/src/svgrid.column-reorder.test.ts +234 -0
  199. package/src/svgrid.features.test.ts +157 -0
  200. package/src/svgrid.interaction.test.ts +355 -0
  201. package/src/svgrid.locale-filtering.test.ts +259 -0
  202. package/src/svgrid.new-features.wrapper.test.ts +164 -0
  203. package/src/svgrid.row-pinning.test.ts +266 -0
  204. package/src/svgrid.set-filter.test.ts +270 -0
  205. package/src/svgrid.wrapper.test.ts +35 -0
  206. package/src/test-setup.ts +37 -0
  207. package/src/transaction.test.ts +100 -0
  208. package/src/virtualization/column-virtualizer.test.ts +27 -0
  209. package/src/virtualization/column-virtualizer.ts +30 -0
  210. package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
  211. package/src/virtualization/types.ts +30 -0
  212. package/src/virtualization/virtualizer.test.ts +47 -0
  213. package/src/virtualization/virtualizer.ts +270 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Component tests for `api.setFacetFilter()` - the imperative entry
3
+ * point that powers the three set-filter UX patterns (Excel-style
4
+ * column-menu, async value loaders, and tree-list cascades). The grid
5
+ * is filter-source-agnostic; this test exercises the API directly so
6
+ * coverage is independent of any specific UI implementation.
7
+ *
8
+ * Covers:
9
+ * - Single column facet filter narrows the displayed rows
10
+ * - Multi-value filter (OR within column) accepts every listed value
11
+ * - Passing null / empty clears the filter
12
+ * - Two columns AND across (different columns)
13
+ * - Combines with operator filter from setFilter()
14
+ * - Survives unknown values (no crash, just no matches)
15
+ * - Unicode / non-ASCII values match exactly (no normalization)
16
+ */
17
+
18
+ import { describe, expect, it } from 'vitest'
19
+ import { mount, unmount } from 'svelte'
20
+ import SvGrid from './SvGrid.svelte'
21
+ import {
22
+ columnFilteringFeature,
23
+ createCoreRowModel,
24
+ createFilteredRowModel,
25
+ createSortedRowModel,
26
+ rowSortingFeature,
27
+ sortFns,
28
+ tableFeatures,
29
+ } from './index'
30
+ import type { ColumnDef, SvGridApi } from './index'
31
+
32
+ type Order = {
33
+ id: string
34
+ region: 'Americas' | 'EMEA' | 'APAC'
35
+ country: string
36
+ status: 'open' | 'paid' | 'shipped' | 'delivered' | 'returned'
37
+ amount: number
38
+ }
39
+
40
+ const features = tableFeatures({ columnFilteringFeature, rowSortingFeature })
41
+
42
+ const rows: Order[] = [
43
+ { id: 'O-1', region: 'Americas', country: 'United States', status: 'open', amount: 120 },
44
+ { id: 'O-2', region: 'Americas', country: 'United States', status: 'paid', amount: 240 },
45
+ { id: 'O-3', region: 'Americas', country: 'Canada', status: 'delivered', amount: 410 },
46
+ { id: 'O-4', region: 'Americas', country: 'Brazil', status: 'shipped', amount: 180 },
47
+ { id: 'O-5', region: 'EMEA', country: 'Germany', status: 'paid', amount: 330 },
48
+ { id: 'O-6', region: 'EMEA', country: 'Germany', status: 'returned', amount: 205 },
49
+ { id: 'O-7', region: 'EMEA', country: 'France', status: 'open', amount: 155 },
50
+ { id: 'O-8', region: 'EMEA', country: 'France', status: 'shipped', amount: 420 },
51
+ { id: 'O-9', region: 'APAC', country: 'Japan', status: 'paid', amount: 890 },
52
+ { id: 'O-10', region: 'APAC', country: 'Japan', status: 'returned', amount: 240 },
53
+ { id: 'O-11', region: 'APAC', country: 'Australia', status: 'open', amount: 175 },
54
+ { id: 'O-12', region: 'APAC', country: 'Singapore', status: 'delivered', amount: 1200 },
55
+ ]
56
+
57
+ const cols: ColumnDef<typeof features, Order>[] = [
58
+ { field: 'id', header: 'ID', width: 90 },
59
+ { field: 'region', header: 'Region', width: 110 },
60
+ { field: 'country', header: 'Country', width: 160 },
61
+ { field: 'status', header: 'Status', width: 110 },
62
+ { field: 'amount', header: 'Amount', width: 100, align: 'right' },
63
+ ]
64
+
65
+ function mountGrid() {
66
+ return new Promise<{
67
+ api: SvGridApi<typeof features, Order>
68
+ destroy: () => void
69
+ }>((res, rej) => {
70
+ const target = document.createElement('div')
71
+ document.body.appendChild(target)
72
+ let api: SvGridApi<typeof features, Order> | null = null
73
+ const app = mount(SvGrid, {
74
+ target,
75
+ props: {
76
+ data: rows,
77
+ columns: cols,
78
+ features,
79
+ _rowModels: {
80
+ coreRowModel: createCoreRowModel(),
81
+ filteredRowModel: createFilteredRowModel(),
82
+ sortedRowModel: createSortedRowModel(sortFns),
83
+ },
84
+ containerHeight: 360,
85
+ virtualization: false,
86
+ columnVirtualization: false,
87
+ showPagination: false,
88
+ showColumnFilters: false,
89
+ showGlobalFilter: false,
90
+ showRowSelection: false,
91
+ onApiReady(received: SvGridApi<typeof features, Order>) {
92
+ api = received
93
+ res({ api, destroy: () => { unmount(app); target.remove() } })
94
+ },
95
+ } as any,
96
+ })
97
+ queueMicrotask(() => { if (!api) rej(new Error('onApiReady never fired')) })
98
+ })
99
+ }
100
+
101
+ const tick = () => new Promise<void>((r) => queueMicrotask(r))
102
+
103
+ describe('SvGridApi.setFacetFilter - single column', () => {
104
+ it('narrows to one allowed value', async () => {
105
+ const { api, destroy } = await mountGrid()
106
+ try {
107
+ api.setFacetFilter('region', ['EMEA'])
108
+ await tick()
109
+ const ids = api.getDisplayedRows().map((r) => r.id)
110
+ expect(ids).toEqual(['O-5', 'O-6', 'O-7', 'O-8'])
111
+ } finally { destroy() }
112
+ })
113
+
114
+ it('accepts multiple values (OR within column)', async () => {
115
+ const { api, destroy } = await mountGrid()
116
+ try {
117
+ api.setFacetFilter('region', ['EMEA', 'APAC'])
118
+ await tick()
119
+ const regions = new Set(api.getDisplayedRows().map((r) => r.region))
120
+ expect(regions).toEqual(new Set(['EMEA', 'APAC']))
121
+ expect(api.getDisplayedRows().length).toBe(8)
122
+ } finally { destroy() }
123
+ })
124
+
125
+ it('empty allowed array filters everything out', async () => {
126
+ // An empty SET is treated as a clear (matches the implementation -
127
+ // see `setFacetFilter`'s null branch). Verify the documented
128
+ // behavior: empty array clears.
129
+ const { api, destroy } = await mountGrid()
130
+ try {
131
+ api.setFacetFilter('region', [])
132
+ await tick()
133
+ expect(api.getDisplayedRows().length).toBe(rows.length)
134
+ } finally { destroy() }
135
+ })
136
+
137
+ it('null clears the filter', async () => {
138
+ const { api, destroy } = await mountGrid()
139
+ try {
140
+ api.setFacetFilter('region', ['EMEA'])
141
+ await tick()
142
+ expect(api.getDisplayedRows().length).toBe(4)
143
+ api.setFacetFilter('region', null)
144
+ await tick()
145
+ expect(api.getDisplayedRows().length).toBe(rows.length)
146
+ } finally { destroy() }
147
+ })
148
+ })
149
+
150
+ describe('SvGridApi.setFacetFilter - multi-column AND', () => {
151
+ it('region + status both narrow', async () => {
152
+ const { api, destroy } = await mountGrid()
153
+ try {
154
+ api.setFacetFilter('region', ['EMEA'])
155
+ api.setFacetFilter('status', ['paid', 'returned'])
156
+ await tick()
157
+ const ids = api.getDisplayedRows().map((r) => r.id)
158
+ expect(ids).toEqual(['O-5', 'O-6']) // EMEA rows whose status is paid or returned
159
+ } finally { destroy() }
160
+ })
161
+
162
+ it('region + country + status all compose', async () => {
163
+ const { api, destroy } = await mountGrid()
164
+ try {
165
+ api.setFacetFilter('region', ['APAC'])
166
+ api.setFacetFilter('country', ['Japan', 'Singapore'])
167
+ api.setFacetFilter('status', ['delivered'])
168
+ await tick()
169
+ const ids = api.getDisplayedRows().map((r) => r.id)
170
+ expect(ids).toEqual(['O-12']) // APAC + Singapore + delivered
171
+ } finally { destroy() }
172
+ })
173
+ })
174
+
175
+ describe('SvGridApi.setFacetFilter - composition with operator filters', () => {
176
+ it('facet narrows; setFilter further narrows on the same column id', async () => {
177
+ const { api, destroy } = await mountGrid()
178
+ try {
179
+ api.setFacetFilter('region', ['Americas', 'EMEA'])
180
+ api.setFilter('country', { operator: 'startsWith', value: 'g' }) // → Germany
181
+ await tick()
182
+ const ids = api.getDisplayedRows().map((r) => r.id)
183
+ expect(ids).toEqual(['O-5', 'O-6'])
184
+ } finally { destroy() }
185
+ })
186
+
187
+ it('clearing the facet but keeping setFilter only honors setFilter', async () => {
188
+ const { api, destroy } = await mountGrid()
189
+ try {
190
+ api.setFacetFilter('region', ['EMEA'])
191
+ api.setFilter('country', { operator: 'contains', value: 'fran' })
192
+ await tick()
193
+ expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8'])
194
+
195
+ api.setFacetFilter('region', null)
196
+ await tick()
197
+ expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8'])
198
+ } finally { destroy() }
199
+ })
200
+ })
201
+
202
+ describe('SvGridApi.setFacetFilter - edge cases', () => {
203
+ it('unknown allowed values silently match nothing', async () => {
204
+ const { api, destroy } = await mountGrid()
205
+ try {
206
+ api.setFacetFilter('status', ['SHIPPED_IN_TRANSIT', 'PENDING_REVIEW'])
207
+ await tick()
208
+ expect(api.getDisplayedRows().length).toBe(0)
209
+ } finally { destroy() }
210
+ })
211
+
212
+ it('mixed known + unknown values: known values still match', async () => {
213
+ const { api, destroy } = await mountGrid()
214
+ try {
215
+ api.setFacetFilter('status', ['shipped', 'UNKNOWN_VALUE'])
216
+ await tick()
217
+ const ids = api.getDisplayedRows().map((r) => r.id)
218
+ expect(ids).toEqual(['O-4', 'O-8'])
219
+ } finally { destroy() }
220
+ })
221
+ })
222
+
223
+ describe('SvGridApi.setFacetFilter - simulates the tree-list pattern', () => {
224
+ // Demo 111's tree picker resolves a hierarchical selection to a flat
225
+ // list of leaf values, then calls setFacetFilter. These tests
226
+ // exercise that contract.
227
+
228
+ it('simulates a tree drop: select Americas → all American countries', async () => {
229
+ const { api, destroy } = await mountGrid()
230
+ try {
231
+ // Leaf descendants of the "Americas" tree node:
232
+ const americansCountries = ['United States', 'Canada', 'Brazil']
233
+ api.setFacetFilter('country', americansCountries)
234
+ await tick()
235
+ const regions = new Set(api.getDisplayedRows().map((r) => r.region))
236
+ expect(regions).toEqual(new Set(['Americas']))
237
+ } finally { destroy() }
238
+ })
239
+
240
+ it('simulates a partial-tree selection: only Japan + Singapore', async () => {
241
+ const { api, destroy } = await mountGrid()
242
+ try {
243
+ api.setFacetFilter('country', ['Japan', 'Singapore'])
244
+ await tick()
245
+ const ids = api.getDisplayedRows().map((r) => r.id)
246
+ expect(ids).toEqual(['O-9', 'O-10', 'O-12'])
247
+ } finally { destroy() }
248
+ })
249
+ })
250
+
251
+ describe('SvGridApi.setFacetFilter - simulates the async loader pattern', () => {
252
+ // Demo 111's async card emulates a server returning the distinct
253
+ // emails. The grid is told the final selection via setFacetFilter -
254
+ // there's no special "loading" code path inside the grid, the
255
+ // consumer's UI handles that. Verify the contract works regardless of
256
+ // whether the values were enumerated client-side or server-side.
257
+
258
+ it('async-loaded values: select user-resolved subset', async () => {
259
+ const { api, destroy } = await mountGrid()
260
+ try {
261
+ // Pretend the user clicked these from a server-loaded value list.
262
+ const userSelected = ['Germany', 'France']
263
+ api.setFacetFilter('country', userSelected)
264
+ await tick()
265
+ const regions = new Set(api.getDisplayedRows().map((r) => r.region))
266
+ expect(regions).toEqual(new Set(['EMEA']))
267
+ expect(api.getDisplayedRows().length).toBe(4)
268
+ } finally { destroy() }
269
+ })
270
+ })
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+ import SvGrid from './SvGrid.svelte'
5
+
6
+ describe('SvGrid wrapper', () => {
7
+ it('exports a production wrapper component', () => {
8
+ expect(SvGrid).toBeDefined()
9
+ })
10
+
11
+ it('contains full-wrapper controls and aria-activedescendant wiring', () => {
12
+ const source = readFileSync(resolve(__dirname, './SvGrid.svelte'), 'utf-8')
13
+ expect(source).toContain('Filter all rows')
14
+ expect(source).toContain('Previous')
15
+ expect(source).toContain('role="status"')
16
+ expect(source).toContain('role="alert"')
17
+ expect(source).toContain('activeDescendantId')
18
+ expect(source).toContain('getGridRootA11yProps')
19
+ expect(source).toContain('data-svgrid-header-col')
20
+ expect(source).toContain('data-svgrid-row')
21
+ expect(source).toContain('createSvelteVirtualizer')
22
+ expect(source).toContain('props.virtualization ?? true')
23
+ expect(source).toContain('virtualizer.getVirtualItems()')
24
+ expect(source).toContain('createColumnVirtualizer')
25
+ expect(source).toContain('data-selected-range')
26
+ expect(source).toContain('enableInlineEditing')
27
+ expect(source).toContain('showFilterMenu')
28
+ expect(source).toContain('<tfoot')
29
+ expect(source).toContain('editorType === "number"')
30
+ expect(source).toContain('editorType === "date"')
31
+ expect(source).toContain('editorType === "checkbox"')
32
+ expect(source).toContain('applyExcelFilter')
33
+ expect(source).toContain('showFilterRow')
34
+ })
35
+ })
@@ -0,0 +1,37 @@
1
+ // Vitest setup. jsdom doesn't ship ResizeObserver, IntersectionObserver,
2
+ // or the layout APIs SvGrid touches inside its mount effects. Provide
3
+ // minimal no-op stubs so component mounting completes without crashing.
4
+
5
+ if (typeof globalThis.ResizeObserver === 'undefined') {
6
+ // @ts-expect-error - assigning to a globalThis property the env doesn't ship
7
+ globalThis.ResizeObserver = class {
8
+ observe() {}
9
+ unobserve() {}
10
+ disconnect() {}
11
+ }
12
+ }
13
+
14
+ if (typeof globalThis.IntersectionObserver === 'undefined') {
15
+ // @ts-expect-error - same
16
+ globalThis.IntersectionObserver = class {
17
+ observe() {}
18
+ unobserve() {}
19
+ disconnect() {}
20
+ takeRecords() {
21
+ return []
22
+ }
23
+ }
24
+ }
25
+
26
+ // jsdom's HTMLElement.scrollIntoView is a no-op; some grid code calls it
27
+ // during the first effect. Make sure the method exists on every element.
28
+ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
29
+ // @ts-expect-error - patching a missing DOM method
30
+ Element.prototype.scrollIntoView = function () {}
31
+ }
32
+
33
+ // jsdom returns 0 for offset* and getBoundingClientRect; the grid only uses
34
+ // these for visual layout (column widths, virtualization windowing), so the
35
+ // zeros are harmless for behavioral tests.
36
+
37
+ export {}
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount, unmount } from 'svelte'
3
+ import SvGrid from './SvGrid.svelte'
4
+ import {
5
+ createCoreRowModel,
6
+ createFilteredRowModel,
7
+ createSortedRowModel,
8
+ sortFns,
9
+ tableFeatures,
10
+ } from './index'
11
+ import type { ColumnDef, SvGridApi } from './index'
12
+
13
+ type Order = { id: string; symbol: string; price: number }
14
+
15
+ const features = tableFeatures({})
16
+ const seed: Order[] = [
17
+ { id: 'a', symbol: 'AAPL', price: 100 },
18
+ { id: 'b', symbol: 'MSFT', price: 200 },
19
+ { id: 'c', symbol: 'NVDA', price: 300 },
20
+ ]
21
+ const columns: ColumnDef<typeof features, Order>[] = [
22
+ { field: 'symbol', header: 'Symbol' },
23
+ { field: 'price', header: 'Price' },
24
+ ]
25
+
26
+ function mountGrid(): Promise<{ api: SvGridApi<typeof features, Order>; destroy: () => void }> {
27
+ return new Promise((resolve, reject) => {
28
+ const target = document.createElement('div')
29
+ document.body.appendChild(target)
30
+ let captured: SvGridApi<typeof features, Order> | null = null
31
+ const app = mount(SvGrid, {
32
+ target,
33
+ props: {
34
+ data: seed,
35
+ columns,
36
+ features,
37
+ getRowId: (o: Order) => o.id,
38
+ _rowModels: {
39
+ coreRowModel: createCoreRowModel(),
40
+ filteredRowModel: createFilteredRowModel(),
41
+ sortedRowModel: createSortedRowModel(sortFns),
42
+ },
43
+ containerHeight: 400,
44
+ virtualization: false,
45
+ onApiReady(api: SvGridApi<typeof features, Order>) {
46
+ captured = api
47
+ resolve({ api, destroy: () => { unmount(app); target.remove() } })
48
+ },
49
+ } as any,
50
+ })
51
+ queueMicrotask(() => { if (!captured) reject(new Error('onApiReady never fired')) })
52
+ })
53
+ }
54
+
55
+ const tick = () => Promise.resolve()
56
+
57
+ describe('api.applyTransaction', () => {
58
+ it('adds, updates (by id), and removes (by id) in one batch', async () => {
59
+ const { api, destroy } = await mountGrid()
60
+ try {
61
+ const result = api.applyTransaction({
62
+ add: [{ id: 'd', symbol: 'AMZN', price: 400 }],
63
+ update: [{ id: 'b', symbol: 'MSFT', price: 222 }],
64
+ remove: ['a'],
65
+ })
66
+ await tick()
67
+ expect(result).toEqual({ added: 1, updated: 1, removed: 1 })
68
+ const data = api.getData()
69
+ expect(data.map((o) => o.id)).toEqual(['b', 'c', 'd'])
70
+ expect(data.find((o) => o.id === 'b')?.price).toBe(222)
71
+ } finally {
72
+ destroy()
73
+ }
74
+ })
75
+
76
+ it('removes by row reference too', async () => {
77
+ const { api, destroy } = await mountGrid()
78
+ try {
79
+ const ref = api.getData()[2]! // 'c'
80
+ const result = api.applyTransaction({ remove: [ref] })
81
+ await tick()
82
+ expect(result.removed).toBe(1)
83
+ expect(api.getData().map((o) => o.id)).toEqual(['a', 'b'])
84
+ } finally {
85
+ destroy()
86
+ }
87
+ })
88
+
89
+ it('ignores unknown ids without throwing', async () => {
90
+ const { api, destroy } = await mountGrid()
91
+ try {
92
+ const result = api.applyTransaction({ update: [{ id: 'zzz', symbol: 'X', price: 1 }], remove: ['nope'] })
93
+ await tick()
94
+ expect(result).toEqual({ added: 0, updated: 0, removed: 0 })
95
+ expect(api.getData()).toHaveLength(3)
96
+ } finally {
97
+ destroy()
98
+ }
99
+ })
100
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createColumnVirtualizer } from './column-virtualizer'
3
+
4
+ describe('column virtualizer', () => {
5
+ it('computes horizontal window', () => {
6
+ const v = createColumnVirtualizer({
7
+ count: 1000,
8
+ viewportWidth: 700,
9
+ overscan: 3,
10
+ estimateSize: () => 140,
11
+ })
12
+ const items = v.getVirtualItems()
13
+ expect(items[0]?.index).toBe(0)
14
+ expect(items.length).toBeGreaterThan(0)
15
+ })
16
+
17
+ it('updates indices on horizontal scroll', () => {
18
+ const v = createColumnVirtualizer({
19
+ count: 1000,
20
+ viewportWidth: 700,
21
+ estimateSize: () => 140,
22
+ })
23
+ v.setHorizontalOffset(2800)
24
+ const state = v.getState()
25
+ expect(state.startIndex).toBeGreaterThan(0)
26
+ })
27
+ })
@@ -0,0 +1,30 @@
1
+ import { createVirtualizer } from './virtualizer'
2
+
3
+ export type ColumnSizeEstimator = (index: number) => number
4
+
5
+ export function createColumnVirtualizer(input: {
6
+ count: number
7
+ viewportWidth: number
8
+ scrollOffset?: number
9
+ overscan?: number
10
+ /** Either a uniform size (number) or a per-column size function. */
11
+ estimateSize?: number | ColumnSizeEstimator
12
+ }) {
13
+ const virtualizer = createVirtualizer({
14
+ count: input.count,
15
+ estimateSize: input.estimateSize ?? 140,
16
+ viewportHeight: input.viewportWidth,
17
+ scrollOffset: input.scrollOffset ?? 0,
18
+ overscan: input.overscan ?? 4,
19
+ })
20
+
21
+ return {
22
+ ...virtualizer,
23
+ setViewportWidth(viewportWidth: number) {
24
+ virtualizer.setViewportHeight(viewportWidth)
25
+ },
26
+ setHorizontalOffset(scrollOffset: number) {
27
+ virtualizer.setScrollOffset(scrollOffset)
28
+ },
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ import { createVirtualizer } from './virtualizer'
2
+ import type { VirtualizerOptions } from './types'
3
+
4
+ export function createSvelteVirtualizer(options: VirtualizerOptions) {
5
+ const virtualizer = createVirtualizer(options)
6
+ let version = $state(0)
7
+
8
+ virtualizer.subscribe(() => {
9
+ version += 1
10
+ })
11
+
12
+ return {
13
+ get version() {
14
+ return version
15
+ },
16
+ setOptions: virtualizer.setOptions,
17
+ setScrollOffset: virtualizer.setScrollOffset,
18
+ setViewportHeight: virtualizer.setViewportHeight,
19
+ scrollToIndex: virtualizer.scrollToIndex,
20
+ getVirtualItems: virtualizer.getVirtualItems,
21
+ getTotalSize: virtualizer.getTotalSize,
22
+ getState: virtualizer.getState,
23
+ }
24
+ }
@@ -0,0 +1,30 @@
1
+ export type VirtualItem = {
2
+ index: number
3
+ start: number
4
+ size: number
5
+ end: number
6
+ key: string
7
+ }
8
+
9
+ export type VirtualizerOptions = {
10
+ count: number
11
+ /**
12
+ * Per-item size. Pass a number for a uniform layout (fast path) or a
13
+ * function for variable sizing - the virtualizer will build cumulative
14
+ * offsets and use binary lookup to find the visible window. The function
15
+ * receives the item index and must return its pixel size.
16
+ */
17
+ estimateSize: number | ((index: number) => number)
18
+ overscan?: number
19
+ viewportHeight: number
20
+ scrollOffset?: number
21
+ }
22
+
23
+ export type VirtualizerState = {
24
+ items: Array<VirtualItem>
25
+ totalSize: number
26
+ startIndex: number
27
+ endIndex: number
28
+ scrollOffset: number
29
+ viewportHeight: number
30
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createVirtualizer } from './virtualizer'
3
+
4
+ describe('virtualizer', () => {
5
+ it('computes bounded virtual range with overscan', () => {
6
+ const v = createVirtualizer({
7
+ count: 100_000,
8
+ estimateSize: 36,
9
+ overscan: 8,
10
+ viewportHeight: 360,
11
+ scrollOffset: 0,
12
+ })
13
+
14
+ const items = v.getVirtualItems()
15
+ expect(items.length).toBeGreaterThan(0)
16
+ expect(items[0]?.index).toBe(0)
17
+ expect(items[items.length - 1]?.index).toBe(18)
18
+ })
19
+
20
+ it('updates range when scroll offset changes', () => {
21
+ const v = createVirtualizer({
22
+ count: 100_000,
23
+ estimateSize: 40,
24
+ overscan: 5,
25
+ viewportHeight: 400,
26
+ scrollOffset: 0,
27
+ })
28
+
29
+ v.setScrollOffset(4_000)
30
+ const state = v.getState()
31
+ expect(state.startIndex).toBeLessThanOrEqual(100)
32
+ expect(state.endIndex).toBeGreaterThanOrEqual(110)
33
+ })
34
+
35
+ it('scrollToIndex clamps target offset', () => {
36
+ const v = createVirtualizer({
37
+ count: 100,
38
+ estimateSize: 30,
39
+ overscan: 2,
40
+ viewportHeight: 300,
41
+ scrollOffset: 0,
42
+ })
43
+
44
+ v.scrollToIndex(9_999)
45
+ expect(v.getState().scrollOffset).toBe(2_700)
46
+ })
47
+ })