@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -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/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -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 +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- 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 +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- 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 +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- 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 +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -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/components/editors/TMDataGridNumberEditor.tsx +70 -0
- 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 +17 -31
- 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/{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 +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -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}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /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}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.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}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
package/docs/testing.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
The grid publishes a fixed set of roles, ARIA attributes and `data-*` hooks so
|
|
4
|
+
that a consumer's suite can be written against structure rather than against
|
|
5
|
+
copy or class names. Everything on this page is supported. Anything else in the
|
|
6
|
+
DOM is internal and may change without notice.
|
|
7
|
+
|
|
8
|
+
> Renaming or dropping anything on this page is a breaking change, so it moves
|
|
9
|
+
> only with a major version.
|
|
10
|
+
|
|
11
|
+
## Selector attributes
|
|
12
|
+
|
|
13
|
+
The grid publishes three kinds of selector, and no `data-testid` of its own:
|
|
14
|
+
|
|
15
|
+
- `data-dg-part` - what an element is (`"row"`, `"filter-button"`)
|
|
16
|
+
- `data-row-id` and `data-column-id` - which one, using your own ids
|
|
17
|
+
- roles and ARIA - the same thing a screen reader reads
|
|
18
|
+
|
|
19
|
+
`data-testid` is left to your suite, whose `testIdAttribute` may be `data-qa` or
|
|
20
|
+
`data-cy` rather than `data-testid`. The one exception is
|
|
21
|
+
`<TMDataGrid data-testid>`, which sets the value you pass.
|
|
22
|
+
|
|
23
|
+
## Naming a grid
|
|
24
|
+
|
|
25
|
+
Parts repeat across grids on the same page. Name the grid and scope through it:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<TMDataGrid {...grid} data-testid="orders">
|
|
29
|
+
<TMDataGrid.Table<Order> aria-label="Orders" />
|
|
30
|
+
</TMDataGrid>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const orders = page.getByTestId("orders");
|
|
35
|
+
await expect(orders.locator('[data-dg-part="row"][data-row-id="42"]')).toBeVisible();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`data-testid` and `id` go on the root element (which also carries
|
|
39
|
+
`data-dg-root`); `aria-label` (or `aria-labelledby`) goes on
|
|
40
|
+
`TMDataGrid.Table`, because the accessible name belongs to the element carrying
|
|
41
|
+
the `grid` role.
|
|
42
|
+
|
|
43
|
+
## Structure
|
|
44
|
+
|
|
45
|
+
| Element | Role | Attributes |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| Root | - | `data-dg-root`, `data-size` |
|
|
48
|
+
| Grid | `table`, or `grid` under cell selection | `aria-rowcount`, `aria-colcount`, `aria-busy`, `data-dg-row-count` |
|
|
49
|
+
| Header row | `row` | `aria-rowindex` |
|
|
50
|
+
| Header cell | `columnheader` | `data-dg-part="header"`, `data-column-id`, `aria-sort`, `data-active` |
|
|
51
|
+
| Body row | `row` | `data-dg-part="row"`, `data-row-id`, `aria-rowindex`, `data-selected`, `data-highlighted`, `data-grouped`, `data-depth`, `data-pinned`, `data-deleted`, `data-dirty`, `data-draft`, `data-new`, `data-striped` |
|
|
52
|
+
| Body cell | `cell`, or `gridcell` under cell selection | `data-row-id`, `data-column-id`, `data-align`, `data-editing`, `data-dirty`, `data-invalid`, `data-focused`, `data-selected` |
|
|
53
|
+
|
|
54
|
+
**The role changes with cell selection.** `cellSelection` turns the grid's
|
|
55
|
+
`table` into a `grid` and every `cell` into a `gridcell`, so a suite written on
|
|
56
|
+
`getByRole("cell")` breaks when the feature is switched on. Query cells by their
|
|
57
|
+
coordinates instead:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const cell = orders.locator('[data-row-id="42"][data-column-id="total"]');
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Body cells carry no `data-dg-part`; the coordinate pair identifies them.
|
|
64
|
+
|
|
65
|
+
## Parts
|
|
66
|
+
|
|
67
|
+
Row and column ids come from your data (`getRowId` and the column definitions),
|
|
68
|
+
so a part that repeats is addressed by adding the coordinate.
|
|
69
|
+
|
|
70
|
+
### Whole-grid
|
|
71
|
+
|
|
72
|
+
| `data-dg-part` | What it is |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `toolbar` | The toolbar row |
|
|
75
|
+
| `summary-count` | The visible/total count |
|
|
76
|
+
| `loading` | The toolbar spinner |
|
|
77
|
+
| `search`, `search-clear` | Quick search input and its ✕ |
|
|
78
|
+
| `filter-button` | The funnel toggle |
|
|
79
|
+
| `filter-panel` | The panel of filter rows, wherever it is rendered |
|
|
80
|
+
| `filter-popup`, `filter-sidebar` | The surface holding it, under `filters.surface` |
|
|
81
|
+
| `filter-panel-close` | The surface's ✕; absent on a hand-placed panel |
|
|
82
|
+
| `filter-add`, `filter-clear-all` | The panel's footer buttons |
|
|
83
|
+
| `header-filter-row` | The header filter row, under `filters.inHeader` |
|
|
84
|
+
| `filter-pills` | The active-filter pill group |
|
|
85
|
+
| `menu-button` | The burger, `TMDataGrid.Menu` |
|
|
86
|
+
| `columns-panel`, `columns-search` | The column chooser panel, and the search box in the panel or in `TMDataGrid.Menu.Columns` |
|
|
87
|
+
| `columns-toggle-all`, `columns-reset` | Show/hide all and Reset layout, in the panel or in the menu |
|
|
88
|
+
| `footer` | The pager row |
|
|
89
|
+
| `page-size`, `page-range`, `page-number`, `page-prev`, `page-next` | The pager |
|
|
90
|
+
| `summary-row` | The footer summary row |
|
|
91
|
+
| `pinned-top`, `pinned-bottom` | The pinned-row edge blocks |
|
|
92
|
+
| `select-all` | The header select-all checkbox |
|
|
93
|
+
| `details-toggle-all` | Expand/collapse every detail panel |
|
|
94
|
+
| `save-all`, `discard-all` | `TMDataGrid.DraftActions`. `save-all` carries `data-draft-count` - the rows the save will send |
|
|
95
|
+
| `editor-confirm`, `editor-cancel` | `cellConfirm`'s ✓ and ✕ |
|
|
96
|
+
| `editor-input` | The input inside a built-in editor |
|
|
97
|
+
| `sort-index` | A column's position in a multi-column sort |
|
|
98
|
+
| `tab-guard` | The body's tab stop under [cell selection](/docs/cell-selection); `data-guard` is `leading` (before the rows) or `trailing` (after them). Zero-size, and focusing one puts the cursor on a cell |
|
|
99
|
+
|
|
100
|
+
### Keyed by `data-row-id`
|
|
101
|
+
|
|
102
|
+
| `data-dg-part` | What it is |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `row` | A body row, pinned or not. A committed new row is one of them, marked `data-new` |
|
|
105
|
+
| `entry-row` | An entry row being typed into. Carries `data-new`, and `data-committed` / `data-draft` once committed, which is where a committed row stays under `editing.newRowsSticky`. Its cells carry `data-column-id` and, on a failed ✓, `data-invalid` |
|
|
106
|
+
| `details` | A row's detail panel |
|
|
107
|
+
| `select-row` | Its selection checkbox |
|
|
108
|
+
| `details-toggle`, `group-toggle` | Its detail and tree chevrons |
|
|
109
|
+
| `edit-row`, `delete-row` | The edit lane, idle. `edit-row` also reopens an entered new row |
|
|
110
|
+
| `save-row`, `cancel-row` | The edit lane's Save and Cancel on an open row |
|
|
111
|
+
| `row-state` | The draft store's change marker; `data-state` is `new`, `edited` or `deleted` |
|
|
112
|
+
| `revert-row` | Drops a committed row's draft |
|
|
113
|
+
| `restore-row` | Undo a deletion mark |
|
|
114
|
+
| `confirm-new-row`, `discard-new-row` | An entry row's ✓ (commit) and ✕ |
|
|
115
|
+
| `open-rows-note` | `DraftActions`' count of rows still open. Carries `data-open-count`; absent while there are none |
|
|
116
|
+
|
|
117
|
+
### Keyed by `data-column-id`
|
|
118
|
+
|
|
119
|
+
| `data-dg-part` | What it is |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `header` | A column header |
|
|
122
|
+
| `header-sort`, `header-menu`, `header-filter` | Its three action buttons. `header-filter` is absent under `filters.inHeader`, where the control below is the indicator |
|
|
123
|
+
| `header-filter-cell`, `header-filter-operator` | One column's header filter control and its operator button |
|
|
124
|
+
| `filter-row` | One row of the filter panel |
|
|
125
|
+
| `filter-pill` | One active-filter pill; its ✕ is the only button inside it |
|
|
126
|
+
| `columns-toggle` | The checkbox of one column, in the panel or in the menu |
|
|
127
|
+
|
|
128
|
+
### Keyed by both
|
|
129
|
+
|
|
130
|
+
| `data-dg-part` | What it is |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `editor` | An open cell editor |
|
|
133
|
+
|
|
134
|
+
Within a filter row the three controls are `filter-column`,
|
|
135
|
+
`filter-operator` and `filter-value` (or `filter-value-from` /
|
|
136
|
+
`filter-value-to` for `between`).
|
|
137
|
+
|
|
138
|
+
A column declaring `meta.filter.control` or `meta.edit.editor` renders your component
|
|
139
|
+
in that slot, so `filter-value` and `editor-input` cover the built-ins only.
|
|
140
|
+
`filter-row` and `editor` still apply; scope your own queries through them.
|
|
141
|
+
|
|
142
|
+
`editor-input` is also where the grid puts the caret when an editor opens. An
|
|
143
|
+
editor that does not publish it is focused on the first focusable element inside
|
|
144
|
+
its `editor` instead, so a custom editor needs the attribute only to name which
|
|
145
|
+
of several inputs the caret should land in.
|
|
146
|
+
|
|
147
|
+
Every icon-only control also carries an `aria-label` drawn from `labels`. Those
|
|
148
|
+
are translated, so they make brittle selectors. Prefer the parts above unless
|
|
149
|
+
your grid runs in one language.
|
|
150
|
+
|
|
151
|
+
## Virtualization
|
|
152
|
+
|
|
153
|
+
The grid is always virtualized: only the rows in the viewport plus overscan are
|
|
154
|
+
in the DOM. A row at index 500 has no element, and Playwright cannot scroll to
|
|
155
|
+
what it cannot find.
|
|
156
|
+
|
|
157
|
+
**Count rows off the grid, not off the DOM.** `aria-rowcount` includes every
|
|
158
|
+
header row - stacked column groups and the `filters.inHeader` row among them -
|
|
159
|
+
and the summary row, so how many it adds is a function of the grid's
|
|
160
|
+
configuration. `data-dg-row-count` counts the body rows alone: the current page
|
|
161
|
+
under pagination, or everything the filters left otherwise. Assert on that one.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const grid = orders.getByRole("table");
|
|
165
|
+
await expect(grid).toHaveAttribute("data-dg-row-count", "3");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Reach a row by narrowing to it.** Filtering or searching is faster and more
|
|
169
|
+
stable than scrolling:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await orders.locator('[data-dg-part="search"]').fill("Nordkvist");
|
|
173
|
+
await expect(orders.locator('[data-row-id="42"]').first()).toBeVisible();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Or scroll to it.** When the row must be reached in place, such as when testing
|
|
177
|
+
the scroll itself, `scrollToRow` moves the virtualizer:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const found = grid.scrollToRow({ rowId: "42", align: "center" });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It returns `false` when the row is not in the current view (filtered out, on
|
|
184
|
+
another page, or an unknown id) and scrolls nothing. From a Playwright test it
|
|
185
|
+
has to be called through the page, since the API lives in React:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
await page.evaluate(() => window.__ordersGrid.scrollToRow({ rowId: "42" }));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
which requires the app to expose the grid on `window`. Narrowing needs no such
|
|
192
|
+
hook.
|
|
193
|
+
|
|
194
|
+
## Waiting
|
|
195
|
+
|
|
196
|
+
`meta.loading` sets `aria-busy` on the grid whether or not the body has rows, so
|
|
197
|
+
a refetch over existing rows is still visible to a test:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
await expect(grid).toHaveAttribute("aria-busy", "true");
|
|
201
|
+
await expect(grid).not.toHaveAttribute("aria-busy");
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Quick search debounces (250 ms by default, `debounce` on `TMDataGrid.Search`).
|
|
205
|
+
Assert on `data-dg-row-count` rather than adding a timeout. Playwright retries
|
|
206
|
+
the assertion until the debounce lands.
|
|
207
|
+
|
|
208
|
+
## A page object
|
|
209
|
+
|
|
210
|
+
A helper class wraps the parts:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { type Locator, type Page, expect } from "@playwright/test";
|
|
214
|
+
|
|
215
|
+
type PartKey = { rowId?: string; columnId?: string };
|
|
216
|
+
|
|
217
|
+
export class DataGrid {
|
|
218
|
+
readonly root: Locator;
|
|
219
|
+
readonly grid: Locator;
|
|
220
|
+
|
|
221
|
+
constructor(page: Page, testId: string) {
|
|
222
|
+
this.root = page.getByTestId(testId);
|
|
223
|
+
this.grid = this.root.getByRole("table");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A named part, narrowed by row or column when the part repeats. */
|
|
227
|
+
part(name: string, key: PartKey = {}): Locator {
|
|
228
|
+
const selector =
|
|
229
|
+
`[data-dg-part="${name}"]` +
|
|
230
|
+
(key.rowId === undefined ? "" : `[data-row-id="${key.rowId}"]`) +
|
|
231
|
+
(key.columnId === undefined ? "" : `[data-column-id="${key.columnId}"]`);
|
|
232
|
+
return this.root.locator(selector);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
cell({ rowId, columnId }: { rowId: string; columnId: string }): Locator {
|
|
236
|
+
return this.root.locator(
|
|
237
|
+
`[data-row-id="${rowId}"][data-column-id="${columnId}"]`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async search(text: string): Promise<void> {
|
|
242
|
+
await this.part("search").fill(text);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async sortBy(columnId: string): Promise<void> {
|
|
246
|
+
await this.part("header-sort", { columnId }).click();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async filterBy({
|
|
250
|
+
columnId,
|
|
251
|
+
value,
|
|
252
|
+
}: {
|
|
253
|
+
columnId: string;
|
|
254
|
+
value: string;
|
|
255
|
+
}): Promise<void> {
|
|
256
|
+
await this.part("filter-button").click();
|
|
257
|
+
await this.part("filter-row", { columnId })
|
|
258
|
+
.locator('[data-dg-part="filter-value"]')
|
|
259
|
+
.fill(value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async toggleColumn(columnId: string): Promise<void> {
|
|
263
|
+
await this.part("menu-button").click();
|
|
264
|
+
await this.part("columns-toggle", { columnId }).click();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async expectRowCount(count: number): Promise<void> {
|
|
268
|
+
await expect(this.grid).toHaveAttribute(
|
|
269
|
+
"data-dg-row-count",
|
|
270
|
+
String(count),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async expectSettled(): Promise<void> {
|
|
275
|
+
await expect(this.grid).not.toHaveAttribute("aria-busy");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
test("filters to one employee", async ({ page }) => {
|
|
282
|
+
const grid = new DataGrid(page, "employees");
|
|
283
|
+
await page.goto("/employees");
|
|
284
|
+
await grid.expectSettled();
|
|
285
|
+
|
|
286
|
+
await grid.filterBy({ columnId: "lastName", value: "Nordkvist" });
|
|
287
|
+
await grid.expectRowCount(1);
|
|
288
|
+
await expect(grid.cell({ rowId: "42", columnId: "city" })).toHaveText(
|
|
289
|
+
"Stockholm",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## React Testing Library
|
|
295
|
+
|
|
296
|
+
Under jsdom there is no layout, so the virtualizer mounts a handful of rows
|
|
297
|
+
whatever the data says. Assert on the grid's own counts rather than on the
|
|
298
|
+
number of row elements:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
const rowCount = Number(
|
|
302
|
+
screen.getByRole("table").getAttribute("data-dg-row-count"),
|
|
303
|
+
);
|
|
304
|
+
expect(rowCount).toBe(3);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Mantine's transitions never settle under jsdom, so a Popover's dropdown mounts
|
|
308
|
+
empty, including the filter and column panels. Render inside
|
|
309
|
+
`<MantineProvider env="test">`.
|
package/docs/toolbar.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Toolbar
|
|
2
|
+
|
|
3
|
+
The toolbar is a flex row. Your own buttons are children of it, beside the
|
|
4
|
+
built-in ones; there is no slots API and no `actions` prop.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
<TMDataGrid.Toolbar>
|
|
8
|
+
<TMDataGrid.SummaryCount />
|
|
9
|
+
<TMDataGrid.Spacer />
|
|
10
|
+
<TMDataGrid.LoadingIndicator />
|
|
11
|
+
<Button size="xs" variant="light" onClick={exportAll}>
|
|
12
|
+
Export
|
|
13
|
+
</Button>
|
|
14
|
+
<TMDataGrid.FilterButton />
|
|
15
|
+
<TMDataGrid.Menu>
|
|
16
|
+
<TMDataGrid.Menu.Columns />
|
|
17
|
+
</TMDataGrid.Menu>
|
|
18
|
+
</TMDataGrid.Toolbar>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`TMDataGrid.Spacer` pushes everything after it to the right.
|
|
22
|
+
`TMDataGrid.Menu` is the burger; it holds the column chooser here, and anything else you put in it.
|
|
23
|
+
See [Grid menu](/docs/menu).
|
|
24
|
+
|
|
25
|
+
```demo
|
|
26
|
+
file: customization/ToolbarComposition.tsx
|
|
27
|
+
extraSources: data/employeeColumns.tsx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Only the parts you render exist. A grid with no `Toolbar` has no toolbar; a
|
|
31
|
+
toolbar with only `Search` has only a search box.
|
|
32
|
+
|
|
33
|
+
## Style props
|
|
34
|
+
|
|
35
|
+
`TMDataGrid.Toolbar` and `TMDataGrid.Spacer` take Mantine's `BoxProps`: the style props (`mb`, `px`, `h`, `hiddenFrom`, …), `className`, `style` and `mod`, set on the element itself.
|
|
36
|
+
`withBottomBorder` draws a 1px line under the toolbar in the theme's default border colour, the same line the header draws under itself.
|
|
37
|
+
It defaults to `false`.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<TMDataGrid.Toolbar withBottomBorder px="sm">
|
|
41
|
+
<TMDataGrid.SummaryCount />
|
|
42
|
+
<TMDataGrid.Spacer hiddenFrom="sm" />
|
|
43
|
+
<TMDataGrid.FilterButton />
|
|
44
|
+
</TMDataGrid.Toolbar>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`TMDataGrid.Footer`, `TMDataGrid.FilterPanel`, `TMDataGrid.FilterPills` and `TMDataGrid.ColumnsPanel` take the same `BoxProps`; see [Components](/docs/components).
|
|
48
|
+
|
|
49
|
+
## The built-in parts
|
|
50
|
+
|
|
51
|
+
| Component | Shows |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `TMDataGrid.Search` | The [quick search](/docs/quick-search) input |
|
|
54
|
+
| `TMDataGrid.FilterButton` | Opens the [filter panel](/docs/filtering), with the active count |
|
|
55
|
+
| `TMDataGrid.Menu` | The burger: a [menu](/docs/menu) you fill; `TMDataGrid.Menu.Columns` is the column chooser as items |
|
|
56
|
+
| `TMDataGrid.SummaryCount` | Visible rows out of total, or the count alone where there is no total to compare it against |
|
|
57
|
+
| `TMDataGrid.LoadingIndicator` | A spinner while `meta.loading` |
|
|
58
|
+
| `TMDataGrid.Spacer` | Pushes what follows to the right |
|
|
59
|
+
| `TMDataGridDraftActions` | Save and Discard for the [draft store](/docs/editing#the-draft-store) |
|
|
60
|
+
|
|
61
|
+
Each renders nothing when its feature is off, so a read-only grid needs no
|
|
62
|
+
conditionals in the toolbar: `FilterButton` under `enableColumnFilters: false`
|
|
63
|
+
renders nothing at all.
|
|
64
|
+
`TMDataGrid.Menu` is the exception, since it cannot see what its children render; see [Grid menu](/docs/menu).
|
|
65
|
+
|
|
66
|
+
## Buttons of your own
|
|
67
|
+
|
|
68
|
+
A button that acts on the grid reads it from context:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useTMDataGridContext } from "@jielga/tmdatagrid";
|
|
72
|
+
|
|
73
|
+
function ClearFiltersButton() {
|
|
74
|
+
const { table } = useTMDataGridContext();
|
|
75
|
+
return (
|
|
76
|
+
<Button size="xs" onClick={() => table.resetColumnFilters()}>
|
|
77
|
+
Clear filters
|
|
78
|
+
</Button>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Anything rendered inside `TMDataGrid` can call it. It returns
|
|
84
|
+
`{ table, ui, features, filters, exportOptions, labels, controlSize, resetSettings }`.
|
|
85
|
+
|
|
86
|
+
An export button uses `useTMDataGridExport`, which hands back the export as click handlers:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useTMDataGridExport } from "@jielga/tmdatagrid";
|
|
90
|
+
|
|
91
|
+
function ExportButton() {
|
|
92
|
+
const { exportAll } = useTMDataGridExport();
|
|
93
|
+
return (
|
|
94
|
+
<Button size="xs" onClick={() => void exportAll()}>
|
|
95
|
+
Export
|
|
96
|
+
</Button>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It writes every filtered row, whatever the cell selection is or whether cell selection is on at all.
|
|
102
|
+
The format, the file name and the selected-rows variant are on [Export](/docs/export).
|
|
103
|
+
|
|
104
|
+
### Hide a button when its feature is off
|
|
105
|
+
|
|
106
|
+
The built-in parts disappear when their feature is off. Your own can use the
|
|
107
|
+
same checks instead of re-deriving them:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { getGridCapabilities, useTMDataGridContext } from "@jielga/tmdatagrid";
|
|
111
|
+
|
|
112
|
+
function ExportButton() {
|
|
113
|
+
const { table, features } = useTMDataGridContext();
|
|
114
|
+
const { canFilterAny } = getGridCapabilities(table, features);
|
|
115
|
+
|
|
116
|
+
if (!canFilterAny) return null;
|
|
117
|
+
// …
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Field | True when |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `canSortAny` | At least one leaf column can be sorted |
|
|
124
|
+
| `canFilterAny` | At least one leaf column can be filtered |
|
|
125
|
+
| `canHideAny` | At least one leaf column can be hidden |
|
|
126
|
+
| `canPinAny` | At least one leaf column can be pinned |
|
|
127
|
+
| `canReorderAny` | At least one leaf column can be moved |
|
|
128
|
+
| `canGroupAny` | At least one leaf column can be grouped on |
|
|
129
|
+
| `canSelectRows` | `enableRowSelection` is not `false` and the mode is not `"highlight"` |
|
|
130
|
+
| `canPaginate` | Paging is configured. See [`isPagingActive`](/docs/pagination#grouping) for whether it is slicing anything |
|
|
131
|
+
| `canSearch` | At least one leaf column takes part in the quick search |
|
|
132
|
+
|
|
133
|
+
`getColumnCapabilities(column, features)` returns the same for one column, as
|
|
134
|
+
`canSort`, `canFilter`, `canHide`, `canPin`, `canResize`, `canReorder` and
|
|
135
|
+
`canGroup`.
|
|
136
|
+
|
|
137
|
+
### Reading options reactively
|
|
138
|
+
|
|
139
|
+
`features` is re-derived from the options object on every render, and both
|
|
140
|
+
capability helpers take it alongside the table or column. A bare
|
|
141
|
+
`column.getCanSort()` is memoized against a column identity that survives an
|
|
142
|
+
options change, so a grid whose `enableSorting` turned `false` would keep
|
|
143
|
+
rendering sort indicators; `features` is the value that changes.
|
|
144
|
+
|
|
145
|
+
Read state through TanStack Store's
|
|
146
|
+
[`useSelector(table.store, …)`](https://tanstack.com/store/latest/docs/framework/react/reference)
|
|
147
|
+
and options through `features`, rather than calling methods on a long-lived
|
|
148
|
+
object.
|
|
149
|
+
|
|
150
|
+
## Reference
|
|
151
|
+
|
|
152
|
+
| Name | Kind | Type | Default | What it does |
|
|
153
|
+
| --- | --- | --- | --- | --- |
|
|
154
|
+
| `TMDataGrid.Toolbar` | Component | `children`, `withBottomBorder`, Mantine `BoxProps` | `withBottomBorder: false` | The flex row above the grid. Style props set on the row. |
|
|
155
|
+
| `TMDataGrid.Spacer` | Component | Mantine `BoxProps` | – | Pushes what follows to the right. |
|
|
156
|
+
| `TMDataGrid.FilterButton` | Component | – | – | Opens the filter panel, with an active count. |
|
|
157
|
+
| `TMDataGrid.Menu` | Component | `children` | – | The burger and its dropdown. See [Grid menu](/docs/menu). |
|
|
158
|
+
| `useTMDataGridContext` | Hook | `() => TMDataGridContextValue` | – | `{ table, ui, features, labels, controlSize, resetSettings }`. |
|
|
159
|
+
| `getGridCapabilities` | Export | `(table, features) => TMDataGridCapabilities` | – | What this grid can do. Reactive to option changes. |
|
|
160
|
+
| `getColumnCapabilities` | Export | `(column, features) => TMDataGridColumnCapabilities` | – | The same for one column. |
|
|
161
|
+
| `readFeatureFlags` | Export | `(options) => TMDataGridFeatureFlags` | – | Derives the flags from an options object. |
|