@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-components
|
|
3
|
+
description: >
|
|
4
|
+
Test a TMDataGrid in a real browser with Playwright component tests: the
|
|
5
|
+
stories-and-gallery model of Playwright 1.62+, the mount fixture, how a story
|
|
6
|
+
wraps the grid (MantineProvider env="test", a fixed-size container, the api on
|
|
7
|
+
window for scrollToRow), and the browser-only behaviour worth testing there -
|
|
8
|
+
virtualization, column resize through header-resize, column reorder by drag,
|
|
9
|
+
sticky pinned columns, the clipboard. Load when setting up or writing
|
|
10
|
+
Playwright component tests for a grid, or when a jsdom test cannot observe
|
|
11
|
+
layout, scrolling or drag.
|
|
12
|
+
metadata:
|
|
13
|
+
type: core
|
|
14
|
+
library: '@jielga/tmdatagrid'
|
|
15
|
+
library_version: '2.0.0'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/test/gallery/main.tsx'
|
|
19
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/test/stories/Grid.story.tsx'
|
|
20
|
+
- 'Jielga/TMDataGrid:playwright/components/virtualization.spec.ts'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# TMDataGrid - Component tests in a browser
|
|
24
|
+
|
|
25
|
+
Builds on the `testing` skill: parts are `[data-dg-part]`, coordinates are
|
|
26
|
+
`[data-row-id]` / `[data-column-id]`, and `DataGrid` is the page object.
|
|
27
|
+
|
|
28
|
+
jsdom has no layout. The virtualizer mounts a handful of rows whatever the data
|
|
29
|
+
says, a drag never moves anything, and jsdom does not apply `position: sticky`.
|
|
30
|
+
The behaviour below needs a browser:
|
|
31
|
+
|
|
32
|
+
- virtualization - rows mount and unmount as the body scrolls; `scrollToRow`
|
|
33
|
+
- column resize - a drag on `header-resize`, and double-click to fit
|
|
34
|
+
- column reorder - a header dragged onto another header (native HTML5 drag)
|
|
35
|
+
- pinned columns - sticky offsets while the body scrolls horizontally
|
|
36
|
+
- sticky header and summary row while the body scrolls vertically
|
|
37
|
+
- the clipboard - Ctrl+C on a cell selection
|
|
38
|
+
|
|
39
|
+
## The model
|
|
40
|
+
|
|
41
|
+
Playwright 1.62 replaced `@playwright/experimental-ct-react` with three pieces
|
|
42
|
+
that live in plain `@playwright/test`:
|
|
43
|
+
|
|
44
|
+
- **a story** - a `*.story.tsx` file; each named export is one scenario, a
|
|
45
|
+
component with hard-coded data, options and providers
|
|
46
|
+
- **a gallery** - one page served by your own dev server that discovers the
|
|
47
|
+
story files, exposes `window.mount({ story, props })` and `window.unmount()`,
|
|
48
|
+
and renders into `#root` through one reused React root
|
|
49
|
+
- **`mount`** - a built-in fixture: `await mount("Orders/Virtualized")`
|
|
50
|
+
navigates to the gallery (`baseURL`), mounts the story and returns a
|
|
51
|
+
`Locator` for the gallery root
|
|
52
|
+
|
|
53
|
+
The story id is the file path under the stories folder without `.story.tsx`,
|
|
54
|
+
then `/` and the export name. `mount` accepts any string, so a renamed story
|
|
55
|
+
fails at run time, not under `tsc`; registering the ids in Playwright's
|
|
56
|
+
`Stories` interface gives completion and prop checking:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
declare module "@playwright/test" {
|
|
60
|
+
interface Stories {
|
|
61
|
+
"Orders/Virtualized": typeof Virtualized;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
What the gallery page must expose is in Playwright's docs
|
|
67
|
+
(https://playwright.dev/docs/test-components); the grid's own gallery is a few
|
|
68
|
+
dozen lines over `import.meta.glob`.
|
|
69
|
+
|
|
70
|
+
Project config, next to the page project:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
{
|
|
74
|
+
name: "components",
|
|
75
|
+
testDir: "playwright/components",
|
|
76
|
+
use: {
|
|
77
|
+
baseURL: "http://localhost:5274/",
|
|
78
|
+
serviceWorkers: "block",
|
|
79
|
+
permissions: ["clipboard-read", "clipboard-write"],
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
with a `webServer` entry such as
|
|
85
|
+
`{ command: "npm run gallery", url: "http://localhost:5274/" }`. Every
|
|
86
|
+
`webServer` entry starts on every run; Playwright does not scope them to a
|
|
87
|
+
project.
|
|
88
|
+
|
|
89
|
+
## Writing a story for the grid
|
|
90
|
+
|
|
91
|
+
The story owns everything the grid needs:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// Orders.story.tsx
|
|
95
|
+
declare global {
|
|
96
|
+
interface Window {
|
|
97
|
+
__grid?: TMDataGridApi<Order>; // for scrollToRow from the test
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Virtualized = () => {
|
|
102
|
+
const grid = useTMDataGrid<Order>({
|
|
103
|
+
data: orders, // 5000 rows
|
|
104
|
+
columns,
|
|
105
|
+
getRowId: (row) => row.id,
|
|
106
|
+
});
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
window.__grid = grid;
|
|
109
|
+
}, [grid]);
|
|
110
|
+
return (
|
|
111
|
+
<div style={{ display: "flex", flexDirection: "column", height: 400 }}>
|
|
112
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }} data-testid="orders">
|
|
113
|
+
<TMDataGrid.Table<Order> />
|
|
114
|
+
</TMDataGrid>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **`<MantineProvider env="test">`** around every story, in the gallery or in
|
|
121
|
+
the story. It turns Mantine's transitions off, so a panel is open the moment
|
|
122
|
+
the click lands.
|
|
123
|
+
- **A flex column of fixed height,** with `flex: 1` and `minHeight: 0` on the
|
|
124
|
+
grid. The grid root is a shrinkable flex item; in a plain block it grows to
|
|
125
|
+
fit every row, nothing scrolls, and the virtualization test proves nothing.
|
|
126
|
+
Give the pinned story a width smaller than its columns (`minSize` sets a
|
|
127
|
+
fluid column's floor; `size` alone does not widen it), so the body overflows
|
|
128
|
+
horizontally.
|
|
129
|
+
- **The api on `window`** only where a test calls `scrollToRow`. Everything
|
|
130
|
+
else is reachable through the DOM contract. Declare the global in the story
|
|
131
|
+
and again in the spec, which is typechecked apart from it.
|
|
132
|
+
- **No props unless needed.** One export per scenario, and `mount(id)` needs
|
|
133
|
+
no generic.
|
|
134
|
+
|
|
135
|
+
## Writing the test
|
|
136
|
+
|
|
137
|
+
`mount` returns the gallery root; the page object takes the grid root inside it:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
declare global {
|
|
141
|
+
interface Window {
|
|
142
|
+
__grid?: { scrollToRow(target: { rowId: string; align?: "start" | "center" | "end" }): boolean };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("reaches a row past the viewport", async ({ mount, page }) => {
|
|
147
|
+
const component = await mount("Orders/Virtualized");
|
|
148
|
+
const grid = new DataGrid(component.locator("[data-dg-root]"));
|
|
149
|
+
await grid.expectRowCount(5000);
|
|
150
|
+
await expect(grid.part("row", { rowId: "4500" })).toHaveCount(0);
|
|
151
|
+
|
|
152
|
+
const found = await page.evaluate(() => {
|
|
153
|
+
return window.__grid!.scrollToRow({ rowId: "4500", align: "center" });
|
|
154
|
+
});
|
|
155
|
+
expect(found).toBe(true);
|
|
156
|
+
await expect(grid.part("row", { rowId: "4500" })).toBeVisible();
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Recipes
|
|
161
|
+
|
|
162
|
+
**Virtualization.** `data-dg-row-count` is the data; the number of
|
|
163
|
+
`[data-dg-part="row"]` elements is the window. Asserting that the second is
|
|
164
|
+
far below the first is the one place counting row elements is right. Scroll
|
|
165
|
+
the element carrying `data-dg-scroll-container` to its `scrollHeight` and the
|
|
166
|
+
last row appears.
|
|
167
|
+
|
|
168
|
+
**Resize.** `header-resize` is the handle, keyed by `data-column-id`:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const handle = grid.part("header-resize", { columnId: "name" });
|
|
172
|
+
const box = (await handle.boundingBox())!;
|
|
173
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
174
|
+
await page.mouse.down();
|
|
175
|
+
await page.mouse.move(box.x + 80, box.y, { steps: 8 });
|
|
176
|
+
await page.mouse.up();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Then compare the header's `boundingBox().width` before and after. A
|
|
180
|
+
`dblclick()` on the handle fits the column to its content.
|
|
181
|
+
|
|
182
|
+
**Reorder.** `await grid.part("header", { columnId: "city" }).dragTo(grid.part("header", { columnId: "name" }), { targetPosition: { x: 10, y: 10 } })`
|
|
183
|
+
drops `city` before `name`; read the new order from `[data-dg-part="header"]`
|
|
184
|
+
`data-column-id`s. A header dragged across a pinned lane does not move.
|
|
185
|
+
|
|
186
|
+
**Pinning.** Scroll the scroll container horizontally (`scrollLeft`) and compare
|
|
187
|
+
`boundingBox().x` of a pinned header before and after: unchanged, while an
|
|
188
|
+
unpinned header moved. A pinned header and a body cell of the same column share
|
|
189
|
+
the same `x`.
|
|
190
|
+
|
|
191
|
+
**Sticky header and summary row.** Scroll vertically; `boundingBox().y` of the
|
|
192
|
+
header row (`[data-dg-header-row]`) and of `summary-row` do not change, and
|
|
193
|
+
both stay inside the scroll container's box.
|
|
194
|
+
|
|
195
|
+
**Clipboard.** In a story with `cellSelection` on: click a cell, shift-click
|
|
196
|
+
another, `page.keyboard.press("Control+C")`, then
|
|
197
|
+
`page.evaluate(() => navigator.clipboard.readText())`. Cells are separated by
|
|
198
|
+
a tab and rows by `\r\n`. Needs the `permissions` above; Chromium only.
|
|
199
|
+
|
|
200
|
+
## Common mistakes
|
|
201
|
+
|
|
202
|
+
### Mounting without a provider
|
|
203
|
+
|
|
204
|
+
A story rendered outside `MantineProvider` throws on the first Mantine
|
|
205
|
+
component. Put the provider in the gallery's `window.mount`, so no story can
|
|
206
|
+
forget it.
|
|
207
|
+
|
|
208
|
+
### A story with no height
|
|
209
|
+
|
|
210
|
+
The grid fills its container. With `height: auto` the container grows with the
|
|
211
|
+
rows, nothing scrolls, and the virtualizer mounts everything; the test passes
|
|
212
|
+
for the wrong reason or hangs on 5000 rows.
|
|
213
|
+
|
|
214
|
+
### Building JSX in the test
|
|
215
|
+
|
|
216
|
+
The test runs in Node and the component in the browser; there is no JSX across
|
|
217
|
+
that boundary. A scenario is a story export. A test that needs a different
|
|
218
|
+
composition gets another export.
|
|
219
|
+
|
|
220
|
+
### Passing a callback as a prop
|
|
221
|
+
|
|
222
|
+
Callbacks do not cross to the browser either. The story owns the state and the
|
|
223
|
+
callbacks, and writes what a test must see into the DOM - a hidden input, or
|
|
224
|
+
the grid's own `data-*` attributes, which already carry most of it.
|
|
225
|
+
|
|
226
|
+
### Counting row elements everywhere
|
|
227
|
+
|
|
228
|
+
Outside the virtualization claim itself, assert `data-dg-row-count`. The
|
|
229
|
+
window size depends on the viewport, overscan and row height, and changes with
|
|
230
|
+
`devices` presets.
|
|
@@ -0,0 +1,240 @@
|
|
|
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'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/testing.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
|
|
19
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/docs/draft-store.md'
|
|
20
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEntryRows.tsx'
|
|
21
|
+
- 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridEditColumn.tsx'
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# TMDataGrid - Testing editing
|
|
25
|
+
|
|
26
|
+
Builds on the `testing` skill: parts are `[data-dg-part]`, coordinates are
|
|
27
|
+
`[data-row-id]` / `[data-column-id]`, and `DataGrid` is its page object, built
|
|
28
|
+
with `DataGrid.byTestId(page, "orders")` or `new DataGrid(rootLocator)`.
|
|
29
|
+
|
|
30
|
+
## The life of an added row
|
|
31
|
+
|
|
32
|
+
`edit.addRow()` opens an entry row keyed by a temporary id. What happens at ✓
|
|
33
|
+
depends on `editing.draft`, and that decides what a test can hold on to:
|
|
34
|
+
|
|
35
|
+
| Step | Without `draft` | With `draft: true` |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `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 |
|
|
38
|
+
| ✓ fails validation | Entry row stays; failing cells carry `data-invalid="true"` | Same |
|
|
39
|
+
| ✓ (`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` |
|
|
40
|
+
| Save (`save-all`) | - | As ✓ without `draft`: temp id gone, row comes back through `data` under the app's id |
|
|
41
|
+
| `newRowsSticky: true` | - | The committed row stays an `entry-row` with `data-committed="true"`, outside `data-dg-row-count`, until Save |
|
|
42
|
+
|
|
43
|
+
The grid never learns the id the app assigns (a database id, a negative
|
|
44
|
+
counter, a uuid - whatever `onRowAdd` or `onSaveDrafts` does). The temp id →
|
|
45
|
+
real id mapping does not exist anywhere in the DOM.
|
|
46
|
+
|
|
47
|
+
## The rule: find an added row by content, not by id
|
|
48
|
+
|
|
49
|
+
Once `onRowAdd` or `saveDrafts` has run, the only thing the test knows about
|
|
50
|
+
the row is what it typed. Narrow the grid to a value unique to that row, assert
|
|
51
|
+
that exactly one row is left, read that row's cells:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
async expectRowAdded(uniqueValue: string, cells: Record<string, string>) {
|
|
55
|
+
await this.search(uniqueValue); // or filterBy on a column when there is no quick search
|
|
56
|
+
await this.expectRowCount(1); // retries until the app has put the row in data
|
|
57
|
+
const row = this.part("row"); // exactly one body row now
|
|
58
|
+
for (const [columnId, text] of Object.entries(cells)) {
|
|
59
|
+
await expect(
|
|
60
|
+
row.locator(`[data-column-id="${columnId}"]:not([data-dg-part])`),
|
|
61
|
+
).toHaveText(text);
|
|
62
|
+
}
|
|
63
|
+
await this.search("");
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- The `data-dg-row-count` assertion is the wait. Nothing else signals that
|
|
68
|
+
`onRowAdd` finished: `aria-busy` reflects `meta.loading` only.
|
|
69
|
+
- Use a value no other row has - put a run id in the name. Two matches means
|
|
70
|
+
`expectRowCount(1)` fails, correctly.
|
|
71
|
+
- Scoped to the grid and to one column: no `page.getByText`, no virtualization
|
|
72
|
+
problem, no strict-mode ambiguity.
|
|
73
|
+
|
|
74
|
+
## Page object methods
|
|
75
|
+
|
|
76
|
+
Add these to the `DataGrid` class from the `testing` skill:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
entryRow(): Locator {
|
|
80
|
+
return this.part("entry-row");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Built-in editors of an open row: an entry row, or a row opened in row mode. */
|
|
84
|
+
async fillRow(rowId: string, values: Record<string, string>): Promise<void> {
|
|
85
|
+
for (const [columnId, value] of Object.entries(values)) {
|
|
86
|
+
await this.part("editor", { rowId, columnId })
|
|
87
|
+
.locator('[data-dg-part="editor-input"]')
|
|
88
|
+
.fill(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async commitEntryRow(rowId: string): Promise<void> {
|
|
93
|
+
await this.part("confirm-new-row", { rowId }).click();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Save in TMDataGrid.DraftActions, waited until the store is empty. */
|
|
97
|
+
async saveDrafts(): Promise<void> {
|
|
98
|
+
await this.part("save-all").click();
|
|
99
|
+
await expect(this.part("save-all")).toHaveAttribute("data-draft-count", "0");
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`fillRow` covers the built-in editors. A column with `meta.edit.editor` renders
|
|
104
|
+
the app's own component inside the `editor` part; scope through
|
|
105
|
+
`part("editor", { rowId, columnId })` and fill whatever is inside.
|
|
106
|
+
|
|
107
|
+
## Recipes
|
|
108
|
+
|
|
109
|
+
### Add a row, without draft
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await page.getByRole("button", { name: "Add row" }).click(); // the app's button → edit.addRow()
|
|
113
|
+
const tempId = (await grid.entryRow().getAttribute("data-row-id"))!;
|
|
114
|
+
await grid.fillRow(tempId, { name: "Nordkvist-4711", city: "Stockholm" });
|
|
115
|
+
await grid.commitEntryRow(tempId);
|
|
116
|
+
|
|
117
|
+
await expect(grid.entryRow()).toHaveCount(0); // entry row gone; tempId is now meaningless
|
|
118
|
+
await grid.expectRowAdded("Nordkvist-4711", { name: "Nordkvist-4711", city: "Stockholm" });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Add a row, with draft, then save
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
await grid.commitEntryRow(tempId);
|
|
125
|
+
const row = grid.part("row", { rowId: tempId }); // tempId is still the row's id
|
|
126
|
+
await expect(row).toHaveAttribute("data-new", "true");
|
|
127
|
+
await expect(row).toHaveAttribute("data-draft", "true");
|
|
128
|
+
await expect(grid.part("row-state", { rowId: tempId })).toHaveAttribute("data-state", "new");
|
|
129
|
+
await expect(grid.part("save-all")).toHaveAttribute("data-draft-count", "1");
|
|
130
|
+
|
|
131
|
+
await grid.saveDrafts();
|
|
132
|
+
await expect(row).toHaveCount(0); // tempId dies here
|
|
133
|
+
await grid.expectRowAdded("Nordkvist-4711", { city: "Stockholm" });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Reopening a committed draft row: `edit-row` (the lane's pencil) or a
|
|
137
|
+
double-click puts it back in the entry block, still under the temp id; ✕
|
|
138
|
+
(`discard-new-row`) removes it.
|
|
139
|
+
|
|
140
|
+
### A ✓ that fails validation
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await grid.commitEntryRow(tempId);
|
|
144
|
+
await expect(grid.entryRow()).toHaveCount(1); // still open
|
|
145
|
+
await expect(
|
|
146
|
+
grid.entryRow().locator('[data-column-id="email"]:not([data-dg-part])'),
|
|
147
|
+
).toHaveAttribute("data-invalid", "true");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The cell and the open `editor` inside it share the column id, so the selector
|
|
151
|
+
leaves the editor out.
|
|
152
|
+
|
|
153
|
+
### Change a cell or a row
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const cell = grid.cell({ rowId: "42", columnId: "salary" });
|
|
157
|
+
await cell.dblclick(); // cell mode: opens the editor
|
|
158
|
+
await grid.fillRow("42", { salary: "52000" });
|
|
159
|
+
await page.keyboard.press("Enter"); // commits
|
|
160
|
+
|
|
161
|
+
// Without draft: onCommit ran, the app applied it, the grid holds nothing
|
|
162
|
+
await expect(cell).toHaveText("52 000");
|
|
163
|
+
|
|
164
|
+
// Under draft: the grid holds it until Save
|
|
165
|
+
await expect(cell).toHaveAttribute("data-dirty", "true");
|
|
166
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-dirty", "true");
|
|
167
|
+
await expect(grid.part("row-state", { rowId: "42" })).toHaveAttribute("data-state", "edited");
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Row mode: `edit-row` opens the row, `fillRow` reaches its editors, `save-row`
|
|
171
|
+
commits, `cancel-row` discards. `cellConfirm` mode: `editor-confirm` /
|
|
172
|
+
`editor-cancel` beside the input, or Enter / Escape.
|
|
173
|
+
|
|
174
|
+
### Delete a row
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const before = Number(await grid.grid.getAttribute("data-dg-row-count"));
|
|
178
|
+
await grid.part("delete-row", { rowId: "42" }).click();
|
|
179
|
+
|
|
180
|
+
// Without draft: onRowDelete ran, the app removed it from data
|
|
181
|
+
await grid.expectRowCount(before - 1);
|
|
182
|
+
|
|
183
|
+
// Under draft: marked until Save; restore-row undoes the mark
|
|
184
|
+
await expect(grid.part("row", { rowId: "42" })).toHaveAttribute("data-deleted", "true");
|
|
185
|
+
await grid.part("restore-row", { rowId: "42" }).click();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Attribute values
|
|
189
|
+
|
|
190
|
+
A state attribute is present, with the value `"true"`, only while it applies:
|
|
191
|
+
`data-new`, `data-draft`, `data-dirty`, `data-deleted` on a body row,
|
|
192
|
+
`data-committed` on an entry row, `data-dirty`, `data-invalid`, `data-editing`
|
|
193
|
+
on a cell. `[data-new]` and `[data-new="true"]` match the same rows,
|
|
194
|
+
`toHaveAttribute("data-new")` and `toHaveAttribute("data-new", "true")` both
|
|
195
|
+
hold, and the negative is `not.toHaveAttribute("data-new")`.
|
|
196
|
+
|
|
197
|
+
## Common mistakes
|
|
198
|
+
|
|
199
|
+
### Looking for the new row's text on the page
|
|
200
|
+
|
|
201
|
+
`page.getByText("Nordkvist")` searches the whole document: other grids, the
|
|
202
|
+
toolbar, a toast. And the row may be virtualized out of the DOM, so the
|
|
203
|
+
locator fails or flakes depending on sort order and viewport. Narrow with
|
|
204
|
+
search or a filter, assert `data-dg-row-count` is 1, read that one row.
|
|
205
|
+
|
|
206
|
+
### Keeping the temp id after ✓ without draft
|
|
207
|
+
|
|
208
|
+
`grid.part("row", { rowId: "__new__1" })` never resolves: without `draft` the
|
|
209
|
+
grid drops the entry and the id at ✓, and the row's real id is the app's. Only
|
|
210
|
+
under `draft: true` does the temp id outlive ✓ - and only until Save.
|
|
211
|
+
|
|
212
|
+
### Waiting for the added row with a timeout
|
|
213
|
+
|
|
214
|
+
The app's `onRowAdd` may be async. `expectRowCount(1)` after narrowing retries
|
|
215
|
+
until the row is in `data`; a `waitForTimeout` either wastes time or is too
|
|
216
|
+
short on CI.
|
|
217
|
+
|
|
218
|
+
### Matching the editor with a cell selector
|
|
219
|
+
|
|
220
|
+
While a cell is open, `[data-row-id="42"][data-column-id="salary"]` matches
|
|
221
|
+
the cell and the `editor` part inside it, and strict mode fails on the pair.
|
|
222
|
+
`DataGrid.cell()` appends `:not([data-dg-part])`; a hand-written locator on a
|
|
223
|
+
row's cells needs the same.
|
|
224
|
+
|
|
225
|
+
### Asserting the negative with `"false"`
|
|
226
|
+
|
|
227
|
+
`toHaveAttribute("data-new", "false")` never passes: a state that does not
|
|
228
|
+
apply is an absent attribute, not `"false"`. Use
|
|
229
|
+
`not.toHaveAttribute("data-new")`, and `:not([data-new])` in CSS.
|
|
230
|
+
|
|
231
|
+
### Asserting a deletion with `toHaveCount(0)`
|
|
232
|
+
|
|
233
|
+
A row outside the viewport has no element either. Assert `data-dg-row-count`
|
|
234
|
+
dropped by one, or, under `draft`, `data-deleted="true"` on the row.
|
|
235
|
+
|
|
236
|
+
### Expecting the grid to show the change without draft
|
|
237
|
+
|
|
238
|
+
Without `draft: true` the grid holds nothing after a commit: no `data-dirty`,
|
|
239
|
+
no `row-state`, `save-all` stays disabled at `data-draft-count="0"`. The
|
|
240
|
+
assertion is the cell's text after the app has applied the change to `data`.
|
|
@@ -32,27 +32,15 @@ export function useTMDataGridContext(): TMDataGridContextValue {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* What a control inside a
|
|
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`
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
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;
|
|
@@ -7,16 +7,17 @@ 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,15 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
|
|
|
55
56
|
"data-testid"?: string;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
60
|
+
// oxlint-disable-next-line react/only-export-components -- exported as the Object.assign compound `TMDataGrid`, which the rule cannot see as a component
|
|
76
61
|
function TMDataGridRoot<TData extends RowData>({
|
|
77
62
|
table,
|
|
78
63
|
ui,
|
|
79
64
|
edit,
|
|
80
65
|
features,
|
|
66
|
+
filters,
|
|
67
|
+
exportOptions,
|
|
81
68
|
labels,
|
|
82
69
|
renderDetails,
|
|
83
70
|
renderDetailsEstHeight,
|
|
@@ -100,6 +87,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
100
87
|
ui,
|
|
101
88
|
edit,
|
|
102
89
|
features,
|
|
90
|
+
filters,
|
|
91
|
+
exportOptions,
|
|
103
92
|
labels,
|
|
104
93
|
renderDetails,
|
|
105
94
|
renderDetailsEstHeight,
|
|
@@ -116,6 +105,8 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
116
105
|
ui,
|
|
117
106
|
edit,
|
|
118
107
|
features,
|
|
108
|
+
filters,
|
|
109
|
+
exportOptions,
|
|
119
110
|
labels,
|
|
120
111
|
renderDetails,
|
|
121
112
|
renderDetailsEstHeight,
|
|
@@ -141,10 +132,33 @@ function TMDataGridRoot<TData extends RowData>({
|
|
|
141
132
|
>
|
|
142
133
|
{children}
|
|
143
134
|
</div>
|
|
135
|
+
{/* Portaled by Mantine, so outside the root element; inside the
|
|
136
|
+
provider, since it reads the grid. */}
|
|
137
|
+
<TMDataGridExportPicker />
|
|
144
138
|
</TMDataGridContextProvider>
|
|
145
139
|
);
|
|
146
140
|
}
|
|
147
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
|
|
144
|
+
* and publishes it to the compound components below it:
|
|
145
|
+
*
|
|
146
|
+
* ```tsx
|
|
147
|
+
* const grid = useTMDataGrid({ data, columns });
|
|
148
|
+
*
|
|
149
|
+
* <TMDataGrid {...grid}>
|
|
150
|
+
* <TMDataGrid.Toolbar>
|
|
151
|
+
* <TMDataGrid.SummaryCount />
|
|
152
|
+
* <TMDataGrid.Spacer />
|
|
153
|
+
* <TMDataGrid.Menu>
|
|
154
|
+
* <TMDataGrid.Menu.Columns />
|
|
155
|
+
* </TMDataGrid.Menu>
|
|
156
|
+
* </TMDataGrid.Toolbar>
|
|
157
|
+
* <TMDataGrid.Table />
|
|
158
|
+
* <TMDataGrid.Footer />
|
|
159
|
+
* </TMDataGrid>
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
148
162
|
export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
149
163
|
Toolbar: TMDataGridToolbar,
|
|
150
164
|
Spacer: TMDataGridToolbarSpacer,
|
|
@@ -152,7 +166,7 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
152
166
|
LoadingIndicator: TMDataGridLoadingIndicator,
|
|
153
167
|
Search: TMDataGridSearch,
|
|
154
168
|
DraftActions: TMDataGridDraftActions,
|
|
155
|
-
|
|
169
|
+
Menu: TMDataGridMenu,
|
|
156
170
|
FilterButton: TMDataGridFilterButton,
|
|
157
171
|
Table: TMDataGridTable,
|
|
158
172
|
Footer: TMDataGridFooter,
|
|
@@ -163,6 +177,9 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
163
177
|
* rendered outside `<TMDataGrid>` - a page header, for instance.
|
|
164
178
|
*/
|
|
165
179
|
FilterPills: TMDataGridFilterPills,
|
|
166
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* The column chooser as plain controls, for a Popover, a Drawer or an
|
|
182
|
+
* inline layout; `TMDataGrid.Menu.Columns` is the same thing as menu items.
|
|
183
|
+
*/
|
|
167
184
|
ColumnsPanel: TMDataGridColumnsPanel,
|
|
168
185
|
});
|