@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
@@ -17,7 +17,6 @@
17
17
  } from "./index";
18
18
  import "./sv-grid-scrollbar";
19
19
  import "./SvGrid.css";
20
- import type { Snippet } from "svelte";
21
20
  import {
22
21
  RenderSnippetConfig,
23
22
  RenderComponentConfig,
@@ -36,16 +35,10 @@
36
35
  import { timeStringToDate, dateToTimeString } from "./SvGrid.helpers";
37
36
  import type {
38
37
  Props,
39
- SelectionPoint,
40
- SelectionRange,
41
- CellEditState,
42
38
  FilterOperator,
43
- FilterOption,
44
- MenuPosition,
45
39
  } from "./SvGrid.types";
46
40
  import {
47
41
  cfTextStyle,
48
- fmtStat,
49
42
  getEditableInputValue,
50
43
  getEditorInputType,
51
44
  toValueArray,
@@ -57,7 +50,6 @@
57
50
  import {
58
51
  splitInTokens,
59
52
  trailingInToken,
60
- joinInTokens,
61
53
  } from "./filtering/excel-filters";
62
54
  import { createSvGridController } from "./SvGrid.controller.svelte";
63
55
  // GridMenus hosts tooltips + all column/filter/context menu overlays - none of
@@ -180,10 +172,8 @@
180
172
 
181
173
  // ---- View facade: re-bind the controller's reactive members as locals so the
182
174
  // markup stays identical. Assignable state + bindings + DOM refs use c.* directly.
183
- const paginationEnabled = $derived(ctrl.paginationEnabled);
184
175
  const filterRowValues = $derived(ctrl.filterRowValues);
185
176
  const filterMenuValues = $derived(ctrl.filterMenuValues);
186
- const tooltip = $derived(ctrl.tooltip);
187
177
  const showTooltipFor = $derived(ctrl.showTooltipFor);
188
178
  const hideTooltip = $derived(ctrl.hideTooltip);
189
179
  const findHits = $derived(ctrl.findHits);
@@ -256,28 +246,41 @@
256
246
  // Server-side group / tree a11y: aria-level on every tree row, aria-expanded
257
247
  // on the expandable ones. Undefined (attribute omitted) when serverGroup is off.
258
248
  const serverGroup = $derived(opt.serverGroup);
249
+ // Client tree data reuses this same treegrid contract - the row model already
250
+ // records depth and expandability, so the accessors just read the row instead
251
+ // of calling back into consumer callbacks.
252
+ const treeData = $derived(opt.treeData);
259
253
  function sgAriaLevel(row: Row<TData>): number | undefined {
254
+ if (treeData) return (row.depth ?? 0) + 1;
260
255
  return serverGroup ? serverGroup.level(row.original as TData) + 1 : undefined;
261
256
  }
262
257
  function sgAriaExpanded(row: Row<TData>): boolean | undefined {
258
+ if (treeData) {
259
+ if (!row.getCanExpand?.()) return undefined;
260
+ return row.getIsExpanded?.() ?? false;
261
+ }
263
262
  if (!serverGroup || !serverGroup.isGroup(row.original as TData)) return undefined;
264
263
  return serverGroup.expanded?.(row.original as TData) ?? false;
265
264
  }
265
+ /** Column that carries the tree expander: explicit, else the first visible one. */
266
+ const treeColumnId = $derived(
267
+ treeData ? (treeData.column ?? allColumns[0]?.id ?? "") : "",
268
+ );
269
+ // Row-grouping display modes: group state lives in synthetic columns instead
270
+ // of a full-width banner row.
271
+ const groupColumnMode = $derived(ctrl.groupColumnMode);
272
+ const autoGroupCell = $derived(ctrl.autoGroupCell);
273
+ const isAutoGroupColumn = (id: string): boolean =>
274
+ id === "__autoGroup" || id.startsWith("__group_");
266
275
  const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
267
276
  const groupingColumns = $derived(ctrl.groupingColumns);
268
- const paginationState = $derived(ctrl.paginationState);
269
- const allRowsBeforePagination = $derived(ctrl.allRowsBeforePagination);
270
277
  const allRows = $derived(ctrl.allRows);
271
- const statusBarEnabled = $derived(ctrl.statusBarEnabled);
272
- const statusBarAggregates = $derived(ctrl.statusBarAggregates);
273
- const statusBarStats = $derived(ctrl.statusBarStats);
274
278
  const toolPanelEnabled = $derived(ctrl.toolPanelEnabled);
275
279
  const toolPanelColumns = $derived(ctrl.toolPanelColumns);
276
280
  const toolPanelHeaderLabel = $derived(ctrl.toolPanelHeaderLabel);
277
281
  const toggleColumnVisibleInPanel = $derived(ctrl.toggleColumnVisibleInPanel);
278
282
  const moveColumnInPanel = $derived(ctrl.moveColumnInPanel);
279
283
  const toggleGroupInPanel = $derived(ctrl.toggleGroupInPanel);
280
- const virtualizer = $derived(ctrl.virtualizer);
281
284
  const rowVirtualizationEnabled = $derived(ctrl.rowVirtualizationEnabled);
282
285
  // The fixed row height, matching what the virtualized path takes from the virtualizer.
283
286
  // The non-virtualized (`virtualization={false}`) body must apply this too, else its rows
@@ -286,10 +289,25 @@
286
289
  const rh = opt.rowHeight;
287
290
  return typeof rh === "function" ? rh(i) : (rh ?? 30);
288
291
  };
292
+ // Auto height applies to both bodies. The non-virtualized one has no
293
+ // `rowItem`, so it sizes from `rowSizePx` - but must still emit `min-height`
294
+ // and the wrap class, or `autoRowHeight` would silently do nothing whenever
295
+ // `virtualization={false}`.
296
+ const rowStyleFor = (i: number): string =>
297
+ autoRowHeightOn
298
+ ? `min-height: ${measuredRowHeightPx(i) ?? rowSizePx(i)}px;`
299
+ : `height: ${rowSizePx(i)}px;`;
289
300
  const columnVirtualizationEnabled = $derived(
290
301
  ctrl.columnVirtualizationEnabled,
291
302
  );
292
303
  const virtualRows = $derived(ctrl.virtualRows);
304
+ const autoRowHeightOn = $derived(ctrl.autoRowHeightOn);
305
+ const measureRowHeight = $derived(ctrl.measureRowHeight);
306
+ const measuredRowHeightPx = $derived(ctrl.measuredRowHeightPx);
307
+ // Under auto height the row must be free to grow, so it gets `min-height`
308
+ // (the pre-measure estimate) instead of a fixed `height` that would clip it.
309
+ const rowHeightStyle = (size: number): string =>
310
+ autoRowHeightOn ? `min-height: ${size}px;` : `height: ${size}px;`;
293
311
  // DOM-space spacer heights + capped total: identical to the logical
294
312
  // virtualizer values for normal grids, scaled down past
295
313
  // MAX_DOM_SCROLL_HEIGHT so huge grids stay scrollable to the last row on
@@ -405,6 +423,16 @@
405
423
  const hasMeasured = $derived(ctrl.hasMeasured);
406
424
  const onBodyScroll = $derived(ctrl.onBodyScroll);
407
425
  const computeRowClass = $derived(ctrl.computeRowClass);
426
+ // Group subtotal rows are clones of their banner, so they render through the
427
+ // normal cell path and their aggregates land under the right columns; only
428
+ // the class distinguishes them. Typed here rather than cast inline - an `as`
429
+ // inside template markup type-checks but breaks the vitest/build parse.
430
+ const isGroupFooterRow = (row: Row<TData>): boolean =>
431
+ (row as { __groupFooter?: boolean }).__groupFooter === true;
432
+ // The grand total is a group footer that closes the whole set rather than one
433
+ // group, so it carries both classes - shared chrome, heavier top rule.
434
+ const isGrandTotalRow = (row: Row<TData>): boolean =>
435
+ (row as { __grandTotal?: boolean }).__grandTotal === true;
408
436
  const computeCellClass = $derived(ctrl.computeCellClass);
409
437
  const computeCellTooltip = $derived(ctrl.computeCellTooltip);
410
438
  const computeCellValidity = $derived(ctrl.computeCellValidity);
@@ -412,6 +440,7 @@
412
440
  const getCellDisplayValue = $derived(ctrl.getCellDisplayValue);
413
441
  const getColumnAlign = $derived(ctrl.getColumnAlign);
414
442
  const getColumnEditorOptions = $derived(ctrl.getColumnEditorOptions);
443
+ const areEditorOptionsLoading = $derived(ctrl.areEditorOptionsLoading);
415
444
  const formatListCellValue = $derived(ctrl.formatListCellValue);
416
445
  const formatCellValue = $derived(ctrl.formatCellValue);
417
446
  const getPinnedCellValue = $derived(ctrl.getPinnedCellValue);
@@ -460,14 +489,26 @@
460
489
  ctrl.editingCell = null;
461
490
  ctrl.gridRootEl?.focus({ preventScroll: true });
462
491
  },
492
+ // Tab out of an editor: commit, then step to the adjacent cell. Without
493
+ // this a registered editor's Tab walks focus out of the grid (#48) - the
494
+ // built-in editors already route through the same helper.
495
+ onCommitAndMove: (v: unknown, direction: 1 | -1) => {
496
+ if (v !== undefined) updateEditingCellValue(v);
497
+ ctrl.commitAndMoveByTab(direction === -1);
498
+ },
499
+ // Close a popover editor without committing. Same effect as cancel today;
500
+ // named separately so an editor can distinguish "dismiss my panel" from
501
+ // "abandon the edit" if that ever diverges.
502
+ onRequestClose: () => {
503
+ ctrl.editingCell = null;
504
+ ctrl.gridRootEl?.focus({ preventScroll: true });
505
+ },
506
+ inCell: true,
463
507
  };
464
508
  }
465
509
  const onHeaderSortClick = $derived(ctrl.onHeaderSortClick);
466
510
  const onGridKeyDown = $derived(ctrl.onGridKeyDown);
467
511
  const onGridPaste = $derived(ctrl.onGridPaste);
468
- const changePage = $derived(ctrl.changePage);
469
- const goToPage = $derived(ctrl.goToPage);
470
- const setPageSize = $derived(ctrl.setPageSize);
471
512
  const updateFilterRow = $derived(ctrl.updateFilterRow);
472
513
  const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
473
514
  const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
@@ -869,6 +910,11 @@
869
910
  <path d="M6 6l12 12" />
870
911
  {:else if name === "chevron-down"}
871
912
  <path d="M6 9l6 6 6-6" />
913
+ {:else if name === "chevron-right"}
914
+ <!-- Same path SvTree's twisty uses, so every expander in the library
915
+ draws the same chevron. Rotated 90deg by CSS when expanded rather
916
+ than swapped for a second glyph. -->
917
+ <path d="m9 18 6-6-6-6" />
872
918
  {:else if name === "op-contains"}
873
919
  <circle cx="11" cy="11" r="6" />
874
920
  <path d="M20 20l-4.5-4.5" />
@@ -1027,7 +1073,12 @@
1027
1073
  </svg>
1028
1074
  {/if}
1029
1075
  {:else}
1030
- {#if row.depth > 0 && column.id === allColumns[0]?.id}
1076
+ <!-- Indent for rows nested under a group BANNER. Tree data and the
1077
+ column display modes draw their own indent next to the expander, so
1078
+ this must not stack on top of theirs - `row.depth` is set in all
1079
+ three cases and this used to fire for every one of them, leaving a
1080
+ 20px gap between a tree row's chevron and its text. -->
1081
+ {#if row.depth > 0 && column.id === allColumns[0]?.id && !treeData && !groupColumnMode}
1031
1082
  <span
1032
1083
  class="sv-grid-group-child-indent"
1033
1084
  style={`width: ${row.depth * 20}px;`}
@@ -1073,10 +1124,75 @@
1073
1124
  color / weight and the icon-set glyph ride on the content wrapper.
1074
1125
  Falls straight through to `cellBody` when no format applies, so the
1075
1126
  common (unformatted) path pays nothing. -->
1127
+ <!-- Tree expander + indentation, prefixed to the tree column's cell. Rows
1128
+ without children still get the indent (and a spacer where the chevron
1129
+ would be) so values stay aligned down a level. -->
1130
+ {#snippet treeAffordance(row: Row<TData>)}
1131
+ {@const canExpand = row.getCanExpand?.() ?? false}
1132
+ <span
1133
+ class="sv-grid-tree-indent"
1134
+ style={`width:${(row.depth ?? 0) * (treeData?.indentPx ?? 12)}px;`}
1135
+ aria-hidden="true"
1136
+ ></span>
1137
+ {#if canExpand}
1138
+ <button
1139
+ type="button"
1140
+ class="sv-grid-tree-toggle"
1141
+ aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
1142
+ aria-label={row.getIsExpanded?.() ? "Collapse row" : "Expand row"}
1143
+ onclick={(event) => {
1144
+ event.stopPropagation();
1145
+ row.toggleExpanded?.();
1146
+ }}>{@render icon("chevron-right")}</button
1147
+ >
1148
+ {:else}
1149
+ <span class="sv-grid-tree-spacer" aria-hidden="true"></span>
1150
+ {/if}
1151
+ {/snippet}
1152
+
1153
+ <!-- The group label + expander for a `singleColumn` / `multipleColumns`
1154
+ auto-group cell. Indented by level in single-column mode; in multi-column
1155
+ mode each level already has its own column, so no indent. -->
1156
+ {#snippet autoGroupAffordance(row: Row<TData>, cell: { label: string; count: number; depth: number })}
1157
+ <span
1158
+ class="sv-grid-tree-indent"
1159
+ style={`width:${cell.depth * 16}px;`}
1160
+ aria-hidden="true"
1161
+ ></span>
1162
+ <button
1163
+ type="button"
1164
+ class="sv-grid-tree-toggle"
1165
+ aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
1166
+ aria-label={row.getIsExpanded?.() ? "Collapse group" : "Expand group"}
1167
+ onclick={(event) => {
1168
+ event.stopPropagation();
1169
+ row.toggleExpanded?.();
1170
+ }}>{@render icon("chevron-right")}</button
1171
+ >
1172
+ <span class="sv-grid-autogroup-label">{cell.label}</span>
1173
+ <span class="sv-grid-group-count">({cell.count})</span>
1174
+ {/snippet}
1175
+
1076
1176
  {#snippet cellBodyWithFormat(
1077
1177
  row: Row<TData>,
1078
1178
  column: Column<TData>,
1079
1179
  cellValue: unknown,
1180
+ )}
1181
+ {#if groupColumnMode && isAutoGroupColumn(column.id)}
1182
+ {@const agc = autoGroupCell(row, column.id)}
1183
+ {#if agc}{@render autoGroupAffordance(row, agc)}{/if}
1184
+ {:else}
1185
+ {#if treeData && column.id === treeColumnId}
1186
+ {@render treeAffordance(row)}
1187
+ {/if}
1188
+ {@render cellBodyFormatted(row, column, cellValue)}
1189
+ {/if}
1190
+ {/snippet}
1191
+
1192
+ {#snippet cellBodyFormatted(
1193
+ row: Row<TData>,
1194
+ column: Column<TData>,
1195
+ cellValue: unknown,
1080
1196
  )}
1081
1197
  {@const cf = cellConditionalFormat(row, column, cellValue)}
1082
1198
  {#if cf && (cf.background || cf.dataBar || cf.icon || cf.color || cf.fontWeight != null)}
@@ -1156,7 +1272,7 @@
1156
1272
  aria-label="Edit checkbox value"
1157
1273
  onclick={(event) => {
1158
1274
  event.stopPropagation();
1159
- const nextValue = !Boolean(ctrl.editingCell?.value);
1275
+ const nextValue = !ctrl.editingCell?.value;
1160
1276
  ctrl.editingCell = ctrl.editingCell
1161
1277
  ? { ...ctrl.editingCell, value: nextValue }
1162
1278
  : ctrl.editingCell;
@@ -1165,7 +1281,7 @@
1165
1281
  onkeydown={(event) =>
1166
1282
  toggleCheckboxWithKeyboard(event, () => {
1167
1283
  event.stopPropagation();
1168
- const nextValue = !Boolean(ctrl.editingCell?.value);
1284
+ const nextValue = !ctrl.editingCell?.value;
1169
1285
  ctrl.editingCell = ctrl.editingCell
1170
1286
  ? { ...ctrl.editingCell, value: nextValue }
1171
1287
  : ctrl.editingCell;
@@ -1175,10 +1291,12 @@
1175
1291
  ></button>
1176
1292
  {:else if ctrl.editingCell?.editorType === "list"}
1177
1293
  {@const opts = getColumnEditorOptions(column, row)}
1294
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
1178
1295
  {@const multi = column.columnDef.editorMultiple === true}
1179
1296
  {#if DropdownEditor}
1180
1297
  <DropdownEditor
1181
1298
  options={opts}
1299
+ loading={optsLoading}
1182
1300
  value={ctrl.editingCell?.value}
1183
1301
  multiple={multi}
1184
1302
  placeholder="Select…"
@@ -1196,9 +1314,10 @@
1196
1314
  {/if}
1197
1315
  {:else if ctrl.editingCell?.editorType === "chips"}
1198
1316
  {@const opts = getColumnEditorOptions(column, row)}
1317
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
1199
1318
  {@const multi = column.columnDef.editorMultiple === true}
1200
1319
  {@const selectedArr = toValueArray(ctrl.editingCell?.value)}
1201
- {@render chipsEditor(opts, multi, selectedArr)}
1320
+ {@render chipsEditor(opts, multi, selectedArr, optsLoading)}
1202
1321
  {:else if ctrl.editingCell?.editorType === "rating"}
1203
1322
  {@const ratingVal = Math.max(
1204
1323
  0,
@@ -1241,9 +1360,11 @@
1241
1360
  <!-- Custom dropdown: opens a themed popover identical in feel to
1242
1361
  the existing 'list' editor (single-select, no typeahead). -->
1243
1362
  {@const selectOpts = getColumnEditorOptions(column, row)}
1363
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
1244
1364
  {#if DropdownEditor}
1245
1365
  <DropdownEditor
1246
1366
  options={selectOpts}
1367
+ loading={optsLoading}
1247
1368
  value={ctrl.editingCell?.value}
1248
1369
  multiple={false}
1249
1370
  placeholder="Select…"
@@ -1263,9 +1384,11 @@
1263
1384
  <!-- Searchable combobox: same popover as 'select' with a
1264
1385
  typeahead filter input baked in at the top. -->
1265
1386
  {@const richOpts = getColumnEditorOptions(column, row)}
1387
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
1266
1388
  {#if DropdownEditor}
1267
1389
  <DropdownEditor
1268
1390
  options={richOpts}
1391
+ loading={optsLoading}
1269
1392
  value={ctrl.editingCell?.value}
1270
1393
  multiple={false}
1271
1394
  searchable={true}
@@ -1527,6 +1650,7 @@
1527
1650
  opts: CellEditorOption[],
1528
1651
  multi: boolean,
1529
1652
  selectedArr: Array<string | number>,
1653
+ optsLoading: boolean,
1530
1654
  )}
1531
1655
  {#if opts.length > 0}
1532
1656
  <!-- Options-driven chips editor: defer to the custom dropdown,
@@ -1536,6 +1660,7 @@
1536
1660
  {#if DropdownEditor}
1537
1661
  <DropdownEditor
1538
1662
  options={opts}
1663
+ loading={optsLoading}
1539
1664
  value={ctrl.editingCell?.value}
1540
1665
  multiple={multi}
1541
1666
  placeholder="Pick…"
@@ -1657,7 +1782,7 @@
1657
1782
  onclick={(event) => {
1658
1783
  event.stopPropagation();
1659
1784
  row.toggleExpanded?.();
1660
- }}>{row.getIsExpanded?.() ? "" : "▸"}</button
1785
+ }}>{@render icon("chevron-right")}</button
1661
1786
  >
1662
1787
  <span class="sv-grid-group-label">{headerLabel}: {groupValue}</span>
1663
1788
  <span class="sv-grid-group-count"
@@ -1853,7 +1978,7 @@
1853
1978
  activeDescendantId,
1854
1979
  rowCount: allRows.length,
1855
1980
  colCount: allColumns.length,
1856
- treegrid: !!opt.serverGroup,
1981
+ treegrid: !!opt.serverGroup || !!opt.treeData,
1857
1982
  })}
1858
1983
  onkeydown={onGridKeyDown}
1859
1984
  onpaste={onGridPaste}
@@ -2438,14 +2563,14 @@
2438
2563
  {#if row}
2439
2564
  {#if opt.isDetailRow?.(row.original as TData, rowIndex)}
2440
2565
  {@render detailRowMarkup(row, rowIndex)}
2441
- {:else if isGroupRow(row)}
2566
+ {:else if isGroupRow(row) && !groupColumnMode}
2442
2567
  <tr
2443
2568
  class="sv-grid-row sv-grid-group-row"
2444
2569
  class:sv-grid-row-selected={isRowSelected(row.id)}
2445
2570
  aria-level={row.depth + 1}
2446
2571
  aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
2447
2572
  {...getGridRowA11yProps(rowIndex + 1)}
2448
- style={`height: ${rowItem.size}px;`}
2573
+ style={rowHeightStyle(rowItem.size)}
2449
2574
  >
2450
2575
  <td
2451
2576
  class="sv-grid-cell sv-grid-group-cell"
@@ -2467,11 +2592,15 @@
2467
2592
  class:sv-grid-row-alt={opt.zebraRows &&
2468
2593
  rowIndex % 2 === 1}
2469
2594
  class:sv-grid-row-draggable={rowDragManagedEffective}
2595
+ class:sv-grid-group-footer-row={isGroupFooterRow(row)}
2596
+ class:sv-grid-grand-total-row={isGrandTotalRow(row)}
2597
+ class:sv-grid-row-auto-height={autoRowHeightOn}
2470
2598
  {...getGridRowA11yProps(rowIndex + 1)}
2471
2599
  aria-level={sgAriaLevel(row)}
2472
2600
  aria-expanded={sgAriaExpanded(row)}
2473
2601
  {...rowDragAttrs(rowIndex)}
2474
- style={`height: ${rowItem.size}px;`}
2602
+ style={rowHeightStyle(rowItem.size)}
2603
+ use:measureRowHeight={rowIndex}
2475
2604
  >
2476
2605
  {#if showRowNumbersEffective}
2477
2606
  <td
@@ -2542,10 +2671,6 @@
2542
2671
  row,
2543
2672
  rendered.column,
2544
2673
  )}
2545
- {@const cellTooltip = computeCellTooltip(
2546
- row,
2547
- rendered.column,
2548
- )}
2549
2674
  {@const cellValidity = computeCellValidity(
2550
2675
  row,
2551
2676
  rendered.column,
@@ -2690,14 +2815,14 @@
2690
2815
  {#each allRows as row, rowIndex (row.id)}
2691
2816
  {#if opt.isDetailRow?.(row.original as TData, rowIndex)}
2692
2817
  {@render detailRowMarkup(row, rowIndex)}
2693
- {:else if isGroupRow(row)}
2818
+ {:else if isGroupRow(row) && !groupColumnMode}
2694
2819
  <tr
2695
2820
  class="sv-grid-row sv-grid-group-row"
2696
2821
  class:sv-grid-row-selected={isRowSelected(row.id)}
2697
2822
  aria-level={row.depth + 1}
2698
2823
  aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
2699
2824
  {...getGridRowA11yProps(rowIndex + 1)}
2700
- style={`height: ${rowSizePx(rowIndex)}px;`}
2825
+ style={rowStyleFor(rowIndex)}
2701
2826
  >
2702
2827
  <td
2703
2828
  class="sv-grid-cell sv-grid-group-cell"
@@ -2719,11 +2844,15 @@
2719
2844
  class:sv-grid-row-alt={opt.zebraRows &&
2720
2845
  rowIndex % 2 === 1}
2721
2846
  class:sv-grid-row-draggable={rowDragManagedEffective}
2847
+ class:sv-grid-group-footer-row={isGroupFooterRow(row)}
2848
+ class:sv-grid-grand-total-row={isGrandTotalRow(row)}
2849
+ class:sv-grid-row-auto-height={autoRowHeightOn}
2722
2850
  {...getGridRowA11yProps(rowIndex + 1)}
2723
2851
  aria-level={sgAriaLevel(row)}
2724
2852
  aria-expanded={sgAriaExpanded(row)}
2725
2853
  {...rowDragAttrs(rowIndex)}
2726
- style={`height: ${rowSizePx(rowIndex)}px;`}
2854
+ style={rowStyleFor(rowIndex)}
2855
+ use:measureRowHeight={rowIndex}
2727
2856
  >
2728
2857
  {#if showRowNumbersEffective}
2729
2858
  <td
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { CellEditorType, ColumnDef, RowData, SvGridApi, TableFeatures } from "./index";
3
3
  import type { ConditionalFormat } from "./conditional-formatting";
4
+ import type { GroupDisplayType } from "./group-display";
4
5
  import type { GridMessages } from "./grid-messages";
5
6
  import type { GridPivotConfig } from "./pivot-view.svelte";
6
7
  import type { MenuItem } from "./SvMenuList.svelte";
@@ -992,7 +993,75 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
992
993
  sortable?: boolean;
993
994
  filterable?: boolean;
994
995
  editable?: boolean;
996
+ /**
997
+ * Client-side tree data: nest rows into a hierarchy by parent id.
998
+ *
999
+ * ```svelte
1000
+ * <SvGrid {data} {columns} treeData={{ parentField: 'managerId', column: 'name' }} />
1001
+ * ```
1002
+ *
1003
+ * Unlike grouping, tree rows are real data rows - they keep their cells,
1004
+ * editing and selection, and only gain an expander plus indentation in
1005
+ * `column`. Rows whose parent is missing become roots rather than vanishing.
1006
+ *
1007
+ * For NESTED source data (`children: [...]`), flatten it first with
1008
+ * `flattenTreeData(data, { childrenField: 'children' })` and point
1009
+ * `parentField` at the `__parentId` it stamps on.
1010
+ *
1011
+ * Setting this replaces row grouping - a row cannot be both.
1012
+ */
1013
+ treeData?: {
1014
+ /** Field holding each row's parent id. */
1015
+ parentField: string;
1016
+ /** Field holding the row's own id. Defaults to `'id'`. */
1017
+ idField?: string;
1018
+ /**
1019
+ * Column id that carries the expander + indentation. Defaults to the first
1020
+ * visible column.
1021
+ */
1022
+ column?: string;
1023
+ /** Indent per depth level, in px. Default `12`. */
1024
+ indentPx?: number;
1025
+ };
995
1026
  groupable?: boolean;
1027
+ /**
1028
+ * Render a subtotal row after each group's children, carrying that group's
1029
+ * aggregate values under the columns they belong to (the columns with an
1030
+ * `aggregate` set). Off by default.
1031
+ *
1032
+ * Footers are inserted after paging, so they never count against `pageSize` -
1033
+ * a page shows its `pageSize` data rows plus whatever footers close on it.
1034
+ */
1035
+ groupFooters?: boolean;
1036
+ /**
1037
+ * Append a grand-total row at the very end, aggregating the whole filtered
1038
+ * set (not just the current page) with each column's `aggregate` function.
1039
+ *
1040
+ * Independent of `groupFooters`: use it on a flat grid for a bottom totals
1041
+ * line, or alongside group subtotals for both. Like group footers it is
1042
+ * appended after paging, so it never counts against `pageSize` - but it is
1043
+ * only appended on the LAST page, so a total never appears mid-dataset.
1044
+ * Columns without an `aggregate` are left blank.
1045
+ */
1046
+ grandTotalRow?: boolean;
1047
+ /**
1048
+ * How grouped rows are displayed.
1049
+ *
1050
+ * - `groupRows` (default): a full-width banner row per group. Unchanged
1051
+ * behaviour - existing grids are untouched.
1052
+ * - `singleColumn`: one synthetic "Group" column holding every level,
1053
+ * indented by depth.
1054
+ * - `multipleColumns`: one synthetic column per grouped field.
1055
+ *
1056
+ * Both column modes hide the grouped source columns, since their values move
1057
+ * into the auto column(s), and render group rows as ordinary rows so their
1058
+ * aggregate cells line up under the real columns.
1059
+ */
1060
+ groupDisplayMode?: GroupDisplayType;
1061
+ /** Header for the combined `singleColumn` auto-group column. Default `"Group"`. */
1062
+ autoGroupColumnHeader?: string;
1063
+ /** Width (px) of each auto-group column. Default `220`. */
1064
+ autoGroupColumnWidth?: number;
996
1065
  pageable?: boolean;
997
1066
  loading?: boolean;
998
1067
  /**
@@ -1073,6 +1142,18 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1073
1142
  * per-row variable heights (e.g. an interactive row-resize feature).
1074
1143
  * Defaults to 30. */
1075
1144
  rowHeight?: number | ((rowIndex: number) => number);
1145
+ /**
1146
+ * Size each row to its own content instead of a fixed height: cell text wraps
1147
+ * and the row grows to fit the tallest cell. Rows are measured after they
1148
+ * render, so this works with virtualization - `rowHeight` (or 30) is the
1149
+ * estimate used before a row has been measured, which keeps the scrollbar
1150
+ * stable while you scroll into new rows.
1151
+ *
1152
+ * Costs a measurement pass per row, so prefer a fixed `rowHeight` when your
1153
+ * content is uniform. Ignored when `rowHeight` is a function (you are already
1154
+ * supplying per-row heights).
1155
+ */
1156
+ autoRowHeight?: boolean;
1076
1157
  /**
1077
1158
  * Height (px) of a single column-header level row. With multi-level
1078
1159
  * (grouped) headers the total header height is `levels * headerHeight`,
@@ -213,15 +213,7 @@
213
213
  return { t0: zoom.i0 / n, t1: (zoom.i1 + 1) / n }
214
214
  })
215
215
 
216
- /** Map a brush-svg-local x to a category index in the FULL data range. */
217
- function brushXToIndex(localX: number): number {
218
- if (!brushGeo) return 0
219
- const w = brushGeo.plot.w
220
- const x = Math.max(0, Math.min(w, localX - brushGeo.plot.x))
221
- const n = spec.categories.length
222
- return Math.max(0, Math.min(n - 1, Math.floor((x / w) * n)))
223
- }
224
- type BrushDrag = { mode: 'move' | 'left' | 'right'; startX: number; startT0: number; startT1: number }
216
+ type BrushDrag ={ mode: 'move' | 'left' | 'right'; startX: number; startT0: number; startT1: number }
225
217
  let brushDrag = $state<BrushDrag | null>(null)
226
218
  function brushSvgX(e: PointerEvent, svg: SVGSVGElement): number {
227
219
  const r = svg.getBoundingClientRect()
@@ -919,7 +911,7 @@
919
911
  {#if isRadar && geo.radarCenter}
920
912
  {@const c = geo.radarCenter}
921
913
  <!-- Concentric grid rings -->
922
- {#each geo.radarRings as v, ri (ri)}
914
+ {#each geo.radarRings as _v, ri (ri)}
923
915
  {@const ringR = c.r * ((ri + 1) / geo.radarRings.length)}
924
916
  <circle class="sv-grid-chart-radar-ring" cx={c.cx} cy={c.cy} r={ringR} />
925
917
  {/each}
@@ -41,6 +41,10 @@
41
41
  onCommit?: () => void
42
42
  /** Called when the user dismisses (Escape, blur with no change). */
43
43
  onCancel?: () => void
44
+ /** The option list is still being fetched (async `editorOptions`). Shows a
45
+ * loading line instead of "No options", which would read as "nothing to
46
+ * pick" when the truth is "not here yet". */
47
+ loading?: boolean
44
48
  }
45
49
 
46
50
  let {
@@ -54,6 +58,7 @@
54
58
  onChange,
55
59
  onCommit,
56
60
  onCancel,
61
+ loading = false,
57
62
  }: Props = $props()
58
63
 
59
64
  /** Stable per-instance id used to build option ids for
@@ -158,9 +163,13 @@
158
163
  function updatePanelPosition() {
159
164
  if (!triggerEl) return
160
165
  // Use the smaller of (option count, 10) for the upward-flip math so
161
- // a 3-option list doesn't think it needs 320px of headroom.
162
- const visibleCount = Math.min(options.length, 10)
163
- const estimatedHeight = visibleCount * optionRowHeight + (multiple ? 40 : 0) + 8
166
+ // a 3-option list doesn't think it needs 320px of headroom. Measured off
167
+ // the VISIBLE options - the search-filtered list is what actually renders,
168
+ // and an async list is empty at open time.
169
+ const visibleCount = Math.min(visibleOptions.length, 10)
170
+ const searchHeight = searchable ? 38 : 0
171
+ const estimatedHeight =
172
+ visibleCount * optionRowHeight + (multiple ? 40 : 0) + searchHeight + 8
164
173
  panelRect = anchoredRect(triggerEl.getBoundingClientRect(), {
165
174
  estimatedHeight,
166
175
  // Preserve the original behavior: anchor to the trigger, no clamping.
@@ -181,6 +190,20 @@
181
190
  }
182
191
  })
183
192
 
193
+ // Re-anchor when the option list itself changes while the panel is open.
194
+ // The geometry is derived from the option COUNT, so an async list - which
195
+ // lands after the panel opened, and a search filter, which shrinks it - would
196
+ // otherwise leave the panel sized for the list it had a moment ago. With an
197
+ // async source that means a panel sized for ZERO options: 8px tall, with the
198
+ // arriving options spilling outside its background and the grid showing
199
+ // through behind them.
200
+ $effect(() => {
201
+ if (!open) return
202
+ void visibleOptions.length
203
+ void optionRowHeight
204
+ updatePanelPosition()
205
+ })
206
+
184
207
  function closePanel() {
185
208
  open = false
186
209
  highlighted = -1
@@ -387,9 +410,15 @@
387
410
  {@const needsScroll = options.length > 10}
388
411
  <!-- Cap to the comfortable 10-row height AND the room the viewport allows,
389
412
  so a panel near the bottom edge scrolls instead of overflowing. -->
413
+ <!-- Capped by the REAL room the viewport allows (`availHeight`), never by
414
+ `maxHeight` - that one is clamped to our estimated height, and the
415
+ estimate assumes a 32px row. Themes with a larger font render taller
416
+ rows, so the estimate came up a few px short and the last option was
417
+ cut off by the panel edge. The estimate still decides which way to
418
+ flip; it must not decide how tall the panel is allowed to be. -->
390
419
  {@const panelMax = Math.min(
391
420
  needsScroll ? 10 * optionRowHeight + (multiple ? 40 : 0) + 8 : Number.POSITIVE_INFINITY,
392
- panelRect.maxHeight,
421
+ panelRect.availHeight,
393
422
  )}
394
423
  <div
395
424
  class="sv-grid-dropdown-panel"
@@ -400,7 +429,8 @@
400
429
  use:portalToBody
401
430
  use:popIn={{ up: panelRect.openUpward }}
402
431
  style:position="fixed"
403
- style:top={`${panelRect.top}px`}
432
+ style:top={panelRect.openUpward ? null : `${panelRect.top}px`}
433
+ style:bottom={panelRect.openUpward ? `${panelRect.bottom}px` : null}
404
434
  style:left={`${panelRect.left}px`}
405
435
  style:width={`${panelRect.width}px`}
406
436
  style:max-height={panelMax !== null ? `${panelMax}px` : null}
@@ -422,7 +452,9 @@
422
452
  }}
423
453
  />
424
454
  {/if}
425
- {#if visibleOptions.length === 0}
455
+ {#if loading && visibleOptions.length === 0}
456
+ <div class="sv-grid-dropdown-empty" role="status" aria-live="polite">Loading…</div>
457
+ {:else if visibleOptions.length === 0}
426
458
  <div class="sv-grid-dropdown-empty">{searchable && searchQuery ? `No matches for "${searchQuery}"` : 'No options'}</div>
427
459
  {:else}
428
460
  {#each visibleOptions as opt, i (String(opt.value))}