@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -17,14 +17,14 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.2'
20
+ library_version: '2.0.0-beta.21'
21
21
  sources:
22
- - 'Jielga/TMDataGrid:src/docs/row-selection.md'
23
- - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
24
- - 'Jielga/TMDataGrid:src/docs/row-styling.md'
25
- - 'Jielga/TMDataGrid:src/docs/row-details.md'
26
- - 'Jielga/TMDataGrid:src/docs/row-pinning.md'
27
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/rowSelection.ts'
22
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-selection.md'
23
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-interaction.md'
24
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-styling.md'
25
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-details.md'
26
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-pinning.md'
27
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/rowSelection.ts'
28
28
  ---
29
29
 
30
30
  # TMDataGrid - Rows
@@ -193,8 +193,8 @@ Returning an empty list leaves the column with no menu button at all.
193
193
  <TMDataGrid.Table<Employee>
194
194
  striped
195
195
  rowStyle={(row) =>
196
- row.original.status === "Terminated"
197
- ? { "--row-bg": "var(--mantine-color-red-0)" }
196
+ !row.getIsGrouped() && row.original.status === "Terminated"
197
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
198
198
  : undefined
199
199
  }
200
200
  rowClassName={(row) => (row.original.overdue ? classes.overdue : undefined)}
@@ -210,7 +210,11 @@ are not striped.
210
210
 
211
211
  Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
212
212
  `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
213
- stylesheet can target any of it without a callback.
213
+ stylesheet can target any of it without a callback. The boolean attributes are
214
+ published on every row as `"true"` or `"false"`, so match the value
215
+ (`[data-grouped="true"]`), not the bare attribute. Pick a `--row-bg` that
216
+ reads under both colour schemes - a `-0` Mantine shade is near-white and
217
+ unreadable in dark mode; mix a mid shade into transparency instead.
214
218
 
215
219
  ## The details panel
216
220
 
@@ -291,7 +295,7 @@ Correct:
291
295
  rowStyle={() => ({ "--row-bg": "pink" })}
292
296
  ```
293
297
 
294
- Source: `src/docs/row-styling.md` (Set `--row-bg`, not `background`).
298
+ Source: `packages/tmdatagrid/docs/row-styling.md` (Set `--row-bg`, not `background`).
295
299
 
296
300
  ### CRITICAL Reading the selection without subscribing
297
301
 
@@ -313,7 +317,7 @@ const selected = useSelector(grid.table.store, () =>
313
317
  );
314
318
  ```
315
319
 
316
- Source: `src/docs/row-selection.md` (Acting on a selection).
320
+ Source: `packages/tmdatagrid/docs/row-selection.md` (Acting on a selection).
317
321
 
318
322
  ### HIGH Looking for the highlight in `rowSelection`
319
323
 
@@ -334,7 +338,7 @@ Correct:
334
338
  const current = useSelector(grid.ui, (state) => state.highlightedRowId);
335
339
  ```
336
340
 
337
- Source: `src/docs/row-selection.md` (The highlight is not a selection).
341
+ Source: `packages/tmdatagrid/docs/row-selection.md` (The highlight is not a selection).
338
342
 
339
343
  ### HIGH Expecting a click handler to replace the built-in behaviour
340
344
 
@@ -349,7 +353,7 @@ Correct, when the click should only navigate:
349
353
  useTMDataGrid({ data, columns, selectionMode: "highlight" });
350
354
  ```
351
355
 
352
- Source: `src/docs/row-interaction.md`.
356
+ Source: `packages/tmdatagrid/docs/row-interaction.md`.
353
357
 
354
358
  ### HIGH Reading `row.getIsExpanded()` in a cell without subscribing
355
359
 
@@ -362,7 +366,7 @@ Correct:
362
366
  const expanded = useSelector(row.table.store, () => row.getIsExpanded());
363
367
  ```
364
368
 
365
- Source: `src/docs/row-details.md` (Opening a row from elsewhere).
369
+ Source: `packages/tmdatagrid/docs/row-details.md` (Opening a row from elsewhere).
366
370
 
367
371
  ### MEDIUM Assuming group rows behave like data rows
368
372
 
@@ -371,7 +375,7 @@ Group rows are built on their first child's record. They do not fire
371
375
  never pin, they take no row number, and they have no details panel. A handler
372
376
  written as though every row reaches it silently skips them.
373
377
 
374
- Source: `src/docs/row-interaction.md`, `src/docs/row-pinning.md`.
378
+ Source: `packages/tmdatagrid/docs/row-interaction.md`, `packages/tmdatagrid/docs/row-pinning.md`.
375
379
 
376
380
  ### MEDIUM Open panels closing when `data` is replaced
377
381
 
@@ -384,7 +388,7 @@ Correct:
384
388
  useTMDataGrid({ data, columns, renderDetails, autoResetExpanded: false });
385
389
  ```
386
390
 
387
- Source: `src/docs/row-details.md`.
391
+ Source: `packages/tmdatagrid/docs/row-details.md`.
388
392
 
389
393
  ### MEDIUM Expecting pinned rows to persist
390
394
 
@@ -392,7 +396,7 @@ Source: `src/docs/row-details.md`.
392
396
  layout store outlives any one data set. A pinned id whose row leaves `data` is
393
397
  not shown, and returns to its edge if the data comes back.
394
398
 
395
- Source: `src/docs/row-pinning.md`.
399
+ Source: `packages/tmdatagrid/docs/row-pinning.md`.
396
400
 
397
401
  ## References
398
402
 
@@ -4,16 +4,22 @@ description: >
4
4
  Drive TMDataGrid from a server with TanStack manual modes - manualPagination,
5
5
  manualSorting, manualFiltering, rowCount, controlled state and onXChange
6
6
  callbacks. Covers the loading and totalRowCount meta fields, forwarding the
7
- plain-JSON columnFilters model to an API with isFilterActive, persistence
8
- interaction, and row selection across pages. Load when the grid is backed by a
9
- paginated API rather than a local array.
7
+ plain-JSON columnFilters model to an API with activeColumnFilters, mapping
8
+ filters, sorting and the page index onto an endpoint's own query language
9
+ (field table, operator table, the three value shapes, meta.filter.operators
10
+ for an endpoint that answers only some operators, keying the fetch on the
11
+ request, paging against a page envelope), the first-page reset on a query
12
+ change, persistence interaction, and row selection across pages. Load when the
13
+ grid is backed by a paginated API rather than a local array, or when
14
+ translating grid filters into server-side queries.
10
15
  metadata:
11
16
  type: core
12
17
  library: '@jielga/tmdatagrid'
13
- library_version: '2.0.0-beta.2'
18
+ library_version: '2.0.0-beta.21'
14
19
  sources:
15
- - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
- - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
20
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-side.md'
21
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-query.md'
22
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
17
23
  ---
18
24
 
19
25
  # TMDataGrid - Server-side data
@@ -68,12 +74,32 @@ const grid = useTMDataGrid({
68
74
  | --- | --- | --- |
69
75
  | Rendered rows | The current page, sliced locally | The rows returned by the server |
70
76
  | Footer total | Pre-paginated row count | `options.rowCount` |
71
- | `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount` |
77
+ | `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount`; the count alone without it |
72
78
  | Loading state | Not applicable | `meta.loading` |
73
79
 
74
80
  Column menus, the filter panel and the column manager behave identically in both
75
81
  modes.
76
82
 
83
+ ## The page index
84
+
85
+ Under `manualPagination` the grid resets `pageIndex` to 0 whenever the query
86
+ changes - a column filter, the quick search or the sort - so the next request
87
+ never asks for page 8 of a result set that now has three. The reset lands in
88
+ the same event as the change, so one request goes out. Pass the plain setters
89
+ as the change callbacks; do not pair them with a page reset of your own.
90
+
91
+ `resetPageOnQueryChange: false` switches it off. TanStack's
92
+ `autoResetPageIndex` is a different rule: it defaults to `!manualPagination`
93
+ and fires on a change to `data`, which server-side is the response landing.
94
+
95
+ ## Column options
96
+
97
+ `meta.options: "faceted"` reads the distinct values in `data`, which
98
+ server-side is one page of them - the dropdown then offers whatever was on the
99
+ page the user is looking at. Declare the set as a list or a function instead.
100
+ The grid warns once per column when a faceted column resolves under
101
+ `manualFiltering` or `manualPagination`.
102
+
77
103
  ## Sending filters
78
104
 
79
105
  Filter values are plain JSON, so `columnFilters` forwards without transformation:
@@ -85,17 +111,136 @@ Filter values are plain JSON, so `columnFilters` forwards without transformation
85
111
  ]
86
112
  ```
87
113
 
88
- Translate at the API boundary, and skip entries whose value is still empty -
89
- those match all rows:
114
+ Translate at the API boundary. `activeColumnFilters` hands back the entries
115
+ that narrow anything, typed - `ColumnFiltersState` types `value` as `unknown`,
116
+ and an entry whose value is still empty matches all rows:
90
117
 
91
118
  ```ts
92
- import { isFilterActive } from "@jielga/tmdatagrid";
119
+ import { activeColumnFilters } from "@jielga/tmdatagrid";
93
120
 
94
- const active = columnFilters.filter((filter) => isFilterActive(filter.value));
121
+ const active = activeColumnFilters(columnFilters);
122
+ // [{ id: "lastName", value: { operator: "contains", value: "holm" } }]
95
123
  ```
96
124
 
125
+ It takes the `columnFilters` array, or the table where the grid owns the slice.
126
+ `isFilterActive(value)` is the single-value test it is built on. Only the
127
+ grid's own `{ operator, value }` shape is read: an entry holding some other
128
+ value - a custom filter control writing raw values - is dropped.
129
+
97
130
  Debounce requests. The filter value input updates on every keystroke.
98
131
 
132
+ ## Mapping onto the endpoint's query language
133
+
134
+ An API takes a request body of its own: its own field names, its own operator
135
+ set, its own status codes, and pages counted from 1. The layer between the
136
+ grid's state and that body is one function over two lookup tables, plus one
137
+ function on the way back:
138
+
139
+ - **A field table** keyed by column id, giving the API field and the cast from
140
+ the string every filter control writes to the type the field holds
141
+ (`Number`, an enum code). A column missing from the table is one the API
142
+ cannot query: the mapping drops the filter rather than sending a field the
143
+ endpoint would reject.
144
+ - **An operator table** from `TMDataGridFilterOperator` to the API's operators.
145
+ Several grid operators collapse onto one - a date `before` and a number
146
+ `lessThan` are both `lt` once the value is cast. Declare it as a `Record`, not
147
+ a `Partial`, so an operator added by a later grid version fails the build
148
+ here rather than reaching the server unmapped.
149
+ - **`toRow`** from the API's record to the grid's row type.
150
+
151
+ ```ts
152
+ const QUERY_FIELDS: Record<string, { field: string; cast: (raw: string) => string | number }> = {
153
+ id: { field: "orderRef", cast: Number },
154
+ amount: { field: "totalAmount", cast: Number },
155
+ status: { field: "status", cast: (raw) => STATUS_CODES[raw] ?? raw },
156
+ };
157
+
158
+ const PREDICATE_OPS: Record<TMDataGridFilterOperator, PredicateOp> = {
159
+ contains: "like",
160
+ between: "range",
161
+ before: "lt",
162
+ lessThan: "lt",
163
+ isAnyOf: "in",
164
+ isEmpty: "isNull",
165
+ // ...one line for every remaining operator.
166
+ };
167
+ ```
168
+
169
+ ### The three value shapes
170
+
171
+ `TMDataGridFilterValue` is `{ operator, value }`, and the operator decides what
172
+ `value` holds. Branch on all four cases, in this order:
173
+
174
+ | Operator | `value` | Sent as |
175
+ | --- | --- | --- |
176
+ | `isEmpty`, `isNotEmpty` | Not used | `{ field, op }` |
177
+ | `isAnyOf`, `isNoneOf` | `ReadonlyArray<string>` | `{ field, op, values }` |
178
+ | `between` | `[min, max]`, either end possibly `""` | `{ field, op, from?, to? }` |
179
+ | Everything else | `string` | `{ field, op, value }` |
180
+
181
+ An empty end of a `between` pair leaves that side open: an absent bound, not an
182
+ empty string. Run `activeColumnFilters` over the slice first so a half-typed
183
+ filter is not sent as a predicate that narrows the result to nothing.
184
+
185
+ ### An endpoint that answers only some operators
186
+
187
+ Most endpoints do not have every operator the grid has - `like` and `eq` but no
188
+ prefix match is common. Do not offer what you would have to drop.
189
+ `meta.filter.operators` narrows the column to the operators the query can
190
+ express, and the mapping table is declared over exactly that list, so one
191
+ cannot be offered without a mapping or mapped without being offered:
192
+
193
+ ```ts
194
+ const TEXT_OPERATORS = [
195
+ "contains",
196
+ "equals",
197
+ "isEmpty",
198
+ "isNotEmpty",
199
+ ] as const satisfies readonly TMDataGridFilterOperator[];
200
+
201
+ const TEXT_OPS: Record<(typeof TEXT_OPERATORS)[number], PredicateOp> = {
202
+ contains: "like",
203
+ equals: "eq",
204
+ isEmpty: "isNull",
205
+ isNotEmpty: "isNotNull",
206
+ };
207
+
208
+ columnHelper.accessor("customer", {
209
+ header: "Customer",
210
+ meta: { filter: { operators: TEXT_OPERATORS } },
211
+ });
212
+ ```
213
+
214
+ A fresh filter opens on `meta.filter.defaultOperator` when set, else on the
215
+ type's default when the list holds it, else on the first entry. The lookup at
216
+ the boundary still returns `undefined` for an unmapped operator: a filter
217
+ restored by `persist` from before the list was narrowed can carry one.
218
+
219
+ ### Keying the fetch on the request
220
+
221
+ Serialize the request with `JSON.stringify` inside `useMemo` over
222
+ `columnFilters`, `sorting` and `pagination`, and key the fetch effect (or the
223
+ TanStack Query `queryKey`) on that string. Opening the panel and adding an
224
+ empty row moves `columnFilters` but leaves the request unchanged, so nothing is
225
+ sent. The effect owes the server a debounce (the value input updates on every
226
+ keystroke) and a cancel (a `cancelled` flag in the cleanup, or an
227
+ `AbortController` on a real `fetch`).
228
+
229
+ ### Paging against a page envelope
230
+
231
+ | The API's | The grid's | Written as |
232
+ | --- | --- | --- |
233
+ | `page.number`, counted from 1 | `pagination.pageIndex`, counted from 0 | `number: pageIndex + 1` |
234
+ | `page.totalItems`, the matched count | `rowCount` | `rowCount: page?.totalItems ?? 0` |
235
+ | `page.totalPages` | `state.pageCount`, derived from `rowCount / pageSize` | Nothing; the grid computes it |
236
+
237
+ Forward `totalPages` only when the server pages by something other than the
238
+ size the grid asked for. `pageCount: -1` when the total is unknown. Show the
239
+ page number through the Footer's `renderPagination` slot with
240
+ `<Controls.PageSize /><Controls.PageNumber /><Controls.Pager />`.
241
+ `meta.totalRowCount` is the unfiltered total, which no filtered response
242
+ carries: take it from a separate count call.
243
+
99
244
  ## Persistence
100
245
 
101
246
  `persist` works unchanged. `dataKey` restores filters, sorting and pagination
@@ -126,18 +271,26 @@ Without `rowCount` the table derives the total from the rows it was handed -
126
271
  one page - so `getPageCount()` returns 1. The footer shows "1–25 of 25" and the
127
272
  next-page button is disabled, with no error. Pass the server total.
128
273
 
129
- ### Filters sent without isFilterActive
274
+ ### Filters sent without activeColumnFilters
130
275
 
131
276
  An empty filter value stays in `columnFilters` while the user is still typing.
132
277
  Forwarded verbatim it becomes `operator: "contains", value: ""` at the API,
133
- which most backends translate into a real predicate. Filter with
134
- `isFilterActive` first.
278
+ which most backends translate into a real predicate. Map over
279
+ `activeColumnFilters(columnFilters)` instead of over the slice.
135
280
 
136
281
  ### SummaryCount without meta.totalRowCount
137
282
 
138
- `SummaryCount` falls back to the pre-filtered row count, which under manual
139
- filtering is just the current page. It reads "25 of 25" regardless of how many
140
- rows the server holds. Pass the unfiltered total as `meta.totalRowCount`.
283
+ Without it there is no denominator to show: the pre-filtered row count is the
284
+ current page, so the grid renders the matched count alone rather than a
285
+ plausible-looking wrong total. Pass the unfiltered total as
286
+ `meta.totalRowCount` to get the "42 / 5000" form back.
287
+
288
+ ### Offering operators the endpoint cannot answer
289
+
290
+ The panel offers every operator of the column's type, and a mapping that drops
291
+ `startsWith` leaves the user with a filter that silently does nothing. Declare
292
+ `meta.filter.operators` on the column with the operators the endpoint answers,
293
+ and type the operator table over that same list.
141
294
 
142
295
  ### Unstable getRowId across pages
143
296
 
@@ -11,11 +11,11 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.2'
14
+ library_version: '2.0.0-beta.21'
15
15
  sources:
16
- - 'Jielga/TMDataGrid:src/docs/testing.md'
17
- - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
18
- - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGridTable.tsx'
16
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
17
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGrid.tsx'
18
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridTable.tsx'
19
19
  ---
20
20
 
21
21
  # TMDataGrid - Testing
@@ -62,8 +62,9 @@ Body cells carry no `data-dg-part` - the coordinate pair already names them.
62
62
 
63
63
  **Whole-grid** (unique, no coordinate needed): `toolbar`, `summary-count`,
64
64
  `loading`, `search`, `search-clear`, `filter-button`, `filter-panel`,
65
- `filter-panel-close`, `filter-add`, `filter-clear-all`, `filter-pills`,
66
- `columns-button`, `columns-panel`, `columns-search`, `columns-toggle-all`,
65
+ `filter-popup`, `filter-sidebar`, `filter-panel-close`, `filter-add`,
66
+ `filter-clear-all`, `filter-pills`, `header-filter-row`,
67
+ `menu-button`, `columns-panel`, `columns-search`, `columns-toggle-all`,
67
68
  `columns-reset`, `footer`, `page-size`, `page-range`, `page-prev`, `page-next`,
68
69
  `summary-row`, `pinned-top`, `pinned-bottom`, `select-all`,
69
70
  `details-toggle-all`, `save-all`, `discard-all`, `editor-confirm`,
@@ -75,7 +76,9 @@ Body cells carry no `data-dg-part` - the coordinate pair already names them.
75
76
  `discard-new-row`.
76
77
 
77
78
  **Keyed by `data-column-id`**: `header`, `header-sort`, `header-menu`,
78
- `header-filter`, `filter-row`, `filter-pill`, `columns-toggle`.
79
+ `header-filter` (absent under `filters.inHeader`), `header-filter-cell`,
80
+ `header-filter-operator`, `filter-row`,
81
+ `filter-pill`, `columns-toggle`.
79
82
 
80
83
  **Keyed by both**: `editor`.
81
84
 
@@ -32,27 +32,15 @@ export function useTMDataGridContext(): TMDataGridContextValue {
32
32
  }
33
33
 
34
34
  /**
35
- * What a control inside a *body* cell should put in its `tabIndex`.
35
+ * What a control inside a body cell puts in its `tabIndex`. Internal: a
36
+ * consumer's own control needs nothing, the tab guards keep the body one stop.
36
37
  *
37
- * `-1` once cell selection is on, and this is what makes the promise of one tab
38
- * stop true. Without it the browser walks Tab into the checkbox of every
39
- * mounted row - a grid showing twenty rows would be twenty tab stops, and
40
- * scrolling would change how many. Enter or F2 steps into the cell instead,
41
- * which reaches a `-1` control perfectly well.
42
- *
43
- * Header controls are not covered: the header row is not part of cell
44
- * navigation, so its sort buttons and menus stay in the tab order, where they
45
- * are the only way to reach them.
46
- *
47
- * A custom cell renderer with a control in it wants the same:
48
- *
49
- * ```tsx
50
- * cell: ({ row }) => (
51
- * <Button tabIndex={useCellControlTabIndex()} onClick={...}>Open</Button>
52
- * )
53
- * ```
38
+ * `-1` under cell selection: the control is reached by stepping into the cell
39
+ * with Enter or by the Tab walk within the row, never by the page's tab order,
40
+ * so a row does not add one tab stop per mounted row. `0` without it - there
41
+ * is no cursor to step in from, so the page's tab order is the only route.
54
42
  */
55
- export function useCellControlTabIndex(): 0 | -1 {
43
+ export function useBodyControlTabIndex(): 0 | -1 {
56
44
  const { features } = useTMDataGridContext();
57
45
  return features.cellSelection ? -1 : 0;
58
46
  }
@@ -82,8 +82,14 @@
82
82
  /* The stacking ladder, stated once. Body cells sit at auto (0); everything
83
83
  sticky stacks over them in this order, and a sticky row crossing a sticky
84
84
  column takes the row's slot plus one. */
85
+ /* The focus ring is drawn inside the focused cell's own box, so the cell
86
+ takes a slot of its own - above the plain cells beside it, but under the
87
+ pinned lanes, which a scrolling row has to pass beneath ring and all. A
88
+ focused cell that is itself pinned goes above them instead, so its ring
89
+ stays whole while the rest of the row slides under it. */
90
+ --dg-z-focused-cell: 1;
85
91
  --dg-z-pinned-cell: 2;
86
- --dg-z-focused-cell: 3;
92
+ --dg-z-pinned-focused-cell: 3;
87
93
  /* The pinned-row slot serves both sticky rows: the summary along the
88
94
  bottom and the entry block under the header. */
89
95
  --dg-z-pinned-row: 4;
@@ -3,20 +3,21 @@ import { type CSSProperties, type ReactNode, useMemo } from "react";
3
3
  import classes from "./TMDataGrid.module.css";
4
4
  import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
5
5
  import { TMDataGridContextProvider } from "../TMDataGridContext";
6
- import { TMDataGridEditActions } from "./TMDataGridEditActions";
6
+ import { TMDataGridDraftActions } from "./TMDataGridDraftActions";
7
7
  import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
8
8
  import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
9
9
  import { TMDataGridFooter } from "./TMDataGridFooter";
10
+ import { TMDataGridMenu } from "./TMDataGridMenu";
10
11
  import { TMDataGridSearch } from "./TMDataGridSearch";
11
12
  import { TMDataGridTable } from "./TMDataGridTable";
12
13
  import {
13
- TMDataGridColumnsButton,
14
14
  TMDataGridFilterButton,
15
15
  TMDataGridLoadingIndicator,
16
16
  TMDataGridSummaryCount,
17
17
  TMDataGridToolbar,
18
18
  TMDataGridToolbarSpacer,
19
19
  } from "./TMDataGridToolbar";
20
+ import { TMDataGridExportPicker } from "./TMDataGridExportPicker";
20
21
  import {
21
22
  DEFAULT_TMDATAGRID_SIZE,
22
23
  SIZE_CONTROL_SIZE,
@@ -55,29 +56,14 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
55
56
  "data-testid"?: string;
56
57
  };
57
58
 
58
- /**
59
- * Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
60
- * and publishes it to the compound components below it:
61
- *
62
- * ```tsx
63
- * const grid = useTMDataGrid({ data, columns });
64
- *
65
- * <TMDataGrid {...grid}>
66
- * <TMDataGrid.Toolbar>
67
- * <TMDataGrid.SummaryCount />
68
- * <TMDataGrid.Spacer />
69
- * <TMDataGrid.ColumnsButton />
70
- * </TMDataGrid.Toolbar>
71
- * <TMDataGrid.Table />
72
- * <TMDataGrid.Footer />
73
- * </TMDataGrid>
74
- * ```
75
- */
59
+ // Documented on the `TMDataGrid` export below.
76
60
  function TMDataGridRoot<TData extends RowData>({
77
61
  table,
78
62
  ui,
79
63
  edit,
80
64
  features,
65
+ filters,
66
+ exportOptions,
81
67
  labels,
82
68
  renderDetails,
83
69
  renderDetailsEstHeight,
@@ -100,6 +86,8 @@ function TMDataGridRoot<TData extends RowData>({
100
86
  ui,
101
87
  edit,
102
88
  features,
89
+ filters,
90
+ exportOptions,
103
91
  labels,
104
92
  renderDetails,
105
93
  renderDetailsEstHeight,
@@ -116,6 +104,8 @@ function TMDataGridRoot<TData extends RowData>({
116
104
  ui,
117
105
  edit,
118
106
  features,
107
+ filters,
108
+ exportOptions,
119
109
  labels,
120
110
  renderDetails,
121
111
  renderDetailsEstHeight,
@@ -141,18 +131,41 @@ function TMDataGridRoot<TData extends RowData>({
141
131
  >
142
132
  {children}
143
133
  </div>
134
+ {/* Portaled by Mantine, so outside the root element; inside the
135
+ provider, since it reads the grid. */}
136
+ <TMDataGridExportPicker />
144
137
  </TMDataGridContextProvider>
145
138
  );
146
139
  }
147
140
 
141
+ /**
142
+ * Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
143
+ * and publishes it to the compound components below it:
144
+ *
145
+ * ```tsx
146
+ * const grid = useTMDataGrid({ data, columns });
147
+ *
148
+ * <TMDataGrid {...grid}>
149
+ * <TMDataGrid.Toolbar>
150
+ * <TMDataGrid.SummaryCount />
151
+ * <TMDataGrid.Spacer />
152
+ * <TMDataGrid.Menu>
153
+ * <TMDataGrid.Menu.Columns />
154
+ * </TMDataGrid.Menu>
155
+ * </TMDataGrid.Toolbar>
156
+ * <TMDataGrid.Table />
157
+ * <TMDataGrid.Footer />
158
+ * </TMDataGrid>
159
+ * ```
160
+ */
148
161
  export const TMDataGrid = Object.assign(TMDataGridRoot, {
149
162
  Toolbar: TMDataGridToolbar,
150
163
  Spacer: TMDataGridToolbarSpacer,
151
164
  SummaryCount: TMDataGridSummaryCount,
152
165
  LoadingIndicator: TMDataGridLoadingIndicator,
153
166
  Search: TMDataGridSearch,
154
- EditActions: TMDataGridEditActions,
155
- ColumnsButton: TMDataGridColumnsButton,
167
+ DraftActions: TMDataGridDraftActions,
168
+ Menu: TMDataGridMenu,
156
169
  FilterButton: TMDataGridFilterButton,
157
170
  Table: TMDataGridTable,
158
171
  Footer: TMDataGridFooter,
@@ -163,6 +176,9 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
163
176
  * rendered outside `<TMDataGrid>` - a page header, for instance.
164
177
  */
165
178
  FilterPills: TMDataGridFilterPills,
166
- /** Rendered by `TMDataGrid.ColumnsButton`; exported for custom layouts. */
179
+ /**
180
+ * The column chooser as plain controls, for a Popover, a Drawer or an
181
+ * inline layout; `TMDataGrid.Menu.Columns` is the same thing as menu items.
182
+ */
167
183
  ColumnsPanel: TMDataGridColumnsPanel,
168
184
  });