@jielga/tmdatagrid 0.5.0 → 1.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 +25 -2
- package/dist/index.d.ts +856 -14
- package/dist/index.js +3370 -1546
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.layer.css +1 -0
- package/package.json +12 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +198 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +11 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +34 -0
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +35 -12
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +77 -62
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +6 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +10 -3
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -2
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +21 -4
- package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +206 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +841 -57
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +12 -7
- package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
- package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
- package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
- package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
- package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
- package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
- package/src/tmdatagrid/components/icons.ts +4 -0
- package/src/tmdatagrid/core/autosize.ts +1 -1
- package/src/tmdatagrid/core/capabilities.ts +37 -1
- package/src/tmdatagrid/core/cellNavigation.ts +41 -0
- package/src/tmdatagrid/core/columnUtils.ts +31 -6
- package/src/tmdatagrid/core/editEngine.ts +857 -0
- package/src/tmdatagrid/core/filterControls.ts +45 -0
- package/src/tmdatagrid/core/filterOperators.ts +72 -4
- package/src/tmdatagrid/core/labels.ts +68 -1
- package/src/tmdatagrid/core/labelsSv.ts +24 -1
- package/src/tmdatagrid/core/matchHighlight.ts +96 -0
- package/src/tmdatagrid/core/persistence.ts +97 -3
- package/src/tmdatagrid/core/quickSearch.ts +144 -0
- package/src/tmdatagrid/core/rowPinning.ts +37 -0
- package/src/tmdatagrid/core/rowSelection.ts +10 -1
- package/src/tmdatagrid/index.ts +51 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +456 -15
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Checkbox, Loader, Menu, type MenuProps, Text } from "@mantine/core";
|
|
2
2
|
import {
|
|
3
3
|
type Cell,
|
|
4
|
+
type Column,
|
|
4
5
|
flexRender,
|
|
5
6
|
type Row,
|
|
6
7
|
type RowData,
|
|
@@ -9,9 +10,11 @@ import {
|
|
|
9
10
|
import { useSelector } from "@tanstack/react-store";
|
|
10
11
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
12
|
import {
|
|
13
|
+
type CSSProperties,
|
|
12
14
|
type KeyboardEvent,
|
|
13
15
|
type MouseEvent,
|
|
14
16
|
type ReactNode,
|
|
17
|
+
type UIEvent,
|
|
15
18
|
useCallback,
|
|
16
19
|
useEffect,
|
|
17
20
|
useRef,
|
|
@@ -22,8 +25,15 @@ import sticky from "./sticky.module.css";
|
|
|
22
25
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
23
26
|
import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
|
|
24
27
|
import { TMDataGridHeaderCell } from "./TMDataGridHeaderCell";
|
|
28
|
+
import {
|
|
29
|
+
TMDataGridCellEditor,
|
|
30
|
+
type TMDataGridCellEditorClose,
|
|
31
|
+
} from "./TMDataGridCellEditor";
|
|
32
|
+
import { TMDataGridEntryRows } from "./TMDataGridEntryRows";
|
|
25
33
|
import { getColumnAlign, isControlColumn } from "../core/columnUtils";
|
|
34
|
+
import { getEditFieldName } from "../core/editEngine";
|
|
26
35
|
import {
|
|
36
|
+
getNextEditableCell,
|
|
27
37
|
resolveCellMove,
|
|
28
38
|
type TMDataGridCellPosition,
|
|
29
39
|
} from "../core/cellNavigation";
|
|
@@ -46,8 +56,12 @@ import { autosizeColumn } from "../core/autosize";
|
|
|
46
56
|
import {
|
|
47
57
|
getDisplayedRows,
|
|
48
58
|
getSelectableRowIds,
|
|
59
|
+
isPagingActive,
|
|
49
60
|
resolveRowSelectionClick,
|
|
50
61
|
} from "../core/rowSelection";
|
|
62
|
+
import { readPinnedRows } from "../core/rowPinning";
|
|
63
|
+
import { buildMatchNeedles, findMatchRanges } from "../core/matchHighlight";
|
|
64
|
+
import { ROW_NUMBER_COLUMN_ID } from "./TMDataGridRowNumberColumn";
|
|
51
65
|
import { SearchIcon } from "./icons";
|
|
52
66
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
53
67
|
|
|
@@ -187,25 +201,87 @@ export type TMDataGridCellNav = {
|
|
|
187
201
|
edges: { top: boolean; bottom: boolean; left: boolean; right: boolean } | null;
|
|
188
202
|
};
|
|
189
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The text with its matched slices wrapped in `<mark>` — or handed back
|
|
206
|
+
* whole when nothing matches. Only ever called on string output; a renderer
|
|
207
|
+
* returning its own elements never reaches this.
|
|
208
|
+
*/
|
|
209
|
+
function withMatchHighlights(
|
|
210
|
+
text: string,
|
|
211
|
+
needles: ReadonlyArray<string>,
|
|
212
|
+
): ReactNode {
|
|
213
|
+
const ranges = findMatchRanges(text, needles);
|
|
214
|
+
if (ranges === null) return text;
|
|
215
|
+
const parts: Array<ReactNode> = [];
|
|
216
|
+
let cursor = 0;
|
|
217
|
+
for (const range of ranges) {
|
|
218
|
+
if (range.start > cursor) parts.push(text.slice(cursor, range.start));
|
|
219
|
+
parts.push(
|
|
220
|
+
<mark key={range.start} className={classes.matchHighlight}>
|
|
221
|
+
{text.slice(range.start, range.end)}
|
|
222
|
+
</mark>,
|
|
223
|
+
);
|
|
224
|
+
cursor = range.end;
|
|
225
|
+
}
|
|
226
|
+
if (cursor < text.length) parts.push(text.slice(cursor));
|
|
227
|
+
return parts;
|
|
228
|
+
}
|
|
229
|
+
|
|
190
230
|
function TMDataGridBodyCell({
|
|
191
231
|
cell,
|
|
192
232
|
rowHeight,
|
|
193
233
|
layout,
|
|
194
234
|
nav,
|
|
235
|
+
editor,
|
|
236
|
+
contentOverride,
|
|
237
|
+
highlightNeedles,
|
|
195
238
|
onFocus,
|
|
239
|
+
onClick,
|
|
196
240
|
onMouseDown,
|
|
197
241
|
onMouseEnter,
|
|
242
|
+
onDoubleClick,
|
|
198
243
|
onContextMenu,
|
|
199
244
|
}: {
|
|
200
245
|
cell: Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
201
246
|
rowHeight: number;
|
|
202
247
|
layout: TMDataGridColumnLayout;
|
|
203
248
|
nav?: TMDataGridCellNav;
|
|
249
|
+
/** The mounted editor, when this is the cell being edited. */
|
|
250
|
+
editor?: ReactNode;
|
|
251
|
+
/**
|
|
252
|
+
* Replaces the column renderer's output — the row-number gutter, whose
|
|
253
|
+
* value only the body knows. `undefined` means "render normally".
|
|
254
|
+
*/
|
|
255
|
+
contentOverride?: ReactNode;
|
|
256
|
+
/**
|
|
257
|
+
* Needles to highlight in string output — see `enableMatchHighlighting`.
|
|
258
|
+
* `undefined` almost always: only set while a highlightable filter is
|
|
259
|
+
* active for this column.
|
|
260
|
+
*/
|
|
261
|
+
highlightNeedles?: ReadonlyArray<string>;
|
|
204
262
|
onFocus?: () => void;
|
|
263
|
+
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
205
264
|
onMouseDown?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
206
265
|
onMouseEnter?: () => void;
|
|
266
|
+
onDoubleClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
207
267
|
onContextMenu?: (event: MouseEvent<HTMLDivElement>) => void;
|
|
208
268
|
}) {
|
|
269
|
+
// The edit markers subscribe here, per cell, all to the one projection
|
|
270
|
+
// store — so typing in an editor repaints the edited row's cells and
|
|
271
|
+
// nothing else. Constant `false` everywhere while editing is off.
|
|
272
|
+
const { table, edit } = useTMDataGridContext();
|
|
273
|
+
const fieldName = getEditFieldName(cell.column);
|
|
274
|
+
const cellRowId = cell.row.id;
|
|
275
|
+
const isDirty = useSelector(edit.store, (state) => {
|
|
276
|
+
if (fieldName === null) return false;
|
|
277
|
+
const projection = state.rows[cellRowId];
|
|
278
|
+
return projection !== undefined && projection.dirtyFields.includes(fieldName);
|
|
279
|
+
});
|
|
280
|
+
const isInvalid = useSelector(edit.store, (state) => {
|
|
281
|
+
if (fieldName === null) return false;
|
|
282
|
+
const projection = state.rows[cellRowId];
|
|
283
|
+
return projection !== undefined && projection.errorFields.includes(fieldName);
|
|
284
|
+
});
|
|
209
285
|
return (
|
|
210
286
|
<div
|
|
211
287
|
// `gridcell` rather than `cell` is the whole promise of cell selection:
|
|
@@ -218,10 +294,12 @@ function TMDataGridBodyCell({
|
|
|
218
294
|
// a cell is the grid's, one from inside a cell belongs to whatever holds
|
|
219
295
|
// the focus in there.
|
|
220
296
|
data-cell={nav ? true : undefined}
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
297
|
+
// Both coordinates are always present, not only under cell selection:
|
|
298
|
+
// autosize finds a column's mounted cells by the column id, and the pair
|
|
299
|
+
// is how a test addresses one cell —
|
|
300
|
+
// `[data-row-id="42"][data-column-id="name"]`. The cell-navigation
|
|
301
|
+
// selectors also require `data-cell`, so they are unaffected.
|
|
302
|
+
data-row-id={cell.row.id}
|
|
225
303
|
data-column-id={cell.column.id}
|
|
226
304
|
data-focused={nav?.focused}
|
|
227
305
|
aria-selected={nav?.selected}
|
|
@@ -237,8 +315,13 @@ function TMDataGridBodyCell({
|
|
|
237
315
|
// landing on the cell — the ring follows the user into the checkbox
|
|
238
316
|
// rather than being left behind on whichever cell they came from.
|
|
239
317
|
onFocus={onFocus}
|
|
318
|
+
onClick={onClick}
|
|
240
319
|
onMouseDown={onMouseDown}
|
|
241
320
|
onMouseEnter={onMouseEnter}
|
|
321
|
+
onDoubleClick={onDoubleClick}
|
|
322
|
+
data-editing={editor !== undefined || undefined}
|
|
323
|
+
data-dirty={isDirty || undefined}
|
|
324
|
+
data-invalid={isInvalid || undefined}
|
|
242
325
|
data-align={getColumnAlign(cell.column)}
|
|
243
326
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
244
327
|
// scale grows for text. See isControlColumn.
|
|
@@ -274,7 +357,29 @@ function TMDataGridBodyCell({
|
|
|
274
357
|
: undefined,
|
|
275
358
|
}}
|
|
276
359
|
>
|
|
277
|
-
|
|
360
|
+
{editor ?? (
|
|
361
|
+
<span className={classes.cellContent}>
|
|
362
|
+
{(() => {
|
|
363
|
+
// Highlighting replicates the *default* renderer — value, then
|
|
364
|
+
// `toString` — with the matched slices marked. A column with its
|
|
365
|
+
// own `cell` opts out by existing: its renderer's identity is not
|
|
366
|
+
// the default's, and the grid will not rummage inside its output.
|
|
367
|
+
if (
|
|
368
|
+
highlightNeedles !== undefined &&
|
|
369
|
+
contentOverride === undefined &&
|
|
370
|
+
!cell.getIsPlaceholder() &&
|
|
371
|
+
!cell.getIsAggregated() &&
|
|
372
|
+
cell.column.columnDef.cell === table.getDefaultColumnDef().cell
|
|
373
|
+
) {
|
|
374
|
+
const value = cell.renderValue();
|
|
375
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
376
|
+
return withMatchHighlights(String(value), highlightNeedles);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return contentOverride ?? renderCellContent(cell);
|
|
380
|
+
})()}
|
|
381
|
+
</span>
|
|
382
|
+
)}
|
|
278
383
|
</div>
|
|
279
384
|
);
|
|
280
385
|
}
|
|
@@ -368,12 +473,101 @@ export type TMDataGridRowContextMenu<TData extends RowData> = (
|
|
|
368
473
|
args: TMDataGridRowContextMenuArgs<TData>,
|
|
369
474
|
) => ReactNode;
|
|
370
475
|
|
|
476
|
+
/**
|
|
477
|
+
* What the `onCellClick` family receives — the cell with its row and column,
|
|
478
|
+
* in the consumer's own row type, plus the pointer event.
|
|
479
|
+
*/
|
|
480
|
+
export type TMDataGridCellEventArgs<TData extends RowData> = {
|
|
481
|
+
cell: Cell<TMDataGridFeatures, TData, unknown>;
|
|
482
|
+
row: Row<TMDataGridFeatures, TData>;
|
|
483
|
+
column: Column<TMDataGridFeatures, TData, unknown>;
|
|
484
|
+
event: MouseEvent<HTMLElement>;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Inline styles for a row. Widened to accept CSS variables, because setting
|
|
489
|
+
* `--row-bg` is the supported way to colour a row — see {@link
|
|
490
|
+
* TMDataGridTableProps.rowStyle}.
|
|
491
|
+
*/
|
|
492
|
+
export type TMDataGridRowStyle =
|
|
493
|
+
| CSSProperties
|
|
494
|
+
| (CSSProperties & Record<`--${string}`, string | number>);
|
|
495
|
+
|
|
371
496
|
export type TMDataGridTableProps<TData extends RowData> = {
|
|
372
497
|
/**
|
|
373
498
|
* Called when a body row is clicked. Runs in addition to row selection under
|
|
374
499
|
* `selectionMode: "row"`, not instead of it.
|
|
375
500
|
*/
|
|
376
501
|
onRowClick?: (row: Row<TMDataGridFeatures, TData>) => void;
|
|
502
|
+
/**
|
|
503
|
+
* Called when a body cell is clicked. Composes, never suppresses: whatever
|
|
504
|
+
* the click already does — selecting, highlighting, moving the cell cursor
|
|
505
|
+
* — still happens. Group rows sit out the cell handlers the way they sit
|
|
506
|
+
* out `onRowClick`, and for the same reason.
|
|
507
|
+
*/
|
|
508
|
+
onCellClick?: (args: TMDataGridCellEventArgs<TData>) => void;
|
|
509
|
+
/** As {@link onCellClick}; a double-click that opens an editor still does. */
|
|
510
|
+
onCellDoubleClick?: (args: TMDataGridCellEventArgs<TData>) => void;
|
|
511
|
+
/**
|
|
512
|
+
* As {@link onCellClick}; `rowContextMenu` and the cell-selection menu still
|
|
513
|
+
* open. To suppress the browser's own menu on a grid with neither, call
|
|
514
|
+
* `event.preventDefault()` here.
|
|
515
|
+
*/
|
|
516
|
+
onCellContextMenu?: (args: TMDataGridCellEventArgs<TData>) => void;
|
|
517
|
+
/**
|
|
518
|
+
* Class for a body row — a string, or a function of the row (group rows
|
|
519
|
+
* included; test `row.getIsGrouped()` to skip them). Added after the grid's
|
|
520
|
+
* own row class.
|
|
521
|
+
*/
|
|
522
|
+
rowClassName?:
|
|
523
|
+
| string
|
|
524
|
+
| ((row: Row<TMDataGridFeatures, TData>) => string | undefined);
|
|
525
|
+
/**
|
|
526
|
+
* Inline style for a body row. To colour a row, set `--row-bg` rather than
|
|
527
|
+
* `background`: the row's own background, its sticky pinned cells and the
|
|
528
|
+
* cell-range tint all read that variable, and hover/selection/highlight
|
|
529
|
+
* keep working on top of it — a raw `background` bypasses all of them.
|
|
530
|
+
*
|
|
531
|
+
* ```tsx
|
|
532
|
+
* rowStyle={(row) =>
|
|
533
|
+
* row.original.overdue ? { "--row-bg": "var(--mantine-color-red-0)" } : undefined
|
|
534
|
+
* }
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
rowStyle?:
|
|
538
|
+
| TMDataGridRowStyle
|
|
539
|
+
| ((row: Row<TMDataGridFeatures, TData>) => TMDataGridRowStyle | undefined);
|
|
540
|
+
/**
|
|
541
|
+
* Every second row takes `--dg-row-striped-bg`. Computed from the row's
|
|
542
|
+
* position in the view — sorting and filtering restripe, and the stripes
|
|
543
|
+
* stay put while the virtualizer mounts and unmounts rows.
|
|
544
|
+
*/
|
|
545
|
+
striped?: boolean;
|
|
546
|
+
/**
|
|
547
|
+
* Called when the scroll *arrives* at an edge — once per arrival, not per
|
|
548
|
+
* scroll event, and not on mount (the grid starts at the top-left edge).
|
|
549
|
+
* For loading more rows as the end approaches, `onReachEnd` is the better
|
|
550
|
+
* hook: it fires rows-early and latches per row count.
|
|
551
|
+
*/
|
|
552
|
+
onScrollToTop?: () => void;
|
|
553
|
+
/** As {@link onScrollToTop}, for the bottom edge. */
|
|
554
|
+
onScrollToBottom?: () => void;
|
|
555
|
+
/** As {@link onScrollToTop}, for the left edge. */
|
|
556
|
+
onScrollToLeft?: () => void;
|
|
557
|
+
/** As {@link onScrollToTop}, for the right edge. */
|
|
558
|
+
onScrollToRight?: () => void;
|
|
559
|
+
/**
|
|
560
|
+
* Rendered centred in the body when the view is empty — the branded blank
|
|
561
|
+
* slate. It takes over states 3 and 4 of the empty matrix (see the docs):
|
|
562
|
+
* loading still shows the loader, and open entry rows still show only
|
|
563
|
+
* themselves. `hasActiveFilters` says which emptiness this is — a filter
|
|
564
|
+
* that matched nothing wants a "clear filters" invitation, a grid with no
|
|
565
|
+
* data wants a "create the first one".
|
|
566
|
+
*/
|
|
567
|
+
renderEmptyState?: (args: {
|
|
568
|
+
hasActiveFilters: boolean;
|
|
569
|
+
table: Table<TMDataGridFeatures, TData>;
|
|
570
|
+
}) => ReactNode;
|
|
377
571
|
/**
|
|
378
572
|
* Contents of the menu a right-click on a row opens, at the pointer. Return
|
|
379
573
|
* `null` to leave a row without one — the browser's own menu stays suppressed
|
|
@@ -428,6 +622,14 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
428
622
|
* How many rows before the end {@link onReachEnd} fires at. Defaults to 10.
|
|
429
623
|
*/
|
|
430
624
|
reachEndThreshold?: number;
|
|
625
|
+
/**
|
|
626
|
+
* Accessible name for the grid — what a screen reader announces on entry,
|
|
627
|
+
* and what `getByRole("grid", { name })` matches. Worth setting on any page
|
|
628
|
+
* holding more than one grid; without it they are all just "grid".
|
|
629
|
+
*/
|
|
630
|
+
"aria-label"?: string;
|
|
631
|
+
/** As {@link "aria-label"}, pointing at an element that already names it. */
|
|
632
|
+
"aria-labelledby"?: string;
|
|
431
633
|
};
|
|
432
634
|
|
|
433
635
|
/**
|
|
@@ -438,15 +640,29 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
438
640
|
*/
|
|
439
641
|
export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
440
642
|
onRowClick,
|
|
643
|
+
onCellClick,
|
|
644
|
+
onCellDoubleClick,
|
|
645
|
+
onCellContextMenu,
|
|
646
|
+
rowClassName,
|
|
647
|
+
rowStyle,
|
|
648
|
+
striped = false,
|
|
649
|
+
onScrollToTop,
|
|
650
|
+
onScrollToBottom,
|
|
651
|
+
onScrollToLeft,
|
|
652
|
+
onScrollToRight,
|
|
653
|
+
renderEmptyState,
|
|
441
654
|
rowContextMenu,
|
|
442
655
|
rowContextMenuProps,
|
|
443
656
|
cellExport,
|
|
444
657
|
onReachEnd,
|
|
445
658
|
reachEndThreshold = 10,
|
|
659
|
+
"aria-label": ariaLabel,
|
|
660
|
+
"aria-labelledby": ariaLabelledBy,
|
|
446
661
|
}: TMDataGridTableProps<TData>) {
|
|
447
662
|
const {
|
|
448
663
|
table,
|
|
449
664
|
ui,
|
|
665
|
+
edit,
|
|
450
666
|
features,
|
|
451
667
|
labels,
|
|
452
668
|
rowHeight,
|
|
@@ -454,6 +670,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
454
670
|
renderDetails,
|
|
455
671
|
renderDetailsEstHeight,
|
|
456
672
|
overscan,
|
|
673
|
+
scrollerRef,
|
|
457
674
|
} = useTMDataGridContext();
|
|
458
675
|
|
|
459
676
|
// The body depends on every state slice (sorting, filters, paging, sizing,
|
|
@@ -465,6 +682,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
465
682
|
// with cell selection off never re-renders for it.
|
|
466
683
|
const focusedCell = useSelector(ui, (state) => state.focusedCell);
|
|
467
684
|
const cellRange = useSelector(ui, (state) => state.cellRange);
|
|
685
|
+
// Which cell has an editor open. The projection (dirty/error markers) is
|
|
686
|
+
// subscribed per cell instead — see TMDataGridBodyCell.
|
|
687
|
+
const editActive = useSelector(edit.store, (state) => state.active);
|
|
688
|
+
const newRowCount = useSelector(edit.store, (state) => state.newRows.length);
|
|
689
|
+
const deletedRowIds = useSelector(edit.store, (state) => state.deletedRowIds);
|
|
468
690
|
|
|
469
691
|
const { loading, noResultsLabel = labels.noResults } =
|
|
470
692
|
table.options.meta ?? {};
|
|
@@ -475,6 +697,37 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
475
697
|
// the rows as delivered, so the same branch serves server paging.
|
|
476
698
|
const rows = getDisplayedRows(table, features);
|
|
477
699
|
|
|
700
|
+
// Per-column highlight needles, or `null` while off or nothing is active —
|
|
701
|
+
// the common case, costing one flag check per render.
|
|
702
|
+
const matchNeedles = features.matchHighlighting
|
|
703
|
+
? buildMatchNeedles(table)
|
|
704
|
+
: null;
|
|
705
|
+
|
|
706
|
+
// The edge blocks. `getDisplayedRows` above has already taken these out of
|
|
707
|
+
// the scrolling order; stale ids resolve to nothing — see readPinnedRows.
|
|
708
|
+
const pinnedTopRows = features.rowPinning ? readPinnedRows(table, "top") : [];
|
|
709
|
+
const pinnedBottomRows = features.rowPinning
|
|
710
|
+
? readPinnedRows(table, "bottom")
|
|
711
|
+
: [];
|
|
712
|
+
const pinnedRowCount = pinnedTopRows.length + pinnedBottomRows.length;
|
|
713
|
+
|
|
714
|
+
// The row-number gutter's numbers: one pass over the view per render, only
|
|
715
|
+
// while the lane exists. A cell cannot know its display position, so the
|
|
716
|
+
// body hands each row its number — group rows get none (they are headings
|
|
717
|
+
// over the rows being counted), and paging continues the count across
|
|
718
|
+
// pages rather than restarting every page at 1.
|
|
719
|
+
let rowNumberById: Map<string, number> | null = null;
|
|
720
|
+
if (features.rowNumbers) {
|
|
721
|
+
rowNumberById = new Map();
|
|
722
|
+
let n = isPagingActive(table, features)
|
|
723
|
+
? table.store.state.pagination.pageIndex *
|
|
724
|
+
table.store.state.pagination.pageSize
|
|
725
|
+
: 0;
|
|
726
|
+
for (const viewRow of rows) {
|
|
727
|
+
if (!viewRow.getIsGrouped()) rowNumberById.set(viewRow.id, ++n);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
478
731
|
/**
|
|
479
732
|
* Whether this row opens a detail panel underneath it.
|
|
480
733
|
*
|
|
@@ -505,6 +758,79 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
505
758
|
}
|
|
506
759
|
});
|
|
507
760
|
|
|
761
|
+
/**
|
|
762
|
+
* The header's height, published as `--dg-header-height` for the entry
|
|
763
|
+
* block to stick under — never measured otherwise (`top: 0` needs no
|
|
764
|
+
* number). A ResizeObserver, mounted only while the block is in use: the
|
|
765
|
+
* same discipline `renderDetails` applies to `measureElement`.
|
|
766
|
+
*/
|
|
767
|
+
const gridElementRef = useRef<HTMLDivElement>(null);
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
if (newRowCount === 0 && pinnedTopRows.length === 0) return;
|
|
770
|
+
const grid = gridElementRef.current;
|
|
771
|
+
if (grid === null) return;
|
|
772
|
+
const headerRows = grid.querySelectorAll<HTMLElement>(
|
|
773
|
+
"[data-dg-header-row]",
|
|
774
|
+
);
|
|
775
|
+
const measure = () => {
|
|
776
|
+
let total = 0;
|
|
777
|
+
for (const headerRow of headerRows) total += headerRow.offsetHeight;
|
|
778
|
+
grid.style.setProperty("--dg-header-height", `${total}px`);
|
|
779
|
+
};
|
|
780
|
+
measure();
|
|
781
|
+
const observer = new ResizeObserver(measure);
|
|
782
|
+
for (const headerRow of headerRows) observer.observe(headerRow);
|
|
783
|
+
return () => observer.disconnect();
|
|
784
|
+
}, [newRowCount, pinnedTopRows.length]);
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* The entry block's height, published as `--dg-entry-height` so the pinned
|
|
788
|
+
* top block can stack under it — measured only while both blocks exist,
|
|
789
|
+
* which is the only time the offset is anything but 0.
|
|
790
|
+
*/
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
if (newRowCount === 0 || pinnedTopRows.length === 0) return;
|
|
793
|
+
const grid = gridElementRef.current;
|
|
794
|
+
if (grid === null) return;
|
|
795
|
+
const entryBlock = grid.querySelector<HTMLElement>("[data-dg-entry-block]");
|
|
796
|
+
if (entryBlock === null) return;
|
|
797
|
+
const measure = () =>
|
|
798
|
+
grid.style.setProperty("--dg-entry-height", `${entryBlock.offsetHeight}px`);
|
|
799
|
+
measure();
|
|
800
|
+
const observer = new ResizeObserver(measure);
|
|
801
|
+
observer.observe(entryBlock);
|
|
802
|
+
return () => {
|
|
803
|
+
observer.disconnect();
|
|
804
|
+
grid.style.removeProperty("--dg-entry-height");
|
|
805
|
+
};
|
|
806
|
+
}, [newRowCount, pinnedTopRows.length]);
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* The summary row's height, published as `--dg-summary-height` so the
|
|
810
|
+
* pinned bottom block sticks above it rather than underneath it.
|
|
811
|
+
*/
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
if (pinnedBottomRows.length === 0) return;
|
|
814
|
+
const grid = gridElementRef.current;
|
|
815
|
+
if (grid === null) return;
|
|
816
|
+
const summaryRow = grid.querySelector<HTMLElement>(
|
|
817
|
+
'[data-dg-part="summary-row"]',
|
|
818
|
+
);
|
|
819
|
+
if (summaryRow === null) return;
|
|
820
|
+
const measure = () =>
|
|
821
|
+
grid.style.setProperty(
|
|
822
|
+
"--dg-summary-height",
|
|
823
|
+
`${summaryRow.offsetHeight}px`,
|
|
824
|
+
);
|
|
825
|
+
measure();
|
|
826
|
+
const observer = new ResizeObserver(measure);
|
|
827
|
+
observer.observe(summaryRow);
|
|
828
|
+
return () => {
|
|
829
|
+
observer.disconnect();
|
|
830
|
+
grid.style.removeProperty("--dg-summary-height");
|
|
831
|
+
};
|
|
832
|
+
}, [pinnedBottomRows.length]);
|
|
833
|
+
|
|
508
834
|
// A persisted pageIndex can outlive the data that produced it; TanStack only
|
|
509
835
|
// auto-resets on live filter/sort/data changes, not on restored state. The
|
|
510
836
|
// guard makes the dependency-free effect idempotent.
|
|
@@ -556,6 +882,35 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
556
882
|
const paddingBottom =
|
|
557
883
|
virtualizer.getTotalSize() - (virtualItems.at(-1)?.end ?? 0);
|
|
558
884
|
|
|
885
|
+
/**
|
|
886
|
+
* `api.scrollToRow`, implemented where the virtualizer is.
|
|
887
|
+
*
|
|
888
|
+
* Registered in an effect rather than during render, and with no dependency
|
|
889
|
+
* array so it re-registers on every one: the closure has to see this render's
|
|
890
|
+
* `rows`, which sorting, filtering and paging all change. A grid whose Table
|
|
891
|
+
* has unmounted leaves the hook's default behind, which answers `false`.
|
|
892
|
+
*/
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
scrollerRef.current = ({ rowId, align = "auto" }) => {
|
|
895
|
+
// Already parked at an edge, and outside the scrolling order — see
|
|
896
|
+
// `getDisplayedRows`. Nothing to scroll, but the row *is* on screen, so
|
|
897
|
+
// reporting failure would be the wrong answer.
|
|
898
|
+
if (
|
|
899
|
+
pinnedTopRows.some((row) => row.id === rowId) ||
|
|
900
|
+
pinnedBottomRows.some((row) => row.id === rowId)
|
|
901
|
+
) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
const index = rows.findIndex((row) => row.id === rowId);
|
|
905
|
+
if (index < 0) return false;
|
|
906
|
+
virtualizer.scrollToIndex(index, { align });
|
|
907
|
+
return true;
|
|
908
|
+
};
|
|
909
|
+
return () => {
|
|
910
|
+
scrollerRef.current = () => false;
|
|
911
|
+
};
|
|
912
|
+
});
|
|
913
|
+
|
|
559
914
|
/**
|
|
560
915
|
* Infinite scroll. Latched on the row count rather than debounced: one call
|
|
561
916
|
* per count means a pending fetch is never asked twice, and the append that
|
|
@@ -679,7 +1034,17 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
679
1034
|
const layoutFor = (columnId: string) =>
|
|
680
1035
|
columnLayout.get(columnId) ?? UNPINNED_LAYOUT;
|
|
681
1036
|
|
|
682
|
-
|
|
1037
|
+
// Pinned rows count as content: with every visible row pinned to an edge,
|
|
1038
|
+
// an empty-state message beside them would contradict what is on screen.
|
|
1039
|
+
const isEmpty = rows.length === 0 && pinnedRowCount === 0;
|
|
1040
|
+
// Which emptiness this is. The body already subscribes to the whole table
|
|
1041
|
+
// store, so these reads are reactive.
|
|
1042
|
+
const emptyGlobalFilter = table.store.state.globalFilter;
|
|
1043
|
+
const hasActiveFilters =
|
|
1044
|
+
table.store.state.columnFilters.length > 0 ||
|
|
1045
|
+
(typeof emptyGlobalFilter === "string"
|
|
1046
|
+
? emptyGlobalFilter.trim() !== ""
|
|
1047
|
+
: emptyGlobalFilter != null);
|
|
683
1048
|
|
|
684
1049
|
/**
|
|
685
1050
|
* The summary row exists by declaration, not by flag: it renders exactly
|
|
@@ -738,6 +1103,35 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
738
1103
|
onRowClick?.(row as unknown as Row<TMDataGridFeatures, TData>);
|
|
739
1104
|
};
|
|
740
1105
|
|
|
1106
|
+
// The value-or-function pair resolved per row, across the same erasure
|
|
1107
|
+
// crossing `onRowClick` makes.
|
|
1108
|
+
const rowClassNameFor = (
|
|
1109
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1110
|
+
): string | undefined =>
|
|
1111
|
+
typeof rowClassName === "function"
|
|
1112
|
+
? rowClassName(row as unknown as Row<TMDataGridFeatures, TData>)
|
|
1113
|
+
: rowClassName;
|
|
1114
|
+
const rowStyleFor = (
|
|
1115
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1116
|
+
): CSSProperties | undefined =>
|
|
1117
|
+
typeof rowStyle === "function"
|
|
1118
|
+
? rowStyle(row as unknown as Row<TMDataGridFeatures, TData>)
|
|
1119
|
+
: rowStyle;
|
|
1120
|
+
|
|
1121
|
+
// The same erasure crossing `onRowClick` makes, once for all three cell
|
|
1122
|
+
// handlers.
|
|
1123
|
+
const cellEventArgs = (
|
|
1124
|
+
cell: Cell<TMDataGridFeatures, TMDataGridRowData, unknown>,
|
|
1125
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1126
|
+
event: MouseEvent<HTMLElement>,
|
|
1127
|
+
): TMDataGridCellEventArgs<TData> =>
|
|
1128
|
+
({
|
|
1129
|
+
cell,
|
|
1130
|
+
row,
|
|
1131
|
+
column: cell.column,
|
|
1132
|
+
event,
|
|
1133
|
+
}) as unknown as TMDataGridCellEventArgs<TData>;
|
|
1134
|
+
|
|
741
1135
|
// ---------------------------------------------------------------------------
|
|
742
1136
|
// Cell selection
|
|
743
1137
|
//
|
|
@@ -956,6 +1350,92 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
956
1350
|
setRangeTo(cell, { extend });
|
|
957
1351
|
};
|
|
958
1352
|
|
|
1353
|
+
// ---------------------------------------------------------------------------
|
|
1354
|
+
// Cell editing — the gestures. The engine owns the forms; this component
|
|
1355
|
+
// owns where the editor opens from (keys, double-click) and where the
|
|
1356
|
+
// focus lands when it closes.
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
|
|
1359
|
+
/** The pending type-to-edit seed, consumed by the editor host on mount. */
|
|
1360
|
+
const editSeedRef = useRef<string | undefined>(undefined);
|
|
1361
|
+
const takeEditSeed = () => {
|
|
1362
|
+
const seed = editSeedRef.current;
|
|
1363
|
+
editSeedRef.current = undefined;
|
|
1364
|
+
return seed;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
/** DOM focus back onto a cell — after Escape, or a commit with nowhere to go. */
|
|
1368
|
+
const refocusCell = (position: TMDataGridCellPosition) => {
|
|
1369
|
+
const container = scrollContainerRef.current;
|
|
1370
|
+
if (container === null) return;
|
|
1371
|
+
wantsCellFocusRef.current = true;
|
|
1372
|
+
findCellElement(container, position)?.focus();
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
const openEditor = (position: TMDataGridCellPosition) => {
|
|
1376
|
+
if (cellSelection) ui.actions.setFocusedCell(position);
|
|
1377
|
+
// Row mode edits whole rows: any editable cell's open gesture opens the
|
|
1378
|
+
// row, exactly as the lane's pencil does.
|
|
1379
|
+
edit.begin({
|
|
1380
|
+
rowId: position.rowId,
|
|
1381
|
+
columnId: features.editMode === "row" ? null : position.columnId,
|
|
1382
|
+
});
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const handleEditorClose = (
|
|
1386
|
+
position: TMDataGridCellPosition,
|
|
1387
|
+
close: TMDataGridCellEditorClose,
|
|
1388
|
+
) => {
|
|
1389
|
+
// Where the keyboard goes: Enter moves down, Tab to the next editable
|
|
1390
|
+
// cell (the deferring batch variants move the same way, draft in tow) —
|
|
1391
|
+
// everything else, and a saved row, goes back to where the edit was.
|
|
1392
|
+
const move =
|
|
1393
|
+
close.via === "enter"
|
|
1394
|
+
? "down"
|
|
1395
|
+
: close.via === "tab" || close.via === "defer-tab"
|
|
1396
|
+
? 1
|
|
1397
|
+
: close.via === "shift-tab" || close.via === "defer-shift-tab"
|
|
1398
|
+
? -1
|
|
1399
|
+
: null;
|
|
1400
|
+
if (move === null || features.editMode === "row") {
|
|
1401
|
+
refocusCell(position);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const rowIndex = rows.findIndex((row) => row.id === position.rowId);
|
|
1405
|
+
const columnIndex = orderedColumns.findIndex(
|
|
1406
|
+
(column) => column.id === position.columnId,
|
|
1407
|
+
);
|
|
1408
|
+
if (rowIndex < 0 || columnIndex < 0) {
|
|
1409
|
+
refocusCell(position);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (move === "down") {
|
|
1413
|
+
if (rowIndex + 1 < rows.length) {
|
|
1414
|
+
moveFocusedCell({ rowIndex: rowIndex + 1, columnIndex });
|
|
1415
|
+
} else {
|
|
1416
|
+
refocusCell(position);
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const next = getNextEditableCell({
|
|
1421
|
+
from: { rowIndex, columnIndex },
|
|
1422
|
+
direction: move,
|
|
1423
|
+
rowCount: rows.length,
|
|
1424
|
+
columnCount: orderedColumns.length,
|
|
1425
|
+
isEditable: (coords) => {
|
|
1426
|
+
const row = rows[coords.rowIndex];
|
|
1427
|
+
const column = orderedColumns[coords.columnIndex];
|
|
1428
|
+
return (
|
|
1429
|
+
row !== undefined &&
|
|
1430
|
+
column !== undefined &&
|
|
1431
|
+
edit.canEditCell(row, column)
|
|
1432
|
+
);
|
|
1433
|
+
},
|
|
1434
|
+
});
|
|
1435
|
+
if (next !== null) moveFocusedCell(next);
|
|
1436
|
+
else refocusCell(position);
|
|
1437
|
+
};
|
|
1438
|
+
|
|
959
1439
|
/**
|
|
960
1440
|
* Space, from a cell. Checkbox semantics rather than click semantics: it adds
|
|
961
1441
|
* to the selection instead of replacing it, so a keyboard user builds one the
|
|
@@ -1031,6 +1511,51 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1031
1511
|
return;
|
|
1032
1512
|
}
|
|
1033
1513
|
|
|
1514
|
+
// Editing keys, ahead of the step-into pair — an editable cell's Enter
|
|
1515
|
+
// opens its editor, and only a non-editable cell's falls through to the
|
|
1516
|
+
// control it holds.
|
|
1517
|
+
const keyRow = rows[rowIndex];
|
|
1518
|
+
const keyColumn = orderedColumns[columnIndex];
|
|
1519
|
+
if (
|
|
1520
|
+
features.editing &&
|
|
1521
|
+
keyRow !== undefined &&
|
|
1522
|
+
keyColumn !== undefined &&
|
|
1523
|
+
edit.canEditCell(keyRow, keyColumn)
|
|
1524
|
+
) {
|
|
1525
|
+
if (event.key === "Enter" || event.key === "F2") {
|
|
1526
|
+
event.preventDefault();
|
|
1527
|
+
openEditor({ rowId, columnId });
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
// The quick single-cell gestures — Delete-to-clear and type-to-edit —
|
|
1531
|
+
// sit out row mode, whose whole point is that nothing commits until
|
|
1532
|
+
// the row's explicit save.
|
|
1533
|
+
if (features.editMode !== "row") {
|
|
1534
|
+
// Clear-and-commit, without opening anything — the spreadsheet Delete.
|
|
1535
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
1536
|
+
event.preventDefault();
|
|
1537
|
+
void edit
|
|
1538
|
+
.clearCell(rowId, columnId)
|
|
1539
|
+
.then(() => refocusCell({ rowId, columnId }));
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
// Type-to-edit: a printable character opens the editor seeded with
|
|
1543
|
+
// itself. Space stays the row-selection key, modifiers stay shortcuts.
|
|
1544
|
+
if (
|
|
1545
|
+
event.key.length === 1 &&
|
|
1546
|
+
event.key !== " " &&
|
|
1547
|
+
!event.ctrlKey &&
|
|
1548
|
+
!event.metaKey &&
|
|
1549
|
+
!event.altKey
|
|
1550
|
+
) {
|
|
1551
|
+
event.preventDefault();
|
|
1552
|
+
editSeedRef.current = event.key;
|
|
1553
|
+
openEditor({ rowId, columnId });
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1034
1559
|
// Step into the cell. The pair Excel and AG Grid both use, and the pair the
|
|
1035
1560
|
// editor will take over: F2 has never meant anything else, and Enter is
|
|
1036
1561
|
// what a keyboard user reaches for. Nothing to step into leaves the key
|
|
@@ -1269,12 +1794,52 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1269
1794
|
);
|
|
1270
1795
|
|
|
1271
1796
|
/**
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
* what every desktop context menu does.
|
|
1797
|
+
* Which edges the scroll position is currently at. Seeded "at top-left",
|
|
1798
|
+
* which is where a grid mounts — so the callbacks report *arrivals*, and
|
|
1799
|
+
* mounting fires nothing.
|
|
1276
1800
|
*/
|
|
1277
|
-
const
|
|
1801
|
+
const scrollEdgesRef = useRef({
|
|
1802
|
+
top: true,
|
|
1803
|
+
bottom: false,
|
|
1804
|
+
left: true,
|
|
1805
|
+
right: false,
|
|
1806
|
+
});
|
|
1807
|
+
const hasEdgeCallbacks =
|
|
1808
|
+
onScrollToTop !== undefined ||
|
|
1809
|
+
onScrollToBottom !== undefined ||
|
|
1810
|
+
onScrollToLeft !== undefined ||
|
|
1811
|
+
onScrollToRight !== undefined;
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* The context menu's dropdown is anchored to a fixed viewport point, so it
|
|
1815
|
+
* does not travel with the row when the body scrolls — and under
|
|
1816
|
+
* virtualization the row it belongs to may unmount entirely. Closing is
|
|
1817
|
+
* the honest answer, and matches what every desktop context menu does.
|
|
1818
|
+
*
|
|
1819
|
+
* The edge callbacks share the handler. Reading scroll offsets here costs
|
|
1820
|
+
* no layout; the 1px tolerance absorbs fractional offsets under browser
|
|
1821
|
+
* zoom, where a "fully scrolled" position can land at 799.5 of 800.
|
|
1822
|
+
*/
|
|
1823
|
+
const handleScroll =
|
|
1824
|
+
contextMenuTarget || hasEdgeCallbacks
|
|
1825
|
+
? (event: UIEvent<HTMLDivElement>) => {
|
|
1826
|
+
if (contextMenuTarget) closeContextMenu();
|
|
1827
|
+
if (!hasEdgeCallbacks) return;
|
|
1828
|
+
const el = event.currentTarget;
|
|
1829
|
+
const next = {
|
|
1830
|
+
top: el.scrollTop <= 1,
|
|
1831
|
+
bottom: el.scrollTop + el.clientHeight >= el.scrollHeight - 1,
|
|
1832
|
+
left: el.scrollLeft <= 1,
|
|
1833
|
+
right: el.scrollLeft + el.clientWidth >= el.scrollWidth - 1,
|
|
1834
|
+
};
|
|
1835
|
+
const previous = scrollEdgesRef.current;
|
|
1836
|
+
scrollEdgesRef.current = next;
|
|
1837
|
+
if (next.top && !previous.top) onScrollToTop?.();
|
|
1838
|
+
if (next.bottom && !previous.bottom) onScrollToBottom?.();
|
|
1839
|
+
if (next.left && !previous.left) onScrollToLeft?.();
|
|
1840
|
+
if (next.right && !previous.right) onScrollToRight?.();
|
|
1841
|
+
}
|
|
1842
|
+
: undefined;
|
|
1278
1843
|
|
|
1279
1844
|
return (
|
|
1280
1845
|
<div className={classes.tableWrapper}>
|
|
@@ -1286,11 +1851,18 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1286
1851
|
onScroll={handleScroll}
|
|
1287
1852
|
>
|
|
1288
1853
|
<div
|
|
1854
|
+
ref={gridElementRef}
|
|
1289
1855
|
// A `table` is content; a `grid` is a widget with a keyboard cursor
|
|
1290
1856
|
// in it. Which one this is depends on whether cell selection is on,
|
|
1291
1857
|
// and saying `grid` without one would promise arrow keys that do
|
|
1292
1858
|
// nothing.
|
|
1293
1859
|
role={cellSelection ? "grid" : "table"}
|
|
1860
|
+
aria-label={ariaLabel}
|
|
1861
|
+
aria-labelledby={ariaLabelledBy}
|
|
1862
|
+
// Set while `meta.loading` is, whether or not the body has rows to
|
|
1863
|
+
// show — a refetch over existing rows is still a fetch, and a test
|
|
1864
|
+
// waiting for the grid to settle has nothing else to wait on.
|
|
1865
|
+
aria-busy={loading === true ? true : undefined}
|
|
1294
1866
|
aria-multiselectable={
|
|
1295
1867
|
cellSelection && features.rowSelection && features.multiRowSelection
|
|
1296
1868
|
? true
|
|
@@ -1306,9 +1878,17 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1306
1878
|
// have to be stated rather than inferred from what is rendered. Every
|
|
1307
1879
|
// row carries its aria-rowindex for the same reason.
|
|
1308
1880
|
aria-rowcount={
|
|
1309
|
-
rows.length +
|
|
1881
|
+
rows.length +
|
|
1882
|
+
pinnedRowCount +
|
|
1883
|
+
headerGroups.length +
|
|
1884
|
+
(hasSummaryRow ? 1 : 0)
|
|
1310
1885
|
}
|
|
1311
1886
|
aria-colcount={orderedColumns.length}
|
|
1887
|
+
// Body rows the grid is currently showing — the page under
|
|
1888
|
+
// pagination, everything the filters left otherwise, edge blocks
|
|
1889
|
+
// included. Most of them are not in the DOM, so this is what a test
|
|
1890
|
+
// waits on rather than counting nodes.
|
|
1891
|
+
data-dg-row-count={rows.length + pinnedRowCount}
|
|
1312
1892
|
style={{ gridTemplateColumns, minWidth: gridMinWidth }}
|
|
1313
1893
|
>
|
|
1314
1894
|
<div role="rowgroup" style={{ display: "contents" }}>
|
|
@@ -1317,6 +1897,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1317
1897
|
key={headerGroup.id}
|
|
1318
1898
|
role="row"
|
|
1319
1899
|
aria-rowindex={groupIndex + 1}
|
|
1900
|
+
data-dg-header-row
|
|
1901
|
+
// The scrolled-under shadow belongs to the boundary between
|
|
1902
|
+
// header and body — the last header row, not every stacked
|
|
1903
|
+
// group row above it. See the stylesheet.
|
|
1904
|
+
data-dg-header-last={
|
|
1905
|
+
groupIndex === headerGroups.length - 1 || undefined
|
|
1906
|
+
}
|
|
1320
1907
|
className={classes.headerRow}
|
|
1321
1908
|
>
|
|
1322
1909
|
{headerGroup.headers.map((header) => (
|
|
@@ -1330,8 +1917,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1330
1917
|
))}
|
|
1331
1918
|
</div>
|
|
1332
1919
|
|
|
1333
|
-
{
|
|
1334
|
-
|
|
1920
|
+
{/* The entry block — new rows being typed, stuck under the header.
|
|
1921
|
+
Present only while `edit.addRow()` has open entries. */}
|
|
1922
|
+
{features.editing && newRowCount > 0 && (
|
|
1923
|
+
<TMDataGridEntryRows
|
|
1924
|
+
orderedColumns={orderedColumns}
|
|
1925
|
+
layoutFor={layoutFor}
|
|
1926
|
+
rowHeight={rowHeight}
|
|
1927
|
+
/>
|
|
1335
1928
|
)}
|
|
1336
1929
|
|
|
1337
1930
|
<TMDataGridBodyRowGroup
|
|
@@ -1343,11 +1936,40 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1343
1936
|
onContextMenu={handleContextMenu}
|
|
1344
1937
|
onClose={closeContextMenu}
|
|
1345
1938
|
>
|
|
1346
|
-
{
|
|
1347
|
-
|
|
1348
|
-
|
|
1939
|
+
{(() => {
|
|
1940
|
+
/**
|
|
1941
|
+
* One renderer for every body row, wherever it sits: the
|
|
1942
|
+
* virtualized centre, or a pinned edge block. `viewIndex` is the
|
|
1943
|
+
* row's index in the scrolling view — `-1` for a pinned row,
|
|
1944
|
+
* which takes it out of striping, row measurement and the cell
|
|
1945
|
+
* range, all of which are statements about the scrolling order.
|
|
1946
|
+
* Scoped here rather than a component so the row keeps closing
|
|
1947
|
+
* over the body's handlers without threading them as props.
|
|
1948
|
+
*/
|
|
1949
|
+
const renderBodyRow = (
|
|
1950
|
+
row: Row<TMDataGridFeatures, TMDataGridRowData>,
|
|
1951
|
+
viewIndex: number,
|
|
1952
|
+
ariaRowIndex: number,
|
|
1953
|
+
pinnedAt?: "top" | "bottom",
|
|
1954
|
+
) => {
|
|
1349
1955
|
const isGroupRow = row.getIsGrouped();
|
|
1350
1956
|
const isInteractive = rowGesturesFor(row);
|
|
1957
|
+
const rowCells = [
|
|
1958
|
+
...row.getLeftVisibleCells(),
|
|
1959
|
+
...row.getCenterVisibleCells(),
|
|
1960
|
+
...row.getRightVisibleCells(),
|
|
1961
|
+
];
|
|
1962
|
+
// Row mode opens every editable cell of the row at once;
|
|
1963
|
+
// exactly one of them may take the focus.
|
|
1964
|
+
const rowEditing =
|
|
1965
|
+
editActive !== null &&
|
|
1966
|
+
editActive.rowId === row.id &&
|
|
1967
|
+
editActive.columnId === null;
|
|
1968
|
+
const firstEditableColumnId = rowEditing
|
|
1969
|
+
? (rowCells.find((cell) =>
|
|
1970
|
+
edit.canEditCell(row, cell.column),
|
|
1971
|
+
)?.column.id ?? null)
|
|
1972
|
+
: null;
|
|
1351
1973
|
// Cell selection takes the body's tab stop off the row and puts
|
|
1352
1974
|
// it on a cell — two stops per row would make Tab a way of
|
|
1353
1975
|
// walking the grid, which is what the arrow keys are for. Space
|
|
@@ -1358,10 +1980,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1358
1980
|
(selectsOnRowClick || features.highlightRow);
|
|
1359
1981
|
return (
|
|
1360
1982
|
<div
|
|
1361
|
-
key={
|
|
1983
|
+
key={row.id}
|
|
1362
1984
|
role="row"
|
|
1363
|
-
aria-rowindex={
|
|
1364
|
-
data-
|
|
1985
|
+
aria-rowindex={ariaRowIndex}
|
|
1986
|
+
data-dg-part="row"
|
|
1987
|
+
data-row-id={row.id}
|
|
1365
1988
|
// Only measured when details are on: without them every row
|
|
1366
1989
|
// is exactly `rowHeight`, and the estimate above is already
|
|
1367
1990
|
// the answer — so a plain grid keeps its ResizeObserver-free
|
|
@@ -1372,13 +1995,21 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1372
1995
|
// element back to its item. The panel is inside this element,
|
|
1373
1996
|
// so what gets measured is the row *and* its detail — which
|
|
1374
1997
|
// is the whole reason no second virtual item is needed.
|
|
1375
|
-
data-index={
|
|
1376
|
-
|
|
1998
|
+
data-index={
|
|
1999
|
+
renderDetails && viewIndex >= 0 ? viewIndex : undefined
|
|
2000
|
+
}
|
|
2001
|
+
ref={
|
|
2002
|
+
renderDetails && viewIndex >= 0
|
|
2003
|
+
? virtualizer.measureElement
|
|
2004
|
+
: undefined
|
|
2005
|
+
}
|
|
1377
2006
|
// The tree's own rows. `data-grouped` rather than a class, to
|
|
1378
2007
|
// match how the rest of the row's state is published — and so
|
|
1379
2008
|
// a consumer can restyle them without reaching into modules.
|
|
1380
2009
|
data-grouped={isGroupRow}
|
|
1381
2010
|
data-depth={row.depth}
|
|
2011
|
+
// Which edge block the row sits in, for styling hooks.
|
|
2012
|
+
data-pinned={pinnedAt}
|
|
1382
2013
|
// Gated on the flag, not just on `getIsSelected()`: under
|
|
1383
2014
|
// `"highlight"` there is no selection, but `rowSelection` may
|
|
1384
2015
|
// still hold ids from before the mode changed — TanStack never
|
|
@@ -1404,6 +2035,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1404
2035
|
data-highlighted={
|
|
1405
2036
|
features.highlightRow && row.id === highlightedRowId
|
|
1406
2037
|
}
|
|
2038
|
+
// Marked deleted under batch: struck through and inert
|
|
2039
|
+
// until submitAll reports it, or the mark is toggled back.
|
|
2040
|
+
data-deleted={
|
|
2041
|
+
deletedRowIds.length > 0 && deletedRowIds.includes(row.id)
|
|
2042
|
+
}
|
|
1407
2043
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
1408
2044
|
// `data-expanded` lands there rather than on a row. This is
|
|
1409
2045
|
// what says which row the open menu is about.
|
|
@@ -1420,10 +2056,18 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1420
2056
|
? true
|
|
1421
2057
|
: undefined
|
|
1422
2058
|
}
|
|
1423
|
-
|
|
2059
|
+
// From the row's position in the whole view, so the stripes
|
|
2060
|
+
// survive the virtualizer's moving window — see the prop.
|
|
2061
|
+
data-striped={
|
|
2062
|
+
striped && viewIndex >= 0 ? viewIndex % 2 === 1 : undefined
|
|
2063
|
+
}
|
|
2064
|
+
className={[classes.bodyRow, rowClassNameFor(row)]
|
|
2065
|
+
.filter(Boolean)
|
|
2066
|
+
.join(" ")}
|
|
1424
2067
|
style={{
|
|
1425
2068
|
minHeight: rowHeight,
|
|
1426
2069
|
cursor: isInteractive ? "pointer" : undefined,
|
|
2070
|
+
...rowStyleFor(row),
|
|
1427
2071
|
}}
|
|
1428
2072
|
// Keyboard parity with the checkbox the row replaces.
|
|
1429
2073
|
tabIndex={takesKeyboard ? 0 : undefined}
|
|
@@ -1480,16 +2124,65 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1480
2124
|
: undefined
|
|
1481
2125
|
}
|
|
1482
2126
|
>
|
|
1483
|
-
{
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2127
|
+
{rowCells.map((cell, cellIndex) => {
|
|
2128
|
+
const isEditingCell =
|
|
2129
|
+
(editActive !== null &&
|
|
2130
|
+
editActive.rowId === row.id &&
|
|
2131
|
+
editActive.columnId === cell.column.id) ||
|
|
2132
|
+
(rowEditing && edit.canEditCell(row, cell.column));
|
|
2133
|
+
return (
|
|
1488
2134
|
<TMDataGridBodyCell
|
|
1489
2135
|
key={cell.id}
|
|
1490
2136
|
cell={cell}
|
|
1491
2137
|
rowHeight={rowHeight}
|
|
1492
2138
|
layout={layoutFor(cell.column.id)}
|
|
2139
|
+
contentOverride={
|
|
2140
|
+
rowNumberById !== null &&
|
|
2141
|
+
cell.column.id === ROW_NUMBER_COLUMN_ID
|
|
2142
|
+
? rowNumberById.get(row.id)
|
|
2143
|
+
: undefined
|
|
2144
|
+
}
|
|
2145
|
+
highlightNeedles={matchNeedles?.get(cell.column.id)}
|
|
2146
|
+
editor={
|
|
2147
|
+
isEditingCell ? (
|
|
2148
|
+
<TMDataGridCellEditor
|
|
2149
|
+
cell={cell}
|
|
2150
|
+
row={row}
|
|
2151
|
+
takeSeedText={takeEditSeed}
|
|
2152
|
+
autoFocus={
|
|
2153
|
+
!rowEditing ||
|
|
2154
|
+
cell.column.id === firstEditableColumnId
|
|
2155
|
+
}
|
|
2156
|
+
onClose={(close) =>
|
|
2157
|
+
handleEditorClose(
|
|
2158
|
+
{ rowId: row.id, columnId: cell.column.id },
|
|
2159
|
+
close,
|
|
2160
|
+
)
|
|
2161
|
+
}
|
|
2162
|
+
/>
|
|
2163
|
+
) : undefined
|
|
2164
|
+
}
|
|
2165
|
+
onClick={
|
|
2166
|
+
onCellClick && !isGroupRow
|
|
2167
|
+
? (event) =>
|
|
2168
|
+
onCellClick(cellEventArgs(cell, row, event))
|
|
2169
|
+
: undefined
|
|
2170
|
+
}
|
|
2171
|
+
onDoubleClick={
|
|
2172
|
+
(features.editing || onCellDoubleClick) && !isGroupRow
|
|
2173
|
+
? (event) => {
|
|
2174
|
+
onCellDoubleClick?.(
|
|
2175
|
+
cellEventArgs(cell, row, event),
|
|
2176
|
+
);
|
|
2177
|
+
if (!features.editing) return;
|
|
2178
|
+
if (!edit.canEditCell(row, cell.column)) return;
|
|
2179
|
+
openEditor({
|
|
2180
|
+
rowId: row.id,
|
|
2181
|
+
columnId: cell.column.id,
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
: undefined
|
|
2185
|
+
}
|
|
1493
2186
|
// The cells come out in the same left → centre → right
|
|
1494
2187
|
// order as `orderedColumns`, which is what lets the index
|
|
1495
2188
|
// here stand for the column index everywhere else.
|
|
@@ -1514,17 +2207,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1514
2207
|
// take" — which under `"single"` is the one cell
|
|
1515
2208
|
// the user is standing on.
|
|
1516
2209
|
selected: cellRangeSelection
|
|
1517
|
-
? isWithinBounds(
|
|
1518
|
-
rangeBounds,
|
|
1519
|
-
virtualItem.index,
|
|
1520
|
-
cellIndex,
|
|
1521
|
-
)
|
|
2210
|
+
? isWithinBounds(rangeBounds, viewIndex, cellIndex)
|
|
1522
2211
|
: isCellAt(focusedCell, row.id, cell.column.id),
|
|
1523
|
-
edges: boundsEdges(
|
|
1524
|
-
rangeBounds,
|
|
1525
|
-
virtualItem.index,
|
|
1526
|
-
cellIndex,
|
|
1527
|
-
),
|
|
2212
|
+
edges: boundsEdges(rangeBounds, viewIndex, cellIndex),
|
|
1528
2213
|
}
|
|
1529
2214
|
: undefined
|
|
1530
2215
|
}
|
|
@@ -1567,14 +2252,19 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1567
2252
|
: undefined
|
|
1568
2253
|
}
|
|
1569
2254
|
onContextMenu={
|
|
1570
|
-
rowContextMenu || cellRangeSelection
|
|
1571
|
-
? () => {
|
|
2255
|
+
rowContextMenu || cellRangeSelection || onCellContextMenu
|
|
2256
|
+
? (event) => {
|
|
1572
2257
|
contextMenuColumnRef.current = cell.column.id;
|
|
2258
|
+
if (isGroupRow) return;
|
|
2259
|
+
onCellContextMenu?.(
|
|
2260
|
+
cellEventArgs(cell, row, event),
|
|
2261
|
+
);
|
|
1573
2262
|
}
|
|
1574
2263
|
: undefined
|
|
1575
2264
|
}
|
|
1576
2265
|
/>
|
|
1577
|
-
|
|
2266
|
+
);
|
|
2267
|
+
})}
|
|
1578
2268
|
|
|
1579
2269
|
{/* A second grid row inside the row element, on the same
|
|
1580
2270
|
subgrid tracks — which is what lets one measurement cover
|
|
@@ -1585,7 +2275,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1585
2275
|
<div
|
|
1586
2276
|
role="cell"
|
|
1587
2277
|
aria-colspan={orderedColumns.length}
|
|
1588
|
-
data-
|
|
2278
|
+
data-dg-part="details"
|
|
2279
|
+
data-row-id={row.id}
|
|
1589
2280
|
className={classes.detailsCell}
|
|
1590
2281
|
// The row underneath may select, highlight or open a
|
|
1591
2282
|
// context menu. A panel is content, not part of that
|
|
@@ -1604,39 +2295,132 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1604
2295
|
)}
|
|
1605
2296
|
</div>
|
|
1606
2297
|
);
|
|
1607
|
-
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
return (
|
|
2301
|
+
<>
|
|
2302
|
+
{/* The pinned edge blocks. Same sticky mechanics as the
|
|
2303
|
+
entry block — the *block* is the sticky element, its rows
|
|
2304
|
+
flow normally inside it. `role="presentation"` keeps the
|
|
2305
|
+
wrapper out of the accessibility tree, so the rows stay
|
|
2306
|
+
direct children of the rowgroup. Inside the rowgroup so
|
|
2307
|
+
a right-click on a pinned row still opens the menu. */}
|
|
2308
|
+
{pinnedTopRows.length > 0 && (
|
|
2309
|
+
<div
|
|
2310
|
+
role="presentation"
|
|
2311
|
+
data-dg-part="pinned-top"
|
|
2312
|
+
className={classes.pinnedTopBlock}
|
|
2313
|
+
>
|
|
2314
|
+
{pinnedTopRows.map((row, index) =>
|
|
2315
|
+
renderBodyRow(
|
|
2316
|
+
row,
|
|
2317
|
+
-1,
|
|
2318
|
+
headerGroups.length + index + 1,
|
|
2319
|
+
"top",
|
|
2320
|
+
),
|
|
2321
|
+
)}
|
|
2322
|
+
</div>
|
|
2323
|
+
)}
|
|
2324
|
+
|
|
2325
|
+
{paddingTop > 0 && (
|
|
2326
|
+
<div
|
|
2327
|
+
aria-hidden
|
|
2328
|
+
style={{ gridColumn: "1/-1", height: paddingTop }}
|
|
2329
|
+
/>
|
|
2330
|
+
)}
|
|
2331
|
+
|
|
2332
|
+
{virtualItems.map((virtualItem) => {
|
|
2333
|
+
const row = rows[virtualItem.index];
|
|
2334
|
+
if (!row) return null;
|
|
2335
|
+
return renderBodyRow(
|
|
2336
|
+
row,
|
|
2337
|
+
virtualItem.index,
|
|
2338
|
+
virtualItem.index +
|
|
2339
|
+
headerGroups.length +
|
|
2340
|
+
pinnedTopRows.length +
|
|
2341
|
+
1,
|
|
2342
|
+
);
|
|
2343
|
+
})}
|
|
2344
|
+
|
|
2345
|
+
{paddingBottom > 0 && (
|
|
2346
|
+
<div
|
|
2347
|
+
aria-hidden
|
|
2348
|
+
style={{ gridColumn: "1/-1", height: paddingBottom }}
|
|
2349
|
+
/>
|
|
2350
|
+
)}
|
|
2351
|
+
|
|
2352
|
+
{pinnedBottomRows.length > 0 && (
|
|
2353
|
+
<div
|
|
2354
|
+
role="presentation"
|
|
2355
|
+
data-dg-part="pinned-bottom"
|
|
2356
|
+
className={classes.pinnedBottomBlock}
|
|
2357
|
+
>
|
|
2358
|
+
{pinnedBottomRows.map((row, index) =>
|
|
2359
|
+
renderBodyRow(
|
|
2360
|
+
row,
|
|
2361
|
+
-1,
|
|
2362
|
+
headerGroups.length +
|
|
2363
|
+
pinnedTopRows.length +
|
|
2364
|
+
rows.length +
|
|
2365
|
+
index +
|
|
2366
|
+
1,
|
|
2367
|
+
"bottom",
|
|
2368
|
+
),
|
|
2369
|
+
)}
|
|
2370
|
+
</div>
|
|
2371
|
+
)}
|
|
2372
|
+
</>
|
|
2373
|
+
);
|
|
2374
|
+
})()}
|
|
1608
2375
|
</TMDataGridBodyRowGroup>
|
|
1609
2376
|
|
|
2377
|
+
{/* The empty matrix, one state at a time: loading wins (data is on
|
|
2378
|
+
its way, "empty" would be a lie), open entry rows show only
|
|
2379
|
+
themselves (a "no rows" under the row being typed reads as a
|
|
2380
|
+
contradiction), then the consumer's blank slate, then the two
|
|
2381
|
+
defaults — filtered-empty with its search icon, truly-empty as
|
|
2382
|
+
plain text, because only one of them is the user's own doing. */}
|
|
1610
2383
|
{isEmpty && loading === true && (
|
|
1611
2384
|
<div role="row" className={classes.messageRow}>
|
|
1612
2385
|
<Loader size="lg" aria-label={labels.loading} />
|
|
1613
2386
|
</div>
|
|
1614
2387
|
)}
|
|
1615
2388
|
|
|
1616
|
-
{isEmpty && loading !== true && (
|
|
2389
|
+
{isEmpty && loading !== true && newRowCount === 0 && (
|
|
1617
2390
|
<div role="row" className={classes.messageRow}>
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2391
|
+
{renderEmptyState !== undefined ? (
|
|
2392
|
+
renderEmptyState({
|
|
2393
|
+
hasActiveFilters,
|
|
2394
|
+
table: table as unknown as Table<TMDataGridFeatures, TData>,
|
|
2395
|
+
})
|
|
2396
|
+
) : hasActiveFilters ? (
|
|
2397
|
+
<>
|
|
2398
|
+
<SearchIcon size={40} stroke={1.4} opacity={0.4} />
|
|
2399
|
+
<Text size={controlSize} c="dimmed">
|
|
2400
|
+
{noResultsLabel}
|
|
2401
|
+
</Text>
|
|
2402
|
+
</>
|
|
2403
|
+
) : (
|
|
2404
|
+
<Text size={controlSize} c="dimmed">
|
|
2405
|
+
{labels.noRows}
|
|
2406
|
+
</Text>
|
|
2407
|
+
)}
|
|
1622
2408
|
</div>
|
|
1623
2409
|
)}
|
|
1624
2410
|
|
|
1625
|
-
{paddingBottom > 0 && (
|
|
1626
|
-
<div
|
|
1627
|
-
aria-hidden
|
|
1628
|
-
style={{ gridColumn: "1/-1", height: paddingBottom }}
|
|
1629
|
-
/>
|
|
1630
|
-
)}
|
|
1631
|
-
|
|
1632
2411
|
{/* Sticky along the bottom edge, the way the header is along the
|
|
1633
2412
|
top. `flexRender` with the header context, which is what a
|
|
1634
2413
|
TanStack `footer` renderer is written against. */}
|
|
1635
2414
|
{hasSummaryRow && (
|
|
1636
2415
|
<div
|
|
1637
2416
|
role="row"
|
|
1638
|
-
aria-rowindex={
|
|
1639
|
-
|
|
2417
|
+
aria-rowindex={
|
|
2418
|
+
rows.length + pinnedRowCount + headerGroups.length + 1
|
|
2419
|
+
}
|
|
2420
|
+
// Also what the height measurement above looks for. One
|
|
2421
|
+
// attribute serves both now that the part name *is* the
|
|
2422
|
+
// published contract rather than a testing-only alias.
|
|
2423
|
+
data-dg-part="summary-row"
|
|
1640
2424
|
className={classes.summaryRow}
|
|
1641
2425
|
>
|
|
1642
2426
|
{leafHeaders.map((header) => {
|