@jielga/tmdatagrid 2.1.4 → 2.3.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/dist/index.d.ts +78 -5
- package/dist/index.js +2194 -1992
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/cell-selection.md +1 -1
- package/docs/column-layout.md +13 -8
- package/docs/columns.md +59 -3
- package/docs/components.md +1 -0
- package/docs/export.md +2 -2
- package/docs/filtering.md +108 -15
- package/docs/getting-started.md +40 -0
- package/docs/grouping.md +9 -6
- package/docs/loading-and-empty.md +29 -9
- package/docs/localization.md +130 -3
- package/docs/menu.md +1 -1
- package/docs/pagination.md +26 -7
- package/docs/persistence.md +101 -2
- package/docs/row-details.md +41 -4
- package/docs/row-interaction.md +2 -2
- package/docs/row-selection.md +6 -7
- package/docs/row-styling.md +17 -0
- package/docs/scrolling.md +1 -1
- package/docs/server-side.md +3 -1
- package/docs/testing.md +18 -0
- package/docs/use-tm-data-grid.md +75 -36
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +6 -4
- package/skills/appearance/references/labels.md +126 -0
- package/skills/cell-selection/SKILL.md +2 -2
- package/skills/columns/SKILL.md +44 -43
- package/skills/columns/references/columns-api.md +16 -1
- package/skills/data/SKILL.md +77 -50
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +94 -60
- package/skills/filtering/references/custom-filters.md +118 -0
- package/skills/getting-started/SKILL.md +37 -2
- package/skills/grouping/SKILL.md +16 -9
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +206 -55
- package/skills/rows/SKILL.md +70 -39
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +12 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridFilterPills.module.css +9 -7
- package/src/components/TMDataGridTable.module.css +15 -6
- package/src/components/TMDataGridTable.tsx +253 -41
- package/src/components/sticky.module.css +27 -7
- package/src/core/autosize.ts +19 -7
- package/src/core/cellRange.ts +11 -2
- package/src/core/colSpan.ts +78 -0
- package/src/core/columnUtils.ts +7 -0
- package/src/core/filterControls.ts +8 -0
- package/src/core/filterOperators.ts +29 -1
- package/src/core/persistence.ts +322 -26
- package/src/index.ts +5 -0
- package/src/useTMDataGrid.tsx +133 -14
package/skills/testing/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ description: >
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: core
|
|
15
15
|
library: '@jielga/tmdatagrid'
|
|
16
|
-
library_version: '2.
|
|
16
|
+
library_version: '2.3.0'
|
|
17
17
|
sources:
|
|
18
18
|
- 'Jielga/react:packages/tmdatagrid/docs/testing.md'
|
|
19
19
|
- 'Jielga/react:playwright/support/DataGrid.ts'
|
|
@@ -104,6 +104,17 @@ match the same rows, and the negative is `:not([data-new])` in CSS and
|
|
|
104
104
|
|
|
105
105
|
**Keyed by both**: `editor`.
|
|
106
106
|
|
|
107
|
+
**The generated lanes** are columns with exported ids: `ROW_NUMBER_COLUMN_ID`
|
|
108
|
+
(`__rowNumber__`), `SELECT_COLUMN_ID` (`__select__`), `GROUP_COLUMN_ID`
|
|
109
|
+
(`__group__`), `DETAILS_COLUMN_ID` (`__details__`) and `EDIT_COLUMN_ID`
|
|
110
|
+
(`__edit__`). A lane's header is
|
|
111
|
+
`[data-dg-part="header"][data-column-id="__select__"]`, a lane cell
|
|
112
|
+
`[data-row-id="42"][data-column-id="__select__"]:not([data-dg-part])`, and the
|
|
113
|
+
control inside it has a row-keyed part: `select-row`, `details-toggle`,
|
|
114
|
+
`group-toggle`, the edit lane's buttons. `select-all` and `details-toggle-all`
|
|
115
|
+
sit in the lane headers. The checkbox parts are on the `<input>` itself, so
|
|
116
|
+
`check()` works on them directly. There is no `data-testid`.
|
|
117
|
+
|
|
107
118
|
Inside a `filter-row` the controls are `filter-column`, `filter-operator` and
|
|
108
119
|
`filter-value` - or `filter-value-from` / `filter-value-to` for `between` - and
|
|
109
120
|
`filter-remove` is its ✕. None of them carries `data-column-id`; scope through
|
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: '@jielga/tmdatagrid'
|
|
15
|
-
library_version: '2.
|
|
15
|
+
library_version: '2.3.0'
|
|
16
16
|
sources:
|
|
17
17
|
- 'Jielga/react:packages/tmdatagrid/docs/testing.md'
|
|
18
18
|
- 'Jielga/react:packages/tmdatagrid/test/gallery/main.tsx'
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
shape readable against any surface. Scoped through `.filterPills` for the
|
|
14
14
|
specificity to beat Mantine's own rule whichever stylesheet loads last. */
|
|
15
15
|
.filterPills .pill {
|
|
16
|
-
background-color:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
background-color: var(--mantine-color-gray-1);
|
|
17
|
+
color: var(--mantine-color-black);
|
|
18
|
+
border: 1px solid var(--mantine-color-gray-3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:where([data-mantine-color-scheme="dark"]) .filterPills .pill {
|
|
22
|
+
background-color: var(--mantine-color-dark-5);
|
|
23
|
+
color: var(--mantine-color-dark-0);
|
|
24
|
+
border-color: var(--mantine-color-dark-3);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* The label is a button so a pill can be clicked back open, but it must read as
|
|
@@ -84,7 +84,13 @@
|
|
|
84
84
|
--row-bg: var(--dg-row-new-bg, var(--mantine-color-body));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
/* The hover tint, on the rows that carry `data-highlight-on-hover` - by default
|
|
88
|
+
the rows that answer a click or a right-click, see the Table's
|
|
89
|
+
`highlightOnHover`. `:where()` adds no specificity, so the rule stays at the
|
|
90
|
+
weight of the state rules around it: declared after the stripe and the entry
|
|
91
|
+
tint, which it wins, and before the selection and highlight rules, which win
|
|
92
|
+
over it. */
|
|
93
|
+
.bodyRow:where([data-highlight-on-hover="true"]):hover {
|
|
88
94
|
--row-bg: var(--mantine-color-default-hover);
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -320,7 +326,9 @@
|
|
|
320
326
|
/* Base and tint, exactly as .bodyCell paints them. */
|
|
321
327
|
background-color: var(--mantine-color-body);
|
|
322
328
|
background-image: linear-gradient(var(--row-bg), var(--row-bg));
|
|
323
|
-
padding:
|
|
329
|
+
/* No padding: the panel is the user's content, and a nested grid or a tab
|
|
330
|
+
strip wants the full width. Content that should line up with the cell
|
|
331
|
+
text pads itself with var(--dg-padding). */
|
|
324
332
|
/* The row above may be the selection control, which turns text selection off
|
|
325
333
|
for everything inside it. A panel is content, so it takes both back. */
|
|
326
334
|
user-select: text;
|
|
@@ -477,14 +485,15 @@
|
|
|
477
485
|
the browser's default so the colour is the grid's to state - override with
|
|
478
486
|
`--dg-match-highlight-bg` on the grid. */
|
|
479
487
|
.matchHighlight {
|
|
480
|
-
background-color: var(
|
|
481
|
-
--dg-match-highlight-bg,
|
|
482
|
-
light-dark(var(--mantine-color-yellow-2), rgba(250, 176, 5, 0.35))
|
|
483
|
-
);
|
|
488
|
+
background-color: var(--dg-match-highlight-bg, var(--mantine-color-yellow-2));
|
|
484
489
|
color: inherit;
|
|
485
490
|
border-radius: 2px;
|
|
486
491
|
}
|
|
487
492
|
|
|
493
|
+
:where([data-mantine-color-scheme="dark"]) .matchHighlight {
|
|
494
|
+
background-color: var(--dg-match-highlight-bg, rgba(250, 176, 5, 0.35));
|
|
495
|
+
}
|
|
496
|
+
|
|
488
497
|
/* The summary row - one sticky row of column footers along the bottom edge,
|
|
489
498
|
rendered only while at least one column defines `footer`. Same subgrid
|
|
490
499
|
placement as the header, so its cells sit exactly on the body's tracks.
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "./TMDataGridCellEditor";
|
|
42
42
|
import { TMDataGridEntryRows } from "./TMDataGridEntryRows";
|
|
43
43
|
import {
|
|
44
|
+
DEFAULT_COLUMN_MIN_SIZE,
|
|
44
45
|
getColumnAlign,
|
|
45
46
|
isControlColumn,
|
|
46
47
|
isGeneratedColumn,
|
|
@@ -76,6 +77,12 @@ import {
|
|
|
76
77
|
writeExportFile,
|
|
77
78
|
} from "../core/export";
|
|
78
79
|
import { autosizeColumn, hasMountedCells } from "../core/autosize";
|
|
80
|
+
import {
|
|
81
|
+
resolveColSpans,
|
|
82
|
+
spanAnchorIndex,
|
|
83
|
+
spanEndIndex,
|
|
84
|
+
type TMDataGridColumnSection,
|
|
85
|
+
} from "../core/colSpan";
|
|
79
86
|
import {
|
|
80
87
|
getDisplayedRows,
|
|
81
88
|
getSelectableRowIds,
|
|
@@ -83,6 +90,7 @@ import {
|
|
|
83
90
|
resolveRowSelectionClick,
|
|
84
91
|
} from "../core/rowSelection";
|
|
85
92
|
import { readPinnedRows } from "../core/rowPinning";
|
|
93
|
+
import { filterNarrowsRows } from "../core/filterOperators";
|
|
86
94
|
import { buildMatchNeedles, findMatchRanges } from "../core/matchHighlight";
|
|
87
95
|
import { ROW_NUMBER_COLUMN_ID } from "./TMDataGridRowNumberColumn";
|
|
88
96
|
import { SearchIcon } from "./icons";
|
|
@@ -253,6 +261,7 @@ function withMatchHighlights(
|
|
|
253
261
|
function TMDataGridBodyCell({
|
|
254
262
|
cell,
|
|
255
263
|
rowHeight,
|
|
264
|
+
colSpan = 1,
|
|
256
265
|
layout,
|
|
257
266
|
nav,
|
|
258
267
|
editor,
|
|
@@ -267,6 +276,8 @@ function TMDataGridBodyCell({
|
|
|
267
276
|
}: {
|
|
268
277
|
cell: Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
269
278
|
rowHeight: number;
|
|
279
|
+
/** Columns this cell covers - see `meta.colSpan`. */
|
|
280
|
+
colSpan?: number;
|
|
270
281
|
layout: TMDataGridColumnLayout;
|
|
271
282
|
nav?: TMDataGridCellNav;
|
|
272
283
|
/** The mounted editor, when this is the cell being edited. */
|
|
@@ -332,6 +343,10 @@ function TMDataGridBodyCell({
|
|
|
332
343
|
// than always.
|
|
333
344
|
role={nav ? "gridcell" : "cell"}
|
|
334
345
|
aria-colindex={nav?.columnIndex}
|
|
346
|
+
aria-colspan={colSpan > 1 ? colSpan : undefined}
|
|
347
|
+
// Published for styling and for autosize, which leaves a spanning cell
|
|
348
|
+
// out of its column's measurement.
|
|
349
|
+
data-col-span={colSpan > 1 ? colSpan : undefined}
|
|
335
350
|
tabIndex={nav?.tabIndex}
|
|
336
351
|
// The marker the delegated key handler tests: a key event whose target is
|
|
337
352
|
// a cell is the grid's, one from inside a cell belongs to whatever holds
|
|
@@ -381,6 +396,7 @@ function TMDataGridBodyCell({
|
|
|
381
396
|
.join(" ")}
|
|
382
397
|
style={{
|
|
383
398
|
minHeight: rowHeight,
|
|
399
|
+
gridColumn: colSpan > 1 ? `span ${colSpan}` : undefined,
|
|
384
400
|
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
385
401
|
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
386
402
|
// The focus ring is drawn inside the cell's own box, so the cell has to
|
|
@@ -615,6 +631,14 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
615
631
|
* stay put while the virtualizer mounts and unmounts rows.
|
|
616
632
|
*/
|
|
617
633
|
striped?: boolean;
|
|
634
|
+
/**
|
|
635
|
+
* Whether a body row takes the hover tint under the pointer. Unset, a row
|
|
636
|
+
* takes it when the row itself answers a gesture: `selectionMode: "row"`,
|
|
637
|
+
* a highlight mode, `onRowClick` or `renderRowContextMenu`. Group rows sit
|
|
638
|
+
* out those gestures and so sit out the tint too. `true` tints every row,
|
|
639
|
+
* `false` none. The row carries `data-highlight-on-hover` while it applies.
|
|
640
|
+
*/
|
|
641
|
+
highlightOnHover?: boolean;
|
|
618
642
|
/**
|
|
619
643
|
* Called when the scroll *arrives* at an edge - once per arrival, not per
|
|
620
644
|
* scroll event, and not on mount (the grid starts at the top-left edge).
|
|
@@ -735,6 +759,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
735
759
|
rowClassName,
|
|
736
760
|
rowStyle,
|
|
737
761
|
striped = false,
|
|
762
|
+
highlightOnHover,
|
|
738
763
|
onScrollToTop,
|
|
739
764
|
onScrollToBottom,
|
|
740
765
|
onScrollToLeft,
|
|
@@ -1182,6 +1207,111 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1182
1207
|
...rightLeafColumns,
|
|
1183
1208
|
];
|
|
1184
1209
|
|
|
1210
|
+
// `meta.colSpan`, resolved per row over the displayed columns. Built from
|
|
1211
|
+
// the three leaf arrays rather than `column.getIsPinned()`, for the reason
|
|
1212
|
+
// the sticky offsets below give. `null` while no column asks to span, so a
|
|
1213
|
+
// plain grid never calls into it.
|
|
1214
|
+
const spanColumns = orderedColumns.some(
|
|
1215
|
+
(column) => column.columnDef.meta?.colSpan !== undefined,
|
|
1216
|
+
)
|
|
1217
|
+
? orderedColumns.map((column, index) => ({
|
|
1218
|
+
id: column.id,
|
|
1219
|
+
colSpan: column.columnDef.meta?.colSpan,
|
|
1220
|
+
section: (index < leftLeafColumns.length
|
|
1221
|
+
? "left"
|
|
1222
|
+
: index < leftLeafColumns.length + centerLeafColumns.length
|
|
1223
|
+
? "center"
|
|
1224
|
+
: "right") as TMDataGridColumnSection,
|
|
1225
|
+
}))
|
|
1226
|
+
: null;
|
|
1227
|
+
/**
|
|
1228
|
+
* Row mode opens every editable cell of the row at once, and rows
|
|
1229
|
+
* accumulate: a second row opening leaves the first one open, so every row
|
|
1230
|
+
* holding a form shows its editors. Which of them holds the caret is not
|
|
1231
|
+
* decided here - see placeEditCaret, which places it once per gesture.
|
|
1232
|
+
*
|
|
1233
|
+
* A parked row is not one of them. It keeps its form, so it is still
|
|
1234
|
+
* "open", but it has been decided: it renders its draft through the
|
|
1235
|
+
* column's own renderer until `begin` reopens it. A committed entry row in
|
|
1236
|
+
* the body has a form too - every entry row does - but it is a value row
|
|
1237
|
+
* until reopened, and reopening takes it back to the entry block.
|
|
1238
|
+
*
|
|
1239
|
+
* Such a row also drops its spans: the form has an editor in every column.
|
|
1240
|
+
*/
|
|
1241
|
+
const isRowEditing = (row: Row<TMDataGridFeatures, TMDataGridRowData>) =>
|
|
1242
|
+
features.editMode === "row" &&
|
|
1243
|
+
editOpenRowIdSet.has(row.id) &&
|
|
1244
|
+
!editDraftRowIdSet.has(row.id) &&
|
|
1245
|
+
!newRowIdSet.has(row.id);
|
|
1246
|
+
const colSpanCache = new Map<string, Array<number> | null>();
|
|
1247
|
+
/**
|
|
1248
|
+
* The spans of one row - see {@link resolveColSpans} - or `null` when every
|
|
1249
|
+
* cell is one column wide. Group rows keep their cells: the tree cell and
|
|
1250
|
+
* the aggregates are the grid's, not a record's.
|
|
1251
|
+
*/
|
|
1252
|
+
const colSpansOf = (
|
|
1253
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1254
|
+
): Array<number> | null => {
|
|
1255
|
+
if (spanColumns === null || row.getIsGrouped() || isRowEditing(row)) {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const cached = colSpanCache.get(row.id);
|
|
1259
|
+
if (cached !== undefined) return cached;
|
|
1260
|
+
const spans = resolveColSpans(spanColumns, (index) => {
|
|
1261
|
+
const colSpan = spanColumns[index]?.colSpan;
|
|
1262
|
+
return typeof colSpan === "function"
|
|
1263
|
+
? colSpan({ row, table })
|
|
1264
|
+
: (colSpan ?? 1);
|
|
1265
|
+
});
|
|
1266
|
+
// The open cell editor lives in its column's cell. A span landing on that
|
|
1267
|
+
// column - the row expanding under the edit, say - would unmount the
|
|
1268
|
+
// editor without closing it, so the row keeps its cells until the edit
|
|
1269
|
+
// ends.
|
|
1270
|
+
const editIndex =
|
|
1271
|
+
editActive !== null && editActive.rowId === row.id
|
|
1272
|
+
? orderedColumns.findIndex(
|
|
1273
|
+
(column) => column.id === editActive.columnId,
|
|
1274
|
+
)
|
|
1275
|
+
: -1;
|
|
1276
|
+
const resolved =
|
|
1277
|
+
spans.every((span) => span === 1) ||
|
|
1278
|
+
(editIndex >= 0 && spans[editIndex] === 0)
|
|
1279
|
+
? null
|
|
1280
|
+
: spans;
|
|
1281
|
+
colSpanCache.set(row.id, resolved);
|
|
1282
|
+
return resolved;
|
|
1283
|
+
};
|
|
1284
|
+
/** Whether the column at `columnIndex` is covered by a span on `row`. */
|
|
1285
|
+
const isCoveredCell = (
|
|
1286
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1287
|
+
columnIndex: number,
|
|
1288
|
+
): boolean => {
|
|
1289
|
+
const spans = colSpansOf(row);
|
|
1290
|
+
return spans !== null && spans[columnIndex] === 0;
|
|
1291
|
+
};
|
|
1292
|
+
/**
|
|
1293
|
+
* A position moved onto the cell that renders it, under spans: the input
|
|
1294
|
+
* itself when its cell renders, so a caller can compare by identity. `row`
|
|
1295
|
+
* is the position's row; `undefined` for a pinned row, which is not in
|
|
1296
|
+
* `rows` and keeps its position as it is.
|
|
1297
|
+
*/
|
|
1298
|
+
const toRenderedCell = (
|
|
1299
|
+
position: TMDataGridCellPosition,
|
|
1300
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData> | undefined,
|
|
1301
|
+
): TMDataGridCellPosition => {
|
|
1302
|
+
if (spanColumns === null || row === undefined) return position;
|
|
1303
|
+
const spans = colSpansOf(row);
|
|
1304
|
+
if (spans === null) return position;
|
|
1305
|
+
const columnIndex = orderedColumns.findIndex(
|
|
1306
|
+
(column) => column.id === position.columnId,
|
|
1307
|
+
);
|
|
1308
|
+
if (columnIndex < 0) return position;
|
|
1309
|
+
const anchorIndex = spanAnchorIndex(spans, columnIndex);
|
|
1310
|
+
if (anchorIndex === columnIndex) return position;
|
|
1311
|
+
const anchor = orderedColumns[anchorIndex];
|
|
1312
|
+
return anchor ? { ...position, columnId: anchor.id } : position;
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1185
1315
|
// Headers and cells must follow the same left → center → right order as the
|
|
1186
1316
|
// column tracks, otherwise pinning a column would shuffle it out of its lane.
|
|
1187
1317
|
const centerHeaderGroups = table.getCenterHeaderGroups();
|
|
@@ -1208,7 +1338,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1208
1338
|
const isFixed = column.columnDef.minSize === column.columnDef.maxSize;
|
|
1209
1339
|
const isResized = column.id in columnSizing;
|
|
1210
1340
|
const isPinned = column.getIsPinned() !== false;
|
|
1211
|
-
const minWidth = column.columnDef.minSize ??
|
|
1341
|
+
const minWidth = column.columnDef.minSize ?? DEFAULT_COLUMN_MIN_SIZE;
|
|
1212
1342
|
if (isFixed || isResized || isPinned) {
|
|
1213
1343
|
return {
|
|
1214
1344
|
id: column.id,
|
|
@@ -1269,6 +1399,31 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1269
1399
|
}
|
|
1270
1400
|
const layoutFor = (columnId: string) =>
|
|
1271
1401
|
columnLayout.get(columnId) ?? UNPINNED_LAYOUT;
|
|
1402
|
+
/**
|
|
1403
|
+
* The layout of a cell spanning from `columnId` to the column at
|
|
1404
|
+
* `lastIndex`. A span never leaves its pinned block, so only the sticky
|
|
1405
|
+
* offset and the boundary shadow change: a right-pinned cell sticks at the
|
|
1406
|
+
* offset of the last column it covers, and the cell covering a block's
|
|
1407
|
+
* boundary column carries its shadow.
|
|
1408
|
+
*/
|
|
1409
|
+
const spanLayout = (
|
|
1410
|
+
firstIndex: number,
|
|
1411
|
+
lastIndex: number,
|
|
1412
|
+
): TMDataGridColumnLayout => {
|
|
1413
|
+
const first = layoutFor(orderedColumns[firstIndex]!.id);
|
|
1414
|
+
if (first.pinnedAt === false) return first;
|
|
1415
|
+
const covered = orderedColumns
|
|
1416
|
+
.slice(firstIndex, lastIndex + 1)
|
|
1417
|
+
.map((column) => layoutFor(column.id));
|
|
1418
|
+
return {
|
|
1419
|
+
...first,
|
|
1420
|
+
offset:
|
|
1421
|
+
first.pinnedAt === "right"
|
|
1422
|
+
? (covered.at(-1)?.offset ?? first.offset)
|
|
1423
|
+
: first.offset,
|
|
1424
|
+
isBoundary: covered.some((layout) => layout.isBoundary),
|
|
1425
|
+
};
|
|
1426
|
+
};
|
|
1272
1427
|
|
|
1273
1428
|
/**
|
|
1274
1429
|
* A running resize drag paints itself, straight onto the grid element.
|
|
@@ -1332,11 +1487,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1332
1487
|
// Pinned rows count as content: with every visible row pinned to an edge,
|
|
1333
1488
|
// an empty-state message beside them would contradict what is on screen.
|
|
1334
1489
|
const isEmpty = rows.length === 0 && pinnedRowCount === 0;
|
|
1335
|
-
// Which emptiness this is.
|
|
1336
|
-
//
|
|
1490
|
+
// Which emptiness this is. A column filter counts by the rule the pills, the
|
|
1491
|
+
// header indicator and the page reset use, so the grid has one definition
|
|
1492
|
+
// of "active" - see `filterNarrowsRows`. The half-typed
|
|
1493
|
+
// `{ operator, value: "" }` the filter panel seeds filters nothing. The body
|
|
1494
|
+
// already subscribes to the whole table store, so these reads are reactive.
|
|
1337
1495
|
const emptyGlobalFilter = table.store.state.globalFilter;
|
|
1338
1496
|
const hasActiveFilters =
|
|
1339
|
-
table.store.state.columnFilters.
|
|
1497
|
+
table.store.state.columnFilters.some(filterNarrowsRows) ||
|
|
1340
1498
|
(typeof emptyGlobalFilter === "string"
|
|
1341
1499
|
? emptyGlobalFilter.trim() !== ""
|
|
1342
1500
|
: emptyGlobalFilter != null);
|
|
@@ -1612,11 +1770,15 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1612
1770
|
findCellElement(container, tabStopCell)?.focus({ preventScroll: true });
|
|
1613
1771
|
};
|
|
1614
1772
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1773
|
+
/**
|
|
1774
|
+
* Whether the focused cell is on this row, in columns `first` to `last` -
|
|
1775
|
+
* one column for most cells, every column it covers for a spanning one.
|
|
1776
|
+
*/
|
|
1777
|
+
const isFocusedWithin = (rowId: string, first: number, last: number) =>
|
|
1778
|
+
focusedCell !== null &&
|
|
1779
|
+
focusedCell.rowId === rowId &&
|
|
1780
|
+
focusedColumnIndex >= first &&
|
|
1781
|
+
focusedColumnIndex <= last;
|
|
1620
1782
|
|
|
1621
1783
|
// The rectangle, resolved from ids to the indices the cells are rendered at.
|
|
1622
1784
|
// `null` whenever a corner has been filtered or hidden away - the range keeps
|
|
@@ -1771,7 +1933,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1771
1933
|
const row = rows[coords.rowIndex];
|
|
1772
1934
|
const column = orderedColumns[coords.columnIndex];
|
|
1773
1935
|
if (!row || !column) return;
|
|
1774
|
-
|
|
1936
|
+
// A column covered by a span on this row has no cell; the one covering it
|
|
1937
|
+
// takes the focus.
|
|
1938
|
+
const cell = toRenderedCell({ rowId: row.id, columnId: column.id }, row);
|
|
1775
1939
|
// The state moves; the effect below is what makes the DOM follow, because
|
|
1776
1940
|
// the element for an off-screen row does not exist yet to be focused.
|
|
1777
1941
|
wantsCellFocusRef.current = true;
|
|
@@ -1911,12 +2075,15 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1911
2075
|
direction: move,
|
|
1912
2076
|
rowCount: rows.length,
|
|
1913
2077
|
columnCount: orderedColumns.length,
|
|
2078
|
+
// A column covered by a span on its row has no cell to hold an editor,
|
|
2079
|
+
// and a move onto it would land back on the spanning cell just left.
|
|
1914
2080
|
isEditable: (coords) => {
|
|
1915
2081
|
const row = rows[coords.rowIndex];
|
|
1916
2082
|
const column = orderedColumns[coords.columnIndex];
|
|
1917
2083
|
return (
|
|
1918
2084
|
row !== undefined &&
|
|
1919
2085
|
column !== undefined &&
|
|
2086
|
+
!isCoveredCell(row, coords.columnIndex) &&
|
|
1920
2087
|
edit.canEditCell(row, column)
|
|
1921
2088
|
);
|
|
1922
2089
|
},
|
|
@@ -2035,10 +2202,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2035
2202
|
const nextCell =
|
|
2036
2203
|
container === null
|
|
2037
2204
|
? null
|
|
2038
|
-
: findCellElement(
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2205
|
+
: findCellElement(
|
|
2206
|
+
container,
|
|
2207
|
+
toRenderedCell(
|
|
2208
|
+
{ rowId: nextRow.id, columnId: nextColumn.id },
|
|
2209
|
+
nextRow,
|
|
2210
|
+
),
|
|
2211
|
+
);
|
|
2042
2212
|
if (nextCell !== null) nextCell.focus({ preventScroll: true });
|
|
2043
2213
|
else target.blur();
|
|
2044
2214
|
return;
|
|
@@ -2175,10 +2345,19 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2175
2345
|
return;
|
|
2176
2346
|
}
|
|
2177
2347
|
|
|
2348
|
+
// Right steps off a spanning cell from the last column it covers, or it
|
|
2349
|
+
// would land back on itself.
|
|
2350
|
+
const spans = rows[rowIndex] ? colSpansOf(rows[rowIndex]) : null;
|
|
2178
2351
|
const next = resolveCellMove({
|
|
2179
2352
|
key: event.key,
|
|
2180
2353
|
ctrlKey: event.ctrlKey || event.metaKey,
|
|
2181
|
-
from: {
|
|
2354
|
+
from: {
|
|
2355
|
+
rowIndex,
|
|
2356
|
+
columnIndex:
|
|
2357
|
+
spans !== null && event.key === "ArrowRight"
|
|
2358
|
+
? spanEndIndex(spans, columnIndex)
|
|
2359
|
+
: columnIndex,
|
|
2360
|
+
},
|
|
2182
2361
|
rowCount: rows.length,
|
|
2183
2362
|
columnCount: orderedColumns.length,
|
|
2184
2363
|
pageRows: Math.max(
|
|
@@ -2192,7 +2371,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2192
2371
|
// through at the last row would scroll the body out from under a focus that
|
|
2193
2372
|
// did not move.
|
|
2194
2373
|
event.preventDefault();
|
|
2195
|
-
|
|
2374
|
+
// A move landing on this cell again - clamped at an edge, or onto a column
|
|
2375
|
+
// this cell covers - changes nothing, the rectangle included.
|
|
2376
|
+
const landing =
|
|
2377
|
+
spans === null ? next.columnIndex : spanAnchorIndex(spans, next.columnIndex);
|
|
2378
|
+
if (next.rowIndex === rowIndex && landing === columnIndex) return;
|
|
2196
2379
|
// Shift extends the rectangle instead of collapsing it - the same key that
|
|
2197
2380
|
// extends a text selection, a file list and a spreadsheet range.
|
|
2198
2381
|
moveFocusedCell(next, { extend: event.shiftKey && cellRangeSelection });
|
|
@@ -2230,6 +2413,23 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2230
2413
|
// never in this grid.
|
|
2231
2414
|
const activeElement = container.ownerDocument.activeElement;
|
|
2232
2415
|
|
|
2416
|
+
// The column the focus sits on has just been covered by a span - the row
|
|
2417
|
+
// expanded under it, say. Its cell is gone, and the browser dropped the
|
|
2418
|
+
// focus with it, so the state follows the cell that now stands for the
|
|
2419
|
+
// column, and the render after this one puts the focus there. A focus
|
|
2420
|
+
// that had moved elsewhere on the page stays there: only the ring moves.
|
|
2421
|
+
const rendered = toRenderedCell(focusedCell, rows[focusedRowIndex]);
|
|
2422
|
+
if (rendered !== focusedCell) {
|
|
2423
|
+
if (
|
|
2424
|
+
activeElement === null ||
|
|
2425
|
+
activeElement === container.ownerDocument.body
|
|
2426
|
+
) {
|
|
2427
|
+
wantsCellFocusRef.current = true;
|
|
2428
|
+
}
|
|
2429
|
+
ui.actions.setFocusedCell(rendered);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2233
2433
|
// Never take the focus from elsewhere on the page. A consumer moving the
|
|
2234
2434
|
// cell while the user is typing in a form somewhere means "move the ring",
|
|
2235
2435
|
// not "move the caret" - the scroll above already did the visible part.
|
|
@@ -2250,7 +2450,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2250
2450
|
return;
|
|
2251
2451
|
}
|
|
2252
2452
|
|
|
2253
|
-
const cellElement = findCellElement(container,
|
|
2453
|
+
const cellElement = findCellElement(container, rendered);
|
|
2254
2454
|
// Still scrolling. The intent stays up, and the next render tries again.
|
|
2255
2455
|
if (cellElement === null) return;
|
|
2256
2456
|
wantsCellFocusRef.current = false;
|
|
@@ -2636,28 +2836,21 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2636
2836
|
) => {
|
|
2637
2837
|
const isGroupRow = row.getIsGrouped();
|
|
2638
2838
|
const isInteractive = rowGesturesFor(row);
|
|
2839
|
+
// The hover tint promises that the row answers the pointer, so
|
|
2840
|
+
// by default it follows the gestures the row takes: the ones
|
|
2841
|
+
// behind `isInteractive`, and the context menu - which a group
|
|
2842
|
+
// row sits out as well, see the row's onContextMenu below.
|
|
2843
|
+
const highlightsOnHover =
|
|
2844
|
+
highlightOnHover ??
|
|
2845
|
+
(isInteractive ||
|
|
2846
|
+
(renderRowContextMenu !== undefined && !isGroupRow));
|
|
2639
2847
|
const rowCells = [
|
|
2640
2848
|
...row.getStartVisibleCells(),
|
|
2641
2849
|
...row.getCenterVisibleCells(),
|
|
2642
2850
|
...row.getEndVisibleCells(),
|
|
2643
2851
|
];
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
// open, so every row holding a form shows its editors. Which of
|
|
2647
|
-
// them holds the caret is not decided here - see
|
|
2648
|
-
// placeEditCaret, which places it once per gesture.
|
|
2649
|
-
//
|
|
2650
|
-
// A parked row is not one of them. It keeps its form, so it is
|
|
2651
|
-
// still "open", but it has been decided: it renders its draft
|
|
2652
|
-
// through the column's own renderer until `begin` reopens it.
|
|
2653
|
-
// A committed entry row in the body has a form too - every
|
|
2654
|
-
// entry row does - but it is a value row until reopened, and
|
|
2655
|
-
// reopening takes it back to the entry block.
|
|
2656
|
-
const rowEditing =
|
|
2657
|
-
features.editMode === "row" &&
|
|
2658
|
-
editOpenRowIdSet.has(row.id) &&
|
|
2659
|
-
!editDraftRowIdSet.has(row.id) &&
|
|
2660
|
-
!newRowIdSet.has(row.id);
|
|
2852
|
+
const rowEditing = isRowEditing(row);
|
|
2853
|
+
const spans = colSpansOf(row);
|
|
2661
2854
|
// Cell selection takes the body's tab stop off the row and puts
|
|
2662
2855
|
// it on a cell - two stops per row would make Tab a way of
|
|
2663
2856
|
// walking the grid, which is what the arrow keys are for. Space
|
|
@@ -2758,6 +2951,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2758
2951
|
data-selects-on-click={
|
|
2759
2952
|
(selectsOnRowClick && !isGroupRow) || undefined
|
|
2760
2953
|
}
|
|
2954
|
+
data-highlight-on-hover={highlightsOnHover || undefined}
|
|
2761
2955
|
aria-selected={
|
|
2762
2956
|
features.rowSelection ? row.getIsSelected() : undefined
|
|
2763
2957
|
}
|
|
@@ -2835,6 +3029,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2835
3029
|
}
|
|
2836
3030
|
>
|
|
2837
3031
|
{rowCells.map((cell, cellIndex) => {
|
|
3032
|
+
// Covered by a spanning cell to its left - see colSpansOf.
|
|
3033
|
+
if (spans !== null && spans[cellIndex] === 0) return null;
|
|
3034
|
+
const colSpan = spans?.[cellIndex] ?? 1;
|
|
3035
|
+
const lastIndex = cellIndex + colSpan - 1;
|
|
2838
3036
|
const isEditingCell =
|
|
2839
3037
|
(editActive !== null &&
|
|
2840
3038
|
editActive.rowId === row.id &&
|
|
@@ -2845,7 +3043,12 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2845
3043
|
key={cell.id}
|
|
2846
3044
|
cell={cell}
|
|
2847
3045
|
rowHeight={rowHeight}
|
|
2848
|
-
|
|
3046
|
+
colSpan={colSpan}
|
|
3047
|
+
layout={
|
|
3048
|
+
colSpan > 1
|
|
3049
|
+
? spanLayout(cellIndex, lastIndex)
|
|
3050
|
+
: layoutFor(cell.column.id)
|
|
3051
|
+
}
|
|
2849
3052
|
contentOverride={
|
|
2850
3053
|
rowNumberById !== null &&
|
|
2851
3054
|
cell.column.id === ROW_NUMBER_COLUMN_ID
|
|
@@ -2897,10 +3100,12 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2897
3100
|
? {
|
|
2898
3101
|
rowId: row.id,
|
|
2899
3102
|
columnIndex: cellIndex + 1,
|
|
2900
|
-
|
|
2901
|
-
|
|
3103
|
+
// A spanning cell stands for every column it
|
|
3104
|
+
// covers, wherever the focus was put.
|
|
3105
|
+
focused: isFocusedWithin(
|
|
2902
3106
|
row.id,
|
|
2903
|
-
|
|
3107
|
+
cellIndex,
|
|
3108
|
+
lastIndex,
|
|
2904
3109
|
),
|
|
2905
3110
|
// Focusable by click and by the grid, never by
|
|
2906
3111
|
// Tab - the guards are the way in. See
|
|
@@ -2908,11 +3113,18 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2908
3113
|
tabIndex: -1,
|
|
2909
3114
|
// "Selected" means "in the block Ctrl+C would
|
|
2910
3115
|
// take" - which under `"single"` is the one cell
|
|
2911
|
-
// the user is standing on.
|
|
3116
|
+
// the user is standing on. A spanning cell is in
|
|
3117
|
+
// it by its first column alone - see
|
|
3118
|
+
// isWithinBounds.
|
|
2912
3119
|
selected: cellRangeSelection
|
|
2913
3120
|
? isWithinBounds(rangeBounds, viewIndex, cellIndex)
|
|
2914
|
-
:
|
|
2915
|
-
edges: boundsEdges(
|
|
3121
|
+
: isFocusedWithin(row.id, cellIndex, lastIndex),
|
|
3122
|
+
edges: boundsEdges(
|
|
3123
|
+
rangeBounds,
|
|
3124
|
+
viewIndex,
|
|
3125
|
+
cellIndex,
|
|
3126
|
+
lastIndex,
|
|
3127
|
+
),
|
|
2916
3128
|
}
|
|
2917
3129
|
: undefined
|
|
2918
3130
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
A box-shadow is drawn per cell, and its blur fades in at each cell's own top
|
|
6
6
|
and bottom edge - stacked down a column that reads as banding at every row
|
|
7
7
|
seam. Instead: a gradient pseudo element pinned to `top: 0; bottom: 0` so
|
|
8
|
-
consecutive cells butt together into one unbroken band.
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
consecutive cells butt together into one unbroken band.
|
|
9
|
+
|
|
10
|
+
The dark scheme is a second rule under Mantine's `data-mantine-color-scheme`
|
|
11
|
+
attribute, not the `light-dark` function: scripts/package-stylesheets.test.mjs
|
|
12
|
+
says why.
|
|
11
13
|
|
|
12
14
|
No border. A 1px line the colour of the grid's own borders drew a hard rule
|
|
13
15
|
between the pinned lanes and the data, so the pinned columns read as a
|
|
@@ -31,8 +33,8 @@
|
|
|
31
33
|
left: 100%;
|
|
32
34
|
background: linear-gradient(
|
|
33
35
|
to right,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
rgba(0, 0, 0, 0.07),
|
|
37
|
+
rgba(0, 0, 0, 0.025) 45%,
|
|
36
38
|
transparent
|
|
37
39
|
);
|
|
38
40
|
}
|
|
@@ -41,8 +43,26 @@
|
|
|
41
43
|
right: 100%;
|
|
42
44
|
background: linear-gradient(
|
|
43
45
|
to left,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
rgba(0, 0, 0, 0.07),
|
|
47
|
+
rgba(0, 0, 0, 0.025) 45%,
|
|
48
|
+
transparent
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:where([data-mantine-color-scheme="dark"]) .stickyLeft::after {
|
|
53
|
+
background: linear-gradient(
|
|
54
|
+
to right,
|
|
55
|
+
rgba(0, 0, 0, 0.26),
|
|
56
|
+
rgba(0, 0, 0, 0.09) 45%,
|
|
57
|
+
transparent
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:where([data-mantine-color-scheme="dark"]) .stickyRight::after {
|
|
62
|
+
background: linear-gradient(
|
|
63
|
+
to left,
|
|
64
|
+
rgba(0, 0, 0, 0.26),
|
|
65
|
+
rgba(0, 0, 0, 0.09) 45%,
|
|
46
66
|
transparent
|
|
47
67
|
);
|
|
48
68
|
}
|