@jielga/tmdatagrid 2.1.4 → 2.3.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 (57) hide show
  1. package/dist/index.d.ts +78 -5
  2. package/dist/index.js +2194 -1992
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/cell-selection.md +1 -1
  6. package/docs/column-layout.md +13 -8
  7. package/docs/columns.md +59 -3
  8. package/docs/components.md +1 -0
  9. package/docs/export.md +2 -2
  10. package/docs/filtering.md +108 -15
  11. package/docs/getting-started.md +40 -0
  12. package/docs/grouping.md +9 -6
  13. package/docs/loading-and-empty.md +29 -9
  14. package/docs/localization.md +130 -3
  15. package/docs/menu.md +1 -1
  16. package/docs/pagination.md +26 -7
  17. package/docs/persistence.md +101 -2
  18. package/docs/row-details.md +41 -4
  19. package/docs/row-interaction.md +2 -2
  20. package/docs/row-selection.md +6 -7
  21. package/docs/row-styling.md +17 -0
  22. package/docs/scrolling.md +1 -1
  23. package/docs/server-side.md +3 -1
  24. package/docs/testing.md +18 -0
  25. package/docs/use-tm-data-grid.md +75 -36
  26. package/package.json +1 -1
  27. package/skills/appearance/SKILL.md +6 -4
  28. package/skills/appearance/references/labels.md +126 -0
  29. package/skills/cell-selection/SKILL.md +2 -2
  30. package/skills/columns/SKILL.md +44 -43
  31. package/skills/columns/references/columns-api.md +16 -1
  32. package/skills/data/SKILL.md +77 -50
  33. package/skills/editing/SKILL.md +1 -1
  34. package/skills/filtering/SKILL.md +94 -60
  35. package/skills/filtering/references/custom-filters.md +118 -0
  36. package/skills/getting-started/SKILL.md +37 -2
  37. package/skills/grouping/SKILL.md +16 -9
  38. package/skills/migrating-to-2/SKILL.md +1 -1
  39. package/skills/options/SKILL.md +206 -55
  40. package/skills/rows/SKILL.md +70 -39
  41. package/skills/server-side/SKILL.md +1 -1
  42. package/skills/testing/SKILL.md +12 -1
  43. package/skills/testing-components/SKILL.md +1 -1
  44. package/skills/testing-editing/SKILL.md +1 -1
  45. package/src/components/TMDataGridFilterPills.module.css +9 -7
  46. package/src/components/TMDataGridTable.module.css +15 -6
  47. package/src/components/TMDataGridTable.tsx +253 -41
  48. package/src/components/sticky.module.css +27 -7
  49. package/src/core/autosize.ts +19 -7
  50. package/src/core/cellRange.ts +11 -2
  51. package/src/core/colSpan.ts +78 -0
  52. package/src/core/columnUtils.ts +7 -0
  53. package/src/core/filterControls.ts +8 -0
  54. package/src/core/filterOperators.ts +29 -1
  55. package/src/core/persistence.ts +322 -26
  56. package/src/index.ts +5 -0
  57. package/src/useTMDataGrid.tsx +133 -14
@@ -1,6 +1,7 @@
1
1
  import type { RowData } from "@tanstack/react-table";
2
2
  import type { TMDataGridRowData } from "../TMDataGridContext";
3
3
  import type { TMDataGridTable } from "../useTMDataGrid";
4
+ import { DEFAULT_COLUMN_MIN_SIZE } from "./columnUtils";
4
5
  import { isHTMLElement } from "./dom";
5
6
 
6
7
  /**
@@ -40,30 +41,41 @@ function contentSpanWidth(content: HTMLElement): number {
40
41
  return width > 0 ? width : content.scrollWidth;
41
42
  }
42
43
 
43
- /** One column's body cells that are currently in the DOM. */
44
+ /**
45
+ * One column's body cells that are currently in the DOM.
46
+ *
47
+ * `measurable` leaves out the cells spanning several columns (`meta.colSpan`):
48
+ * such a cell says nothing about this one column's width.
49
+ */
44
50
  function mountedCells(
45
51
  container: HTMLElement,
46
52
  columnId: string,
53
+ { measurable }: { measurable: boolean },
47
54
  ): NodeListOf<HTMLElement> {
48
55
  const id = CSS.escape(columnId);
56
+ const unspanned = measurable ? ":not([data-col-span])" : "";
49
57
  return container.querySelectorAll<HTMLElement>(
50
- `[role="cell"][data-column-id="${id}"],` +
51
- `[role="gridcell"][data-column-id="${id}"]`,
58
+ `[role="cell"][data-column-id="${id}"]${unspanned},` +
59
+ `[role="gridcell"][data-column-id="${id}"]${unspanned}`,
52
60
  );
53
61
  }
54
62
 
55
63
  /**
56
- * Whether this column has any body cell in the DOM to measure.
64
+ * Whether this column has any body cell in the DOM.
57
65
  *
58
66
  * A caller sizing a column on its own schedule rather than from a gesture has
59
67
  * to ask: the virtualizer mounts its first rows a render *after* the grid
60
68
  * itself mounts, and a measurement taken before that fits the header alone.
69
+ *
70
+ * A spanning cell counts. A column whose every mounted cell spans has its
71
+ * rows, and measuring it fits the header, which is the width it gets; left
72
+ * out, the column would wait for cells that never come.
61
73
  */
62
74
  export function hasMountedCells(
63
75
  container: HTMLElement,
64
76
  columnId: string,
65
77
  ): boolean {
66
- return mountedCells(container, columnId).length > 0;
78
+ return mountedCells(container, columnId, { measurable: false }).length > 0;
67
79
  }
68
80
 
69
81
  /**
@@ -85,7 +97,7 @@ export function measureColumnContentWidth({
85
97
  }): number {
86
98
  let widest = 0;
87
99
 
88
- for (const cell of mountedCells(container, columnId)) {
100
+ for (const cell of mountedCells(container, columnId, { measurable: true })) {
89
101
  const content = cell.firstElementChild;
90
102
  if (!isHTMLElement(content)) continue;
91
103
  const styles = getComputedStyle(cell);
@@ -143,7 +155,7 @@ export function autosizeColumn<TData extends RowData>({
143
155
  const measured = measureColumnContentWidth({ container, columnId });
144
156
  if (measured <= 0) return false;
145
157
 
146
- const minSize = column.columnDef.minSize ?? 80;
158
+ const minSize = column.columnDef.minSize ?? DEFAULT_COLUMN_MIN_SIZE;
147
159
  const maxSize = column.columnDef.maxSize ?? Number.POSITIVE_INFINITY;
148
160
  const width = Math.round(Math.min(Math.max(measured, minSize), maxSize));
149
161
 
@@ -70,7 +70,13 @@ export function resolveRangeBounds({
70
70
  };
71
71
  }
72
72
 
73
- /** Whether a cell is inside the rectangle. */
73
+ /**
74
+ * Whether a cell is inside the rectangle.
75
+ *
76
+ * A cell spanning several columns (`meta.colSpan`) is addressed by its first
77
+ * column: it is inside when that column is, and a rectangle over only the
78
+ * columns it covers leaves it out.
79
+ */
74
80
  export function isWithinBounds(
75
81
  bounds: TMDataGridRangeBounds | null,
76
82
  rowIndex: number,
@@ -105,14 +111,17 @@ export function boundsEdges(
105
111
  bounds: TMDataGridRangeBounds | null,
106
112
  rowIndex: number,
107
113
  columnIndex: number,
114
+ lastColumnIndex: number = columnIndex,
108
115
  ): { top: boolean; bottom: boolean; left: boolean; right: boolean } | null {
109
116
  if (!isWithinBounds(bounds, rowIndex, columnIndex) || bounds === null) {
110
117
  return null;
111
118
  }
119
+ // A cell spanning columns up to `lastColumnIndex` is one box: the
120
+ // rectangle's right edge falling anywhere inside it is drawn on its side.
112
121
  return {
113
122
  top: rowIndex === bounds.top,
114
123
  bottom: rowIndex === bounds.bottom,
115
124
  left: columnIndex === bounds.left,
116
- right: columnIndex === bounds.right,
125
+ right: bounds.right <= lastColumnIndex,
117
126
  };
118
127
  }
@@ -0,0 +1,78 @@
1
+ import { isGeneratedColumn } from "./columnUtils";
2
+
3
+ /** Which pinned block a displayed column sits in. */
4
+ export type TMDataGridColumnSection = "left" | "center" | "right";
5
+
6
+ /**
7
+ * How wide each displayed cell of one row is, in columns.
8
+ *
9
+ * `spans[i]` is `n >= 1` for a cell that renders and covers `n` tracks from
10
+ * column `i` rightwards, and `0` for a cell covered by one to its left, which
11
+ * does not render. The array runs in display order, the order of
12
+ * `orderedColumns`, and its entries always sum to the column count.
13
+ *
14
+ * A span stops short at the first column it may not cover:
15
+ *
16
+ * - a generated lane - the checkbox, tree, details and edit lanes stay
17
+ * where they are whatever a data cell asks for, which is also why
18
+ * `Infinity` reads as "to the last of my columns";
19
+ * - the edge of the cell's pinned block - a pinned cell is sticky at the
20
+ * offset of its own column, and one stretching into the scrolling centre
21
+ * would slide over the cells it is meant to leave.
22
+ *
23
+ * A generated lane never spans, and a request below 1, or one that is not a
24
+ * number, is 1.
25
+ */
26
+ export function resolveColSpans(
27
+ columns: ReadonlyArray<{ id: string; section: TMDataGridColumnSection }>,
28
+ requestedSpan: (columnIndex: number) => number,
29
+ ): Array<number> {
30
+ const spans: Array<number> = [];
31
+ let index = 0;
32
+ while (index < columns.length) {
33
+ const column = columns[index]!;
34
+ const requested = isGeneratedColumn(column.id) ? 1 : requestedSpan(index);
35
+ const wanted =
36
+ typeof requested === "number" && requested >= 1
37
+ ? Math.floor(requested)
38
+ : 1;
39
+ let span = 1;
40
+ while (span < wanted) {
41
+ const next = columns[index + span];
42
+ if (
43
+ next === undefined ||
44
+ next.section !== column.section ||
45
+ isGeneratedColumn(next.id)
46
+ ) {
47
+ break;
48
+ }
49
+ span += 1;
50
+ }
51
+ spans.push(span);
52
+ for (let covered = 1; covered < span; covered += 1) spans.push(0);
53
+ index += span;
54
+ }
55
+ return spans;
56
+ }
57
+
58
+ /**
59
+ * The column a position on a row lands on once spans are applied: itself
60
+ * when its cell renders, otherwise the spanning cell covering it.
61
+ */
62
+ export function spanAnchorIndex(
63
+ spans: ReadonlyArray<number>,
64
+ columnIndex: number,
65
+ ): number {
66
+ let index = columnIndex;
67
+ while (index > 0 && spans[index] === 0) index -= 1;
68
+ return index;
69
+ }
70
+
71
+ /** The last column the cell at `columnIndex` covers - itself when unspanned. */
72
+ export function spanEndIndex(
73
+ spans: ReadonlyArray<number>,
74
+ columnIndex: number,
75
+ ): number {
76
+ const anchor = spanAnchorIndex(spans, columnIndex);
77
+ return anchor + Math.max(1, spans[anchor] ?? 1) - 1;
78
+ }
@@ -160,6 +160,13 @@ export function isColumnReorderable(column: ColumnLike): boolean {
160
160
  return column.parent === undefined;
161
161
  }
162
162
 
163
+ /**
164
+ * The `minSize` of a column that sets none: the floor of its fluid track, of a
165
+ * resize drag and of its pinned width. Passed to TanStack as
166
+ * `defaultColumn.minSize`, which otherwise fills in 20.
167
+ */
168
+ export const DEFAULT_COLUMN_MIN_SIZE = 80;
169
+
163
170
  /** The list length from which a column chooser shows its search box. */
164
171
  export const COLUMN_SEARCH_MIN = 6;
165
172
 
@@ -106,4 +106,12 @@ export type TMDataGridColumnFilterOptions = {
106
106
  * See {@link TMDataGridFilterControlComponent}.
107
107
  */
108
108
  control?: TMDataGridFilterControlComponent;
109
+ /**
110
+ * Whether this column's filter goes into the `persist.urlKey` search param.
111
+ * Defaults to `true`. `false` keeps it out of the param, on read and on
112
+ * write. Only has an effect when `urlKey` carries `columnFilters`, and since such
113
+ * a slice is not stored under `dataKey` either, the filter is then not
114
+ * persisted anywhere.
115
+ */
116
+ url?: boolean;
109
117
  };
@@ -1,10 +1,11 @@
1
1
  import type {
2
2
  ColumnFiltersState,
3
+ FilterMeta,
3
4
  Row,
4
5
  RowData,
5
6
  TableFeatures,
6
7
  } from "@tanstack/react-table";
7
- import type { TMDataGridTable } from "../useTMDataGrid";
8
+ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
8
9
 
9
10
  /**
10
11
  * The value shape stored in `columnFilters` for every TMDataGrid column.
@@ -250,6 +251,19 @@ export function isFilterActive(value: unknown): boolean {
250
251
  : typeof value.value === "string" && value.value.trim() !== "";
251
252
  }
252
253
 
254
+ /**
255
+ * Whether one `columnFilters` entry narrows the rows, by the one rule the grid
256
+ * uses for "active": an envelope counts once {@link isFilterActive} says so,
257
+ * and a value that is not an envelope always counts, since a raw value written
258
+ * by consumer code narrows the rows as far as the grid can tell.
259
+ *
260
+ * @internal Shared by the empty state and the `urlKey` param; not exported
261
+ * from the package.
262
+ */
263
+ export function filterNarrowsRows(entry: { value: unknown }): boolean {
264
+ return !isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value);
265
+ }
266
+
253
267
  /** One column's filter, typed - what `columnFilters` holds per entry. */
254
268
  export type TMDataGridColumnFilter = {
255
269
  id: string;
@@ -474,6 +488,20 @@ export function matchesFilter(
474
488
  }
475
489
  }
476
490
 
491
+ /**
492
+ * The signature of a column's own `filterFn`. The third argument is the
493
+ * `{ operator, value }` envelope the grid writes into `columnFilters`; a
494
+ * column `filterFn` replaces `tmDataGridFilterFn` outright, so it is handed
495
+ * the envelope, not the unwrapped value. Delegate to `tmDataGridFilterFn`
496
+ * for the operators the function does not handle itself.
497
+ */
498
+ export type TMDataGridFilterFn<TData extends RowData> = (
499
+ row: Row<TMDataGridFeatures, TData>,
500
+ columnId: string,
501
+ filterValue: TMDataGridFilterValue,
502
+ addMeta?: (meta: FilterMeta) => void,
503
+ ) => boolean;
504
+
477
505
  /**
478
506
  * The single filter function every TMDataGrid column uses. It dispatches on the
479
507
  * operator carried by the filter value, which is what lets one column offer