@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/dist/index.js +1101 -1099
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/cell-selection.md +1 -1
- package/docs/editing.md +321 -336
- package/docs/grouping.md +6 -6
- package/docs/row-styling.md +3 -3
- package/docs/styling.md +1 -1
- package/docs/testing.md +181 -0
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +4 -3
- package/skills/editing/references/common-mistakes.md +11 -12
- package/skills/editing/references/editing-api.md +2 -0
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +4 -4
- package/skills/options/SKILL.md +1 -1
- package/skills/rows/SKILL.md +4 -3
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +10 -1
- package/skills/testing-editing/SKILL.md +225 -0
- package/src/components/TMDataGridDetailsColumn.tsx +2 -2
- package/src/components/TMDataGridEntryRows.tsx +49 -24
- package/src/components/TMDataGridFooter.tsx +1 -1
- package/src/components/TMDataGridGroupColumn.tsx +1 -1
- package/src/components/TMDataGridHeaderCell.module.css +2 -2
- package/src/components/TMDataGridHeaderCell.tsx +5 -5
- package/src/components/TMDataGridHeaderFilterRow.tsx +1 -1
- package/src/components/TMDataGridTable.tsx +34 -22
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-editing
|
|
3
|
+
description: >
|
|
4
|
+
Test TMDataGrid editing flows from Playwright: adding a row through the entry
|
|
5
|
+
row, where its temporary id (__new__1) goes at commit and at save, finding an
|
|
6
|
+
added row when the grid does not know the id the app gave it, asserting a
|
|
7
|
+
failed validation on an entry row, cell and row edits, deletions, and the
|
|
8
|
+
draft store (data-new, data-draft, data-dirty, data-deleted, row-state,
|
|
9
|
+
save-all and data-draft-count). Load when writing or fixing a test that adds,
|
|
10
|
+
edits, deletes or saves rows, or when a locator for a new row stops resolving
|
|
11
|
+
after the row is committed.
|
|
12
|
+
metadata:
|
|
13
|
+
type: core
|
|
14
|
+
library: '@jielga/tmdatagrid'
|
|
15
|
+
library_version: '2.0.0-beta.23'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
|
|
19
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEntryRows.tsx'
|
|
20
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEditColumn.tsx'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# TMDataGrid - Testing editing
|
|
24
|
+
|
|
25
|
+
Builds on the `testing` skill: parts are `[data-dg-part]`, coordinates are
|
|
26
|
+
`[data-row-id]` / `[data-column-id]`, and `DataGrid` is its page object.
|
|
27
|
+
|
|
28
|
+
## The life of an added row
|
|
29
|
+
|
|
30
|
+
`edit.addRow()` opens an entry row keyed by a temporary id. What happens at ✓
|
|
31
|
+
depends on `editing.draft`, and that decides what a test can hold on to:
|
|
32
|
+
|
|
33
|
+
| Step | Without `draft` | With `draft: true` |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `addRow()` | `[data-dg-part="entry-row"][data-row-id="__new__1"]`; editors are `[data-dg-part="editor"][data-row-id="__new__1"][data-column-id="..."]` | Same |
|
|
36
|
+
| ✓ fails validation | Entry row stays; failing cells carry `data-invalid="true"` | Same |
|
|
37
|
+
| ✓ (`confirm-new-row`) | **Entry row removed, temp id gone.** `onRowAdd({ tempId, value })` runs in the app; the row exists again only when the app puts it in `data`, under the id the app's `getRowId` returns | Becomes a body row **still keyed by the temp id**: `[data-dg-part="row"][data-row-id="__new__1"][data-new="true"][data-draft="true"]`, lane shows `[data-dg-part="row-state"][data-state="new"]`, `[data-dg-part="save-all"]` counts it in `data-draft-count` |
|
|
38
|
+
| Save (`save-all`) | - | As ✓ without `draft`: temp id gone, row comes back through `data` under the app's id |
|
|
39
|
+
| `newRowsSticky: true` | - | The committed row stays an `entry-row` with `data-committed="true"`, outside `data-dg-row-count`, until Save |
|
|
40
|
+
|
|
41
|
+
The grid never learns the id the app assigns (a database id, a negative
|
|
42
|
+
counter, a uuid - whatever `onRowAdd` or `onSaveDrafts` does). The temp id →
|
|
43
|
+
real id mapping does not exist anywhere in the DOM.
|
|
44
|
+
|
|
45
|
+
## The rule: find an added row by content, not by id
|
|
46
|
+
|
|
47
|
+
Once `onRowAdd` or `saveDrafts` has run, the only thing the test knows about
|
|
48
|
+
the row is what it typed. Narrow the grid to a value unique to that row, assert
|
|
49
|
+
that exactly one row is left, read that row's cells:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
async expectRowAdded(uniqueValue: string, cells: Record<string, string>) {
|
|
53
|
+
await this.search(uniqueValue); // or filterBy on a column when there is no quick search
|
|
54
|
+
await this.expectRowCount(1); // retries until the app has put the row in data
|
|
55
|
+
const row = this.part("row"); // exactly one body row now
|
|
56
|
+
for (const [columnId, text] of Object.entries(cells)) {
|
|
57
|
+
await expect(row.locator(`[data-column-id="${columnId}"]`)).toHaveText(text);
|
|
58
|
+
}
|
|
59
|
+
await this.search("");
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- The `data-dg-row-count` assertion is the wait. Nothing else signals that
|
|
64
|
+
`onRowAdd` finished: `aria-busy` reflects `meta.loading` only.
|
|
65
|
+
- Use a value no other row has - put a run id in the name. Two matches means
|
|
66
|
+
`expectRowCount(1)` fails, correctly.
|
|
67
|
+
- Scoped to the grid and to one column: no `page.getByText`, no virtualization
|
|
68
|
+
problem, no strict-mode ambiguity.
|
|
69
|
+
|
|
70
|
+
## Page object methods
|
|
71
|
+
|
|
72
|
+
Add these to the `DataGrid` class from the `testing` skill:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
entryRow(): Locator {
|
|
76
|
+
return this.part("entry-row");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Built-in editors of an open row: an entry row, or a row opened in row mode. */
|
|
80
|
+
async fillRow(rowId: string, values: Record<string, string>): Promise<void> {
|
|
81
|
+
for (const [columnId, value] of Object.entries(values)) {
|
|
82
|
+
await this.part("editor", { rowId, columnId })
|
|
83
|
+
.locator('[data-dg-part="editor-input"]')
|
|
84
|
+
.fill(value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async commitEntryRow(rowId: string): Promise<void> {
|
|
89
|
+
await this.part("confirm-new-row", { rowId }).click();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Save in TMDataGrid.DraftActions, waited until the store is empty. */
|
|
93
|
+
async saveDrafts(): Promise<void> {
|
|
94
|
+
await this.part("save-all").click();
|
|
95
|
+
await expect(this.part("save-all")).toHaveAttribute("data-draft-count", "0");
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`fillRow` covers the built-in editors. A column with `meta.edit.editor` renders
|
|
100
|
+
the app's own component inside the `editor` part; scope through
|
|
101
|
+
`part("editor", { rowId, columnId })` and fill whatever is inside.
|
|
102
|
+
|
|
103
|
+
## Recipes
|
|
104
|
+
|
|
105
|
+
### Add a row, without draft
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await page.getByRole("button", { name: "Add row" }).click(); // the app's button → edit.addRow()
|
|
109
|
+
const tempId = (await grid.entryRow().getAttribute("data-row-id"))!;
|
|
110
|
+
await grid.fillRow(tempId, { name: "Nordkvist-4711", city: "Stockholm" });
|
|
111
|
+
await grid.commitEntryRow(tempId);
|
|
112
|
+
|
|
113
|
+
await expect(grid.entryRow()).toHaveCount(0); // entry row gone; tempId is now meaningless
|
|
114
|
+
await grid.expectRowAdded("Nordkvist-4711", { name: "Nordkvist-4711", city: "Stockholm" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Add a row, with draft, then save
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await grid.commitEntryRow(tempId);
|
|
121
|
+
const row = grid.part("row", { rowId: tempId }); // tempId is still the row's id
|
|
122
|
+
await expect(row).toHaveAttribute("data-new", "true");
|
|
123
|
+
await expect(row).toHaveAttribute("data-draft", "true");
|
|
124
|
+
await expect(grid.part("row-state", { rowId: tempId })).toHaveAttribute("data-state", "new");
|
|
125
|
+
await expect(grid.part("save-all")).toHaveAttribute("data-draft-count", "1");
|
|
126
|
+
|
|
127
|
+
await grid.saveDrafts();
|
|
128
|
+
await expect(row).toHaveCount(0); // tempId dies here
|
|
129
|
+
await grid.expectRowAdded("Nordkvist-4711", { city: "Stockholm" });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Reopening a committed draft row: `edit-row` (the lane's pencil) or a
|
|
133
|
+
double-click puts it back in the entry block, still under the temp id; ✕
|
|
134
|
+
(`discard-new-row`) removes it.
|
|
135
|
+
|
|
136
|
+
### A ✓ that fails validation
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await grid.commitEntryRow(tempId);
|
|
140
|
+
await expect(grid.entryRow()).toHaveCount(1); // still open
|
|
141
|
+
await expect(grid.entryRow().locator('[data-column-id="email"]'))
|
|
142
|
+
.toHaveAttribute("data-invalid", "true");
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Change a cell or a row
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const cell = grid.cell({ rowId: "42", columnId: "salary" });
|
|
149
|
+
await cell.dblclick(); // cell mode: opens the editor
|
|
150
|
+
await grid.fillRow("42", { salary: "52000" });
|
|
151
|
+
await page.keyboard.press("Enter"); // commits
|
|
152
|
+
|
|
153
|
+
// Without draft: onCommit ran, the app applied it, the grid holds nothing
|
|
154
|
+
await expect(cell).toHaveText("52 000");
|
|
155
|
+
|
|
156
|
+
// Under draft: the grid holds it until Save
|
|
157
|
+
await expect(cell).toHaveAttribute("data-dirty", "true");
|
|
158
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-dirty", "true");
|
|
159
|
+
await expect(grid.part("row-state", { rowId: "42" })).toHaveAttribute("data-state", "edited");
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Row mode: `edit-row` opens the row, `fillRow` reaches its editors, `save-row`
|
|
163
|
+
commits, `cancel-row` discards. `cellConfirm` mode: `editor-confirm` /
|
|
164
|
+
`editor-cancel` beside the input, or Enter / Escape.
|
|
165
|
+
|
|
166
|
+
### Delete a row
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const before = Number(await grid.grid.getAttribute("data-dg-row-count"));
|
|
170
|
+
await grid.part("delete-row", { rowId: "42" }).click();
|
|
171
|
+
|
|
172
|
+
// Without draft: onRowDelete ran, the app removed it from data
|
|
173
|
+
await grid.expectRowCount(before - 1);
|
|
174
|
+
|
|
175
|
+
// Under draft: marked until Save; restore-row undoes the mark
|
|
176
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-deleted", "true");
|
|
177
|
+
await grid.part("restore-row", { rowId: "42" }).click();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Attribute values
|
|
181
|
+
|
|
182
|
+
A state attribute is present, with the value `"true"`, only while it applies:
|
|
183
|
+
`data-new`, `data-draft`, `data-dirty`, `data-deleted` on a body row,
|
|
184
|
+
`data-committed` on an entry row, `data-dirty`, `data-invalid`, `data-editing`
|
|
185
|
+
on a cell. `[data-new]` and `[data-new="true"]` match the same rows,
|
|
186
|
+
`toHaveAttribute("data-new")` and `toHaveAttribute("data-new", "true")` both
|
|
187
|
+
hold, and the negative is `not.toHaveAttribute("data-new")`.
|
|
188
|
+
|
|
189
|
+
## Common mistakes
|
|
190
|
+
|
|
191
|
+
### Looking for the new row's text on the page
|
|
192
|
+
|
|
193
|
+
`page.getByText("Nordkvist")` searches the whole document: other grids, the
|
|
194
|
+
toolbar, a toast. And the row may be virtualized out of the DOM, so the
|
|
195
|
+
locator fails or flakes depending on sort order and viewport. Narrow with
|
|
196
|
+
search or a filter, assert `data-dg-row-count` is 1, read that one row.
|
|
197
|
+
|
|
198
|
+
### Keeping the temp id after ✓ without draft
|
|
199
|
+
|
|
200
|
+
`grid.part("row", { rowId: "__new__1" })` never resolves: without `draft` the
|
|
201
|
+
grid drops the entry and the id at ✓, and the row's real id is the app's. Only
|
|
202
|
+
under `draft: true` does the temp id outlive ✓ - and only until Save.
|
|
203
|
+
|
|
204
|
+
### Waiting for the added row with a timeout
|
|
205
|
+
|
|
206
|
+
The app's `onRowAdd` may be async. `expectRowCount(1)` after narrowing retries
|
|
207
|
+
until the row is in `data`; a `waitForTimeout` either wastes time or is too
|
|
208
|
+
short on CI.
|
|
209
|
+
|
|
210
|
+
### Asserting the negative with `"false"`
|
|
211
|
+
|
|
212
|
+
`toHaveAttribute("data-new", "false")` never passes: a state that does not
|
|
213
|
+
apply is an absent attribute, not `"false"`. Use
|
|
214
|
+
`not.toHaveAttribute("data-new")`, and `:not([data-new])` in CSS.
|
|
215
|
+
|
|
216
|
+
### Asserting a deletion with `toHaveCount(0)`
|
|
217
|
+
|
|
218
|
+
A row outside the viewport has no element either. Assert `data-dg-row-count`
|
|
219
|
+
dropped by one, or, under `draft`, `data-deleted="true"` on the row.
|
|
220
|
+
|
|
221
|
+
### Expecting the grid to show the change without draft
|
|
222
|
+
|
|
223
|
+
Without `draft: true` the grid holds nothing after a commit: no `data-dirty`,
|
|
224
|
+
no `row-state`, `save-all` stays disabled at `data-draft-count="0"`. The
|
|
225
|
+
assertion is the cell's text after the app has applied the change to `data`.
|
|
@@ -51,7 +51,7 @@ function DetailsCell<TData extends RowData>({
|
|
|
51
51
|
row.toggleExpanded();
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
|
-
<span className={classes.chevron} data-expanded={expanded}>
|
|
54
|
+
<span className={classes.chevron} data-expanded={expanded || undefined}>
|
|
55
55
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
56
56
|
</span>
|
|
57
57
|
</UnstyledButton>
|
|
@@ -106,7 +106,7 @@ function DetailsHeader<TData extends RowData>({
|
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
108
|
>
|
|
109
|
-
<span className={classes.chevron} data-expanded={allExpanded}>
|
|
109
|
+
<span className={classes.chevron} data-expanded={allExpanded || undefined}>
|
|
110
110
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
111
111
|
</span>
|
|
112
112
|
</UnstyledButton>
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isColumnEditableForRow,
|
|
17
17
|
isControlColumn,
|
|
18
18
|
} from "../core/columnUtils";
|
|
19
|
+
import { draftCellContext } from "../core/draftCellContext";
|
|
19
20
|
import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
|
|
20
21
|
import { focusEditorContent } from "../core/editorFocus";
|
|
21
22
|
import {
|
|
@@ -45,10 +46,11 @@ function isEntryCellEditable(
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* One cell of an entry row: the lane's controls,
|
|
49
|
-
*
|
|
50
|
-
* a body cell's does - a refused ✓
|
|
51
|
-
* nothing else. No dirty marker:
|
|
49
|
+
* One cell of an entry row: the lane's controls, an editor for a cell the
|
|
50
|
+
* open row edits, or a value through the column's own renderer. The invalid
|
|
51
|
+
* marker subscribes here, per cell, as a body cell's does - a refused ✓
|
|
52
|
+
* paints the cells its issues name and nothing else. No dirty marker:
|
|
53
|
+
* everything in an entry row is new.
|
|
52
54
|
*/
|
|
53
55
|
function EntryCell({
|
|
54
56
|
entryRow,
|
|
@@ -76,12 +78,22 @@ function EntryCell({
|
|
|
76
78
|
});
|
|
77
79
|
const editable =
|
|
78
80
|
cell !== undefined && isEntryCellEditable(entryRow, column, edit);
|
|
81
|
+
// An open row's cells outside the editors follow the form as it is typed,
|
|
82
|
+
// as a body row's do: the column's renderer over a context reading the
|
|
83
|
+
// form's values - the whole row, so a computed cell repaints with any
|
|
84
|
+
// field. `undefined` for a committed row, whose values the entry table
|
|
85
|
+
// already holds as data, and for the lanes.
|
|
86
|
+
const draftValues = useSelector(edit.store, (state) =>
|
|
87
|
+
committed || isControlColumn(column.id)
|
|
88
|
+
? undefined
|
|
89
|
+
: state.rows[entryRow.id]?.values,
|
|
90
|
+
);
|
|
79
91
|
return (
|
|
80
92
|
<div
|
|
81
93
|
role="cell"
|
|
82
94
|
data-column-id={column.id}
|
|
83
95
|
data-align={getColumnAlign(column)}
|
|
84
|
-
data-control-column={isControlColumn(column.id)}
|
|
96
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
85
97
|
data-invalid={isInvalid || undefined}
|
|
86
98
|
// A committed row re-opens where it is double-clicked, the same
|
|
87
99
|
// gesture a body cell answers - and, like a body cell, a cell that
|
|
@@ -108,16 +120,10 @@ function EntryCell({
|
|
|
108
120
|
zIndex: layout.pinnedAt ? pinnedZ : undefined,
|
|
109
121
|
}}
|
|
110
122
|
>
|
|
111
|
-
{cell
|
|
123
|
+
{cell === undefined ? null : column.id === EDIT_COLUMN_ID ? (
|
|
112
124
|
// The lane's cell - the entry row's controls.
|
|
113
125
|
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
114
|
-
) : committed &&
|
|
115
|
-
// Entered, awaiting Save all: a value row through the columns' own
|
|
116
|
-
// renderers, over the draft the entry table was fed.
|
|
117
|
-
<span className={classes.cellContent}>
|
|
118
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
119
|
-
</span>
|
|
120
|
-
) : editable && cell !== undefined ? (
|
|
126
|
+
) : !committed && editable ? (
|
|
121
127
|
<TMDataGridCellEditor
|
|
122
128
|
cell={cell}
|
|
123
129
|
row={entryRow}
|
|
@@ -125,6 +131,24 @@ function EntryCell({
|
|
|
125
131
|
onClose={() => {}}
|
|
126
132
|
inEntryBlock
|
|
127
133
|
/>
|
|
134
|
+
) : committed || !isControlColumn(column.id) ? (
|
|
135
|
+
// A value through the column's own renderer. Every cell of a row
|
|
136
|
+
// entered and awaiting Save all, over the draft the entry table was
|
|
137
|
+
// fed - and, while the row is open, every cell it does not open (a
|
|
138
|
+
// display column, `meta.edit.enabled` off), over the form as it is
|
|
139
|
+
// typed, as a body row in row mode shows them. Blank was how a
|
|
140
|
+
// calculated column or a button cell vanished the moment a
|
|
141
|
+
// committed row was reopened. The generated lanes stay blank while
|
|
142
|
+
// the row is open: a checkbox or a chevron there would act on the
|
|
143
|
+
// entry table's own state, which reaches nothing.
|
|
144
|
+
<span className={classes.cellContent}>
|
|
145
|
+
{flexRender(
|
|
146
|
+
cell.column.columnDef.cell,
|
|
147
|
+
draftValues !== undefined
|
|
148
|
+
? draftCellContext(cell, draftValues)
|
|
149
|
+
: cell.getContext(),
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
128
152
|
) : null}
|
|
129
153
|
</div>
|
|
130
154
|
);
|
|
@@ -245,18 +269,19 @@ export function TMDataGridEntryRows({
|
|
|
245
269
|
focusEditorContent(editor);
|
|
246
270
|
});
|
|
247
271
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
272
|
+
// What the rows render over. A committed row is data: its values are the
|
|
273
|
+
// draft store's, which stand through a reopen. An open row's cells read
|
|
274
|
+
// the form itself - the editors directly, the rest through EntryCell's
|
|
275
|
+
// draft overlay - so its entry here gives the row an identity and a
|
|
276
|
+
// fallback: the committed values it was reopened from, else the seed
|
|
277
|
+
// frozen at addRow (the form's `defaultValues`, which a reopen keeps at
|
|
278
|
+
// the seed and writes the committed values over).
|
|
253
279
|
const data = useMemo(
|
|
254
280
|
() =>
|
|
255
281
|
newRows.map(
|
|
256
|
-
({ tempId
|
|
257
|
-
(
|
|
258
|
-
|
|
259
|
-
: edit.getForm(tempId)?.options.defaultValues) ??
|
|
282
|
+
({ tempId }) =>
|
|
283
|
+
(committedValues[tempId] ??
|
|
284
|
+
edit.getForm(tempId)?.options.defaultValues ??
|
|
260
285
|
{}) as TMDataGridRowData,
|
|
261
286
|
),
|
|
262
287
|
[newRows, committedValues, edit],
|
|
@@ -288,10 +313,10 @@ export function TMDataGridEntryRows({
|
|
|
288
313
|
data-dg-part="entry-row"
|
|
289
314
|
data-row-id={entryRow.id}
|
|
290
315
|
data-new
|
|
291
|
-
data-committed={committed}
|
|
316
|
+
data-committed={committed || undefined}
|
|
292
317
|
// The same marker body rows carry once committed, so one selector
|
|
293
318
|
// reaches everything parked in the draft store.
|
|
294
|
-
data-draft={committed}
|
|
319
|
+
data-draft={committed || undefined}
|
|
295
320
|
className={classes.entryRow}
|
|
296
321
|
>
|
|
297
322
|
{orderedColumns.map((column) => (
|
|
@@ -330,7 +330,7 @@ export function TMDataGridFooter({
|
|
|
330
330
|
<Box
|
|
331
331
|
data-dg-part="footer"
|
|
332
332
|
className={footerClassName}
|
|
333
|
-
data-paging-suspended={!paging}
|
|
333
|
+
data-paging-suspended={!paging || undefined}
|
|
334
334
|
{...others}
|
|
335
335
|
>
|
|
336
336
|
{/* The default render is the controls in order - which is what makes
|
|
@@ -82,7 +82,7 @@ function GroupCell<TData extends RowData>({
|
|
|
82
82
|
row.toggleExpanded();
|
|
83
83
|
}}
|
|
84
84
|
>
|
|
85
|
-
<span className={classes.chevron} data-expanded={expanded}>
|
|
85
|
+
<span className={classes.chevron} data-expanded={expanded || undefined}>
|
|
86
86
|
<ChevronRightIcon size={16} stroke={1.6} />
|
|
87
87
|
</span>
|
|
88
88
|
<span className={classes.groupLabel}>{label}</span>
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
|
|
91
91
|
/* See the sort ActionIcon: an unsorted column's arrow is an invitation, not a
|
|
92
92
|
state, so it sits back until the pointer is on it. */
|
|
93
|
-
.sortAction[data-sorted
|
|
93
|
+
.sortAction:not([data-sorted]) {
|
|
94
94
|
opacity: 0.55;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.sortAction[data-sorted
|
|
97
|
+
.sortAction:not([data-sorted]):hover {
|
|
98
98
|
opacity: 1;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -540,11 +540,11 @@ export function TMDataGridHeaderCell({
|
|
|
540
540
|
// The same coordinate the body, summary and entry cells carry, so one
|
|
541
541
|
// selector reaches a column's header and its cells alike.
|
|
542
542
|
data-column-id={column.id}
|
|
543
|
-
data-active={isSorted || isFiltered}
|
|
543
|
+
data-active={isSorted || isFiltered || undefined}
|
|
544
544
|
data-align={align}
|
|
545
545
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
546
546
|
// scale grows for text. See isControlColumn.
|
|
547
|
-
data-control-column={isControlColumn(column.id)}
|
|
547
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
548
548
|
// Only meaningful on a sortable column: "none" announces that this
|
|
549
549
|
// header sorts, which is wrong on one that does not.
|
|
550
550
|
aria-sort={
|
|
@@ -639,12 +639,12 @@ export function TMDataGridHeaderCell({
|
|
|
639
639
|
{canSort && (
|
|
640
640
|
<ActionIcon
|
|
641
641
|
className={`${classes.headerAction} ${classes.sortAction}`}
|
|
642
|
-
data-pinned-visible={isSorted}
|
|
642
|
+
data-pinned-visible={isSorted || undefined}
|
|
643
643
|
// The arrow is the only sort indicator now that the title no
|
|
644
644
|
// longer tints, so it takes the colour while the sort holds. On
|
|
645
645
|
// an unsorted column it is only the hover affordance, and stays a
|
|
646
646
|
// faded grey so the two are not confused.
|
|
647
|
-
data-sorted={isSorted}
|
|
647
|
+
data-sorted={isSorted || undefined}
|
|
648
648
|
variant="subtle"
|
|
649
649
|
color={isSorted ? undefined : "gray"}
|
|
650
650
|
size="xs"
|
|
@@ -685,7 +685,7 @@ export function TMDataGridHeaderCell({
|
|
|
685
685
|
// Held visible for the right-click menu too: that one opens at
|
|
686
686
|
// the pointer and is portaled, so the hover that revealed the
|
|
687
687
|
// actions is lost the moment the pointer enters the dropdown.
|
|
688
|
-
data-pinned-visible={menuOpened || contextMenuOpened}
|
|
688
|
+
data-pinned-visible={menuOpened || contextMenuOpened || undefined}
|
|
689
689
|
variant="subtle"
|
|
690
690
|
color="gray"
|
|
691
691
|
size="xs"
|
|
@@ -271,7 +271,7 @@ export function TMDataGridHeaderFilterRow({
|
|
|
271
271
|
aria-label={labels.filterOn(getColumnLabel(column))}
|
|
272
272
|
data-dg-part="header-filter-cell"
|
|
273
273
|
data-column-id={column.id}
|
|
274
|
-
data-control-column={isControlColumn(column.id)}
|
|
274
|
+
data-control-column={isControlColumn(column.id) || undefined}
|
|
275
275
|
className={[
|
|
276
276
|
classes.headerFilterCell,
|
|
277
277
|
layout.isBoundary && layout.pinnedAt === "left"
|
|
@@ -332,16 +332,16 @@ function TMDataGridBodyCell({
|
|
|
332
332
|
// selectors also require `data-cell`, so they are unaffected.
|
|
333
333
|
data-row-id={cell.row.id}
|
|
334
334
|
data-column-id={cell.column.id}
|
|
335
|
-
data-focused={nav?.focused}
|
|
335
|
+
data-focused={nav?.focused || undefined}
|
|
336
336
|
aria-selected={nav?.selected}
|
|
337
|
-
data-selected={nav?.selected}
|
|
337
|
+
data-selected={nav?.selected || undefined}
|
|
338
338
|
// One attribute per side rather than a class per combination: the
|
|
339
339
|
// stylesheet draws the outline edge by edge, and sixteen classes for the
|
|
340
340
|
// sixteen corners of a rectangle is not a stylesheet anyone can read.
|
|
341
|
-
data-edge-top={nav?.edges?.top}
|
|
342
|
-
data-edge-bottom={nav?.edges?.bottom}
|
|
343
|
-
data-edge-left={nav?.edges?.left}
|
|
344
|
-
data-edge-right={nav?.edges?.right}
|
|
341
|
+
data-edge-top={nav?.edges?.top || undefined}
|
|
342
|
+
data-edge-bottom={nav?.edges?.bottom || undefined}
|
|
343
|
+
data-edge-left={nav?.edges?.left || undefined}
|
|
344
|
+
data-edge-right={nav?.edges?.right || undefined}
|
|
345
345
|
// Bubbles, so focus landing on a control inside the cell counts as
|
|
346
346
|
// landing on the cell - the ring follows the user into the checkbox
|
|
347
347
|
// rather than being left behind on whichever cell they came from.
|
|
@@ -356,7 +356,7 @@ function TMDataGridBodyCell({
|
|
|
356
356
|
data-align={getColumnAlign(cell.column)}
|
|
357
357
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
358
358
|
// scale grows for text. See isControlColumn.
|
|
359
|
-
data-control-column={isControl}
|
|
359
|
+
data-control-column={isControl || undefined}
|
|
360
360
|
onContextMenu={onContextMenu}
|
|
361
361
|
className={[
|
|
362
362
|
classes.bodyCell,
|
|
@@ -2674,7 +2674,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2674
2674
|
// The tree's own rows. `data-grouped` rather than a class, to
|
|
2675
2675
|
// match how the rest of the row's state is published - and so
|
|
2676
2676
|
// a consumer can restyle them without reaching into modules.
|
|
2677
|
-
data-grouped={isGroupRow}
|
|
2677
|
+
data-grouped={isGroupRow || undefined}
|
|
2678
2678
|
data-depth={row.depth}
|
|
2679
2679
|
// Which edge block the row sits in, for styling hooks.
|
|
2680
2680
|
data-pinned={pinnedAt}
|
|
@@ -2683,13 +2683,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2683
2683
|
// still hold ids from before the mode changed - TanStack never
|
|
2684
2684
|
// prunes it - and a row must not report itself selected in a
|
|
2685
2685
|
// mode where selecting is not a thing.
|
|
2686
|
-
data-selected={
|
|
2686
|
+
data-selected={
|
|
2687
|
+
(features.rowSelection && row.getIsSelected()) || undefined
|
|
2688
|
+
}
|
|
2687
2689
|
// Being selected is state; painting it is a display choice, so
|
|
2688
2690
|
// the two are separate attributes.
|
|
2689
2691
|
data-selected-bg={
|
|
2690
|
-
features.rowSelection &&
|
|
2691
|
-
|
|
2692
|
-
|
|
2692
|
+
(features.rowSelection &&
|
|
2693
|
+
features.showSelectedBackground &&
|
|
2694
|
+
row.getIsSelected()) ||
|
|
2695
|
+
undefined
|
|
2693
2696
|
}
|
|
2694
2697
|
// The highlighted row is its own concept, so its own attribute
|
|
2695
2698
|
// pair - `data-highlighted` / `aria-current` against
|
|
@@ -2701,33 +2704,40 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2701
2704
|
// so a consumer styling or querying rows by it would cast far
|
|
2702
2705
|
// wider than they meant to.
|
|
2703
2706
|
data-highlighted={
|
|
2704
|
-
features.highlightRow && row.id === highlightedRowId
|
|
2707
|
+
(features.highlightRow && row.id === highlightedRowId) ||
|
|
2708
|
+
undefined
|
|
2705
2709
|
}
|
|
2706
2710
|
// Marked deleted under draft: struck through and inert
|
|
2707
2711
|
// until submitAll reports it, or the mark is toggled back.
|
|
2708
2712
|
data-deleted={
|
|
2709
|
-
deletedRowIds.length > 0 && deletedRowIds.includes(row.id)
|
|
2713
|
+
(deletedRowIds.length > 0 && deletedRowIds.includes(row.id)) ||
|
|
2714
|
+
undefined
|
|
2710
2715
|
}
|
|
2711
2716
|
// Carrying a dirty draft - the row-level face of the cells'
|
|
2712
2717
|
// own data-dirty markers, for row-scoped styling.
|
|
2713
|
-
data-dirty={editDirtyRowIdSet.has(row.id)}
|
|
2718
|
+
data-dirty={editDirtyRowIdSet.has(row.id) || undefined}
|
|
2714
2719
|
// Committed into the draft store, waiting for Save. A
|
|
2715
2720
|
// committed entry row is one too - it is in the body
|
|
2716
2721
|
// because it is committed - and carries `data-new` besides.
|
|
2717
2722
|
data-draft={
|
|
2718
|
-
editDraftRowIdSet.has(row.id) ||
|
|
2723
|
+
editDraftRowIdSet.has(row.id) ||
|
|
2724
|
+
newRowIdSet.has(row.id) ||
|
|
2725
|
+
undefined
|
|
2719
2726
|
}
|
|
2720
2727
|
// An entered row not yet in `data`, committed into the draft
|
|
2721
2728
|
// store and sorted, filtered and grouped with the rest.
|
|
2722
|
-
data-new={newRowIdSet.has(row.id)}
|
|
2729
|
+
data-new={newRowIdSet.has(row.id) || undefined}
|
|
2723
2730
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
2724
2731
|
// `data-expanded` lands there rather than on a row. This is
|
|
2725
2732
|
// what says which row the open menu is about.
|
|
2726
2733
|
data-context-menu={
|
|
2727
|
-
contextMenuContent !== null &&
|
|
2728
|
-
|
|
2734
|
+
(contextMenuContent !== null &&
|
|
2735
|
+
contextMenuTarget?.rowId === row.id) ||
|
|
2736
|
+
undefined
|
|
2737
|
+
}
|
|
2738
|
+
data-selects-on-click={
|
|
2739
|
+
(selectsOnRowClick && !isGroupRow) || undefined
|
|
2729
2740
|
}
|
|
2730
|
-
data-selects-on-click={selectsOnRowClick && !isGroupRow}
|
|
2731
2741
|
aria-selected={
|
|
2732
2742
|
features.rowSelection ? row.getIsSelected() : undefined
|
|
2733
2743
|
}
|
|
@@ -2739,7 +2749,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2739
2749
|
// From the row's position in the whole view, so the stripes
|
|
2740
2750
|
// survive the virtualizer's moving window - see the prop.
|
|
2741
2751
|
data-striped={
|
|
2742
|
-
striped && viewIndex >= 0
|
|
2752
|
+
(striped && viewIndex >= 0 && viewIndex % 2 === 1) || undefined
|
|
2743
2753
|
}
|
|
2744
2754
|
className={[classes.bodyRow, rowClassNameFor(row)]
|
|
2745
2755
|
.filter(Boolean)
|
|
@@ -3122,7 +3132,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
3122
3132
|
role="cell"
|
|
3123
3133
|
data-column-id={header.column.id}
|
|
3124
3134
|
data-align={getColumnAlign(header.column)}
|
|
3125
|
-
data-control-column={
|
|
3135
|
+
data-control-column={
|
|
3136
|
+
isControlColumn(header.column.id) || undefined
|
|
3137
|
+
}
|
|
3126
3138
|
className={[
|
|
3127
3139
|
classes.summaryCell,
|
|
3128
3140
|
layout.isBoundary && layout.pinnedAt === "left"
|