@svgrid/grid 2.2.31 → 2.2.33

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 (196) hide show
  1. package/README.md +1 -1
  2. package/dist/DockNodeView.svelte +1 -1
  3. package/dist/GridFooter.svelte +0 -40
  4. package/dist/GridMenus.svelte +0 -77
  5. package/dist/SvAvatar.svelte +1 -1
  6. package/dist/SvCheckBox.svelte +2 -1
  7. package/dist/SvCircularProgress.svelte +1 -1
  8. package/dist/SvDateRangeInput.svelte +1 -1
  9. package/dist/SvGrid.controller.svelte.d.ts +20 -0
  10. package/dist/SvGrid.controller.svelte.js +358 -18
  11. package/dist/SvGrid.css +103 -3
  12. package/dist/SvGrid.svelte +164 -35
  13. package/dist/SvGrid.types.d.ts +81 -0
  14. package/dist/SvGridChart.svelte +2 -10
  15. package/dist/SvGridDropdown.svelte +38 -6
  16. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  17. package/dist/SvGridSelect.svelte +12 -5
  18. package/dist/SvGridSelect.svelte.d.ts +6 -0
  19. package/dist/SvGroupCell.svelte +9 -2
  20. package/dist/SvKbd.svelte +1 -1
  21. package/dist/SvList.svelte +1 -1
  22. package/dist/SvMenubar.svelte +1 -1
  23. package/dist/SvMultiSelect.svelte +12 -4
  24. package/dist/SvMultiSelect.svelte.d.ts +6 -0
  25. package/dist/SvOtpInput.svelte +1 -1
  26. package/dist/SvPasswordInput.svelte +1 -1
  27. package/dist/SvProgress.svelte +1 -1
  28. package/dist/SvRating.svelte +1 -1
  29. package/dist/SvResult.svelte +1 -1
  30. package/dist/SvRichText.svelte +35 -16
  31. package/dist/SvRichText.svelte.d.ts +9 -0
  32. package/dist/SvSwitchButton.svelte +2 -1
  33. package/dist/SvToaster.svelte +1 -1
  34. package/dist/SvTour.svelte +4 -0
  35. package/dist/SvTree.svelte +9 -2
  36. package/dist/SvTree.svelte.d.ts +5 -0
  37. package/dist/SvTreeSelect.svelte +6 -4
  38. package/dist/build-api.js +16 -0
  39. package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-B7XhhqdN.js} +34 -36
  40. package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-BxmIvCJh.js} +35 -37
  41. package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
  42. package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-o94wfSUa.js} +3 -2
  43. package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Cr_90pPD.js} +1 -1
  44. package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-ICbxiUvK.js} +4 -4
  45. package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-Bx42PMvf.js} +2 -2
  46. package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CUOdm7YA.js} +93 -88
  47. package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
  48. package/dist/cdn/{disclose-version-DoD9AFD_.js → disclose-version-DljhZohK.js} +45 -45
  49. package/dist/cdn/{src-DYDCiC0D.js → src-erjBRYfX.js} +5952 -5615
  50. package/dist/cdn/{src-BLJfdyL0.js → src-ktbQVOk-.js} +4190 -3851
  51. package/dist/cdn/svgrid.js +9 -9
  52. package/dist/cdn/svgrid.svelte-external.js +8 -8
  53. package/dist/cell-render.d.ts +1 -0
  54. package/dist/cell-render.js +115 -1
  55. package/dist/cell-values.js +5 -0
  56. package/dist/chart.d.ts +2 -0
  57. package/dist/chart.js +1 -4
  58. package/dist/core.d.ts +80 -9
  59. package/dist/core.js +89 -0
  60. package/dist/createMenu.svelte.js +0 -1
  61. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  62. package/dist/editing.js +1 -1
  63. package/dist/editor-contract.d.ts +12 -5
  64. package/dist/editor-registry.d.ts +18 -7
  65. package/dist/editor-registry.js +10 -1
  66. package/dist/export-format.js +3 -0
  67. package/dist/group-display.d.ts +39 -0
  68. package/dist/group-display.js +52 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.js +2 -2
  71. package/dist/keyboard-handlers.js +16 -0
  72. package/dist/recurrence.js +3 -2
  73. package/dist/selection.js +0 -1
  74. package/dist/svgrid-wrapper.types.d.ts +8 -2
  75. package/dist/themes/index.d.ts +10 -0
  76. package/dist/themes/index.js +26 -0
  77. package/package.json +3 -2
  78. package/src/DockNodeView.svelte +1 -1
  79. package/src/GridFooter.svelte +0 -40
  80. package/src/GridMenus.svelte +0 -77
  81. package/src/SvAccordion.test.ts +5 -5
  82. package/src/SvAvatar.svelte +1 -1
  83. package/src/SvButtonGroup.test.ts +6 -6
  84. package/src/SvCalendar.test.ts +1 -1
  85. package/src/SvCheckBox.svelte +2 -1
  86. package/src/SvCircularProgress.svelte +1 -1
  87. package/src/SvDateRangeInput.svelte +1 -1
  88. package/src/SvFileUpload.test.ts +1 -1
  89. package/src/SvGrid.controller.svelte.ts +367 -22
  90. package/src/SvGrid.css +103 -3
  91. package/src/SvGrid.svelte +164 -35
  92. package/src/SvGrid.types.ts +81 -1
  93. package/src/SvGridChart.svelte +2 -10
  94. package/src/SvGridDropdown.svelte +38 -6
  95. package/src/SvGridSelect.svelte +12 -5
  96. package/src/SvGroupCell.svelte +9 -2
  97. package/src/SvKbd.svelte +1 -1
  98. package/src/SvList.svelte +1 -1
  99. package/src/SvMenubar.svelte +1 -1
  100. package/src/SvMultiSelect.svelte +12 -4
  101. package/src/SvOtpInput.svelte +1 -1
  102. package/src/SvPasswordInput.svelte +1 -1
  103. package/src/SvProgress.svelte +1 -1
  104. package/src/SvRating.svelte +1 -1
  105. package/src/SvResult.svelte +1 -1
  106. package/src/SvRichText.svelte +35 -16
  107. package/src/SvSwitchButton.svelte +2 -1
  108. package/src/SvToaster.svelte +1 -1
  109. package/src/SvTour.svelte +4 -0
  110. package/src/SvTree.svelte +9 -2
  111. package/src/SvTreeSelect.svelte +6 -4
  112. package/src/ai.test.ts +1 -1
  113. package/src/build-api.ts +15 -105
  114. package/src/builtin-editors.test.ts +3 -1
  115. package/src/cell-formatting.test.ts +1 -1
  116. package/src/cell-render.test.ts +27 -3
  117. package/src/cell-render.ts +122 -95
  118. package/src/cell-values.ts +4 -0
  119. package/src/chart-export.test.ts +2 -2
  120. package/src/chart.ts +1 -2
  121. package/src/clipboard.test.ts +0 -2
  122. package/src/clipboard.ts +0 -102
  123. package/src/columns.ts +0 -115
  124. package/src/core.ts +157 -8
  125. package/src/createMenu.svelte.ts +0 -1
  126. package/src/editing.test.ts +1 -1
  127. package/src/editing.ts +2 -105
  128. package/src/editor-contract.ts +12 -5
  129. package/src/editor-registry.grid.test.ts +48 -2
  130. package/src/editor-registry.test.ts +3 -1
  131. package/src/editor-registry.ts +23 -7
  132. package/src/export-format.ts +3 -0
  133. package/src/facet-buckets.test.ts +33 -33
  134. package/src/features.ts +0 -90
  135. package/src/group-display.test.ts +91 -1
  136. package/src/group-display.ts +83 -0
  137. package/src/index.ts +9 -0
  138. package/src/keyboard-handlers.coverage.test.ts +1 -1
  139. package/src/keyboard-handlers.ts +17 -120
  140. package/src/list-power.test.ts +2 -2
  141. package/src/menus.test.ts +4 -5
  142. package/src/menus.ts +0 -112
  143. package/src/named-views.coverage.test.ts +3 -3
  144. package/src/new-features.test.ts +3 -1
  145. package/src/recurrence.ts +3 -2
  146. package/src/row-drag.test.ts +1 -1
  147. package/src/row-resize.test.ts +41 -42
  148. package/src/scheduler-model.ts +1 -1
  149. package/src/scroll-sync.test.ts +4 -7
  150. package/src/scroll-sync.ts +0 -122
  151. package/src/selection.ts +0 -109
  152. package/src/server-data-source.coverage.test.ts +1 -1
  153. package/src/server-group-model.test.ts +8 -8
  154. package/src/spreadsheet.test.ts +0 -1
  155. package/src/summaries.ts +0 -112
  156. package/src/svgrid-wrapper.types.ts +11 -9
  157. package/src/svgrid.async-editor-options.test.ts +271 -0
  158. package/src/svgrid.auto-row-height.test.ts +204 -0
  159. package/src/svgrid.behavior.test.ts +0 -1
  160. package/src/svgrid.charting.test.ts +4 -4
  161. package/src/svgrid.column-reorder.test.ts +0 -8
  162. package/src/svgrid.comments-autocomplete.test.ts +8 -2
  163. package/src/svgrid.context-menu.test.ts +25 -9
  164. package/src/svgrid.filter-depth.test.ts +163 -0
  165. package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
  166. package/src/svgrid.filter-menu-scroll.test.ts +10 -2
  167. package/src/svgrid.grand-total.test.ts +188 -0
  168. package/src/svgrid.group-display-mode.test.ts +171 -0
  169. package/src/svgrid.group-footers.test.ts +121 -0
  170. package/src/svgrid.group-pagination.test.ts +153 -0
  171. package/src/svgrid.tree-data.test.ts +186 -0
  172. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
  173. package/src/themes/index.ts +42 -0
  174. package/src/tree-edit.test.ts +4 -4
  175. package/src/tree-row-model.test.ts +168 -0
  176. package/src/ui-localization.test.ts +113 -0
  177. package/themes/ag-alpine.css +22 -0
  178. package/themes/antd.css +22 -0
  179. package/themes/atlassian.css +22 -0
  180. package/themes/bootstrap.css +22 -0
  181. package/themes/carbon.css +22 -0
  182. package/themes/catppuccin.css +22 -0
  183. package/themes/dracula.css +22 -0
  184. package/themes/ember.css +22 -0
  185. package/themes/excel.css +22 -0
  186. package/themes/fluent.css +22 -0
  187. package/themes/github.css +22 -0
  188. package/themes/linear.css +22 -0
  189. package/themes/material.css +22 -0
  190. package/themes/nord.css +22 -0
  191. package/themes/notion.css +22 -0
  192. package/themes/salesforce.css +22 -0
  193. package/themes/sap.css +22 -0
  194. package/themes/shadcn.css +22 -0
  195. package/themes/tailwind.css +22 -0
  196. package/themes/vercel.css +22 -0
@@ -62,11 +62,11 @@ describe('isBucketableColumn', () => {
62
62
  const cols = makeColumns()
63
63
 
64
64
  it('returns { isDate: false } for a number column', () => {
65
- expect(isBucketableColumn(cols.num)).toEqual({ isDate: false })
65
+ expect(isBucketableColumn(cols.num!)).toEqual({ isDate: false })
66
66
  })
67
67
 
68
68
  it('returns { isDate: true } for a date column', () => {
69
- expect(isBucketableColumn(cols.when)).toEqual({ isDate: true })
69
+ expect(isBucketableColumn(cols.when!)).toEqual({ isDate: true })
70
70
  })
71
71
 
72
72
  it('returns { isDate: true } for a datetime column', () => {
@@ -81,15 +81,15 @@ describe('isBucketableColumn', () => {
81
81
  })
82
82
 
83
83
  it('returns null for a text column', () => {
84
- expect(isBucketableColumn(cols.id)).toBeNull()
84
+ expect(isBucketableColumn(cols.id!)).toBeNull()
85
85
  })
86
86
 
87
87
  it('returns null for a checkbox column', () => {
88
- expect(isBucketableColumn(cols.flag)).toBeNull()
88
+ expect(isBucketableColumn(cols.flag!)).toBeNull()
89
89
  })
90
90
 
91
91
  it('returns null for a column with no editorType', () => {
92
- expect(isBucketableColumn(cols.name)).toBeNull()
92
+ expect(isBucketableColumn(cols.name!)).toBeNull()
93
93
  })
94
94
  })
95
95
 
@@ -105,7 +105,7 @@ describe('buildBuckets - numeric', () => {
105
105
  name: '',
106
106
  flag: false,
107
107
  }))
108
- expect(buildBuckets(cols.num, false, data, accessor)).toBeNull()
108
+ expect(buildBuckets(cols.num!, false, data, accessor)).toBeNull()
109
109
  })
110
110
 
111
111
  it('builds 10 buckets when distinct values exceed the threshold', () => {
@@ -117,7 +117,7 @@ describe('buildBuckets - numeric', () => {
117
117
  name: '',
118
118
  flag: false,
119
119
  }))
120
- const buckets = buildBuckets(cols.num, false, data, accessor)
120
+ const buckets = buildBuckets(cols.num!, false, data, accessor)
121
121
  expect(buckets).not.toBeNull()
122
122
  expect(buckets!).toHaveLength(10)
123
123
  })
@@ -130,12 +130,12 @@ describe('buildBuckets - numeric', () => {
130
130
  name: '',
131
131
  flag: false,
132
132
  }))
133
- const buckets = buildBuckets(cols.num, false, data, accessor)!
134
- expect(buckets[0].numericMin).toBe(0)
135
- expect(buckets[9].numericMax).toBe(30)
133
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
134
+ expect(buckets[0]!.numericMin).toBe(0)
135
+ expect(buckets[9]!.numericMax).toBe(30)
136
136
  // bucket i's max equals bucket i+1's min (contiguous, no gaps)
137
137
  for (let i = 0; i < buckets.length - 1; i += 1) {
138
- expect(buckets[i].numericMax).toBeCloseTo(buckets[i + 1].numericMin, 10)
138
+ expect(buckets[i]!.numericMax).toBeCloseTo(buckets[i + 1]!.numericMin, 10)
139
139
  }
140
140
  })
141
141
 
@@ -147,10 +147,10 @@ describe('buildBuckets - numeric', () => {
147
147
  name: '',
148
148
  flag: false,
149
149
  }))
150
- const buckets = buildBuckets(cols.num, false, data, accessor)!
150
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
151
151
  expect(buckets.filter((b) => b.isLast)).toHaveLength(1)
152
- expect(buckets[9].isLast).toBe(true)
153
- expect(buckets[0].isLast).toBe(false)
152
+ expect(buckets[9]!.isLast).toBe(true)
153
+ expect(buckets[0]!.isLast).toBe(false)
154
154
  expect(buckets.every((b) => b.isDate === false)).toBe(true)
155
155
  })
156
156
 
@@ -162,8 +162,8 @@ describe('buildBuckets - numeric', () => {
162
162
  name: '',
163
163
  flag: false,
164
164
  }))
165
- const buckets = buildBuckets(cols.num, false, data, accessor)!
166
- expect(buckets[0].label).toContain('–')
165
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
166
+ expect(buckets[0]!.label).toContain('–')
167
167
  })
168
168
 
169
169
  it('ignores non-finite / blank values when computing min/max and distinct', () => {
@@ -180,10 +180,10 @@ describe('buildBuckets - numeric', () => {
180
180
  { id: 'b', num: undefined as unknown as number, when: '', name: '', flag: false },
181
181
  { id: 'c', num: 'not-a-number' as unknown as number, when: '', name: '', flag: false },
182
182
  ]
183
- const buckets = buildBuckets(cols.num, false, data, accessor)!
183
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
184
184
  // range is still 0..30 - the junk rows were skipped
185
- expect(buckets[0].numericMin).toBe(0)
186
- expect(buckets[9].numericMax).toBe(30)
185
+ expect(buckets[0]!.numericMin).toBe(0)
186
+ expect(buckets[9]!.numericMax).toBe(30)
187
187
  })
188
188
 
189
189
  it('returns null when all finite values are identical (min === max)', () => {
@@ -196,7 +196,7 @@ describe('buildBuckets - numeric', () => {
196
196
  name: '',
197
197
  flag: false,
198
198
  }))
199
- expect(buildBuckets(cols.num, false, data, accessor)).toBeNull()
199
+ expect(buildBuckets(cols.num!, false, data, accessor)).toBeNull()
200
200
  })
201
201
 
202
202
  it('returns null when there is no finite data at all', () => {
@@ -207,11 +207,11 @@ describe('buildBuckets - numeric', () => {
207
207
  name: '',
208
208
  flag: false,
209
209
  }))
210
- expect(buildBuckets(cols.num, false, data, accessor)).toBeNull()
210
+ expect(buildBuckets(cols.num!, false, data, accessor)).toBeNull()
211
211
  })
212
212
 
213
213
  it('returns null for an empty dataset', () => {
214
- expect(buildBuckets(cols.num, false, [], accessor)).toBeNull()
214
+ expect(buildBuckets(cols.num!, false, [], accessor)).toBeNull()
215
215
  })
216
216
 
217
217
  it('handles negative ranges', () => {
@@ -222,9 +222,9 @@ describe('buildBuckets - numeric', () => {
222
222
  name: '',
223
223
  flag: false,
224
224
  }))
225
- const buckets = buildBuckets(cols.num, false, data, accessor)!
226
- expect(buckets[0].numericMin).toBe(-20)
227
- expect(buckets[9].numericMax).toBe(20)
225
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
226
+ expect(buckets[0]!.numericMin).toBe(-20)
227
+ expect(buckets[9]!.numericMax).toBe(20)
228
228
  })
229
229
  })
230
230
 
@@ -241,11 +241,11 @@ describe('buildBuckets - date', () => {
241
241
  name: '',
242
242
  flag: false,
243
243
  }))
244
- const buckets = buildBuckets(cols.when, true, data, accessor)!
244
+ const buckets = buildBuckets(cols.when!, true, data, accessor)!
245
245
  expect(buckets).toHaveLength(10)
246
246
  expect(buckets.every((b) => b.isDate === true)).toBe(true)
247
247
  // labels should look like dates (contain a 4-digit year)
248
- expect(buckets[0].label).toMatch(/\d{4}/)
248
+ expect(buckets[0]!.label).toMatch(/\d{4}/)
249
249
  })
250
250
 
251
251
  it('accepts Date instances directly', () => {
@@ -257,9 +257,9 @@ describe('buildBuckets - date', () => {
257
257
  )
258
258
  // accessor reads row[column.id]; column id is 'when'
259
259
  const rows = data.map((d) => ({ when: d.when })) as unknown as Row[]
260
- const buckets = buildBuckets(cols.when, true, rows, accessor)!
260
+ const buckets = buildBuckets(cols.when!, true, rows, accessor)!
261
261
  expect(buckets).toHaveLength(10)
262
- expect(buckets[0].numericMin).toBe(start)
262
+ expect(buckets[0]!.numericMin).toBe(start)
263
263
  })
264
264
 
265
265
  it('returns null when date values are too few', () => {
@@ -270,7 +270,7 @@ describe('buildBuckets - date', () => {
270
270
  name: '',
271
271
  flag: false,
272
272
  }))
273
- expect(buildBuckets(cols.when, true, data, accessor)).toBeNull()
273
+ expect(buildBuckets(cols.when!, true, data, accessor)).toBeNull()
274
274
  })
275
275
 
276
276
  it('skips invalid date strings', () => {
@@ -286,9 +286,9 @@ describe('buildBuckets - date', () => {
286
286
  })),
287
287
  { id: 'bad', num: 0, when: 'not-a-date', name: '', flag: false },
288
288
  ]
289
- const buckets = buildBuckets(cols.when, true, data, accessor)!
289
+ const buckets = buildBuckets(cols.when!, true, data, accessor)!
290
290
  expect(buckets).toHaveLength(10)
291
- expect(buckets[0].numericMin).toBe(start)
291
+ expect(buckets[0]!.numericMin).toBe(start)
292
292
  })
293
293
  })
294
294
 
@@ -344,7 +344,7 @@ describe('isInBucket', () => {
344
344
  name: '',
345
345
  flag: false,
346
346
  }))
347
- const buckets = buildBuckets(cols.num, false, data, accessor)!
347
+ const buckets = buildBuckets(cols.num!, false, data, accessor)!
348
348
  for (const v of [0, 1, 15, 50, 99, 100]) {
349
349
  const hits = buckets.filter((b) => isInBucket(v, b))
350
350
  expect(hits).toHaveLength(1)
package/src/features.ts CHANGED
@@ -2,104 +2,14 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createSvelteVirtualizer,
13
- createSortedRowModel,
14
- createSvGrid,
15
- getGridCellDomId,
16
- sortFns,
17
5
  tableFeatures,
18
6
  rowSortingFeature,
19
7
  columnFilteringFeature,
20
8
  columnGroupingFeature,
21
- type CellEditorOption,
22
- type Column,
23
- type ColumnDef,
24
- type Row,
25
9
  type RowData,
26
10
  type TableFeatures,
27
11
  } from "./index";
28
12
  import "./sv-grid-scrollbar";
29
- import {
30
- computeColumnStat,
31
- formatsNeedingStats,
32
- type ColumnStat,
33
- } from "./conditional-formatting";
34
- import SvGridDropdown from "./SvGridDropdown.svelte";
35
- import type {
36
- Props,
37
- SelectionRange,
38
- CellEditState,
39
- FilterOperator,
40
- MenuPosition,
41
- } from "./SvGrid.types";
42
- import {
43
- rawToNumber,
44
- } from "./SvGrid.helpers";
45
- import {
46
- createScrollSync,
47
- } from "./scroll-sync";
48
- import {
49
- createKeyboard,
50
- } from "./keyboard-handlers";
51
- import {
52
- createSummaries,
53
- } from "./summaries";
54
- import {
55
- createMenus,
56
- } from "./menus";
57
- import {
58
- createCellRender,
59
- } from "./cell-render";
60
- import {
61
- createEditing,
62
- } from "./editing";
63
- import {
64
- createSelection,
65
- } from "./selection";
66
- import {
67
- createColumns,
68
- } from "./columns";
69
- import {
70
- createGridApi,
71
- } from "./build-api";
72
- import {
73
- createClipboard,
74
- } from "./clipboard";
75
- import {
76
- filterOperatorOptions,
77
- fallbackOperatorOption,
78
- TEXT_OPERATORS,
79
- NUMBER_OPERATORS,
80
- DATE_OPERATORS,
81
- CHECKBOX_OPERATORS,
82
- operatorOption,
83
- operatorsForColumn,
84
- defaultOperatorFor,
85
- operatorLabelFor,
86
- } from "./filter-operators";
87
- import {
88
- type FacetBucket,
89
- isBucketableColumn,
90
- buildBuckets,
91
- isInBucket,
92
- } from "./facet-buckets";
93
- import {
94
- getColumnBaseValue,
95
- isGroupRow,
96
- toolPanelHeaderLabel,
97
- formatSummaryNumeric,
98
- getColumnAlign,
99
- getPinnedCellValue,
100
- getColumnAccessorValue,
101
- columnDefMatchesId,
102
- } from "./cell-values";
103
13
 
104
14
  export function createFeatures<
105
15
  TFeatures extends TableFeatures = TableFeatures,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { buildAutoGroupColumns, insertGroupFooters } from './group-display'
2
+ import { buildAutoGroupColumns, insertGroupFooters, paginateGroupedRows } from './group-display'
3
3
 
4
4
  type R = { id: string; depth: number; group?: boolean; total?: number; footer?: boolean }
5
5
 
@@ -75,3 +75,93 @@ describe('buildAutoGroupColumns', () => {
75
75
  expect(r.autoColumns.map((c) => c.field)).toEqual(['region', 'category'])
76
76
  })
77
77
  })
78
+
79
+ describe('paginateGroupedRows (#73)', () => {
80
+ // Ten groups of two rows each - the shape from the issue report, where
81
+ // slicing the flat list by pageSize leaves almost no data on a page.
82
+ const many: R[] = []
83
+ for (let g = 0; g < 10; g += 1) {
84
+ many.push({ id: `g${g}`, depth: 0, group: true })
85
+ many.push({ id: `g${g}r0`, depth: 1 })
86
+ many.push({ id: `g${g}r1`, depth: 1 })
87
+ }
88
+ const base = { getDepth: (r: R) => r.depth, isGroup: (r: R) => !!r.group }
89
+
90
+ it('counts only data rows against pageSize', () => {
91
+ const { rows, dataRowCount } = paginateGroupedRows(many, { ...base, pageIndex: 0, pageSize: 10 })
92
+ expect(dataRowCount).toBe(20)
93
+ // A naive slice would have yielded 10 rows TOTAL, only ~3 of them data.
94
+ expect(rows.filter((r) => !r.group)).toHaveLength(10)
95
+ })
96
+
97
+ it('reprints the banner each page a group appears on', () => {
98
+ const page0 = paginateGroupedRows(many, { ...base, pageIndex: 0, pageSize: 3 }).rows
99
+ expect(page0.map((r) => r.id)).toEqual(['g0', 'g0r0', 'g0r1', 'g1', 'g1r0'])
100
+ // g1 is reprinted because its second row leads the next page.
101
+ const page1 = paginateGroupedRows(many, { ...base, pageIndex: 1, pageSize: 3 }).rows
102
+ expect(page1.map((r) => r.id)).toEqual(['g1', 'g1r1', 'g2', 'g2r0', 'g2r1'])
103
+ })
104
+
105
+ it('emits a banner once for a group spanning many rows on one page', () => {
106
+ const wide: R[] = [{ id: 'g', depth: 0, group: true }]
107
+ for (let i = 0; i < 5; i += 1) wide.push({ id: `r${i}`, depth: 1 })
108
+ const { rows } = paginateGroupedRows(wide, { ...base, pageIndex: 0, pageSize: 5 })
109
+ expect(rows.filter((r) => r.group)).toHaveLength(1)
110
+ expect(rows).toHaveLength(6)
111
+ })
112
+
113
+ it('reprints the whole ancestor chain for nested groups', () => {
114
+ const nested: R[] = [
115
+ { id: 'A', depth: 0, group: true },
116
+ { id: 'A1', depth: 1, group: true },
117
+ { id: 'x', depth: 2 },
118
+ { id: 'y', depth: 2 },
119
+ ]
120
+ const page1 = paginateGroupedRows(nested, { ...base, pageIndex: 1, pageSize: 1 }).rows
121
+ expect(page1.map((r) => r.id)).toEqual(['A', 'A1', 'y'])
122
+ })
123
+
124
+ it('gives a COLLAPSED group its own page slot', () => {
125
+ // All groups collapsed: the flat model is banners only. Treating those as
126
+ // zero-cost headers left every page empty - a real bug caught by the DOM test.
127
+ const collapsed: R[] = []
128
+ for (let g = 0; g < 10; g += 1) collapsed.push({ id: `c${g}`, depth: 0, group: true })
129
+ const { rows, dataRowCount } = paginateGroupedRows(collapsed, {
130
+ ...base,
131
+ isExpanded: () => false,
132
+ pageIndex: 0,
133
+ pageSize: 4,
134
+ })
135
+ expect(dataRowCount).toBe(10)
136
+ expect(rows.map((r) => r.id)).toEqual(['c0', 'c1', 'c2', 'c3'])
137
+ })
138
+
139
+ it('mixes an expanded group with a collapsed sibling', () => {
140
+ const mixed: R[] = [
141
+ { id: 'open', depth: 0, group: true },
142
+ { id: 'o1', depth: 1 },
143
+ { id: 'o2', depth: 1 },
144
+ { id: 'shut', depth: 0, group: true },
145
+ ]
146
+ const { rows, dataRowCount } = paginateGroupedRows(mixed, {
147
+ ...base,
148
+ isExpanded: (r: R) => r.id === 'open',
149
+ pageIndex: 0,
150
+ pageSize: 5,
151
+ })
152
+ // 2 data rows + 1 collapsed group = 3 units; the open banner is free.
153
+ expect(dataRowCount).toBe(3)
154
+ expect(rows.map((r) => r.id)).toEqual(['open', 'o1', 'o2', 'shut'])
155
+ })
156
+
157
+ it('handles an out-of-range page', () => {
158
+ expect(paginateGroupedRows(many, { ...base, pageIndex: 99, pageSize: 10 }).rows).toEqual([])
159
+ })
160
+
161
+ it('leaves ungrouped data untouched', () => {
162
+ const flat: R[] = [{ id: 'a', depth: 0 }, { id: 'b', depth: 0 }, { id: 'c', depth: 0 }]
163
+ const { rows, dataRowCount } = paginateGroupedRows(flat, { ...base, pageIndex: 1, pageSize: 2 })
164
+ expect(dataRowCount).toBe(3)
165
+ expect(rows.map((r) => r.id)).toEqual(['c'])
166
+ })
167
+ })
@@ -70,6 +70,89 @@ export function insertGroupFooters<R>(rows: ReadonlyArray<R>, opts: GroupFooterO
70
70
  return out
71
71
  }
72
72
 
73
+ export type GroupPaginationOptions<R> = {
74
+ /** Depth of a row (group rows and their descendants). */
75
+ getDepth: (row: R) => number
76
+ /** True when a row is a group (banner) row. */
77
+ isGroup: (row: R) => boolean
78
+ /**
79
+ * True when a group row is expanded. Defaults to treating every group as
80
+ * expanded. This matters: an EXPANDED group is a header, reprinted above its
81
+ * children and costing no page budget, while a COLLAPSED group is the visible
82
+ * unit itself and must consume a slot - otherwise a fully collapsed grid has
83
+ * no budget-consuming rows at all and every page comes back empty.
84
+ */
85
+ isExpanded?: (row: R) => boolean
86
+ pageIndex: number
87
+ pageSize: number
88
+ }
89
+
90
+ export type GroupPaginationResult<R> = {
91
+ /** The rows to render for this page: the page's budget-consuming rows, each
92
+ * preceded by whichever ancestor banners have not been printed yet. */
93
+ rows: R[]
94
+ /** Budget-consuming rows across every page - what the pager divides by.
95
+ * Data rows plus collapsed group rows; expanded banners do not count. */
96
+ dataRowCount: number
97
+ }
98
+
99
+ /**
100
+ * Paginate a grouped row list by DATA rows, repeating each page's ancestor
101
+ * group headers.
102
+ *
103
+ * Slicing the flat grouped list directly counts group banners against
104
+ * `pageSize`, so `pageSize: 10` over ten groups of two shows almost no data
105
+ * (#73). Here only non-group rows consume the page budget, and a page reprints
106
+ * the group chain its first rows sit under - the same thing a spreadsheet does
107
+ * when a grouped table breaks across pages.
108
+ *
109
+ * Ancestors are tracked with a depth stack rather than parent pointers, so this
110
+ * works on the already-flattened expanded model and needs nothing from the row
111
+ * objects but depth and group-ness.
112
+ */
113
+ export function paginateGroupedRows<R>(
114
+ rows: ReadonlyArray<R>,
115
+ opts: GroupPaginationOptions<R>,
116
+ ): GroupPaginationResult<R> {
117
+ const { getDepth, isGroup, isExpanded, pageIndex, pageSize } = opts
118
+ const expanded = isExpanded ?? (() => true)
119
+ const start = pageIndex * pageSize
120
+ const end = start + pageSize
121
+
122
+ const out: R[] = []
123
+ // Expanded banners currently open, outermost first. `printed` marks the ones
124
+ // this page has already emitted, so a group spanning rows 3-8 prints its
125
+ // banner once, not once per row.
126
+ const openGroups: Array<{ row: R; printed: boolean }> = []
127
+ let unitsSeen = 0
128
+
129
+ for (const row of rows) {
130
+ const depth = getDepth(row)
131
+ // A row at depth <= an open banner's depth closes it.
132
+ while (openGroups.length > 0 && getDepth(openGroups[openGroups.length - 1]!.row) >= depth) {
133
+ openGroups.pop()
134
+ }
135
+ if (isGroup(row) && expanded(row)) {
136
+ // A header for rows that follow: reprinted per page, costs no budget.
137
+ openGroups.push({ row, printed: false })
138
+ continue
139
+ }
140
+ // Everything else is a visible unit: a data row, or a collapsed group
141
+ // standing in for its whole subtree.
142
+ const index = unitsSeen
143
+ unitsSeen += 1
144
+ if (index < start || index >= end) continue
145
+ for (const g of openGroups) {
146
+ if (g.printed) continue
147
+ g.printed = true
148
+ out.push(g.row)
149
+ }
150
+ out.push(row)
151
+ }
152
+
153
+ return { rows: out, dataRowCount: unitsSeen }
154
+ }
155
+
73
156
  export type AutoGroupColumnSpec = {
74
157
  /** Synthetic column id (e.g. `__autoGroup` or `__group_<field>`). */
75
158
  id: string
package/src/index.ts CHANGED
@@ -32,6 +32,8 @@ export {
32
32
  createGroupedRowModel,
33
33
  createPaginatedRowModel,
34
34
  createSortedRowModel,
35
+ createTreeRowModel,
36
+ flattenTreeData,
35
37
  applyGroupAggregate,
36
38
  filterFns,
37
39
  rowExpandingFeature,
@@ -52,6 +54,8 @@ export {
52
54
  type Column,
53
55
  type ColumnDef,
54
56
  type ColumnDefTemplate,
57
+ type GridColumnDef,
58
+ type GridColumns,
55
59
  type GroupAggregator,
56
60
  type Header,
57
61
  type HeaderContext,
@@ -62,6 +66,8 @@ export {
62
66
  type SvGrid as SvGridInstance,
63
67
  type SvGridOptions,
64
68
  type TableFeatures,
69
+ type TreeRowModelOptions,
70
+ type FlattenTreeOptions,
65
71
  type Updater,
66
72
  } from './core'
67
73
 
@@ -671,10 +677,13 @@ export {
671
677
  export {
672
678
  insertGroupFooters,
673
679
  buildAutoGroupColumns,
680
+ paginateGroupedRows,
674
681
  type GroupDisplayType,
675
682
  type GroupFooterOptions,
676
683
  type AutoGroupColumnSpec,
677
684
  type AutoGroupResult,
685
+ type GroupPaginationOptions,
686
+ type GroupPaginationResult,
678
687
  } from './group-display'
679
688
  export { getKeyboardIntent, getNextActiveCell, type GridKeyboardIntent } from './keyboard'
680
689
  export { createVirtualizer } from './virtualization/virtualizer'
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
  import { createKeyboard } from './keyboard-handlers'
3
3
 
4
4
  // createKeyboard returns imperative handlers that read/write a controller
@@ -2,130 +2,10 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createPaginatedRowModel,
13
- createSvelteVirtualizer,
14
- createSortedRowModel,
15
- createSvGrid,
16
- filterFns,
17
- getGridCellA11yProps,
18
- getGridCellDomId,
19
- getGridHeaderA11yProps,
20
- getGridRootA11yProps,
21
- getGridRowA11yProps,
22
- parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
- type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
- type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
- type Column,
36
- type ColumnDef,
37
- type Row,
38
5
  type RowData,
39
- type SvGridApi,
40
6
  type TableFeatures,
41
7
  } from "./index";
42
8
  import "./sv-grid-scrollbar";
43
- import type { Snippet } from "svelte";
44
- import {
45
- formatNumericWithConfig,
46
- getDateFormatter,
47
- resolveDatePattern,
48
- } from "./cell-formatting";
49
- import {
50
- RenderSnippetConfig,
51
- RenderComponentConfig,
52
- } from "./render-component";
53
- import { buildFillPattern } from "./fill-patterns";
54
- import { buildSparkline, toSparklineValues } from "./sparkline";
55
- import {
56
- resolveCellFormat,
57
- computeColumnStat,
58
- formatsNeedingStats,
59
- type ColumnStat,
60
- type ResolvedCellFormat,
61
- } from "./conditional-formatting";
62
- import SvGridDropdown from "./SvGridDropdown.svelte";
63
- import type {
64
- Props,
65
- SelectionPoint,
66
- SelectionRange,
67
- CellEditState,
68
- FilterOperator,
69
- FilterOption,
70
- MenuPosition,
71
- } from "./SvGrid.types";
72
- import {
73
- cfTextStyle,
74
- fmtStat,
75
- getCellKey,
76
- resolveClassList,
77
- toDateInputValue,
78
- toDateTimeLocalInputValue,
79
- getEditableInputValue,
80
- getEditorInputType,
81
- toValueArray,
82
- getOptionLabel,
83
- getOptionColor,
84
- colorfulChipStyle,
85
- getEditorClass,
86
- asDate,
87
- clampMenuX,
88
- cssEscape,
89
- rawToNumber,
90
- formatFacetNumber,
91
- formatFacetDate,
92
- } from "./SvGrid.helpers";
93
- import { createSummaries } from "./summaries";
94
- import { createMenus } from "./menus";
95
- import { createCellRender } from "./cell-render";
96
- import { createEditing } from "./editing";
97
- import { createSelection } from "./selection";
98
- import { createColumns } from "./columns";
99
- import { createGridApi } from "./build-api";
100
- import { createClipboard } from "./clipboard";
101
- import {
102
- filterOperatorOptions,
103
- fallbackOperatorOption,
104
- TEXT_OPERATORS,
105
- NUMBER_OPERATORS,
106
- DATE_OPERATORS,
107
- CHECKBOX_OPERATORS,
108
- operatorOption,
109
- operatorsForColumn,
110
- defaultOperatorFor,
111
- operatorLabelFor,
112
- } from "./filter-operators";
113
- import {
114
- type FacetBucket,
115
- isBucketableColumn,
116
- buildBuckets,
117
- isInBucket,
118
- } from "./facet-buckets";
119
- import {
120
- getColumnBaseValue,
121
- isGroupRow,
122
- toolPanelHeaderLabel,
123
- formatSummaryNumeric,
124
- getColumnAlign,
125
- getPinnedCellValue,
126
- getColumnAccessorValue,
127
- columnDefMatchesId,
128
- } from "./cell-values";
129
9
 
130
10
  import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
131
11
 
@@ -249,6 +129,23 @@ export function createKeyboard<
249
129
  // expands a collapsed group row, ArrowLeft collapses an expanded one,
250
130
  // instead of moving the active cell (treegrid-style). Leaves fall through
251
131
  // to normal navigation.
132
+ // Client tree data gets the same ArrowRight/ArrowLeft contract, driven off
133
+ // the row model rather than consumer callbacks.
134
+ if (ctx.props.treeData && (intent === "moveRight" || intent === "moveLeft")) {
135
+ const row = ctx.allRows[current.rowIndex];
136
+ if (row?.getCanExpand?.()) {
137
+ const expanded = row.getIsExpanded?.() ?? false;
138
+ if (intent === "moveRight" && !expanded) {
139
+ row.toggleExpanded?.();
140
+ return;
141
+ }
142
+ if (intent === "moveLeft" && expanded) {
143
+ row.toggleExpanded?.();
144
+ return;
145
+ }
146
+ }
147
+ }
148
+
252
149
  const serverGroup = ctx.props.serverGroup;
253
150
  if (serverGroup && (intent === "moveRight" || intent === "moveLeft")) {
254
151
  const data = ctx.allRows[current.rowIndex]?.original;