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

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/docs/grouping.md CHANGED
@@ -56,7 +56,7 @@ grouped columns to the front. Note that the kept column's data cells render as
56
56
  TanStack's grouped-cell placeholder - blank - with the value only on group
57
57
  rows, so it repeats what the tree lane already shows and cannot be typed into.
58
58
  To set the grouped field on a new row, seed it through `edit.addRow(values)` -
59
- see [Editing](/docs/editing#adding-and-deleting-rows).
59
+ see [Editing](/docs/editing#adding-rows).
60
60
 
61
61
  ## Aggregation
62
62
 
@@ -114,10 +114,10 @@ so it does not fire `onRowClick`, cannot be highlighted, cannot be
114
114
  `rowStyle` and `rowClassName` are the exception: they are called for group rows too, with that same child's record as `original`.
115
115
  Guard a callback that reads `original` with `row.getIsGrouped()`, and set `--dg-row-group-bg` to colour the group rows themselves.
116
116
 
117
- `data-grouped` is published on every row, `"true"` on group rows and `"false"`
118
- on the rest, so match the value - `[data-grouped="true"]` - rather than the
119
- bare attribute. `data-depth` carries the nesting level, and
120
- `--dg-row-group-bg` sets a group row's background.
117
+ `data-grouped` is present, with the value `"true"`, only on group rows, so
118
+ `[data-grouped]` and `[data-grouped="true"]` are equivalent. `data-depth`
119
+ carries the nesting level, and `--dg-row-group-bg` sets a group row's
120
+ background.
121
121
 
122
122
  ## Grouping and pagination
123
123
 
@@ -161,5 +161,5 @@ server-side can set `enableGrouping: true` alongside `manualGrouping: true`. See
161
161
  | `getGroupDataRows` | Export | `(row) => Row[]` | – | Every record under a group row, at any depth. |
162
162
  | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
163
163
  | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
164
- | `data-grouped` | Data attribute | `"true" \| "false"` | – | `"true"` on group rows. Published on every row. |
164
+ | `data-grouped` | Data attribute | `"true"` | – | `"true"` on group rows. Absent on the rest. |
165
165
  | `data-depth` | Data attribute | `number` | – | Nesting level, on every row. |
@@ -105,13 +105,13 @@ it without a callback:
105
105
  | `data-selected` | Selected rows |
106
106
  | `data-selected-bg` | Selected rows that also take the background |
107
107
  | `data-highlighted` | The highlighted row |
108
- | `data-grouped` | Every row: `"true"` on group rows, `"false"` on the rest |
108
+ | `data-grouped` | Group rows |
109
109
  | `data-depth` | Every row. The nesting level |
110
110
  | `data-context-menu` | The row whose context menu is open |
111
111
  | `data-row-id` | Every row. Its id, which [tests](/docs/testing) key off |
112
112
 
113
- The state attributes are published as `"true"` or `"false"`, so match the
114
- value; the bare attribute selector matches every row:
113
+ The state attributes are present, with the value `"true"`, only on the rows
114
+ they apply to, so the bare attribute and the value match the same rows:
115
115
 
116
116
  ```css
117
117
  [data-dg-part="row"][data-grouped="true"] {
package/docs/styling.md CHANGED
@@ -50,7 +50,7 @@ The wrapper components - `Toolbar`, `Spacer`, `Footer`, `FilterPanel`, `FilterPi
50
50
  | `--dg-row-height` | From `size` | Row height. Prefer `meta.rowHeight` - the virtualizer needs the number. |
51
51
  | `--dg-header-height` | From `size` | Header row height |
52
52
  | `--dg-summary-height` | From `size` | [Summary row](/docs/summary-row) height |
53
- | `--dg-entry-height` | From `size` | The sticky [entry block](/docs/editing#adding-and-deleting-rows) |
53
+ | `--dg-entry-height` | From `size` | The sticky [entry block](/docs/editing#adding-rows) |
54
54
  | `--dg-font-size` | From `size` | Cell and header font size |
55
55
  | `--dg-padding` | From `size` | Horizontal cell padding. The generated lanes are excluded: they are fixed 36px tracks that centre their control. |
56
56
  | `--dg-radius` | `--mantine-radius-md` | The frame's corner radius. `0` squares the grid off. The root clips its overflow, so the header and the last row follow it. |
package/docs/testing.md CHANGED
@@ -62,6 +62,17 @@ const cell = orders.locator('[data-row-id="42"][data-column-id="total"]');
62
62
 
63
63
  Body cells carry no `data-dg-part`; the coordinate pair identifies them.
64
64
 
65
+ **State attributes are present only while they apply.** A boolean attribute
66
+ such as `data-selected`, `data-new` or `data-invalid` is rendered with the
67
+ value `"true"` while its state holds and omitted otherwise, so the bare
68
+ attribute and the value match the same elements, and the negative is the
69
+ absence:
70
+
71
+ ```ts
72
+ const drafts = orders.locator('[data-dg-part="row"][data-draft]');
73
+ await expect(row).not.toHaveAttribute("data-deleted");
74
+ ```
75
+
65
76
  ## Parts
66
77
 
67
78
  Row and column ids come from your data (`getRowId` and the column definitions),
@@ -264,6 +275,49 @@ export class DataGrid {
264
275
  await this.part("columns-toggle", { columnId }).click();
265
276
  }
266
277
 
278
+ /** The entry row opened by `edit.addRow()`; `data-row-id` is its temp id. */
279
+ entryRow(): Locator {
280
+ return this.part("entry-row");
281
+ }
282
+
283
+ /** Types into the built-in editors of an open row: an entry row, or a row
284
+ * opened in row mode. */
285
+ async fillRow(rowId: string, values: Record<string, string>): Promise<void> {
286
+ for (const [columnId, value] of Object.entries(values)) {
287
+ await this.part("editor", { rowId, columnId })
288
+ .locator('[data-dg-part="editor-input"]')
289
+ .fill(value);
290
+ }
291
+ }
292
+
293
+ async commitEntryRow(rowId: string): Promise<void> {
294
+ await this.part("confirm-new-row", { rowId }).click();
295
+ }
296
+
297
+ /** Presses Save in `TMDataGrid.DraftActions` and waits for the store to empty. */
298
+ async saveDrafts(): Promise<void> {
299
+ await this.part("save-all").click();
300
+ await expect(this.part("save-all")).toHaveAttribute("data-draft-count", "0");
301
+ }
302
+
303
+ /** Finds a row the test added by a value unique to it, since the grid does
304
+ * not know the id the app gave the row. Retries until the app has put the
305
+ * row in `data`. */
306
+ async expectRowAdded(
307
+ uniqueValue: string,
308
+ cells: Record<string, string>,
309
+ ): Promise<void> {
310
+ await this.search(uniqueValue);
311
+ await this.expectRowCount(1);
312
+ const row = this.part("row");
313
+ for (const [columnId, text] of Object.entries(cells)) {
314
+ await expect(row.locator(`[data-column-id="${columnId}"]`)).toHaveText(
315
+ text,
316
+ );
317
+ }
318
+ await this.search("");
319
+ }
320
+
267
321
  async expectRowCount(count: number): Promise<void> {
268
322
  await expect(this.grid).toHaveAttribute(
269
323
  "data-dg-row-count",
@@ -291,6 +345,133 @@ test("filters to one employee", async ({ page }) => {
291
345
  });
292
346
  ```
293
347
 
348
+ ## Editing
349
+
350
+ ### Adding a row
351
+
352
+ [`edit.addRow()`](/docs/editing) opens an entry row, `data-dg-part="entry-row"`,
353
+ keyed by a temporary id (`__new__1`, `__new__2`, …). Its editors are `editor`
354
+ parts keyed by that id and the column. What ✓ does with the row depends on
355
+ `editing.draft`:
356
+
357
+ | Step | Without `draft` | With `draft: true` |
358
+ | --- | --- | --- |
359
+ | `addRow()` | `entry-row[data-row-id="__new__1"]` | Same |
360
+ | ✓ fails validation | The entry row stays; the failing cells carry `data-invalid` | Same |
361
+ | ✓ (`confirm-new-row`) | The entry row is removed and the temp id is gone. `onRowAdd` receives the values, and the row exists again only once your app puts it in `data`, under the id your `getRowId` returns | The row becomes a body row, still keyed by the temp id: `row[data-row-id="__new__1"][data-new="true"][data-draft="true"]`, with `row-state[data-state="new"]` in its lane and `save-all[data-draft-count]` counting it |
362
+ | Save (`save-all`) | - | As ✓ without `draft`: the temp id is gone, and the row comes back through `data` under your id |
363
+
364
+ The grid never learns which id your app gave the row, so once `onRowAdd` or
365
+ `saveDrafts` has run, the row cannot be addressed by id. Find it by its content
366
+ instead: narrow the grid to a value unique to the row, assert that one row is
367
+ left, and read that row's cells. The row-count assertion is also the wait,
368
+ since it retries until the app has put the row in `data`:
369
+
370
+ ```ts
371
+ test("adds an employee", async ({ page }) => {
372
+ const grid = new DataGrid(page, "employees");
373
+ await page.goto("/employees");
374
+ await grid.expectSettled();
375
+
376
+ // The app's own button, calling edit.addRow()
377
+ await page.getByRole("button", { name: "Add row" }).click();
378
+ const tempId = (await grid.entryRow().getAttribute("data-row-id"))!;
379
+ await grid.fillRow(tempId, { name: "Nordkvist-4711", city: "Stockholm" });
380
+ await grid.commitEntryRow(tempId);
381
+
382
+ await expect(grid.entryRow()).toHaveCount(0);
383
+ await grid.expectRowAdded("Nordkvist-4711", {
384
+ name: "Nordkvist-4711",
385
+ city: "Stockholm",
386
+ });
387
+ });
388
+ ```
389
+
390
+ Use a value no other row has, such as a name with a run id in it, so that the
391
+ search leaves one row. If the grid has no quick search, narrow with `filterBy`
392
+ on a column instead.
393
+
394
+ Under `draft: true` the temp id stays valid until Save, so the committed row is
395
+ asserted directly, and the content-based check applies after the save:
396
+
397
+ ```ts
398
+ await grid.commitEntryRow(tempId);
399
+ const row = grid.part("row", { rowId: tempId });
400
+ await expect(row).toHaveAttribute("data-new", "true");
401
+ await expect(grid.part("row-state", { rowId: tempId })).toHaveAttribute(
402
+ "data-state",
403
+ "new",
404
+ );
405
+ await expect(grid.part("save-all")).toHaveAttribute("data-draft-count", "1");
406
+
407
+ await grid.saveDrafts();
408
+ await expect(row).toHaveCount(0);
409
+ await grid.expectRowAdded("Nordkvist-4711", { city: "Stockholm" });
410
+ ```
411
+
412
+ Under `newRowsSticky: true` the committed row stays in the entry block instead,
413
+ as `entry-row[data-row-id="__new__1"][data-committed="true"]`, outside
414
+ `data-dg-row-count`, until the save.
415
+
416
+ A ✓ that fails validation keeps the entry row open, with `data-invalid` on the
417
+ failing cells:
418
+
419
+ ```ts
420
+ await grid.commitEntryRow(tempId);
421
+ await expect(grid.entryRow()).toHaveCount(1);
422
+ await expect(
423
+ grid.entryRow().locator('[data-column-id="email"]'),
424
+ ).toHaveAttribute("data-invalid", "true");
425
+ ```
426
+
427
+ ### Changing a row
428
+
429
+ Without `draft`, a commit goes to `onCommit` and the grid keeps nothing of it, so
430
+ the assertion is the cell's text once your app has applied the change. Under
431
+ `draft: true` the change stays in the grid until Save, marked on the cell, the
432
+ row and the lane:
433
+
434
+ ```ts
435
+ const cell = grid.cell({ rowId: "42", columnId: "salary" });
436
+ await cell.dblclick();
437
+ await grid.fillRow("42", { salary: "52000" });
438
+ await page.keyboard.press("Enter");
439
+
440
+ // Without draft: the app applied it
441
+ await expect(cell).toHaveText("52 000");
442
+
443
+ // Under draft: the grid holds it
444
+ await expect(cell).toHaveAttribute("data-dirty", "true");
445
+ await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-dirty", "true");
446
+ await expect(grid.part("row-state", { rowId: "42" })).toHaveAttribute(
447
+ "data-state",
448
+ "edited",
449
+ );
450
+ ```
451
+
452
+ In row mode, `edit-row` opens the row, `save-row` commits it and `cancel-row`
453
+ discards it; `fillRow` reaches the open row's editors the same way.
454
+
455
+ ### Deleting a row
456
+
457
+ `delete-row` calls `onRowDelete` at once, or marks the row under `draft: true`:
458
+
459
+ ```ts
460
+ const before = Number(await grid.grid.getAttribute("data-dg-row-count"));
461
+ await grid.part("delete-row", { rowId: "42" }).click();
462
+
463
+ // Without draft: the app removed it from data
464
+ await grid.expectRowCount(before - 1);
465
+
466
+ // Under draft: marked until Save, and Restore undoes the mark
467
+ await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-deleted", "true");
468
+ await grid.part("restore-row", { rowId: "42" }).click();
469
+ ```
470
+
471
+ Assert a deletion on `data-dg-row-count`, not on the row's element: a row
472
+ outside the viewport has no element either, so `toHaveCount(0)` passes for it
473
+ whether or not it was deleted.
474
+
294
475
  ## React Testing Library
295
476
 
296
477
  Under jsdom there is no layout, so the virtualizer mounts a handful of rows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "2.0.0-beta.21",
3
+ "version": "2.0.0-beta.23",
4
4
  "description": "A React data grid built on TanStack Table v9 and Mantine - always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.21'
21
+ library_version: '2.0.0-beta.23'
22
22
  sources:
23
23
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/styling.md'
24
24
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/toolbar.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.21'
17
+ library_version: '2.0.0-beta.23'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/cell-selection.md'
20
20
  - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/cellNavigation.ts'
@@ -16,7 +16,7 @@ description: >
16
16
  metadata:
17
17
  type: core
18
18
  library: '@jielga/tmdatagrid'
19
- library_version: '2.0.0-beta.21'
19
+ library_version: '2.0.0-beta.23'
20
20
  sources:
21
21
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/columns.md'
22
22
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/column-layout.md'
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.21'
21
+ library_version: '2.0.0-beta.23'
22
22
  sources:
23
23
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/pagination.md'
24
24
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/scrolling.md'
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.21'
21
+ library_version: '2.0.0-beta.23'
22
22
  sources:
23
23
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
24
24
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/query-builder.md'
@@ -385,8 +385,9 @@ row it reaches.
385
385
  `deleteRow`, `getForm`, and `store` for `useSelector` (an example is under
386
386
  [Submitting an outer form](#high-submitting-an-outer-form-while-the-grid-holds-a-draft)).
387
387
  `edit.store` publishes each open or committed row's drafted values as
388
- `rows[rowId].values`, which is what a computed cell or a cross-row check reads
389
- - `useTMDataGridContext()` reaches the engine from inside a cell renderer.
388
+ `rows[rowId].values`, which is what a cross-row check reads. A `cell`
389
+ renderer needs no lookup: its `row.original` is already the row as shown, and
390
+ `getRowValues(rowId)` is the same row for a handler with no cell context.
390
391
  Every member with its signature, the gates, `isColumnEditable`, `deactivate`,
391
392
  and the `edit.store` shape are in
392
393
  [references/editing-api.md](references/editing-api.md#the-edit-engine).
@@ -187,24 +187,23 @@ A refused value leaves the row open carrying its errors and the call resolves `f
187
187
 
188
188
  Source: `packages/tmdatagrid/src/core/editEngine.ts` (`begin`, `writeFields`).
189
189
 
190
- ## MEDIUM A computed column frozen while a row is edited
190
+ ## MEDIUM Reading an open row's values off `row.original` outside a cell
191
191
 
192
- A held draft is displayed by the column that owns the field. A column computed
193
- from other fields - `accessorFn` or `display` - reads `row.original`, which is
194
- `data`, so it keeps showing the saved record while the values it derives from
195
- are being typed.
192
+ Inside a `cell` renderer, `row.original` and `getValue()` are the row as
193
+ shown: the open form's values while the row is edited, the committed draft
194
+ after ✓, and `data` otherwise - a computed column follows the draft as it is
195
+ typed, and a button in a cell sends the draft the user sees. Outside it, the
196
+ row callbacks (`onRowClick`, `renderRowContextMenu`, `rowClassName`) and any
197
+ code holding a row id see `data`, or the committed draft, and never an open
198
+ form's values.
196
199
 
197
- Correct: read the drafted row from `edit.store` inside the cell renderer:
200
+ Correct: reach the row as shown by id:
198
201
 
199
202
  ```tsx
200
- function useDraftedRow(rowId: string, original: Product): Product {
201
- const { edit } = useTMDataGridContext();
202
- const values = useSelector(edit.store, (state) => state.rows[rowId]?.values);
203
- return (values as Product | undefined) ?? original;
204
- }
203
+ const shown = grid.edit.getRowValues(row.id) ?? row.original;
205
204
  ```
206
205
 
207
- Source: `packages/tmdatagrid/docs/editing.md` (Draft lifetime).
206
+ Source: `packages/tmdatagrid/docs/editing.md` (How a draft renders).
208
207
 
209
208
  ## MEDIUM Reading a commit's result as the saved value
210
209
 
@@ -72,6 +72,8 @@ path, which may be dotted.
72
72
  | `isColumnEditable` | `(column) => boolean` | The column's half alone, with no row in hand: it maps to a field, `editing.columns` lists it when that is set, and `meta.edit.enabled` is not `false`. |
73
73
  | `canDeleteRows` | `() => boolean` | Whether the delete control should be shown. |
74
74
  | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The open row's live `FormApi`; `undefined` for a committed row. |
75
+ | `getRowValues` | `(rowId) => TData \| undefined` | The row as shown: the open form's values, else the committed draft, else the `data` value. `undefined` for an unknown row. |
76
+ | `getRows` | `() => ReadonlyArray<TMDataGridEditRowSnapshot>` | Every row as shown, from the core row model: drafts overlaid, entry rows appended, deletion-marked rows included and flagged `deleted`. |
75
77
  | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
76
78
  | `store` | `Store<TMDataGridEditState>` | For `useSelector`. |
77
79
 
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.21'
21
+ library_version: '2.0.0-beta.23'
22
22
  sources:
23
23
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/filtering.md'
24
24
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/quick-search.md'
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.21'
14
+ library_version: '2.0.0-beta.23'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/getting-started.md'
17
17
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/anatomy.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.21'
17
+ library_version: '2.0.0-beta.23'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/grouping.md'
20
20
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/summary-row.md'
@@ -90,8 +90,8 @@ would pass a row that looks real but is the wrong one. Group rows therefore:
90
90
  as `original`, so guard a callback reading `original` with
91
91
  `row.getIsGrouped()` and colour the group rows with `--dg-row-group-bg`
92
92
  - carry `data-grouped="true"` and `data-depth`, with `--dg-row-group-bg`
93
- behind them. `data-grouped` is on every row, `"true"` or `"false"`, so match
94
- the value rather than the bare attribute
93
+ behind them. `data-grouped` is present only on group rows, so
94
+ `[data-grouped]` and `[data-grouped="true"]` are equivalent
95
95
 
96
96
  A group row's checkbox selects every record under it at any depth, including
97
97
  records inside collapsed sub-groups, showing a tick once all are selected and a
@@ -273,7 +273,7 @@ Source: `packages/tmdatagrid/docs/grouping.md` (Server-side grids).
273
273
  | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
274
274
  | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
275
275
  | `--dg-summary-height` | CSS variable | length | From `size` | Height of the summary row. |
276
- | `data-grouped` · `data-depth` | Data attributes | – | – | `"true"` on group rows (published on every row), and the nesting level on every row. |
276
+ | `data-grouped` · `data-depth` | Data attributes | – | – | `"true"` on group rows (absent on the rest), and the nesting level on every row. |
277
277
 
278
278
  See also: the `rows` skill for selection and the details lane, and the `data`
279
279
  skill for the pager grouping suspends.
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.21'
17
+ library_version: '2.0.0-beta.23'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/use-tm-data-grid.md'
20
20
  - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.21'
20
+ library_version: '2.0.0-beta.23'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-selection.md'
23
23
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/row-interaction.md'
@@ -211,8 +211,9 @@ are not striped.
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
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
214
+ present, with the value `"true"`, only while they apply - `data-grouped` only
215
+ on group rows - so `[data-grouped]` and `[data-grouped="true"]` are
216
+ equivalent. Pick a `--row-bg` that
216
217
  reads under both colour schemes - a `-0` Mantine shade is near-white and
217
218
  unreadable in dark mode; mix a mid shade into transparency instead.
218
219
 
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.0.0-beta.21'
18
+ library_version: '2.0.0-beta.23'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-side.md'
21
21
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/server-query.md'
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.21'
14
+ library_version: '2.0.0-beta.23'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
17
17
  - 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGrid.tsx'
@@ -33,6 +33,9 @@ The grid mints no `data-testid` of its own - that attribute belongs to the app,
33
33
  and Playwright's `testIdAttribute` is configurable. `@mantine/core` and
34
34
  `@tanstack/*` ship none either.
35
35
 
36
+ Adding, changing and deleting rows, the draft store, and where a new row's
37
+ temporary id goes at commit are in the `testing-editing` skill.
38
+
36
39
  ## Naming a grid
37
40
 
38
41
  Parts repeat across grids on a page. Name the grid, scope through it:
@@ -58,6 +61,12 @@ accessible name belongs to the element carrying the `grid` role.
58
61
 
59
62
  Body cells carry no `data-dg-part` - the coordinate pair already names them.
60
63
 
64
+ State attributes are present only while they apply: `data-selected`,
65
+ `data-new`, `data-invalid` and the rest are rendered as `"true"` while the
66
+ state holds and omitted otherwise, so `[data-new]` and `[data-new="true"]`
67
+ match the same rows, and the negative is `:not([data-new])` in CSS and
68
+ `not.toHaveAttribute("data-new")` in a test.
69
+
61
70
  ## Parts
62
71
 
63
72
  **Whole-grid** (unique, no coordinate needed): `toolbar`, `summary-count`,