@bmsuisse/datagrid 0.32.2 → 0.33.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.
package/dist/index.d.ts CHANGED
@@ -219,6 +219,26 @@ interface BaseColumn<TRow> {
219
219
  * override for their own concerns.
220
220
  */
221
221
  formatForClipboard?: (value: unknown, row: TRow) => string;
222
+ /**
223
+ * Computes this column's aggregate cell content from a set of rows — the
224
+ * same function feeds both `DataGridProps`/`TreeDataGridProps`
225
+ * `groupBy`'s optional per-group summary row (called once per bucket, with
226
+ * that bucket's own rows) and `showTotals`'s grand-total row (called once,
227
+ * with every currently-rendered row). No calculation happens by default:
228
+ * omit this to leave the column's cell blank in both rows. There's
229
+ * deliberately no separate "group subtotal" vs. "grand total" variant —
230
+ * both are just "reduce this set of rows to a value for this column", so
231
+ * one function covers both.
232
+ *
233
+ * `showTotals`'s grand-total row calls this with EVERY currently-rendered
234
+ * row, which can be an empty array (e.g. a filter narrows the current page
235
+ * to zero rows) — a bare `rows.reduce((a, b) => ...)` with no seed throws
236
+ * on an empty array, so seed your reducer (`rows.reduce((a, b) => ..., 0)`)
237
+ * rather than assuming at least one row. A per-group summary row never
238
+ * has this problem: a bucket only exists because at least one row produced
239
+ * its key.
240
+ */
241
+ summary?: (rows: TRow[]) => ReactNode;
222
242
  }
223
243
  type StringColumn<TRow> = BaseColumn<TRow> & {
224
244
  type: "string";
@@ -1498,10 +1518,14 @@ interface DataGridProps<TRow> {
1498
1518
  * rendering one full-width `colSpan`'d group-header row before each
1499
1519
  * bucket's own rows, in first-seen bucket order (rows are never re-sorted
1500
1520
  * to cluster a group together — the caller's own sort already determines
1501
- * which key appears first). Single level only — no nested grouping, no
1502
- * aggregate/summary calculation; a caller wanting a subtotal computes it
1503
- * themselves in `renderGroupHeader` off that bucket's own row array.
1504
- * Operates on whatever `rows` already resolved to — i.e. the current page,
1521
+ * which key appears first). Single level only — no nested grouping. A
1522
+ * caller wanting a combined label (e.g. `"Acme (5 pending)"`) still
1523
+ * computes that themselves in `renderGroupHeader` off that bucket's own
1524
+ * row array; a caller wanting a value aligned under one or more specific
1525
+ * *columns* instead sets `ColumnDef.summary` on those columns — see its
1526
+ * own doc — which renders a second row of per-column cells right below
1527
+ * the label, once at least one visible column defines it. Operates on
1528
+ * whatever `rows` already resolved to — i.e. the current page,
1505
1529
  * if paginated — with no special-casing; pair with a large `pageSize`/
1506
1530
  * `showPagination: false` if a group's full membership should never be
1507
1531
  * split across pages. Omit entirely to disable — no grouping, no header
@@ -1545,6 +1569,17 @@ interface DataGridProps<TRow> {
1545
1569
  onExpandedGroupsChange?: (expanded: Record<string, boolean>) => void;
1546
1570
  /** Alternates body row backgrounds for readability on wide/dense tables. Defaults to true. */
1547
1571
  zebra?: boolean;
1572
+ /**
1573
+ * Renders a `<tfoot>` totals row below the grid's body, with one cell per
1574
+ * visible column computed via that column's own `ColumnDef.summary(rows)`
1575
+ * — `rows` being every row `<DataGrid>` currently has (the current page,
1576
+ * if paginated; same scope `groupBy` itself operates on). A column with no
1577
+ * `summary` renders a blank cell in this row. Independent of `groupBy` —
1578
+ * the two compose (a per-group summary row under each bucket, plus one
1579
+ * grand-total row at the very bottom) but neither requires the other.
1580
+ * Defaults to false: no totals row, unchanged rendering.
1581
+ */
1582
+ showTotals?: boolean;
1548
1583
  /**
1549
1584
  * Enables the built-in inline-editing workflow: any column with
1550
1585
  * `editable` set renders as an interactive editor, edits accumulate
@@ -1575,7 +1610,7 @@ interface DataGridProps<TRow> {
1575
1610
  * (`"server"`). Column filter UI defaults to the type-appropriate widget
1576
1611
  * from `src/filter` unless a column supplies its own `cell` renderer.
1577
1612
  */
1578
- declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, editing, cellEditing, }: DataGridProps<TRow>): ReactElement;
1613
+ declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, showTotals, editing, cellEditing, }: DataGridProps<TRow>): ReactElement;
1579
1614
 
1580
1615
  interface GridStateController {
1581
1616
  state: GridState;
@@ -1921,7 +1956,12 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
1921
1956
  * than "which root it belongs to" wouldn't have a sensible rendering.
1922
1957
  * Groups appear in first-seen bucket order — roots are never re-sorted to
1923
1958
  * cluster a group together. Omit entirely to disable — no grouping, no
1924
- * header rows, unchanged default rendering.
1959
+ * header rows, unchanged default rendering. A caller wanting a value
1960
+ * aligned under one or more specific columns (rather than folded into
1961
+ * `renderGroupHeader`'s single label) sets `ColumnDef.summary` on those
1962
+ * columns — see its own doc — which renders a second row of per-column
1963
+ * cells, computed from that bucket's own *root* rows, right below the
1964
+ * label once at least one visible column defines it.
1925
1965
  *
1926
1966
  * Not supported together with virtualization — when both are set,
1927
1967
  * virtualization is silently disabled and the tree renders as a plain,
@@ -1942,6 +1982,17 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
1942
1982
  onExpandedGroupsChange?: (expanded: Record<string, boolean>) => void;
1943
1983
  /** Alternates body row backgrounds for readability on wide/dense tables. Defaults to true. */
1944
1984
  zebra?: boolean;
1985
+ /**
1986
+ * Renders a `<tfoot>` totals row below the tree, with one cell per visible
1987
+ * column computed via that column's own `ColumnDef.summary(data)` — the
1988
+ * root-level rows in `data`, NOT the flattened tree (a child's value is
1989
+ * usually already reflected in its parent's, so summing every flattened
1990
+ * row would typically double-count). A column with no `summary` renders a
1991
+ * blank cell. Independent of `groupBy` — the two compose (a per-group
1992
+ * summary row under each bucket, plus one grand-total row at the very
1993
+ * bottom) but neither requires the other. Defaults to false.
1994
+ */
1995
+ showTotals?: boolean;
1945
1996
  /**
1946
1997
  * Enables the same built-in inline-editing workflow `<DataGrid>` has:
1947
1998
  * any column with `editable` set renders as an interactive editor
@@ -1970,7 +2021,7 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
1970
2021
  * column's cell matches `treeColumnId` (defaults to `columns[0]`); every
1971
2022
  * other column renders exactly like it would in `<DataGrid>`.
1972
2023
  */
1973
- declare function TreeDataGrid<TRow>({ columns, data, loading, treeColumnId, columnVisibility, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, testId, selectedIds: controlledSelectedIds, onSelectedIdsChange, getRowSelectionState, isRowSelectionDisabled, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, editing, }: TreeDataGridProps<TRow>): ReactElement;
2024
+ declare function TreeDataGrid<TRow>({ columns, data, loading, treeColumnId, columnVisibility, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, childrenMap: controlledChildrenMap, onChildrenMapChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, testId, selectedIds: controlledSelectedIds, onSelectedIdsChange, getRowSelectionState, isRowSelectionDisabled, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, zebra, showTotals, editing, }: TreeDataGridProps<TRow>): ReactElement;
1974
2025
 
1975
2026
  interface FlatTreeRow<TRow> {
1976
2027
  row: TRow;
package/dist/index.js CHANGED
@@ -3245,6 +3245,7 @@ function DataGrid({
3245
3245
  expandedGroups: controlledExpandedGroups,
3246
3246
  onExpandedGroupsChange,
3247
3247
  zebra = true,
3248
+ showTotals = false,
3248
3249
  editing,
3249
3250
  cellEditing
3250
3251
  }) {
@@ -3349,6 +3350,7 @@ function DataGrid({
3349
3350
  const hasFilterRow = visibleColumns.some((column) => isFilterable(column) && column.filterDisplay === "row");
3350
3351
  const headerRuns = useMemo8(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
3351
3352
  const hasHeaderGroups = visibleColumns.some((column) => column.headerGroup);
3353
+ const hasColumnSummary = visibleColumns.some((column) => column.summary);
3352
3354
  function renderFilterWidget(column) {
3353
3355
  const value = filtersByColumn.get(column.id);
3354
3356
  const onChange = (next) => setColumnFilter(column.id, next);
@@ -3826,35 +3828,92 @@ function DataGrid({
3826
3828
  showDetailColumn && isExpanded && renderDetail && /* @__PURE__ */ jsx33("tr", { "data-testid": `${rowTestId}-detail`, children: /* @__PURE__ */ jsx33("td", { colSpan: totalColumnCount, className: "border-b border-border p-2", children: renderDetail(row.original) }) })
3827
3829
  ] }, row.id);
3828
3830
  }
3831
+ function renderGroupSummaryCells(rows2, bgClassName) {
3832
+ return visibleColumns.map((column) => {
3833
+ const cellProps = bodyCellPropsByColumn.get(column.id);
3834
+ return /* @__PURE__ */ jsx33(
3835
+ "td",
3836
+ {
3837
+ className: cn(cellProps?.className, bgClassName, "font-medium", alignClassName(column)),
3838
+ style: cellProps?.style,
3839
+ children: column.summary?.(rows2)
3840
+ },
3841
+ column.id
3842
+ );
3843
+ });
3844
+ }
3845
+ function summaryLeadingCellSpan() {
3846
+ return (showDetailColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0);
3847
+ }
3848
+ function renderGroupSummaryRow(rows2) {
3849
+ const leadingSpan = summaryLeadingCellSpan();
3850
+ const bgClassName = zebra ? "bg-muted" : "bg-background";
3851
+ const fillerClass = cn("border-b border-border p-2", bgClassName);
3852
+ return /* @__PURE__ */ jsxs20("tr", { "data-testid": "group-summary-row", className: "divide-x divide-border", children: [
3853
+ leadingSpan > 0 && /* @__PURE__ */ jsx33("td", { colSpan: leadingSpan, className: fillerClass }),
3854
+ renderGroupSummaryCells(rows2, bgClassName),
3855
+ showRowActionsColumn && /* @__PURE__ */ jsx33("td", { className: fillerClass, "aria-hidden": true })
3856
+ ] });
3857
+ }
3858
+ function renderTotalsFooter() {
3859
+ if (!showTotals || !hasColumnSummary) return null;
3860
+ const summaryRows = tableRows.map((row) => row.original);
3861
+ const leadingSpan = summaryLeadingCellSpan();
3862
+ const fillerClass = "sticky bottom-0 z-20 border-t border-border bg-muted p-2";
3863
+ return /* @__PURE__ */ jsx33("tfoot", { children: /* @__PURE__ */ jsxs20("tr", { "data-testid": "totals-row", className: "divide-x divide-border", children: [
3864
+ leadingSpan > 0 && /* @__PURE__ */ jsx33("td", { colSpan: leadingSpan, className: fillerClass }),
3865
+ visibleColumns.map((column) => {
3866
+ const pinnedProps = pinnedCellProps(column, "header");
3867
+ return /* @__PURE__ */ jsx33(
3868
+ "td",
3869
+ {
3870
+ className: cn(
3871
+ "border-t border-border p-2 font-medium",
3872
+ alignClassName(column),
3873
+ pinnedProps.className,
3874
+ "sticky bottom-0 z-20 bg-muted"
3875
+ ),
3876
+ style: pinnedProps.style,
3877
+ children: column.summary?.(summaryRows)
3878
+ },
3879
+ column.id
3880
+ );
3881
+ }),
3882
+ showRowActionsColumn && /* @__PURE__ */ jsx33("td", { className: fillerClass, "aria-hidden": true })
3883
+ ] }) });
3884
+ }
3829
3885
  function renderGroupHeaderTbody(key, rows2, measureRef, dataIndex) {
3830
3886
  const expanded = isGroupExpanded(key);
3831
- return /* @__PURE__ */ jsx33("tbody", { ref: measureRef, "data-index": dataIndex, children: /* @__PURE__ */ jsx33("tr", { "data-testid": `group-header-${key}`, children: /* @__PURE__ */ jsx33(
3832
- "td",
3833
- {
3834
- colSpan: totalColumnCount,
3835
- className: cn("sticky z-20 border-b p-2 font-medium", zebra ? "bg-muted" : "bg-background"),
3836
- style: { top: groupHeaderTop },
3837
- children: /* @__PURE__ */ jsxs20(
3838
- "button",
3839
- {
3840
- type: "button",
3841
- className: "flex w-full items-center gap-1 text-left",
3842
- onClick: () => toggleGroupExpanded(key),
3843
- "aria-expanded": expanded,
3844
- children: [
3845
- /* @__PURE__ */ jsx33(
3846
- ChevronRight,
3847
- {
3848
- className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
3849
- "aria-hidden": true
3850
- }
3851
- ),
3852
- renderGroupHeader ? renderGroupHeader(key, rows2, expanded) : `${key} (${rows2.length})`
3853
- ]
3854
- }
3855
- )
3856
- }
3857
- ) }) }, `group-${key}`);
3887
+ return /* @__PURE__ */ jsxs20("tbody", { ref: measureRef, "data-index": dataIndex, children: [
3888
+ /* @__PURE__ */ jsx33("tr", { "data-testid": `group-header-${key}`, children: /* @__PURE__ */ jsx33(
3889
+ "td",
3890
+ {
3891
+ colSpan: totalColumnCount,
3892
+ className: cn("sticky z-20 border-b p-2 font-medium", zebra ? "bg-muted" : "bg-background"),
3893
+ style: { top: groupHeaderTop },
3894
+ children: /* @__PURE__ */ jsxs20(
3895
+ "button",
3896
+ {
3897
+ type: "button",
3898
+ className: "flex w-full items-center gap-1 text-left",
3899
+ onClick: () => toggleGroupExpanded(key),
3900
+ "aria-expanded": expanded,
3901
+ children: [
3902
+ /* @__PURE__ */ jsx33(
3903
+ ChevronRight,
3904
+ {
3905
+ className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
3906
+ "aria-hidden": true
3907
+ }
3908
+ ),
3909
+ renderGroupHeader ? renderGroupHeader(key, rows2, expanded) : `${key} (${rows2.length})`
3910
+ ]
3911
+ }
3912
+ )
3913
+ }
3914
+ ) }),
3915
+ hasColumnSummary && renderGroupSummaryRow(rows2)
3916
+ ] }, `group-${key}`);
3858
3917
  }
3859
3918
  function renderGroupedBucket(bucket) {
3860
3919
  const expanded = isGroupExpanded(bucket.key);
@@ -4135,7 +4194,8 @@ function DataGrid({
4135
4194
  )
4136
4195
  ),
4137
4196
  paddingBottom > 0 && /* @__PURE__ */ jsx33("tbody", { children: /* @__PURE__ */ jsx33("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx33("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingBottom } }) }) })
4138
- ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))
4197
+ ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row)),
4198
+ renderTotalsFooter()
4139
4199
  ] }),
4140
4200
  hasCellEditing && /* @__PURE__ */ jsx33(
4141
4201
  SelectionOverlay,
@@ -4427,6 +4487,7 @@ function TreeDataGrid({
4427
4487
  expandedGroups: controlledExpandedGroups,
4428
4488
  onExpandedGroupsChange,
4429
4489
  zebra = true,
4490
+ showTotals = false,
4430
4491
  editing
4431
4492
  }) {
4432
4493
  const accessors = useMemo10(
@@ -4521,6 +4582,10 @@ function TreeDataGrid({
4521
4582
  for (const column of visibleColumns) map.set(column.id, `p-2 ${alignClassName(column)}`);
4522
4583
  return map;
4523
4584
  }, [visibleColumns]);
4585
+ const hasColumnSummary = visibleColumns.some((column) => column.summary);
4586
+ function renderGroupSummaryCells(rows) {
4587
+ return visibleColumns.map((column) => /* @__PURE__ */ jsx34("td", { className: cn(bodyTdClassByColumn.get(column.id), "font-medium"), children: column.summary?.(rows) }, column.id));
4588
+ }
4524
4589
  const scrollRef = useRef13(null);
4525
4590
  const shouldVirtualize = !groupBy && flatRows.length > virtualizeThreshold;
4526
4591
  const virtualizer = useVirtualizer2({
@@ -4675,6 +4740,24 @@ function TreeDataGrid({
4675
4740
  )
4676
4741
  }
4677
4742
  ) }),
4743
+ hasColumnSummary && /* @__PURE__ */ jsxs21(
4744
+ "tr",
4745
+ {
4746
+ "data-testid": `group-summary-${bucket.key}`,
4747
+ className: "border-b border-border divide-x divide-border",
4748
+ children: [
4749
+ showSelectionColumn && /* @__PURE__ */ jsx34(
4750
+ "td",
4751
+ {
4752
+ className: cn("p-2", zebra ? "bg-muted" : "bg-background"),
4753
+ style: { width: SELECTION_COLUMN_WIDTH }
4754
+ }
4755
+ ),
4756
+ renderGroupSummaryCells(bucket.roots),
4757
+ showRowActionsColumn && /* @__PURE__ */ jsx34("td", { className: cn("p-2", zebra ? "bg-muted" : "bg-background"), "aria-hidden": true })
4758
+ ]
4759
+ }
4760
+ ),
4678
4761
  groupExpanded && (() => {
4679
4762
  const startIndex = groupedRender?.startIndexByBucket.get(bucket.key) ?? 0;
4680
4763
  return bucket.flatRows.map((flatRow, i) => renderRow(flatRow, startIndex + i));
@@ -4692,7 +4775,36 @@ function TreeDataGrid({
4692
4775
  style: { padding: 0, border: "none", height: paddingBottom }
4693
4776
  }
4694
4777
  ) })
4695
- ] }) : flatRows.map((flatRow, index) => renderRow(flatRow, index)) })
4778
+ ] }) : flatRows.map((flatRow, index) => renderRow(flatRow, index)) }),
4779
+ showTotals && hasColumnSummary && // Sticky to the BOTTOM of the scroll container, mirroring the
4780
+ // `<thead>`'s own `top-0` stickiness — see `<DataGrid>`'s own
4781
+ // `renderTotalsFooter` for the full reasoning. Summarizes `data`
4782
+ // (the root-level rows), not `flatRows`/`allFlatRows` — same
4783
+ // "roots only" scope `groupBy` itself uses, and for the same
4784
+ // reason (a child's value is usually already folded into its
4785
+ // parent's).
4786
+ /* @__PURE__ */ jsx34("tfoot", { children: /* @__PURE__ */ jsxs21("tr", { "data-testid": "totals-row", className: "divide-x divide-border", children: [
4787
+ showSelectionColumn && /* @__PURE__ */ jsx34(
4788
+ "td",
4789
+ {
4790
+ className: "sticky bottom-0 z-20 border-t border-border bg-muted p-2",
4791
+ style: { width: SELECTION_COLUMN_WIDTH }
4792
+ }
4793
+ ),
4794
+ visibleColumns.map((column) => /* @__PURE__ */ jsx34(
4795
+ "td",
4796
+ {
4797
+ style: column.width ? { width: column.width } : void 0,
4798
+ className: cn(
4799
+ "sticky bottom-0 z-20 border-t border-border bg-muted p-2 font-medium",
4800
+ alignClassName(column)
4801
+ ),
4802
+ children: column.summary?.(data)
4803
+ },
4804
+ column.id
4805
+ )),
4806
+ showRowActionsColumn && /* @__PURE__ */ jsx34("td", { className: "sticky bottom-0 z-20 border-t border-border bg-muted p-2", "aria-hidden": true })
4807
+ ] }) })
4696
4808
  ] })
4697
4809
  }
4698
4810
  )