@jielga/tmdatagrid 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/index.d.ts +1337 -94
  2. package/dist/index.js +2812 -1189
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +2 -2
  6. package/skills/columns/SKILL.md +1 -1
  7. package/skills/features/SKILL.md +61 -11
  8. package/skills/getting-started/SKILL.md +41 -6
  9. package/skills/options/SKILL.md +12 -2
  10. package/skills/server-side/SKILL.md +1 -1
  11. package/src/tmdatagrid/TMDataGridContext.ts +26 -0
  12. package/src/tmdatagrid/components/TMDataGrid.module.css +21 -1
  13. package/src/tmdatagrid/components/TMDataGrid.tsx +29 -1
  14. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +12 -6
  15. package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
  16. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
  17. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
  18. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +192 -37
  19. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
  20. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +136 -0
  21. package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
  22. package/src/tmdatagrid/components/TMDataGridFooter.tsx +63 -43
  23. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
  24. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +159 -0
  25. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +18 -0
  26. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +249 -44
  27. package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
  28. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +103 -16
  29. package/src/tmdatagrid/components/TMDataGridTable.module.css +160 -5
  30. package/src/tmdatagrid/components/TMDataGridTable.tsx +1380 -43
  31. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +33 -7
  32. package/src/tmdatagrid/components/icons.ts +6 -0
  33. package/src/tmdatagrid/components/sticky.module.css +77 -16
  34. package/src/tmdatagrid/core/autosize.ts +125 -0
  35. package/src/tmdatagrid/core/capabilities.ts +122 -11
  36. package/src/tmdatagrid/core/cellExport.ts +321 -0
  37. package/src/tmdatagrid/core/cellNavigation.ts +103 -0
  38. package/src/tmdatagrid/core/cellRange.ts +118 -0
  39. package/src/tmdatagrid/core/columnOptions.ts +123 -0
  40. package/src/tmdatagrid/core/columnUtils.ts +18 -0
  41. package/src/tmdatagrid/core/expanding.ts +93 -0
  42. package/src/tmdatagrid/core/filterOperators.ts +198 -12
  43. package/src/tmdatagrid/core/grouping.ts +21 -0
  44. package/src/tmdatagrid/core/labels.ts +211 -0
  45. package/src/tmdatagrid/core/labelsSv.ts +108 -0
  46. package/src/tmdatagrid/core/persistence.ts +12 -0
  47. package/src/tmdatagrid/core/rowSelection.ts +202 -0
  48. package/src/tmdatagrid/core/summary.ts +35 -0
  49. package/src/tmdatagrid/index.ts +96 -2
  50. package/src/tmdatagrid/useTMDataGrid.tsx +558 -37
@@ -1,4 +1,4 @@
1
- import { ActionIcon, Popover, Text, Tooltip } from "@mantine/core";
1
+ import { ActionIcon, Loader, Popover, Text, Tooltip } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
3
  import type { ReactNode } from "react";
4
4
  import classes from "./TMDataGridToolbar.module.css";
@@ -19,6 +19,29 @@ export function TMDataGridToolbarSpacer() {
19
19
  return <div className={classes.toolbarSpacer} />;
20
20
  }
21
21
 
22
+ /**
23
+ * A small spinner shown while `meta.loading` is true, and nothing otherwise.
24
+ *
25
+ * The body only shows its loading state while the grid is *empty* — a
26
+ * server-driven grid refetching with rows on screen keeps showing them, which
27
+ * is right, but leaves nothing saying a fetch is running. This is that signal,
28
+ * and the consumer decides where it sits by placing it in the toolbar:
29
+ *
30
+ * ```tsx
31
+ * <TMDataGrid.Toolbar>
32
+ * <TMDataGrid.SummaryCount />
33
+ * <TMDataGrid.Spacer />
34
+ * <TMDataGrid.LoadingIndicator />
35
+ * <TMDataGrid.FilterButton />
36
+ * </TMDataGrid.Toolbar>
37
+ * ```
38
+ */
39
+ export function TMDataGridLoadingIndicator() {
40
+ const { table, labels } = useTMDataGridContext();
41
+ if (table.options.meta?.loading !== true) return null;
42
+ return <Loader size="xs" aria-label={labels.loading} />;
43
+ }
44
+
22
45
  /**
23
46
  * Visible rows over total rows. On a server-driven grid, set
24
47
  * `meta.totalRowCount` for the denominator — the client never sees every row.
@@ -52,7 +75,7 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
52
75
  * Renders nothing when no column can be hidden (`enableHiding: false`).
53
76
  */
54
77
  export function TMDataGridColumnsButton() {
55
- const { table, ui, features, controlSize } = useTMDataGridContext();
78
+ const { table, ui, features, labels, controlSize } = useTMDataGridContext();
56
79
  const opened = useSelector(ui, (state) => state.columnsPanelOpen);
57
80
 
58
81
  if (!getGridCapabilities(table, features).canHideAny) return null;
@@ -68,12 +91,12 @@ export function TMDataGridColumnsButton() {
68
91
  trapFocus
69
92
  >
70
93
  <Popover.Target>
71
- <Tooltip label="Manage columns" openDelay={400}>
94
+ <Tooltip label={labels.manageColumns} openDelay={400}>
72
95
  <ActionIcon
73
96
  variant="subtle"
74
97
  color="gray"
75
98
  size={controlSize}
76
- aria-label="Manage columns"
99
+ aria-label={labels.manageColumns}
77
100
  onClick={ui.actions.toggleColumnsPanel}
78
101
  >
79
102
  <BurgerIcon size={18} stroke={1.6} />
@@ -93,7 +116,7 @@ export function TMDataGridColumnsButton() {
93
116
  */
94
117
  export function TMDataGridFilterButton() {
95
118
  const api = useTMDataGridContext();
96
- const { table, ui, features, controlSize } = api;
119
+ const { table, ui, features, labels, controlSize } = api;
97
120
  const opened = useSelector(ui, (state) => state.filterPanelOpen);
98
121
  const columnFilters = useSelector(
99
122
  table.store,
@@ -106,12 +129,15 @@ export function TMDataGridFilterButton() {
106
129
  if (!getGridCapabilities(table, features).canFilterAny) return null;
107
130
 
108
131
  return (
109
- <Tooltip label="Filters" openDelay={400}>
132
+ <Tooltip label={labels.filters} openDelay={400}>
110
133
  <ActionIcon
111
134
  variant={opened || activeCount > 0 ? "light" : "subtle"}
112
135
  color={activeCount > 0 ? undefined : "gray"}
113
136
  size={controlSize}
114
- aria-label="Filters"
137
+ aria-label={labels.filters}
138
+ // Marks this as the panel's own toggle, so the panel's click-away
139
+ // handler leaves it alone and the button stays a toggle.
140
+ data-dg-filter-toggle
115
141
  onClick={() => {
116
142
  if (opened) {
117
143
  ui.actions.closeFilterPanel();
@@ -6,16 +6,22 @@
6
6
  * `sideEffects: false`, so only the icons re-exported here reach the bundle.
7
7
  */
8
8
  export {
9
+ IconArrowAutofitWidth as AutosizeIcon,
9
10
  IconArrowDown as ArrowDownIcon,
10
11
  IconArrowLeft as MoveLeftIcon,
11
12
  IconArrowRight as MoveRightIcon,
12
13
  IconArrowUp as ArrowUpIcon,
14
+ IconChevronDown as ChevronDownIcon,
13
15
  IconChevronLeft as ChevronLeftIcon,
14
16
  IconChevronRight as ChevronRightIcon,
15
17
  IconColumns3 as ColumnsIcon,
16
18
  IconDotsVertical as DotsVerticalIcon,
17
19
  IconEyeOff as EyeOffIcon,
18
20
  IconFilter as FilterIcon,
21
+ IconFoldDown as ExpandAllIcon,
22
+ IconFoldUp as CollapseAllIcon,
23
+ IconIndentDecrease as UngroupIcon,
24
+ IconIndentIncrease as GroupIcon,
19
25
  IconArrowBarToLeft as PinLeftIcon,
20
26
  IconArrowBarToRight as PinRightIcon,
21
27
  IconMenu2 as BurgerIcon,
@@ -1,36 +1,38 @@
1
1
  /* Boundary of a pinned column group. Shared, because the edge is cast by both
2
- the header cell and every body cell in the same column — the two have to
3
- draw an identical band or the seam shows at the header.
2
+ the header cell and every body cell in the same column — the two have to draw
3
+ an identical band or the seam shows at the header.
4
4
 
5
5
  A box-shadow is drawn per cell, and its blur fades in at each cell's own top
6
6
  and bottom edge — stacked down a column that reads as banding at every row
7
- seam. Instead: a crisp 1px border for the hard edge, plus a gradient pseudo
8
- element pinned to `top: 0; bottom: 0` so consecutive cells butt together into
9
- one unbroken band. Mantine sets `color-scheme` on the root, so light-dark()
10
- resolves without a second selector. */
11
- .stickyLeft {
12
- border-right: 1px solid var(--mantine-color-default-border);
13
- }
14
-
15
- .stickyRight {
16
- border-left: 1px solid var(--mantine-color-default-border);
17
- }
7
+ seam. Instead: a gradient pseudo element pinned to `top: 0; bottom: 0` so
8
+ consecutive cells butt together into one unbroken band. Mantine sets
9
+ `color-scheme` on the root, so light-dark() resolves without a second
10
+ selector.
18
11
 
12
+ No border. A 1px line the colour of the grid's own borders drew a hard rule
13
+ between the pinned lanes and the data, so the pinned columns read as a
14
+ separate table rather than as part of this one — and at the system lanes,
15
+ which are only a control wide, it framed them like a sidebar. */
19
16
  .stickyLeft::after,
20
17
  .stickyRight::after {
21
18
  content: "";
22
19
  position: absolute;
23
20
  top: 0;
24
21
  bottom: 0;
25
- width: 4px;
22
+ width: 8px;
26
23
  pointer-events: none;
27
24
  }
28
25
 
26
+ /* Three stops rather than two. A straight linear ramp to `transparent` fades
27
+ fastest at the start, so its own outer edge stays visible as a faint line —
28
+ the sharpness a hard border was blamed for. Front-loading the falloff puts
29
+ the density against the pinned cell and lets the tail reach zero unnoticed. */
29
30
  .stickyLeft::after {
30
31
  left: 100%;
31
32
  background: linear-gradient(
32
33
  to right,
33
- light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)),
34
+ light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
35
+ light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
34
36
  transparent
35
37
  );
36
38
  }
@@ -39,7 +41,66 @@
39
41
  right: 100%;
40
42
  background: linear-gradient(
41
43
  to left,
42
- light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)),
44
+ light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
45
+ light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
43
46
  transparent
44
47
  );
45
48
  }
49
+
50
+ /* The band says "there is more table under this lane". So it should only be
51
+ there while that is true: nothing is hidden at either end of the scroll, and
52
+ nothing at all is hidden by a grid narrow enough not to scroll.
53
+
54
+ Driven by the scroll position itself rather than by a listener writing a
55
+ class. A scroll-driven animation is evaluated on the compositor, so the band
56
+ tracks the scroll frame for frame without a single React render — and the
57
+ body re-renders on every scroll frame already, from virtualization.
58
+
59
+ `scroll(nearest inline)` resolves to `.scrollContainer`, the one horizontal
60
+ scroller above these cells. An inactive timeline — which is what a container
61
+ with nothing to scroll gives — leaves the base style in place, so `opacity:
62
+ 0` inside this block is also the answer for a grid that does not scroll. */
63
+ @supports (animation-timeline: scroll()) {
64
+ .stickyLeft::after,
65
+ .stickyRight::after {
66
+ /* Distance over which the band arrives. Short enough to read as an edge
67
+ appearing under the lane rather than as a fade. */
68
+ --dg-sticky-edge-range: 20px;
69
+ opacity: 0;
70
+ animation-timeline: scroll(nearest inline);
71
+ animation-timing-function: linear;
72
+ animation-fill-mode: both;
73
+ }
74
+
75
+ .stickyLeft::after {
76
+ animation-name: stickyEdgeIn;
77
+ animation-range: 0 var(--dg-sticky-edge-range);
78
+ }
79
+
80
+ /* The mirror: on at rest, gone as the last column arrives. The range is
81
+ measured from the end, and `both` holds the first value everywhere before
82
+ it — so the band stays fully on until the scroller is within 20px of home.
83
+ */
84
+ .stickyRight::after {
85
+ animation-name: stickyEdgeOut;
86
+ animation-range: calc(100% - var(--dg-sticky-edge-range)) 100%;
87
+ }
88
+ }
89
+
90
+ @keyframes stickyEdgeIn {
91
+ from {
92
+ opacity: 0;
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ }
97
+ }
98
+
99
+ @keyframes stickyEdgeOut {
100
+ from {
101
+ opacity: 1;
102
+ }
103
+ to {
104
+ opacity: 0;
105
+ }
106
+ }
@@ -0,0 +1,125 @@
1
+ import type { RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridRowData } from "../TMDataGridContext";
3
+ import type { TMDataGridTable } from "../useTMDataGrid";
4
+
5
+ /**
6
+ * Room the header keeps for its hover-revealed actions (sort arrow, menu) and
7
+ * the resize divider, which measure zero while idle. Without it a column sized
8
+ * exactly to its header text puts the actions on top of the title.
9
+ */
10
+ const HEADER_ACTIONS_ALLOWANCE = 30;
11
+
12
+ /** Breathing room so the widest value does not touch the next divider. */
13
+ const CONTENT_ALLOWANCE = 2;
14
+
15
+ /**
16
+ * What the content inside one cell actually spans, in px.
17
+ *
18
+ * A Range over the children rather than the span's `scrollWidth`, because the
19
+ * two disagree in opposite directions: a left-aligned cell stretches its
20
+ * content span to fill the cell (`flex: 1 1 auto`), so `scrollWidth` reports
21
+ * the stretched box and autosize could never *shrink* a column — while for
22
+ * clipped content both see past the `overflow: hidden`. The Range measures
23
+ * the laid-out text and elements themselves, whatever box they sit in.
24
+ *
25
+ * `scrollWidth` stays as the fallback for environments whose Ranges do not
26
+ * measure (jsdom reports 0 there).
27
+ */
28
+ function contentSpanWidth(content: HTMLElement): number {
29
+ const range = document.createRange();
30
+ range.selectNodeContents(content);
31
+ // jsdom's Range has no getBoundingClientRect at all, hence the guard
32
+ // rather than a zero-check alone.
33
+ const width =
34
+ typeof range.getBoundingClientRect === "function"
35
+ ? range.getBoundingClientRect().width
36
+ : 0;
37
+ return width > 0 ? width : content.scrollWidth;
38
+ }
39
+
40
+ /**
41
+ * Widest rendered content of one column, in px, measured from the DOM.
42
+ *
43
+ * Mounted cells only: under virtualization the unmounted rows do not exist to
44
+ * be measured, so this reads the visible window plus overscan — the same
45
+ * trade AG Grid's autosize makes by default.
46
+ */
47
+ export function measureColumnContentWidth({
48
+ container,
49
+ columnId,
50
+ }: {
51
+ /** The grid's scroll container — anything enclosing the column's cells. */
52
+ container: HTMLElement;
53
+ columnId: string;
54
+ }): number {
55
+ let widest = 0;
56
+
57
+ const cells = container.querySelectorAll<HTMLElement>(
58
+ `[role="cell"][data-column-id="${CSS.escape(columnId)}"],` +
59
+ `[role="gridcell"][data-column-id="${CSS.escape(columnId)}"]`,
60
+ );
61
+ for (const cell of cells) {
62
+ const content = cell.firstElementChild;
63
+ if (!(content instanceof HTMLElement)) continue;
64
+ const styles = getComputedStyle(cell);
65
+ const padding =
66
+ (Number.parseFloat(styles.paddingLeft) || 0) +
67
+ (Number.parseFloat(styles.paddingRight) || 0);
68
+ widest = Math.max(
69
+ widest,
70
+ contentSpanWidth(content) + padding + CONTENT_ALLOWANCE,
71
+ );
72
+ }
73
+
74
+ const title = container.querySelector<HTMLElement>(
75
+ `[data-testid="dg-header-${CSS.escape(columnId)}"] [data-dg-header-title]`,
76
+ );
77
+ if (title) {
78
+ const header = title.closest<HTMLElement>('[role="columnheader"]');
79
+ const styles = header ? getComputedStyle(header) : null;
80
+ const padding = styles
81
+ ? (Number.parseFloat(styles.paddingLeft) || 0) +
82
+ (Number.parseFloat(styles.paddingRight) || 0)
83
+ : 0;
84
+ widest = Math.max(
85
+ widest,
86
+ contentSpanWidth(title) + padding + HEADER_ACTIONS_ALLOWANCE,
87
+ );
88
+ }
89
+
90
+ return widest;
91
+ }
92
+
93
+ /**
94
+ * Sizes a column to its widest mounted content — what double-clicking the
95
+ * resize divider does, exported so a menu item or consumer code can trigger
96
+ * the same thing.
97
+ *
98
+ * The width lands in `columnSizing`, so it persists with the other widths and
99
+ * a later resize drag takes over from it. Clamped to the column's
100
+ * `minSize`/`maxSize`; a no-op (returning `false`) for a column that cannot
101
+ * be resized or has nothing mounted to measure.
102
+ */
103
+ export function autosizeColumn<TData extends RowData>({
104
+ table,
105
+ columnId,
106
+ container,
107
+ }: {
108
+ table: TMDataGridTable<TData>;
109
+ columnId: string;
110
+ container: HTMLElement;
111
+ }): boolean {
112
+ const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
113
+ const column = erased.getColumn(columnId);
114
+ if (!column || !column.getCanResize()) return false;
115
+
116
+ const measured = measureColumnContentWidth({ container, columnId });
117
+ if (measured <= 0) return false;
118
+
119
+ const minSize = column.columnDef.minSize ?? 80;
120
+ const maxSize = column.columnDef.maxSize ?? Number.POSITIVE_INFINITY;
121
+ const width = Math.round(Math.min(Math.max(measured, minSize), maxSize));
122
+
123
+ erased.setColumnSizing((previous) => ({ ...previous, [columnId]: width }));
124
+ return true;
125
+ }
@@ -20,28 +20,103 @@ import type {
20
20
  * what applies the per-column overrides.
21
21
  */
22
22
  /**
23
- * How rows are selected — see `rowSelectionMode` on {@link UseTMDataGridOptions}.
23
+ * How rows are selected — see `selectionMode` on {@link UseTMDataGridOptions}.
24
+ *
25
+ * One axis, because the two things a config could set independently — which
26
+ * chrome selects, and what a bare row click does — cannot both be free: a click
27
+ * can either toggle a multi-selection or move the highlight, never both. Folding
28
+ * them into one value makes that conflict unrepresentable rather than something
29
+ * to warn about.
24
30
  */
25
- export type TMDataGridRowSelectionMode = "checkbox" | "row";
31
+ export type TMDataGridSelectionMode =
32
+ /** Checkbox column, multi-select. Clicking a row elsewhere does nothing. */
33
+ | "checkbox"
34
+ /** No checkbox column; row click multi-selects, with Ctrl/Shift modifiers. */
35
+ | "row"
36
+ /** Checkbox column multi-selects, and a row click highlights one row. */
37
+ | "checkboxAndHighlight"
38
+ /** No selection at all — a row click only highlights. Master-detail. */
39
+ | "highlight";
40
+
41
+ /**
42
+ * How cells are selected — see `cellSelection` on {@link UseTMDataGridOptions}.
43
+ *
44
+ * A mode rather than a pair of booleans, for the reason
45
+ * {@link TMDataGridSelectionMode} is one: "cells can be selected" and "more
46
+ * than one at a time" are not independent, and a range that cannot be anchored
47
+ * anywhere is not a state worth being able to write down.
48
+ */
49
+ export type TMDataGridCellSelectionMode =
50
+ /** No cell cursor at all. The body's tab stop stays on the row. */
51
+ | "none"
52
+ /** One cell takes the focus and the arrow keys move it. */
53
+ | "single"
54
+ /** As `"single"`, plus a rectangle: drag, Shift+click, Shift+arrows, Ctrl+C. */
55
+ | "range";
26
56
 
27
57
  export type TMDataGridFeatureFlags = {
28
58
  sorting: boolean;
29
59
  filtering: boolean;
60
+ /** Whether the quick search may filter — `enableGlobalFilter`. */
61
+ globalFiltering: boolean;
30
62
  hiding: boolean;
31
63
  pinning: boolean;
32
64
  resizing: boolean;
33
65
  ordering: boolean;
66
+ /** Whether any row selection is possible. Off outright under `"highlight"`. */
34
67
  rowSelection: boolean;
35
- /** Only meaningful while `rowSelection` is on. Defaults to `"checkbox"`. */
36
- rowSelectionMode: TMDataGridRowSelectionMode;
68
+ /** Defaults to `"checkbox"`. */
69
+ selectionMode: TMDataGridSelectionMode;
70
+ /** Whether the generated checkbox column is prepended. */
71
+ selectColumn: boolean;
72
+ /** Whether a bare row click toggles the selection. Only under `"row"`. */
73
+ rowClickSelects: boolean;
74
+ /**
75
+ * Whether more than one row can be selected at once — TanStack's
76
+ * `enableMultiRowSelection`. Off, the grid drops the select-all header
77
+ * checkbox: TanStack's `toggleAllRowsSelected` only consults `getCanSelect`,
78
+ * so that control would select every row and walk straight past the limit.
79
+ *
80
+ * A per-row predicate counts as on, since some rows may still multi-select.
81
+ */
82
+ multiRowSelection: boolean;
37
83
  /** Whether a selected row takes the highlight colour. Follows the mode. */
38
- highlightSelectedRows: boolean;
84
+ showSelectedBackground: boolean;
85
+ /**
86
+ * The single highlighted row — clicking a row highlights it, for a detail
87
+ * panel to follow. State of its own, not a slice of `rowSelection`, which is
88
+ * what lets it coexist with a checkbox multi-selection.
89
+ */
90
+ highlightRow: boolean;
91
+ /**
92
+ * Whether cells can be selected at all. Off unless `cellSelection` asks for
93
+ * it — it takes the body's tab stop off the row and puts it on a cell, and
94
+ * reports the grid to assistive technology as a `grid` rather than a `table`,
95
+ * which is a different promise about what the keyboard does.
96
+ */
97
+ cellSelection: boolean;
98
+ /** Defaults to `"none"`. */
99
+ cellSelectionMode: TMDataGridCellSelectionMode;
100
+ /** Whether a rectangle of cells can be selected. Only under `"range"`. */
101
+ cellRangeSelection: boolean;
39
102
  /**
40
103
  * The one default-off flag: pagination must be asked for, either with the
41
104
  * grid's `enablePagination` or implicitly by declaring `manualPagination`.
42
105
  * Off, the grid renders every filtered row and relies on virtualization.
43
106
  */
44
107
  pagination: boolean;
108
+ /**
109
+ * Row grouping: the "Group by" items in the header menu and the generated
110
+ * tree column. On unless `enableGrouping: false`, since an ungrouped grid
111
+ * looks and behaves exactly as it did before — `grouping` starts empty, and
112
+ * an empty grouping state passes straight through the row model.
113
+ *
114
+ * The exception is `manualPagination`, where the client holds one page rather
115
+ * than the whole set: grouping that page would build groups out of an
116
+ * arbitrary slice and quietly show wrong counts. A server-side grid that does
117
+ * its own grouping can still say `enableGrouping: true` to override.
118
+ */
119
+ grouping: boolean;
45
120
  };
46
121
 
47
122
  export function readFeatureFlags<TData extends RowData>(
@@ -49,32 +124,56 @@ export function readFeatureFlags<TData extends RowData>(
49
124
  UseTMDataGridOptions<TData>,
50
125
  | "enableSorting"
51
126
  | "enableColumnFilters"
127
+ | "enableGlobalFilter"
52
128
  | "enableHiding"
53
129
  | "enableColumnPinning"
54
130
  | "enableColumnResizing"
55
131
  | "enableColumnOrdering"
56
132
  | "enableRowSelection"
57
- | "rowSelectionMode"
58
- | "highlightSelectedRows"
133
+ | "enableMultiRowSelection"
134
+ | "selectionMode"
135
+ | "showSelectedBackground"
136
+ | "cellSelection"
59
137
  | "enablePagination"
60
138
  | "manualPagination"
139
+ | "enableGrouping"
61
140
  >,
62
141
  ): TMDataGridFeatureFlags {
142
+ const selectionMode = options.selectionMode ?? "checkbox";
143
+ const cellSelectionMode = options.cellSelection ?? "none";
144
+ // `"highlight"` is the master-detail mode: no selection to speak of, so the
145
+ // checkbox column and the click-to-select behaviour both fall away. Otherwise
146
+ // `enableRowSelection` still has the final say, including its predicate form.
147
+ const rowSelection =
148
+ selectionMode !== "highlight" && options.enableRowSelection !== false;
149
+
63
150
  return {
64
151
  sorting: options.enableSorting !== false,
65
152
  filtering: options.enableColumnFilters !== false,
153
+ globalFiltering: options.enableGlobalFilter !== false,
66
154
  hiding: options.enableHiding !== false,
67
155
  pinning: options.enableColumnPinning !== false,
68
156
  resizing: options.enableColumnResizing !== false,
69
157
  ordering: options.enableColumnOrdering !== false,
70
- rowSelection: options.enableRowSelection !== false,
71
- rowSelectionMode: options.rowSelectionMode ?? "checkbox",
158
+ rowSelection,
159
+ selectionMode,
160
+ selectColumn:
161
+ rowSelection &&
162
+ (selectionMode === "checkbox" || selectionMode === "checkboxAndHighlight"),
163
+ rowClickSelects: rowSelection && selectionMode === "row",
164
+ multiRowSelection: options.enableMultiRowSelection !== false,
72
165
  // In "row" mode the highlight *is* the feedback for a click, so it is on.
73
166
  // With checkboxes the box already says it, so it is off unless asked for.
74
- highlightSelectedRows:
75
- options.highlightSelectedRows ?? options.rowSelectionMode === "row",
167
+ showSelectedBackground:
168
+ options.showSelectedBackground ?? selectionMode === "row",
169
+ highlightRow:
170
+ selectionMode === "checkboxAndHighlight" || selectionMode === "highlight",
171
+ cellSelection: cellSelectionMode !== "none",
172
+ cellSelectionMode,
173
+ cellRangeSelection: cellSelectionMode === "range",
76
174
  pagination:
77
175
  options.enablePagination === true || options.manualPagination === true,
176
+ grouping: options.enableGrouping ?? options.manualPagination !== true,
78
177
  };
79
178
  }
80
179
 
@@ -89,6 +188,7 @@ export function readFeatureFlags<TData extends RowData>(
89
188
  * | Pin to left / right | `enableColumnPinning` / `enablePinning` |
90
189
  * | Resize dragging | `enableColumnResizing` / `enableResizing` |
91
190
  * | Header dragging, Move left / right | `enableColumnOrdering` / `meta.enableOrdering` |
191
+ * | Group by / Ungroup | `enableGrouping` (table or column) |
92
192
  */
93
193
  export type TMDataGridColumnCapabilities = {
94
194
  canSort: boolean;
@@ -97,6 +197,7 @@ export type TMDataGridColumnCapabilities = {
97
197
  canPin: boolean;
98
198
  canResize: boolean;
99
199
  canReorder: boolean;
200
+ canGroup: boolean;
100
201
  };
101
202
 
102
203
  export function getColumnCapabilities(
@@ -111,6 +212,9 @@ export function getColumnCapabilities(
111
212
  canResize: features.resizing && column.getCanResize(),
112
213
  // Ordering has no TanStack capability method — see isColumnReorderable.
113
214
  canReorder: features.ordering && isColumnReorderable(column),
215
+ // `getCanGroup()` also insists on an accessor, which is what keeps the
216
+ // generated checkbox and tree columns from offering to group on themselves.
217
+ canGroup: features.grouping && column.getCanGroup(),
114
218
  };
115
219
  }
116
220
 
@@ -121,8 +225,11 @@ export type TMDataGridCapabilities = {
121
225
  canHideAny: boolean;
122
226
  canPinAny: boolean;
123
227
  canReorderAny: boolean;
228
+ canGroupAny: boolean;
124
229
  canSelectRows: boolean;
125
230
  canPaginate: boolean;
231
+ /** At least one leaf column takes part in the global quick search. */
232
+ canSearch: boolean;
126
233
  };
127
234
 
128
235
  export function getGridCapabilities(
@@ -139,7 +246,11 @@ export function getGridCapabilities(
139
246
  canHideAny: any((c) => c.canHide),
140
247
  canPinAny: any((c) => c.canPin),
141
248
  canReorderAny: any((c) => c.canReorder),
249
+ canGroupAny: any((c) => c.canGroup),
142
250
  canSelectRows: features.rowSelection,
143
251
  canPaginate: features.pagination,
252
+ canSearch:
253
+ features.globalFiltering &&
254
+ columns.some((column) => column.getCanGlobalFilter()),
144
255
  };
145
256
  }