@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/components/generatedColumns.tsx +187 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -18,13 +18,19 @@ import {
|
|
|
18
18
|
useCallback,
|
|
19
19
|
useEffect,
|
|
20
20
|
useLayoutEffect,
|
|
21
|
+
useMemo,
|
|
21
22
|
useRef,
|
|
22
23
|
useState,
|
|
23
24
|
} from "react";
|
|
24
25
|
import classes from "./TMDataGridTable.module.css";
|
|
25
26
|
import sticky from "./sticky.module.css";
|
|
26
27
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
27
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
TMDataGridFilterPopup,
|
|
30
|
+
TMDataGridFilterSidebar,
|
|
31
|
+
} from "./TMDataGridFilterSurface";
|
|
32
|
+
import { TMDataGridHeaderFilterRow } from "./TMDataGridHeaderFilterRow";
|
|
33
|
+
import { getGridCapabilities } from "../core/capabilities";
|
|
28
34
|
import {
|
|
29
35
|
TMDataGridHeaderCell,
|
|
30
36
|
type TMDataGridColumnMenuItemsRenderer,
|
|
@@ -34,12 +40,17 @@ import {
|
|
|
34
40
|
type TMDataGridCellEditorClose,
|
|
35
41
|
} from "./TMDataGridCellEditor";
|
|
36
42
|
import { TMDataGridEntryRows } from "./TMDataGridEntryRows";
|
|
37
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getColumnAlign,
|
|
45
|
+
isControlColumn,
|
|
46
|
+
isGeneratedColumn,
|
|
47
|
+
} from "../core/columnUtils";
|
|
38
48
|
import {
|
|
39
49
|
resizePreview,
|
|
40
50
|
type TMDataGridColumnTrack,
|
|
41
51
|
} from "../core/resizePreview";
|
|
42
52
|
import { useSettledTableState } from "../core/useSettledTableState";
|
|
53
|
+
import { isInputElement } from "../core/dom";
|
|
43
54
|
import { draftCellContext } from "../core/draftCellContext";
|
|
44
55
|
import {
|
|
45
56
|
getEditFieldName,
|
|
@@ -58,14 +69,12 @@ import {
|
|
|
58
69
|
resolveRangeBounds,
|
|
59
70
|
} from "../core/cellRange";
|
|
60
71
|
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
downloadTextFile,
|
|
72
|
+
buildExportData,
|
|
73
|
+
resolveExportOptions,
|
|
64
74
|
toClipboardText,
|
|
65
|
-
toExcelCsv,
|
|
66
75
|
writeClipboardText,
|
|
67
|
-
|
|
68
|
-
} from "../core/
|
|
76
|
+
writeExportFile,
|
|
77
|
+
} from "../core/export";
|
|
69
78
|
import { autosizeColumn, hasMountedCells } from "../core/autosize";
|
|
70
79
|
import {
|
|
71
80
|
getDisplayedRows,
|
|
@@ -286,10 +295,22 @@ function TMDataGridBodyCell({
|
|
|
286
295
|
// The whole draft, not one field: a computed or custom renderer may read any
|
|
287
296
|
// field off the row, so every data cell of a drafted row repaints together
|
|
288
297
|
// when its draft moves - and only then, `values` being reference-stable
|
|
289
|
-
// across meta-only form events. `undefined` everywhere else
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
298
|
+
// across meta-only form events. `undefined` everywhere else - including a
|
|
299
|
+
// committed row, whose draft already *is* the row: the hook feeds the draft
|
|
300
|
+
// store's values to the table as `data`, so `row.original` holds them.
|
|
301
|
+
// Only an open form's undecided values need the overlay.
|
|
302
|
+
const draftValues = useSelector(edit.store, (state) => {
|
|
303
|
+
if (isControl) return undefined;
|
|
304
|
+
if (state.committedRowIds.includes(cellRowId)) return undefined;
|
|
305
|
+
if (
|
|
306
|
+
state.newRows.some(
|
|
307
|
+
(newRow) => newRow.tempId === cellRowId && newRow.committed,
|
|
308
|
+
)
|
|
309
|
+
) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
return state.rows[cellRowId]?.values;
|
|
313
|
+
});
|
|
293
314
|
return (
|
|
294
315
|
<div
|
|
295
316
|
// `gridcell` rather than `cell` is what tells a screen reader the arrow
|
|
@@ -309,16 +330,16 @@ function TMDataGridBodyCell({
|
|
|
309
330
|
// selectors also require `data-cell`, so they are unaffected.
|
|
310
331
|
data-row-id={cell.row.id}
|
|
311
332
|
data-column-id={cell.column.id}
|
|
312
|
-
data-focused={nav?.focused}
|
|
333
|
+
data-focused={nav?.focused || undefined}
|
|
313
334
|
aria-selected={nav?.selected}
|
|
314
|
-
data-selected={nav?.selected}
|
|
335
|
+
data-selected={nav?.selected || undefined}
|
|
315
336
|
// One attribute per side rather than a class per combination: the
|
|
316
337
|
// stylesheet draws the outline edge by edge, and sixteen classes for the
|
|
317
338
|
// sixteen corners of a rectangle is not a stylesheet anyone can read.
|
|
318
|
-
data-edge-top={nav?.edges?.top}
|
|
319
|
-
data-edge-bottom={nav?.edges?.bottom}
|
|
320
|
-
data-edge-left={nav?.edges?.left}
|
|
321
|
-
data-edge-right={nav?.edges?.right}
|
|
339
|
+
data-edge-top={nav?.edges?.top || undefined}
|
|
340
|
+
data-edge-bottom={nav?.edges?.bottom || undefined}
|
|
341
|
+
data-edge-left={nav?.edges?.left || undefined}
|
|
342
|
+
data-edge-right={nav?.edges?.right || undefined}
|
|
322
343
|
// Bubbles, so focus landing on a control inside the cell counts as
|
|
323
344
|
// landing on the cell - the ring follows the user into the checkbox
|
|
324
345
|
// rather than being left behind on whichever cell they came from.
|
|
@@ -333,7 +354,7 @@ function TMDataGridBodyCell({
|
|
|
333
354
|
data-align={getColumnAlign(cell.column)}
|
|
334
355
|
// A control lane is a fixed track, so it cannot take the cell padding the
|
|
335
356
|
// scale grows for text. See isControlColumn.
|
|
336
|
-
data-control-column={isControl}
|
|
357
|
+
data-control-column={isControl || undefined}
|
|
337
358
|
onContextMenu={onContextMenu}
|
|
338
359
|
className={[
|
|
339
360
|
classes.bodyCell,
|
|
@@ -349,19 +370,22 @@ function TMDataGridBodyCell({
|
|
|
349
370
|
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
350
371
|
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
351
372
|
// The focus ring is drawn inside the cell's own box, so the cell has to
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
373
|
+
// stack above the cells it shares an edge with - but not above the
|
|
374
|
+
// pinned lanes, or the ring rides over them once the row scrolls
|
|
375
|
+
// sideways. A focused cell that is itself pinned takes the slot above
|
|
376
|
+
// them, keeping its ring whole while the rest of the row slides under
|
|
377
|
+
// it. Values from the stacking ladder in TMDataGrid.module.css.
|
|
356
378
|
position: layout.pinnedAt
|
|
357
379
|
? "sticky"
|
|
358
380
|
: nav?.focused
|
|
359
381
|
? "relative"
|
|
360
382
|
: undefined,
|
|
361
|
-
zIndex:
|
|
362
|
-
?
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
zIndex: layout.pinnedAt
|
|
384
|
+
? nav?.focused
|
|
385
|
+
? "var(--dg-z-pinned-focused-cell, 3)"
|
|
386
|
+
: "var(--dg-z-pinned-cell, 2)"
|
|
387
|
+
: nav?.focused
|
|
388
|
+
? "var(--dg-z-focused-cell, 1)"
|
|
365
389
|
: undefined,
|
|
366
390
|
}}
|
|
367
391
|
>
|
|
@@ -654,12 +678,6 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
654
678
|
* `position`, `transitionProps` and the rest. Its open state is the grid's.
|
|
655
679
|
*/
|
|
656
680
|
rowContextMenuProps?: Omit<MenuProps, "opened" | "onChange" | "children">;
|
|
657
|
-
/**
|
|
658
|
-
* How Ctrl+C and the export item write values, under
|
|
659
|
-
* `cellSelection: "range"`. Defaults to the Nordic Excel conventions - see
|
|
660
|
-
* {@link TMDataGridCellExportOptions}.
|
|
661
|
-
*/
|
|
662
|
-
cellExport?: TMDataGridCellExportOptions;
|
|
663
681
|
/**
|
|
664
682
|
* Called when the scroll approaches the last row - the infinite-scroll
|
|
665
683
|
* hook-in. Append the next page to `data` and the virtualizer keeps its
|
|
@@ -711,7 +729,6 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
711
729
|
renderRowContextMenu,
|
|
712
730
|
renderColumnMenuItems,
|
|
713
731
|
rowContextMenuProps,
|
|
714
|
-
cellExport,
|
|
715
732
|
onReachEnd,
|
|
716
733
|
reachEndThreshold = 10,
|
|
717
734
|
"aria-label": ariaLabel,
|
|
@@ -722,6 +739,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
722
739
|
ui,
|
|
723
740
|
edit,
|
|
724
741
|
features,
|
|
742
|
+
filters,
|
|
743
|
+
exportOptions: gridExportOptions,
|
|
725
744
|
labels,
|
|
726
745
|
rowHeight,
|
|
727
746
|
controlSize,
|
|
@@ -747,7 +766,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
747
766
|
// Row mode opens whole rows and any number of them at once, so which rows
|
|
748
767
|
// are showing editors is the set of open forms, not the single active slot.
|
|
749
768
|
const editOpenRowIds = useSelector(edit.store, (state) => state.openRowIds);
|
|
750
|
-
|
|
769
|
+
// Entry rows, for the block's mount and for `data-new` on the committed
|
|
770
|
+
// ones that have joined the body. Identity-stable across typing.
|
|
771
|
+
const newRows = useSelector(edit.store, (state) => state.newRows);
|
|
772
|
+
const newRowCount = newRows.length;
|
|
751
773
|
const deletedRowIds = useSelector(edit.store, (state) => state.deletedRowIds);
|
|
752
774
|
// Rows carrying a dirty draft, for the row-level marker. Derived from the
|
|
753
775
|
// same projections the cells subscribe to; the array's identity churns with
|
|
@@ -765,6 +787,24 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
765
787
|
edit.store,
|
|
766
788
|
(state) => state.committedRowIds,
|
|
767
789
|
);
|
|
790
|
+
// The same lists by id: the body asks them once per rendered row, and an
|
|
791
|
+
// import runs each of them to thousands.
|
|
792
|
+
const editOpenRowIdSet = useMemo(
|
|
793
|
+
() => new Set(editOpenRowIds),
|
|
794
|
+
[editOpenRowIds],
|
|
795
|
+
);
|
|
796
|
+
const editDirtyRowIdSet = useMemo(
|
|
797
|
+
() => new Set(editDirtyRowIds),
|
|
798
|
+
[editDirtyRowIds],
|
|
799
|
+
);
|
|
800
|
+
const editDraftRowIdSet = useMemo(
|
|
801
|
+
() => new Set(editDraftRowIds),
|
|
802
|
+
[editDraftRowIds],
|
|
803
|
+
);
|
|
804
|
+
const newRowIdSet = useMemo(
|
|
805
|
+
() => new Set(newRows.map((newRow) => newRow.tempId)),
|
|
806
|
+
[newRows],
|
|
807
|
+
);
|
|
768
808
|
|
|
769
809
|
const { loading, noResultsLabel = labels.noResults } =
|
|
770
810
|
table.options.meta ?? {};
|
|
@@ -858,29 +898,99 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
858
898
|
});
|
|
859
899
|
|
|
860
900
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
901
|
+
* Everything the body rows start after, and how much of it stays on screen
|
|
902
|
+
* once they scroll: the header rows, the entry block and the pinned top
|
|
903
|
+
* block all sit in the same scroll content, above the first virtual row.
|
|
904
|
+
*
|
|
905
|
+
* Three readers, one measurement.
|
|
906
|
+
*
|
|
907
|
+
* `--dg-header-height` is the header total alone, published for the entry
|
|
908
|
+
* block to stick under and only while a block is in use - the stylesheet's
|
|
909
|
+
* per-`size` value is what the header cells are laid out against otherwise,
|
|
910
|
+
* and it is not the sum of a stacked group's rows.
|
|
911
|
+
*
|
|
912
|
+
* `bodyOffsetTop` is the whole stack, and it is the virtualizer's
|
|
913
|
+
* `scrollMargin`: item offsets start at zero, so without it every one of
|
|
914
|
+
* them is short by the height of what the rows begin after. `stickyCoverTop`
|
|
915
|
+
* leaves out the entry rows that flow with the body (`editNewRowsSticky`
|
|
916
|
+
* off), and is its `scrollPaddingStart` - the part of the stack that stays
|
|
917
|
+
* put and would otherwise be covering the row `align: "auto"` just scrolled
|
|
918
|
+
* to.
|
|
919
|
+
*
|
|
920
|
+
* The observer is mounted whatever the grid shows, unlike the block
|
|
921
|
+
* measurements below it: the header is there in every case, and the
|
|
922
|
+
* virtualizer needs its height from the first paint.
|
|
865
923
|
*/
|
|
866
924
|
const gridElementRef = useRef<HTMLDivElement>(null);
|
|
925
|
+
/**
|
|
926
|
+
* Header filters only render where something can be filtered - the row would
|
|
927
|
+
* otherwise be a band of empty cells on a grid with `enableColumnFilters:
|
|
928
|
+
* false`. Derived up here because the header measurement below depends on
|
|
929
|
+
* whether the row is in the DOM.
|
|
930
|
+
*/
|
|
931
|
+
const hasHeaderFilterRow =
|
|
932
|
+
filters.inHeader && getGridCapabilities(table, features).canFilterAny;
|
|
933
|
+
/**
|
|
934
|
+
* How many rows deep the header is. In the measurement's dependencies
|
|
935
|
+
* because the rows are found once, by query: a `columns` change that adds or
|
|
936
|
+
* drops a group level changes which elements have to be measured and given
|
|
937
|
+
* their sticky inset, and nothing else in the list moves when it does.
|
|
938
|
+
*/
|
|
939
|
+
const headerDepth = table.getCenterHeaderGroups().length;
|
|
940
|
+
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
|
941
|
+
const [bodyOffsetTop, setBodyOffsetTop] = useState(0);
|
|
942
|
+
const [stickyCoverTop, setStickyCoverTop] = useState(0);
|
|
943
|
+
const [stickyCoverBottom, setStickyCoverBottom] = useState(0);
|
|
867
944
|
useEffect(() => {
|
|
868
|
-
if (newRowCount === 0 && pinnedTopRows.length === 0) return;
|
|
869
945
|
const grid = gridElementRef.current;
|
|
870
946
|
if (grid === null) return;
|
|
871
947
|
const headerRows = grid.querySelectorAll<HTMLElement>(
|
|
872
948
|
"[data-dg-header-row]",
|
|
873
949
|
);
|
|
950
|
+
const stickyBlocks = grid.querySelectorAll<HTMLElement>(
|
|
951
|
+
'[data-dg-entry-block], [data-dg-part="pinned-top"]',
|
|
952
|
+
);
|
|
953
|
+
const flowBlocks = grid.querySelectorAll<HTMLElement>(
|
|
954
|
+
"[data-dg-entry-flow-block]",
|
|
955
|
+
);
|
|
874
956
|
const measure = () => {
|
|
875
|
-
let
|
|
876
|
-
for (const headerRow of headerRows)
|
|
877
|
-
|
|
957
|
+
let header = 0;
|
|
958
|
+
for (const headerRow of headerRows) {
|
|
959
|
+
// Every header row is sticky, so each one needs the height of the rows
|
|
960
|
+
// above it as its own inset - otherwise a stacked group and the filter
|
|
961
|
+
// row all pin to the top edge and paint over each other. Written on the
|
|
962
|
+
// element because only JS knows where the row before it ended; the
|
|
963
|
+
// stylesheet's `top` is the pre-measurement fallback.
|
|
964
|
+
headerRow.style.top = `${header}px`;
|
|
965
|
+
header += headerRow.offsetHeight;
|
|
966
|
+
}
|
|
967
|
+
if (newRowCount > 0 || pinnedTopRows.length > 0) {
|
|
968
|
+
grid.style.setProperty("--dg-header-height", `${header}px`);
|
|
969
|
+
}
|
|
970
|
+
// The whole header's height, on the wrapper rather than on the grid,
|
|
971
|
+
// because the popup is a sibling of the scroll container and never sees
|
|
972
|
+
// a property set inside it. A second name rather than reusing
|
|
973
|
+
// `--dg-header-height`: the header cells read that one for their own
|
|
974
|
+
// `min-height`, so publishing a stacked total there would feed a
|
|
975
|
+
// two-row header back in as the height of each of its rows.
|
|
976
|
+
tableWrapperRef.current?.style.setProperty(
|
|
977
|
+
"--dg-header-stack-height",
|
|
978
|
+
`${header}px`,
|
|
979
|
+
);
|
|
980
|
+
let cover = header;
|
|
981
|
+
for (const block of stickyBlocks) cover += block.offsetHeight;
|
|
982
|
+
let offset = cover;
|
|
983
|
+
for (const block of flowBlocks) offset += block.offsetHeight;
|
|
984
|
+
setStickyCoverTop(cover);
|
|
985
|
+
setBodyOffsetTop(offset);
|
|
878
986
|
};
|
|
879
987
|
measure();
|
|
880
988
|
const observer = new ResizeObserver(measure);
|
|
881
|
-
for (const
|
|
989
|
+
for (const element of [...headerRows, ...stickyBlocks, ...flowBlocks]) {
|
|
990
|
+
observer.observe(element);
|
|
991
|
+
}
|
|
882
992
|
return () => observer.disconnect();
|
|
883
|
-
}, [newRowCount, pinnedTopRows.length]);
|
|
993
|
+
}, [newRowCount, pinnedTopRows.length, hasHeaderFilterRow, headerDepth]);
|
|
884
994
|
|
|
885
995
|
/**
|
|
886
996
|
* The entry block's height, published as `--dg-entry-height` so the pinned
|
|
@@ -904,32 +1014,6 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
904
1014
|
};
|
|
905
1015
|
}, [newRowCount, pinnedTopRows.length]);
|
|
906
1016
|
|
|
907
|
-
/**
|
|
908
|
-
* The summary row's height, published as `--dg-summary-height` so the
|
|
909
|
-
* pinned bottom block sticks above it rather than underneath it.
|
|
910
|
-
*/
|
|
911
|
-
useEffect(() => {
|
|
912
|
-
if (pinnedBottomRows.length === 0) return;
|
|
913
|
-
const grid = gridElementRef.current;
|
|
914
|
-
if (grid === null) return;
|
|
915
|
-
const summaryRow = grid.querySelector<HTMLElement>(
|
|
916
|
-
'[data-dg-part="summary-row"]',
|
|
917
|
-
);
|
|
918
|
-
if (summaryRow === null) return;
|
|
919
|
-
const measure = () =>
|
|
920
|
-
grid.style.setProperty(
|
|
921
|
-
"--dg-summary-height",
|
|
922
|
-
`${summaryRow.offsetHeight}px`,
|
|
923
|
-
);
|
|
924
|
-
measure();
|
|
925
|
-
const observer = new ResizeObserver(measure);
|
|
926
|
-
observer.observe(summaryRow);
|
|
927
|
-
return () => {
|
|
928
|
-
observer.disconnect();
|
|
929
|
-
grid.style.removeProperty("--dg-summary-height");
|
|
930
|
-
};
|
|
931
|
-
}, [pinnedBottomRows.length]);
|
|
932
|
-
|
|
933
1017
|
// A persisted pageIndex can outlive the data that produced it; TanStack only
|
|
934
1018
|
// auto-resets on live filter/sort/data changes, not on restored state. The
|
|
935
1019
|
// guard makes the dependency-free effect idempotent.
|
|
@@ -974,6 +1058,17 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
974
1058
|
getItemKey: useCallback((index: number) => rows[index]?.id ?? index, [rows]),
|
|
975
1059
|
overscan,
|
|
976
1060
|
initialRect: { height: 600, width: 1200 },
|
|
1061
|
+
/**
|
|
1062
|
+
* The rows are not the first thing in the scroll container - the header,
|
|
1063
|
+
* the entry block and the pinned top block come before them. The margin is
|
|
1064
|
+
* what puts an item's offset in the container's own scroll coordinates;
|
|
1065
|
+
* the two paddings are the sticky blocks at either end, which cover the
|
|
1066
|
+
* viewport without shrinking it, so `align: "auto"` has to scroll past
|
|
1067
|
+
* them rather than stopping underneath.
|
|
1068
|
+
*/
|
|
1069
|
+
scrollMargin: bodyOffsetTop,
|
|
1070
|
+
scrollPaddingStart: stickyCoverTop,
|
|
1071
|
+
scrollPaddingEnd: stickyCoverBottom,
|
|
977
1072
|
});
|
|
978
1073
|
|
|
979
1074
|
/**
|
|
@@ -991,10 +1086,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
991
1086
|
virtualizer.measure();
|
|
992
1087
|
}, [rowHeight, virtualizer]);
|
|
993
1088
|
|
|
1089
|
+
// The spacers stand in for the rows that are not mounted, so they measure
|
|
1090
|
+
// from the first row rather than from the top of the scroll container:
|
|
1091
|
+
// `scrollMargin` is included in every item's `start` and `end` and excluded
|
|
1092
|
+
// from `getTotalSize()`, and taking it back off here is what keeps a spacer
|
|
1093
|
+
// the height of the rows it replaces.
|
|
994
1094
|
const virtualItems = virtualizer.getVirtualItems();
|
|
995
|
-
const paddingTop = virtualItems[0]?.start ??
|
|
1095
|
+
const paddingTop = (virtualItems[0]?.start ?? bodyOffsetTop) - bodyOffsetTop;
|
|
996
1096
|
const paddingBottom =
|
|
997
|
-
virtualizer.getTotalSize() -
|
|
1097
|
+
virtualizer.getTotalSize() -
|
|
1098
|
+
((virtualItems.at(-1)?.end ?? bodyOffsetTop) - bodyOffsetTop);
|
|
998
1099
|
|
|
999
1100
|
/**
|
|
1000
1101
|
* `api.scrollToRow`, implemented where the virtualizer is.
|
|
@@ -1051,14 +1152,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1051
1152
|
onReachEnd();
|
|
1052
1153
|
});
|
|
1053
1154
|
|
|
1054
|
-
// The *Visible* variants throughout: `
|
|
1155
|
+
// The *Visible* variants throughout: `getStartLeafColumns()` and friends
|
|
1055
1156
|
// include hidden columns, while the cells below come from
|
|
1056
|
-
// `row.
|
|
1157
|
+
// `row.getStartVisibleCells()`, which does not. Mixing the two would lay down
|
|
1057
1158
|
// a grid track for a column that renders no cell, shifting every column after
|
|
1058
1159
|
// it out of its header. The tree column is hidden exactly this way while
|
|
1059
1160
|
// nothing is grouped.
|
|
1060
|
-
const leftLeafColumns = table.
|
|
1061
|
-
const rightLeafColumns = table.
|
|
1161
|
+
const leftLeafColumns = table.getStartVisibleLeafColumns();
|
|
1162
|
+
const rightLeafColumns = table.getEndVisibleLeafColumns();
|
|
1062
1163
|
const centerLeafColumns = table.getCenterVisibleLeafColumns();
|
|
1063
1164
|
|
|
1064
1165
|
const orderedColumns = [
|
|
@@ -1070,8 +1171,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1070
1171
|
// Headers and cells must follow the same left → center → right order as the
|
|
1071
1172
|
// column tracks, otherwise pinning a column would shuffle it out of its lane.
|
|
1072
1173
|
const centerHeaderGroups = table.getCenterHeaderGroups();
|
|
1073
|
-
const leftHeaderGroups = table.
|
|
1074
|
-
const rightHeaderGroups = table.
|
|
1174
|
+
const leftHeaderGroups = table.getStartHeaderGroups();
|
|
1175
|
+
const rightHeaderGroups = table.getEndHeaderGroups();
|
|
1075
1176
|
const headerGroups = centerHeaderGroups.map((group, index) => ({
|
|
1076
1177
|
id: group.id,
|
|
1077
1178
|
headers: [
|
|
@@ -1237,6 +1338,68 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1237
1338
|
const hasSummaryRow = orderedColumns.some(
|
|
1238
1339
|
(column) => column.columnDef.footer !== undefined,
|
|
1239
1340
|
);
|
|
1341
|
+
/**
|
|
1342
|
+
* Rows above the first body row, which is what every `aria-rowindex` in the
|
|
1343
|
+
* body counts from and what `aria-rowcount` adds up. The header groups plus
|
|
1344
|
+
* the filter row, when there is one.
|
|
1345
|
+
*/
|
|
1346
|
+
const headerRowCount = headerGroups.length + (hasHeaderFilterRow ? 1 : 0);
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* The summary row's height, published as `--dg-summary-height`, which two
|
|
1350
|
+
* things read: the pinned bottom block sticks above the summary row rather
|
|
1351
|
+
* than underneath it, and the summary row's own `top` inset holds it on the
|
|
1352
|
+
* bottom edge of a body too short to scroll. A layout effect, so the first
|
|
1353
|
+
* paint already has the measurement - `top` would otherwise resolve to 100%
|
|
1354
|
+
* for a frame and take an overflowing grid's summary row out of view. Down
|
|
1355
|
+
* here rather than beside the other measurements because `hasSummaryRow` is
|
|
1356
|
+
* only known this far into the render.
|
|
1357
|
+
*/
|
|
1358
|
+
useLayoutEffect(() => {
|
|
1359
|
+
if (!hasSummaryRow) return;
|
|
1360
|
+
const grid = gridElementRef.current;
|
|
1361
|
+
if (grid === null) return;
|
|
1362
|
+
const summaryRow = grid.querySelector<HTMLElement>(
|
|
1363
|
+
'[data-dg-part="summary-row"]',
|
|
1364
|
+
);
|
|
1365
|
+
if (summaryRow === null) return;
|
|
1366
|
+
const measure = () =>
|
|
1367
|
+
grid.style.setProperty(
|
|
1368
|
+
"--dg-summary-height",
|
|
1369
|
+
`${summaryRow.offsetHeight}px`,
|
|
1370
|
+
);
|
|
1371
|
+
measure();
|
|
1372
|
+
const observer = new ResizeObserver(measure);
|
|
1373
|
+
observer.observe(summaryRow);
|
|
1374
|
+
return () => {
|
|
1375
|
+
observer.disconnect();
|
|
1376
|
+
grid.style.removeProperty("--dg-summary-height");
|
|
1377
|
+
};
|
|
1378
|
+
}, [hasSummaryRow]);
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* The other end of the same story as `stickyCoverTop`: the pinned bottom
|
|
1382
|
+
* block and the summary row stick to the container's bottom edge, so the
|
|
1383
|
+
* virtualizer takes their height as `scrollPaddingEnd` and stops scrolling a
|
|
1384
|
+
* row to just underneath them. Down here for the same reason as the
|
|
1385
|
+
* measurement above.
|
|
1386
|
+
*/
|
|
1387
|
+
useEffect(() => {
|
|
1388
|
+
const grid = gridElementRef.current;
|
|
1389
|
+
if (grid === null) return;
|
|
1390
|
+
const blocks = grid.querySelectorAll<HTMLElement>(
|
|
1391
|
+
'[data-dg-part="pinned-bottom"], [data-dg-part="summary-row"]',
|
|
1392
|
+
);
|
|
1393
|
+
const measure = () => {
|
|
1394
|
+
let total = 0;
|
|
1395
|
+
for (const block of blocks) total += block.offsetHeight;
|
|
1396
|
+
setStickyCoverBottom(total);
|
|
1397
|
+
};
|
|
1398
|
+
measure();
|
|
1399
|
+
const observer = new ResizeObserver(measure);
|
|
1400
|
+
for (const block of blocks) observer.observe(block);
|
|
1401
|
+
return () => observer.disconnect();
|
|
1402
|
+
}, [pinnedBottomRows.length, hasSummaryRow]);
|
|
1240
1403
|
|
|
1241
1404
|
// "row" mode: the row itself is the selection control, with the modifier
|
|
1242
1405
|
// conventions of any desktop list - see resolveRowSelectionClick.
|
|
@@ -1337,6 +1500,23 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1337
1500
|
/** The last position the effect acted on, so it can tell a move from a re-render. */
|
|
1338
1501
|
const appliedCellRef = useRef<TMDataGridCellPosition | null>(null);
|
|
1339
1502
|
|
|
1503
|
+
/**
|
|
1504
|
+
* The two focusable elements that bracket the body, and the flag that says
|
|
1505
|
+
* which way focus is passing through one.
|
|
1506
|
+
*
|
|
1507
|
+
* They are what makes the body one tab stop without asking anything of the
|
|
1508
|
+
* cell renderers. Tab inside the body parks the focus on the guard past the
|
|
1509
|
+
* edge being left, and the browser's default Tab then continues from there -
|
|
1510
|
+
* so the next stop is whatever follows the grid, however many tabbable
|
|
1511
|
+
* controls the mounted rows happen to hold. Focus arriving at a guard any
|
|
1512
|
+
* other way came from outside the body, and is forwarded to the cursor cell.
|
|
1513
|
+
* `guardHopRef` is what tells the two apart; the grid raises it just before
|
|
1514
|
+
* it moves the focus itself.
|
|
1515
|
+
*/
|
|
1516
|
+
const leadingGuardRef = useRef<HTMLDivElement>(null);
|
|
1517
|
+
const trailingGuardRef = useRef<HTMLDivElement>(null);
|
|
1518
|
+
const guardHopRef = useRef(false);
|
|
1519
|
+
|
|
1340
1520
|
// O(n) per keystroke and per render while a cell is focused. Fine at any row
|
|
1341
1521
|
// count a grid actually holds, and the alternative - an id→index map rebuilt
|
|
1342
1522
|
// whenever sorting, filtering or grouping changes - costs more than it saves.
|
|
@@ -1353,13 +1533,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1353
1533
|
virtualItems.some((item) => item.index === focusedRowIndex);
|
|
1354
1534
|
|
|
1355
1535
|
/**
|
|
1356
|
-
* The
|
|
1536
|
+
* The cell the tab guards hand the focus to when Tab enters the body.
|
|
1357
1537
|
*
|
|
1358
1538
|
* The focused cell when it is on screen. When it is not - the grid has never
|
|
1359
1539
|
* been entered, or the user scrolled the focus away with the wheel - the
|
|
1360
|
-
* first mounted cell stands in, so the body always
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1540
|
+
* first mounted cell stands in, so entering the body always lands somewhere.
|
|
1541
|
+
* No body cell is itself in the tab order: the guards are the body's only
|
|
1542
|
+
* tab stops, which is what keeps a cell from turning up between two of a
|
|
1543
|
+
* row's controls when Tab walks them.
|
|
1363
1544
|
*/
|
|
1364
1545
|
const firstMountedRow = rows[virtualItems[0]?.index ?? -1];
|
|
1365
1546
|
const tabStopCell: TMDataGridCellPosition | null = !cellSelection
|
|
@@ -1370,6 +1551,35 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1370
1551
|
? { rowId: firstMountedRow.id, columnId: orderedColumns[0].id }
|
|
1371
1552
|
: null;
|
|
1372
1553
|
|
|
1554
|
+
/**
|
|
1555
|
+
* Focus landing on either guard.
|
|
1556
|
+
*
|
|
1557
|
+
* A raised hop flag means the grid put it there on its way out, so the
|
|
1558
|
+
* browser's default Tab is about to carry on from the guard - nothing to do
|
|
1559
|
+
* but lower the flag. Anything else is Tab off the last header control onto
|
|
1560
|
+
* the leading guard, or Shift+Tab from below the grid onto the trailing one:
|
|
1561
|
+
* both mean "enter the body", and the body is entered at the cursor cell.
|
|
1562
|
+
*
|
|
1563
|
+
* The state is moved as well as the DOM focus, because the cell may not be
|
|
1564
|
+
* the one the ring is on; the direct focus is so it happens in this event
|
|
1565
|
+
* rather than a frame later.
|
|
1566
|
+
*
|
|
1567
|
+
* With no rows there is no cell to hand it to. The guard keeps the focus and
|
|
1568
|
+
* the next Tab leaves the grid the way it came in.
|
|
1569
|
+
*/
|
|
1570
|
+
const handleGuardFocus = () => {
|
|
1571
|
+
if (guardHopRef.current) {
|
|
1572
|
+
guardHopRef.current = false;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (tabStopCell === null) return;
|
|
1576
|
+
wantsCellFocusRef.current = true;
|
|
1577
|
+
ui.actions.setFocusedCell(tabStopCell);
|
|
1578
|
+
const container = scrollContainerRef.current;
|
|
1579
|
+
if (container === null) return;
|
|
1580
|
+
findCellElement(container, tabStopCell)?.focus({ preventScroll: true });
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1373
1583
|
const isCellAt = (
|
|
1374
1584
|
position: TMDataGridCellPosition | null,
|
|
1375
1585
|
rowId: string,
|
|
@@ -1436,24 +1646,23 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1436
1646
|
? 0
|
|
1437
1647
|
: orderedColumns
|
|
1438
1648
|
.slice(selectionBounds.left, selectionBounds.right + 1)
|
|
1439
|
-
.filter(
|
|
1649
|
+
.filter(
|
|
1650
|
+
(column) =>
|
|
1651
|
+
!isGeneratedColumn(column.id) &&
|
|
1652
|
+
column.columnDef.meta?.enableExport !== false,
|
|
1653
|
+
).length;
|
|
1440
1654
|
|
|
1441
|
-
const exportOptions =
|
|
1655
|
+
const exportOptions = resolveExportOptions(gridExportOptions);
|
|
1442
1656
|
// Sticky across menus, the way a checkbox in a dialog is: a user who exports
|
|
1443
1657
|
// with headers once almost always wants them the next time too.
|
|
1444
1658
|
const [exportIncludesHeaders, setExportIncludesHeaders] = useState(
|
|
1445
1659
|
exportOptions.includeHeaders,
|
|
1446
1660
|
);
|
|
1447
|
-
|
|
1661
|
+
// The rectangle's values, over the displayed rows the bounds index into.
|
|
1662
|
+
const buildSelectionData = () =>
|
|
1448
1663
|
selectionBounds === null
|
|
1449
|
-
?
|
|
1450
|
-
:
|
|
1451
|
-
rows,
|
|
1452
|
-
columns: orderedColumns,
|
|
1453
|
-
bounds: selectionBounds,
|
|
1454
|
-
includeHeaders,
|
|
1455
|
-
decimalComma: exportOptions.decimalComma,
|
|
1456
|
-
});
|
|
1664
|
+
? null
|
|
1665
|
+
: buildExportData({ table, rows, bounds: selectionBounds });
|
|
1457
1666
|
|
|
1458
1667
|
/**
|
|
1459
1668
|
* Ctrl+C. Values only, no header row - Excel's own copy does not include one
|
|
@@ -1462,9 +1671,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1462
1671
|
* because a file is a document and a clipboard is a fragment.
|
|
1463
1672
|
*/
|
|
1464
1673
|
const copySelection = async () => {
|
|
1465
|
-
const
|
|
1466
|
-
if (
|
|
1467
|
-
await writeClipboardText(
|
|
1674
|
+
const data = buildSelectionData();
|
|
1675
|
+
if (data === null || data.columnIds.length === 0) return;
|
|
1676
|
+
await writeClipboardText(
|
|
1677
|
+
toClipboardText(data, {
|
|
1678
|
+
decimalComma: exportOptions.format.decimalComma,
|
|
1679
|
+
}),
|
|
1680
|
+
);
|
|
1468
1681
|
};
|
|
1469
1682
|
|
|
1470
1683
|
/**
|
|
@@ -1478,12 +1691,15 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1478
1691
|
|
|
1479
1692
|
// The drag ends wherever the button comes up, which is routinely outside the
|
|
1480
1693
|
// grid - over the scrollbar, or past the window edge. Only the window hears
|
|
1481
|
-
// that, so only the window can end it.
|
|
1694
|
+
// that, so only the window can end it. The grid's own window, not the
|
|
1695
|
+
// global one: rendered through a portal into a window opened with
|
|
1696
|
+
// `window.open`, the global is the opener and never hears the release.
|
|
1482
1697
|
useEffect(() => {
|
|
1483
1698
|
if (!isDraggingRange) return;
|
|
1484
1699
|
const stop = () => setIsDraggingRange(false);
|
|
1485
|
-
|
|
1486
|
-
|
|
1700
|
+
const view = gridElementRef.current?.ownerDocument.defaultView ?? window;
|
|
1701
|
+
view.addEventListener("mouseup", stop);
|
|
1702
|
+
return () => view.removeEventListener("mouseup", stop);
|
|
1487
1703
|
}, [isDraggingRange]);
|
|
1488
1704
|
|
|
1489
1705
|
const startRangeDrag = (
|
|
@@ -1497,12 +1713,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1497
1713
|
if (!extend) setIsDraggingRange(true);
|
|
1498
1714
|
};
|
|
1499
1715
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1716
|
+
/** The export item: the rectangle, in the grid's export format. */
|
|
1717
|
+
const exportSelection = () => {
|
|
1718
|
+
const data = buildSelectionData();
|
|
1719
|
+
if (data === null || data.columnIds.length === 0) return;
|
|
1720
|
+
void writeExportFile(data, {
|
|
1721
|
+
...exportOptions,
|
|
1722
|
+
includeHeaders: exportIncludesHeaders,
|
|
1506
1723
|
});
|
|
1507
1724
|
};
|
|
1508
1725
|
|
|
@@ -1552,9 +1769,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1552
1769
|
const row = rows.find((candidate) => candidate.id === rowId);
|
|
1553
1770
|
if (row === undefined) return null;
|
|
1554
1771
|
const cells = [
|
|
1555
|
-
...row.
|
|
1772
|
+
...row.getStartVisibleCells(),
|
|
1556
1773
|
...row.getCenterVisibleCells(),
|
|
1557
|
-
...row.
|
|
1774
|
+
...row.getEndVisibleCells(),
|
|
1558
1775
|
];
|
|
1559
1776
|
return (
|
|
1560
1777
|
cells.find((cell) => edit.canEditCell(row, cell.column))?.column.id ??
|
|
@@ -1719,14 +1936,109 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1719
1936
|
const handleGridKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
1720
1937
|
const target = event.target as HTMLElement;
|
|
1721
1938
|
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1939
|
+
// The body cell the key was pressed in, whether the focus is on the cell
|
|
1940
|
+
// itself or on a control inside it. `null` for a header control or a tab
|
|
1941
|
+
// guard, neither of which is part of cell navigation.
|
|
1942
|
+
const keyCellElement =
|
|
1943
|
+
target.dataset.cell === "true"
|
|
1944
|
+
? target
|
|
1945
|
+
: target.closest<HTMLElement>('[data-cell="true"]');
|
|
1946
|
+
if (keyCellElement === null) return;
|
|
1947
|
+
|
|
1948
|
+
if (event.key === "Tab") {
|
|
1949
|
+
// Inside a row's controls the row is a form, and Tab walks it the way
|
|
1950
|
+
// the browser walks any form: through the row's open editors, the
|
|
1951
|
+
// buttons in its cells and, on an open row, the lane's save and cancel.
|
|
1952
|
+
// That part is left to the browser. What the grid decides is the two
|
|
1953
|
+
// ends: past the row's last control the cursor moves to the next row's
|
|
1954
|
+
// first cell, and before its first control to the previous row's last -
|
|
1955
|
+
// the cell is put on the cursor and left alone, whatever it holds. A cell
|
|
1956
|
+
// editor never gets here, it claims Tab itself to commit and move on.
|
|
1957
|
+
if (target.dataset.cell !== "true") {
|
|
1958
|
+
const rowElement = keyCellElement.closest<HTMLElement>(
|
|
1959
|
+
'[data-dg-part="row"]',
|
|
1960
|
+
);
|
|
1961
|
+
const tabbables =
|
|
1962
|
+
rowElement === null
|
|
1963
|
+
? []
|
|
1964
|
+
: Array.from(
|
|
1965
|
+
rowElement.querySelectorAll<HTMLElement>(FOCUSABLE_IN_CELL),
|
|
1966
|
+
).filter((element) => element.tabIndex >= 0);
|
|
1967
|
+
const index = tabbables.indexOf(target);
|
|
1968
|
+
// A control the walk cannot place is left to the browser rather than
|
|
1969
|
+
// guessed about.
|
|
1970
|
+
if (index < 0) return;
|
|
1971
|
+
// The step itself is taken here rather than left to the browser: a
|
|
1972
|
+
// select editor keeps its option list in the row, and the browser's
|
|
1973
|
+
// Tab lands on that scrollable list, which the editor then closes
|
|
1974
|
+
// under the focus. Walking the controls, and only the controls, is
|
|
1975
|
+
// what the browser would do if the list were not there.
|
|
1976
|
+
const nextControl = tabbables[index + (event.shiftKey ? -1 : 1)];
|
|
1977
|
+
if (nextControl !== undefined) {
|
|
1978
|
+
event.preventDefault();
|
|
1979
|
+
nextControl.focus();
|
|
1980
|
+
// What Tab does on a text field, and what the caret otherwise loses.
|
|
1981
|
+
if (isInputElement(nextControl)) nextControl.select();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const rowIndex = rows.findIndex(
|
|
1985
|
+
(row) => row.id === keyCellElement.dataset.rowId,
|
|
1986
|
+
);
|
|
1987
|
+
const nextRowIndex = rowIndex + (event.shiftKey ? -1 : 1);
|
|
1988
|
+
const nextRow = rowIndex < 0 ? undefined : rows[nextRowIndex];
|
|
1989
|
+
const nextColumn = event.shiftKey
|
|
1990
|
+
? orderedColumns[orderedColumns.length - 1]
|
|
1991
|
+
: orderedColumns[0];
|
|
1992
|
+
if (nextRow !== undefined && nextColumn !== undefined) {
|
|
1993
|
+
event.preventDefault();
|
|
1994
|
+
moveFocusedCell({
|
|
1995
|
+
rowIndex: nextRowIndex,
|
|
1996
|
+
columnIndex: event.shiftKey ? orderedColumns.length - 1 : 0,
|
|
1997
|
+
});
|
|
1998
|
+
// The focus-following effect stands down while the focus sits in an
|
|
1999
|
+
// editor, which is exactly where it sits now: move it by hand. The
|
|
2000
|
+
// adjacent row is mounted whenever there is any overscan; without
|
|
2001
|
+
// one, letting go of the editor is what lets the effect take over.
|
|
2002
|
+
const container = scrollContainerRef.current;
|
|
2003
|
+
const nextCell =
|
|
2004
|
+
container === null
|
|
2005
|
+
? null
|
|
2006
|
+
: findCellElement(container, {
|
|
2007
|
+
rowId: nextRow.id,
|
|
2008
|
+
columnId: nextColumn.id,
|
|
2009
|
+
});
|
|
2010
|
+
if (nextCell !== null) nextCell.focus({ preventScroll: true });
|
|
2011
|
+
else target.blur();
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
// No row past this one: the body is left, below.
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Tab leaves the body in one hop, from a cell or from the last control
|
|
2018
|
+
// of the last row. Not prevented: the focus is moved onto the guard past
|
|
2019
|
+
// the edge being left and the browser's own Tab then runs from there,
|
|
2020
|
+
// which is what puts it on the next thing after the grid - or, going
|
|
2021
|
+
// back, on the last header control, since the leading guard sits below
|
|
2022
|
+
// them.
|
|
2023
|
+
const guard = event.shiftKey
|
|
2024
|
+
? leadingGuardRef.current
|
|
2025
|
+
: trailingGuardRef.current;
|
|
2026
|
+
if (guard === null) return;
|
|
2027
|
+
guardHopRef.current = true;
|
|
2028
|
+
guard.focus({ preventScroll: true });
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// An open editor owns the rest of its keys; the cell editor stops the
|
|
2033
|
+
// ones it acts on before they get here, a row-shaped one lets them pass.
|
|
2034
|
+
if (target.closest('[data-dg-part="editor"]') !== null) return;
|
|
2035
|
+
|
|
2036
|
+
// Focus is on something inside a cell - a checkbox, a link. Its keys are
|
|
2037
|
+
// its own; the one the grid still answers is the way back out.
|
|
1724
2038
|
if (target.dataset.cell !== "true") {
|
|
1725
2039
|
if (event.key !== "Escape") return;
|
|
1726
|
-
const cellElement = target.closest<HTMLElement>('[data-cell="true"]');
|
|
1727
|
-
if (cellElement === null) return;
|
|
1728
2040
|
event.preventDefault();
|
|
1729
|
-
|
|
2041
|
+
keyCellElement.focus();
|
|
1730
2042
|
return;
|
|
1731
2043
|
}
|
|
1732
2044
|
|
|
@@ -1821,9 +2133,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1821
2133
|
// Space selects the row, from any of its cells.
|
|
1822
2134
|
//
|
|
1823
2135
|
// Not routed through `handleRowActivate`, which is the *click* path and only
|
|
1824
|
-
// selects in `"row"` mode:
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
2136
|
+
// selects in `"row"` mode: the checkbox is not in the page's tab order, so
|
|
2137
|
+
// this is the keyboard's way to a selection under every mode that has one -
|
|
2138
|
+
// checkbox included.
|
|
1827
2139
|
if (event.key === " ") {
|
|
1828
2140
|
event.preventDefault();
|
|
1829
2141
|
const row = rows[rowIndex];
|
|
@@ -1881,12 +2193,27 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1881
2193
|
return;
|
|
1882
2194
|
}
|
|
1883
2195
|
|
|
2196
|
+
// The grid's own document, not the global one: in a window opened with
|
|
2197
|
+
// `window.open` the global document is the opener's, and its focus is
|
|
2198
|
+
// never in this grid.
|
|
2199
|
+
const activeElement = container.ownerDocument.activeElement;
|
|
2200
|
+
|
|
1884
2201
|
// Never take the focus from elsewhere on the page. A consumer moving the
|
|
1885
2202
|
// cell while the user is typing in a form somewhere means "move the ring",
|
|
1886
2203
|
// not "move the caret" - the scroll above already did the visible part.
|
|
2204
|
+
if (!wantsCellFocusRef.current && !container.contains(activeElement)) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
// Mid-hop: Tab parked the focus on a guard and the browser is about to
|
|
2208
|
+
// carry it out of the grid. This effect can run in between - a discrete
|
|
2209
|
+
// event flushes its renders before the default action - and pulling the
|
|
2210
|
+
// focus back onto the cell here would send the browser's Tab off from
|
|
2211
|
+
// the cell instead. A guard the user *arrived* on has already asked for
|
|
2212
|
+
// the cell, and that intent is honoured.
|
|
1887
2213
|
if (
|
|
1888
2214
|
!wantsCellFocusRef.current &&
|
|
1889
|
-
|
|
2215
|
+
activeElement !== null &&
|
|
2216
|
+
activeElement.closest('[data-dg-part="tab-guard"]') !== null
|
|
1890
2217
|
) {
|
|
1891
2218
|
return;
|
|
1892
2219
|
}
|
|
@@ -1897,7 +2224,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1897
2224
|
wantsCellFocusRef.current = false;
|
|
1898
2225
|
// Focus is already inside - on the control Enter stepped into. The cell is
|
|
1899
2226
|
// where the ring belongs, so there is nothing to move.
|
|
1900
|
-
if (cellElement.contains(
|
|
2227
|
+
if (cellElement.contains(activeElement)) return;
|
|
1901
2228
|
// An open editor outranks the ring, wherever it is. Row mode opens a whole
|
|
1902
2229
|
// row from one gesture, so the caret legitimately sits in a cell other
|
|
1903
2230
|
// than the one that gesture landed the ring on - the lane's pencil is the
|
|
@@ -1905,8 +2232,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1905
2232
|
// the caret back onto that cell would leave the row open with the caret in
|
|
1906
2233
|
// none of its editors, and the user with nothing to type into.
|
|
1907
2234
|
if (
|
|
1908
|
-
|
|
1909
|
-
|
|
2235
|
+
activeElement !== null &&
|
|
2236
|
+
activeElement.closest('[data-dg-part="editor"]') !== null
|
|
1910
2237
|
) {
|
|
1911
2238
|
return;
|
|
1912
2239
|
}
|
|
@@ -2005,9 +2332,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2005
2332
|
</Menu.Item>
|
|
2006
2333
|
<Menu.Item
|
|
2007
2334
|
disabled={selectedDataColumnCount === 0}
|
|
2008
|
-
onClick={
|
|
2335
|
+
onClick={exportSelection}
|
|
2009
2336
|
>
|
|
2010
|
-
{labels.
|
|
2337
|
+
{labels.exportCells}
|
|
2011
2338
|
</Menu.Item>
|
|
2012
2339
|
{/* Stays open on click: it is the setting the item above reads, and
|
|
2013
2340
|
reopening the menu to change your mind about headers is a worse
|
|
@@ -2128,7 +2455,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2128
2455
|
: undefined;
|
|
2129
2456
|
|
|
2130
2457
|
return (
|
|
2131
|
-
<div
|
|
2458
|
+
<div
|
|
2459
|
+
ref={tableWrapperRef}
|
|
2460
|
+
className={classes.tableWrapper}
|
|
2461
|
+
data-dg-filter-sidebar={filters.surface === "sidebar" || undefined}
|
|
2462
|
+
>
|
|
2132
2463
|
<div
|
|
2133
2464
|
ref={scrollContainerRef}
|
|
2134
2465
|
className={classes.scrollContainer}
|
|
@@ -2166,7 +2497,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2166
2497
|
aria-rowcount={
|
|
2167
2498
|
rows.length +
|
|
2168
2499
|
pinnedRowCount +
|
|
2169
|
-
|
|
2500
|
+
headerRowCount +
|
|
2170
2501
|
(hasSummaryRow ? 1 : 0)
|
|
2171
2502
|
}
|
|
2172
2503
|
aria-colcount={orderedColumns.length}
|
|
@@ -2186,11 +2517,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2186
2517
|
data-dg-header-row
|
|
2187
2518
|
// The scrolled-under shadow belongs to the boundary between
|
|
2188
2519
|
// header and body - the last header row, not every stacked
|
|
2189
|
-
// group row above it
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2520
|
+
// group row above it, and not this one at all when the filter
|
|
2521
|
+
// row is below it. See sticky.module.css.
|
|
2522
|
+
className={[
|
|
2523
|
+
classes.headerRow,
|
|
2524
|
+
!hasHeaderFilterRow && groupIndex === headerGroups.length - 1
|
|
2525
|
+
? sticky.headerBoundary
|
|
2526
|
+
: "",
|
|
2527
|
+
]
|
|
2528
|
+
.filter(Boolean)
|
|
2529
|
+
.join(" ")}
|
|
2194
2530
|
>
|
|
2195
2531
|
{headerGroup.headers.map((header) => (
|
|
2196
2532
|
<TMDataGridHeaderCell
|
|
@@ -2202,8 +2538,34 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2202
2538
|
))}
|
|
2203
2539
|
</div>
|
|
2204
2540
|
))}
|
|
2541
|
+
|
|
2542
|
+
{hasHeaderFilterRow && (
|
|
2543
|
+
<TMDataGridHeaderFilterRow
|
|
2544
|
+
leafHeaders={leafHeaders}
|
|
2545
|
+
layoutFor={layoutFor}
|
|
2546
|
+
ariaRowIndex={headerRowCount}
|
|
2547
|
+
/>
|
|
2548
|
+
)}
|
|
2205
2549
|
</div>
|
|
2206
2550
|
|
|
2551
|
+
{/* The leading tab guard, below the header controls and above the
|
|
2552
|
+
first body row - see `handleGuardFocus`. Not `aria-hidden`: it
|
|
2553
|
+
is focusable, and a focusable node hidden from the tree is what
|
|
2554
|
+
screen readers have no way to describe.
|
|
2555
|
+
Out of the tab order while there is no cell to hand focus to,
|
|
2556
|
+
so an empty body is not two dead stops. */}
|
|
2557
|
+
{cellSelection && (
|
|
2558
|
+
<div
|
|
2559
|
+
ref={leadingGuardRef}
|
|
2560
|
+
tabIndex={tabStopCell === null ? -1 : 0}
|
|
2561
|
+
role="presentation"
|
|
2562
|
+
data-dg-part="tab-guard"
|
|
2563
|
+
data-guard="leading"
|
|
2564
|
+
className={classes.tabGuard}
|
|
2565
|
+
onFocus={handleGuardFocus}
|
|
2566
|
+
/>
|
|
2567
|
+
)}
|
|
2568
|
+
|
|
2207
2569
|
{/* The entry block - new rows being typed, stuck under the header.
|
|
2208
2570
|
Present only while `edit.addRow()` has open entries. */}
|
|
2209
2571
|
{features.editing && newRowCount > 0 && (
|
|
@@ -2242,9 +2604,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2242
2604
|
const isGroupRow = row.getIsGrouped();
|
|
2243
2605
|
const isInteractive = rowGesturesFor(row);
|
|
2244
2606
|
const rowCells = [
|
|
2245
|
-
...row.
|
|
2607
|
+
...row.getStartVisibleCells(),
|
|
2246
2608
|
...row.getCenterVisibleCells(),
|
|
2247
|
-
...row.
|
|
2609
|
+
...row.getEndVisibleCells(),
|
|
2248
2610
|
];
|
|
2249
2611
|
// Row mode opens every editable cell of the row at once, and
|
|
2250
2612
|
// rows accumulate: a second row opening leaves the first one
|
|
@@ -2255,10 +2617,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2255
2617
|
// A parked row is not one of them. It keeps its form, so it is
|
|
2256
2618
|
// still "open", but it has been decided: it renders its draft
|
|
2257
2619
|
// through the column's own renderer until `begin` reopens it.
|
|
2620
|
+
// A committed entry row in the body has a form too - every
|
|
2621
|
+
// entry row does - but it is a value row until reopened, and
|
|
2622
|
+
// reopening takes it back to the entry block.
|
|
2258
2623
|
const rowEditing =
|
|
2259
2624
|
features.editMode === "row" &&
|
|
2260
|
-
|
|
2261
|
-
!
|
|
2625
|
+
editOpenRowIdSet.has(row.id) &&
|
|
2626
|
+
!editDraftRowIdSet.has(row.id) &&
|
|
2627
|
+
!newRowIdSet.has(row.id);
|
|
2262
2628
|
// Cell selection takes the body's tab stop off the row and puts
|
|
2263
2629
|
// it on a cell - two stops per row would make Tab a way of
|
|
2264
2630
|
// walking the grid, which is what the arrow keys are for. Space
|
|
@@ -2295,7 +2661,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2295
2661
|
// The tree's own rows. `data-grouped` rather than a class, to
|
|
2296
2662
|
// match how the rest of the row's state is published - and so
|
|
2297
2663
|
// a consumer can restyle them without reaching into modules.
|
|
2298
|
-
data-grouped={isGroupRow}
|
|
2664
|
+
data-grouped={isGroupRow || undefined}
|
|
2299
2665
|
data-depth={row.depth}
|
|
2300
2666
|
// Which edge block the row sits in, for styling hooks.
|
|
2301
2667
|
data-pinned={pinnedAt}
|
|
@@ -2304,13 +2670,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2304
2670
|
// still hold ids from before the mode changed - TanStack never
|
|
2305
2671
|
// prunes it - and a row must not report itself selected in a
|
|
2306
2672
|
// mode where selecting is not a thing.
|
|
2307
|
-
data-selected={
|
|
2673
|
+
data-selected={
|
|
2674
|
+
(features.rowSelection && row.getIsSelected()) || undefined
|
|
2675
|
+
}
|
|
2308
2676
|
// Being selected is state; painting it is a display choice, so
|
|
2309
2677
|
// the two are separate attributes.
|
|
2310
2678
|
data-selected-bg={
|
|
2311
|
-
features.rowSelection &&
|
|
2312
|
-
|
|
2313
|
-
|
|
2679
|
+
(features.rowSelection &&
|
|
2680
|
+
features.showSelectedBackground &&
|
|
2681
|
+
row.getIsSelected()) ||
|
|
2682
|
+
undefined
|
|
2314
2683
|
}
|
|
2315
2684
|
// The highlighted row is its own concept, so its own attribute
|
|
2316
2685
|
// pair - `data-highlighted` / `aria-current` against
|
|
@@ -2322,32 +2691,40 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2322
2691
|
// so a consumer styling or querying rows by it would cast far
|
|
2323
2692
|
// wider than they meant to.
|
|
2324
2693
|
data-highlighted={
|
|
2325
|
-
features.highlightRow && row.id === highlightedRowId
|
|
2694
|
+
(features.highlightRow && row.id === highlightedRowId) ||
|
|
2695
|
+
undefined
|
|
2326
2696
|
}
|
|
2327
2697
|
// Marked deleted under draft: struck through and inert
|
|
2328
|
-
// until
|
|
2698
|
+
// until saveDrafts reports it, or the mark is toggled back.
|
|
2329
2699
|
data-deleted={
|
|
2330
|
-
deletedRowIds.length > 0 && deletedRowIds.includes(row.id)
|
|
2700
|
+
(deletedRowIds.length > 0 && deletedRowIds.includes(row.id)) ||
|
|
2701
|
+
undefined
|
|
2331
2702
|
}
|
|
2332
2703
|
// Carrying a dirty draft - the row-level face of the cells'
|
|
2333
2704
|
// own data-dirty markers, for row-scoped styling.
|
|
2334
|
-
data-dirty={
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
// Committed into the draft store, waiting for Save.
|
|
2705
|
+
data-dirty={editDirtyRowIdSet.has(row.id) || undefined}
|
|
2706
|
+
// Committed into the draft store, waiting for Save. A
|
|
2707
|
+
// committed entry row is one too - it is in the body
|
|
2708
|
+
// because it is committed - and carries `data-new` besides.
|
|
2339
2709
|
data-draft={
|
|
2340
|
-
|
|
2341
|
-
|
|
2710
|
+
editDraftRowIdSet.has(row.id) ||
|
|
2711
|
+
newRowIdSet.has(row.id) ||
|
|
2712
|
+
undefined
|
|
2342
2713
|
}
|
|
2714
|
+
// An entered row not yet in `data`, committed into the draft
|
|
2715
|
+
// store and sorted, filtered and grouped with the rest.
|
|
2716
|
+
data-new={newRowIdSet.has(row.id) || undefined}
|
|
2343
2717
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
2344
2718
|
// `data-expanded` lands there rather than on a row. This is
|
|
2345
2719
|
// what says which row the open menu is about.
|
|
2346
2720
|
data-context-menu={
|
|
2347
|
-
contextMenuContent !== null &&
|
|
2348
|
-
|
|
2721
|
+
(contextMenuContent !== null &&
|
|
2722
|
+
contextMenuTarget?.rowId === row.id) ||
|
|
2723
|
+
undefined
|
|
2724
|
+
}
|
|
2725
|
+
data-selects-on-click={
|
|
2726
|
+
(selectsOnRowClick && !isGroupRow) || undefined
|
|
2349
2727
|
}
|
|
2350
|
-
data-selects-on-click={selectsOnRowClick && !isGroupRow}
|
|
2351
2728
|
aria-selected={
|
|
2352
2729
|
features.rowSelection ? row.getIsSelected() : undefined
|
|
2353
2730
|
}
|
|
@@ -2359,7 +2736,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2359
2736
|
// From the row's position in the whole view, so the stripes
|
|
2360
2737
|
// survive the virtualizer's moving window - see the prop.
|
|
2361
2738
|
data-striped={
|
|
2362
|
-
striped && viewIndex >= 0
|
|
2739
|
+
(striped && viewIndex >= 0 && viewIndex % 2 === 1) || undefined
|
|
2363
2740
|
}
|
|
2364
2741
|
className={[classes.bodyRow, rowClassNameFor(row)]
|
|
2365
2742
|
.filter(Boolean)
|
|
@@ -2492,13 +2869,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2492
2869
|
row.id,
|
|
2493
2870
|
cell.column.id,
|
|
2494
2871
|
),
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
)
|
|
2500
|
-
? 0
|
|
2501
|
-
: -1,
|
|
2872
|
+
// Focusable by click and by the grid, never by
|
|
2873
|
+
// Tab - the guards are the way in. See
|
|
2874
|
+
// `tabStopCell`.
|
|
2875
|
+
tabIndex: -1,
|
|
2502
2876
|
// "Selected" means "in the block Ctrl+C would
|
|
2503
2877
|
// take" - which under `"single"` is the one cell
|
|
2504
2878
|
// the user is standing on.
|
|
@@ -2524,6 +2898,24 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2524
2898
|
// Right-click is the menu's; the middle button is
|
|
2525
2899
|
// the browser's.
|
|
2526
2900
|
if (event.button !== 0) return;
|
|
2901
|
+
// Pressing a control in the cell - a checkbox, a
|
|
2902
|
+
// chevron, a button a renderer put there - is not
|
|
2903
|
+
// a selection gesture, and a block the user built
|
|
2904
|
+
// should not pay for it. The focus still follows
|
|
2905
|
+
// the press, through the bubbling `onFocus`, so
|
|
2906
|
+
// the cursor ends up on the row acted on; only
|
|
2907
|
+
// the range survives.
|
|
2908
|
+
const pressed = event.target as HTMLElement;
|
|
2909
|
+
const control =
|
|
2910
|
+
pressed === event.currentTarget
|
|
2911
|
+
? null
|
|
2912
|
+
: pressed.closest(FOCUSABLE_IN_CELL);
|
|
2913
|
+
if (
|
|
2914
|
+
control !== null &&
|
|
2915
|
+
event.currentTarget.contains(control)
|
|
2916
|
+
) {
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2527
2919
|
// Shift+click would otherwise smear the browser's
|
|
2528
2920
|
// own text selection across everything between
|
|
2529
2921
|
// the two cells.
|
|
@@ -2611,7 +3003,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2611
3003
|
renderBodyRow(
|
|
2612
3004
|
row,
|
|
2613
3005
|
-1,
|
|
2614
|
-
|
|
3006
|
+
headerRowCount + index + 1,
|
|
2615
3007
|
"top",
|
|
2616
3008
|
),
|
|
2617
3009
|
)}
|
|
@@ -2632,7 +3024,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2632
3024
|
row,
|
|
2633
3025
|
virtualItem.index,
|
|
2634
3026
|
virtualItem.index +
|
|
2635
|
-
|
|
3027
|
+
headerRowCount +
|
|
2636
3028
|
pinnedTopRows.length +
|
|
2637
3029
|
1,
|
|
2638
3030
|
);
|
|
@@ -2655,7 +3047,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2655
3047
|
renderBodyRow(
|
|
2656
3048
|
row,
|
|
2657
3049
|
-1,
|
|
2658
|
-
|
|
3050
|
+
headerRowCount +
|
|
2659
3051
|
pinnedTopRows.length +
|
|
2660
3052
|
rows.length +
|
|
2661
3053
|
index +
|
|
@@ -2711,7 +3103,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2711
3103
|
<div
|
|
2712
3104
|
role="row"
|
|
2713
3105
|
aria-rowindex={
|
|
2714
|
-
rows.length + pinnedRowCount +
|
|
3106
|
+
rows.length + pinnedRowCount + headerRowCount + 1
|
|
2715
3107
|
}
|
|
2716
3108
|
// Also what the height measurement above looks for. One
|
|
2717
3109
|
// attribute serves both now that the part name *is* the
|
|
@@ -2727,7 +3119,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2727
3119
|
role="cell"
|
|
2728
3120
|
data-column-id={header.column.id}
|
|
2729
3121
|
data-align={getColumnAlign(header.column)}
|
|
2730
|
-
data-control-column={
|
|
3122
|
+
data-control-column={
|
|
3123
|
+
isControlColumn(header.column.id) || undefined
|
|
3124
|
+
}
|
|
2731
3125
|
className={[
|
|
2732
3126
|
classes.summaryCell,
|
|
2733
3127
|
layout.isBoundary && layout.pinnedAt === "left"
|
|
@@ -2764,10 +3158,30 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2764
3158
|
})}
|
|
2765
3159
|
</div>
|
|
2766
3160
|
)}
|
|
3161
|
+
|
|
3162
|
+
{/* The trailing tab guard, last child of the grid so that Shift+Tab
|
|
3163
|
+
from below the grid meets it before any cell. */}
|
|
3164
|
+
{cellSelection && (
|
|
3165
|
+
<div
|
|
3166
|
+
ref={trailingGuardRef}
|
|
3167
|
+
tabIndex={tabStopCell === null ? -1 : 0}
|
|
3168
|
+
role="presentation"
|
|
3169
|
+
data-dg-part="tab-guard"
|
|
3170
|
+
data-guard="trailing"
|
|
3171
|
+
className={classes.tabGuard}
|
|
3172
|
+
onFocus={handleGuardFocus}
|
|
3173
|
+
/>
|
|
3174
|
+
)}
|
|
2767
3175
|
</div>
|
|
2768
3176
|
</div>
|
|
2769
3177
|
|
|
2770
|
-
|
|
3178
|
+
{/* The automatic filter surfaces. The popup floats over the first body
|
|
3179
|
+
rows, anchored to the wrapper; the sidebar is a second column of it,
|
|
3180
|
+
which is why the wrapper turns into a flex row for one. Under
|
|
3181
|
+
`filters.surface: "none"` neither renders and the consumer's own
|
|
3182
|
+
`TMDataGrid.FilterPanel` is the only panel on the page. */}
|
|
3183
|
+
{filters.surface === "popup" && <TMDataGridFilterPopup />}
|
|
3184
|
+
{filters.surface === "sidebar" && <TMDataGridFilterSidebar />}
|
|
2771
3185
|
</div>
|
|
2772
3186
|
);
|
|
2773
3187
|
}
|