@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -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,7 +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";
|
|
48
|
+
import {
|
|
49
|
+
resizePreview,
|
|
50
|
+
type TMDataGridColumnTrack,
|
|
51
|
+
} from "../core/resizePreview";
|
|
52
|
+
import { useSettledTableState } from "../core/useSettledTableState";
|
|
53
|
+
import { isInputElement } from "../core/dom";
|
|
38
54
|
import { draftCellContext } from "../core/draftCellContext";
|
|
39
55
|
import {
|
|
40
56
|
getEditFieldName,
|
|
@@ -53,14 +69,14 @@ import {
|
|
|
53
69
|
resolveRangeBounds,
|
|
54
70
|
} from "../core/cellRange";
|
|
55
71
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
buildExportData,
|
|
73
|
+
fromCellExportOptions,
|
|
74
|
+
resolveExportOptions,
|
|
59
75
|
toClipboardText,
|
|
60
|
-
toExcelCsv,
|
|
61
76
|
writeClipboardText,
|
|
77
|
+
writeExportFile,
|
|
62
78
|
type TMDataGridCellExportOptions,
|
|
63
|
-
} from "../core/
|
|
79
|
+
} from "../core/export";
|
|
64
80
|
import { autosizeColumn, hasMountedCells } from "../core/autosize";
|
|
65
81
|
import {
|
|
66
82
|
getDisplayedRows,
|
|
@@ -281,10 +297,22 @@ function TMDataGridBodyCell({
|
|
|
281
297
|
// The whole draft, not one field: a computed or custom renderer may read any
|
|
282
298
|
// field off the row, so every data cell of a drafted row repaints together
|
|
283
299
|
// when its draft moves - and only then, `values` being reference-stable
|
|
284
|
-
// across meta-only form events. `undefined` everywhere else
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
300
|
+
// across meta-only form events. `undefined` everywhere else - including a
|
|
301
|
+
// committed row, whose draft already *is* the row: the hook feeds the draft
|
|
302
|
+
// store's values to the table as `data`, so `row.original` holds them.
|
|
303
|
+
// Only an open form's undecided values need the overlay.
|
|
304
|
+
const draftValues = useSelector(edit.store, (state) => {
|
|
305
|
+
if (isControl) return undefined;
|
|
306
|
+
if (state.committedRowIds.includes(cellRowId)) return undefined;
|
|
307
|
+
if (
|
|
308
|
+
state.newRows.some(
|
|
309
|
+
(newRow) => newRow.tempId === cellRowId && newRow.committed,
|
|
310
|
+
)
|
|
311
|
+
) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return state.rows[cellRowId]?.values;
|
|
315
|
+
});
|
|
288
316
|
return (
|
|
289
317
|
<div
|
|
290
318
|
// `gridcell` rather than `cell` is what tells a screen reader the arrow
|
|
@@ -344,19 +372,22 @@ function TMDataGridBodyCell({
|
|
|
344
372
|
left: layout.pinnedAt === "left" ? layout.offset : undefined,
|
|
345
373
|
right: layout.pinnedAt === "right" ? layout.offset : undefined,
|
|
346
374
|
// The focus ring is drawn inside the cell's own box, so the cell has to
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
375
|
+
// stack above the cells it shares an edge with - but not above the
|
|
376
|
+
// pinned lanes, or the ring rides over them once the row scrolls
|
|
377
|
+
// sideways. A focused cell that is itself pinned takes the slot above
|
|
378
|
+
// them, keeping its ring whole while the rest of the row slides under
|
|
379
|
+
// it. Values from the stacking ladder in TMDataGrid.module.css.
|
|
351
380
|
position: layout.pinnedAt
|
|
352
381
|
? "sticky"
|
|
353
382
|
: nav?.focused
|
|
354
383
|
? "relative"
|
|
355
384
|
: undefined,
|
|
356
|
-
zIndex:
|
|
357
|
-
?
|
|
358
|
-
|
|
359
|
-
|
|
385
|
+
zIndex: layout.pinnedAt
|
|
386
|
+
? nav?.focused
|
|
387
|
+
? "var(--dg-z-pinned-focused-cell, 3)"
|
|
388
|
+
: "var(--dg-z-pinned-cell, 2)"
|
|
389
|
+
: nav?.focused
|
|
390
|
+
? "var(--dg-z-focused-cell, 1)"
|
|
360
391
|
: undefined,
|
|
361
392
|
}}
|
|
362
393
|
>
|
|
@@ -364,9 +395,10 @@ function TMDataGridBodyCell({
|
|
|
364
395
|
<span className={classes.cellContent}>
|
|
365
396
|
{(() => {
|
|
366
397
|
// A row with a live form shows its draft, not `data`: the column's
|
|
367
|
-
// own renderer over a context reading the form's values.
|
|
368
|
-
// drafts -
|
|
369
|
-
//
|
|
398
|
+
// own renderer over a context reading the form's values. The
|
|
399
|
+
// drafts that outlive their editor - parked ones, cellConfirm's
|
|
400
|
+
// kept ones - are the point; group cells are excluded because no
|
|
401
|
+
// group row ever has a form.
|
|
370
402
|
const draftContext =
|
|
371
403
|
draftValues !== undefined &&
|
|
372
404
|
contentOverride === undefined &&
|
|
@@ -649,9 +681,10 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
649
681
|
*/
|
|
650
682
|
rowContextMenuProps?: Omit<MenuProps, "opened" | "onChange" | "children">;
|
|
651
683
|
/**
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
684
|
+
* @deprecated Set `exportOptions` on `useTMDataGrid` instead; it covers the
|
|
685
|
+
* cell-range menu and every other export alike. Until it goes, this is
|
|
686
|
+
* converted (`separator` and `decimalComma` become a `csvExcelFormat`) and
|
|
687
|
+
* merged over `exportOptions` for the cell-range menu only.
|
|
655
688
|
*/
|
|
656
689
|
cellExport?: TMDataGridCellExportOptions;
|
|
657
690
|
/**
|
|
@@ -716,6 +749,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
716
749
|
ui,
|
|
717
750
|
edit,
|
|
718
751
|
features,
|
|
752
|
+
filters,
|
|
753
|
+
exportOptions: gridExportOptions,
|
|
719
754
|
labels,
|
|
720
755
|
rowHeight,
|
|
721
756
|
controlSize,
|
|
@@ -725,9 +760,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
725
760
|
scrollerRef,
|
|
726
761
|
} = useTMDataGridContext();
|
|
727
762
|
|
|
728
|
-
// The body depends on every state slice (sorting, filters, paging,
|
|
729
|
-
// visibility, selection)
|
|
730
|
-
|
|
763
|
+
// The body depends on every settled state slice (sorting, filters, paging,
|
|
764
|
+
// sizing, visibility, selection). Not on the transient resize deltas: those
|
|
765
|
+
// publish on every pointer move and the drag paints itself, see below.
|
|
766
|
+
useSettledTableState(table.store);
|
|
731
767
|
// The active row lives in the chrome store, so it needs its own subscription.
|
|
732
768
|
const highlightedRowId = useSelector(ui, (state) => state.highlightedRowId);
|
|
733
769
|
// As does the focused cell. Selected separately from the row above so a grid
|
|
@@ -740,7 +776,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
740
776
|
// Row mode opens whole rows and any number of them at once, so which rows
|
|
741
777
|
// are showing editors is the set of open forms, not the single active slot.
|
|
742
778
|
const editOpenRowIds = useSelector(edit.store, (state) => state.openRowIds);
|
|
743
|
-
|
|
779
|
+
// Entry rows, for the block's mount and for `data-new` on the committed
|
|
780
|
+
// ones that have joined the body. Identity-stable across typing.
|
|
781
|
+
const newRows = useSelector(edit.store, (state) => state.newRows);
|
|
782
|
+
const newRowCount = newRows.length;
|
|
744
783
|
const deletedRowIds = useSelector(edit.store, (state) => state.deletedRowIds);
|
|
745
784
|
// Rows carrying a dirty draft, for the row-level marker. Derived from the
|
|
746
785
|
// same projections the cells subscribe to; the array's identity churns with
|
|
@@ -750,6 +789,32 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
750
789
|
(id) => (state.rows[id]?.dirtyFields.length ?? 0) > 0,
|
|
751
790
|
),
|
|
752
791
|
);
|
|
792
|
+
// Rows parked in the draft store, waiting for Save. A separate marker from
|
|
793
|
+
// `data-dirty`, which covers any row being typed into: after a partial save
|
|
794
|
+
// these are what is left, so one selector highlights everything still
|
|
795
|
+
// pending.
|
|
796
|
+
const editDraftRowIds = useSelector(
|
|
797
|
+
edit.store,
|
|
798
|
+
(state) => state.committedRowIds,
|
|
799
|
+
);
|
|
800
|
+
// The same lists by id: the body asks them once per rendered row, and an
|
|
801
|
+
// import runs each of them to thousands.
|
|
802
|
+
const editOpenRowIdSet = useMemo(
|
|
803
|
+
() => new Set(editOpenRowIds),
|
|
804
|
+
[editOpenRowIds],
|
|
805
|
+
);
|
|
806
|
+
const editDirtyRowIdSet = useMemo(
|
|
807
|
+
() => new Set(editDirtyRowIds),
|
|
808
|
+
[editDirtyRowIds],
|
|
809
|
+
);
|
|
810
|
+
const editDraftRowIdSet = useMemo(
|
|
811
|
+
() => new Set(editDraftRowIds),
|
|
812
|
+
[editDraftRowIds],
|
|
813
|
+
);
|
|
814
|
+
const newRowIdSet = useMemo(
|
|
815
|
+
() => new Set(newRows.map((newRow) => newRow.tempId)),
|
|
816
|
+
[newRows],
|
|
817
|
+
);
|
|
753
818
|
|
|
754
819
|
const { loading, noResultsLabel = labels.noResults } =
|
|
755
820
|
table.options.meta ?? {};
|
|
@@ -843,29 +908,99 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
843
908
|
});
|
|
844
909
|
|
|
845
910
|
/**
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
911
|
+
* Everything the body rows start after, and how much of it stays on screen
|
|
912
|
+
* once they scroll: the header rows, the entry block and the pinned top
|
|
913
|
+
* block all sit in the same scroll content, above the first virtual row.
|
|
914
|
+
*
|
|
915
|
+
* Three readers, one measurement.
|
|
916
|
+
*
|
|
917
|
+
* `--dg-header-height` is the header total alone, published for the entry
|
|
918
|
+
* block to stick under and only while a block is in use - the stylesheet's
|
|
919
|
+
* per-`size` value is what the header cells are laid out against otherwise,
|
|
920
|
+
* and it is not the sum of a stacked group's rows.
|
|
921
|
+
*
|
|
922
|
+
* `bodyOffsetTop` is the whole stack, and it is the virtualizer's
|
|
923
|
+
* `scrollMargin`: item offsets start at zero, so without it every one of
|
|
924
|
+
* them is short by the height of what the rows begin after. `stickyCoverTop`
|
|
925
|
+
* leaves out the entry rows that flow with the body (`editNewRowsSticky`
|
|
926
|
+
* off), and is its `scrollPaddingStart` - the part of the stack that stays
|
|
927
|
+
* put and would otherwise be covering the row `align: "auto"` just scrolled
|
|
928
|
+
* to.
|
|
929
|
+
*
|
|
930
|
+
* The observer is mounted whatever the grid shows, unlike the block
|
|
931
|
+
* measurements below it: the header is there in every case, and the
|
|
932
|
+
* virtualizer needs its height from the first paint.
|
|
850
933
|
*/
|
|
851
934
|
const gridElementRef = useRef<HTMLDivElement>(null);
|
|
935
|
+
/**
|
|
936
|
+
* Header filters only render where something can be filtered - the row would
|
|
937
|
+
* otherwise be a band of empty cells on a grid with `enableColumnFilters:
|
|
938
|
+
* false`. Derived up here because the header measurement below depends on
|
|
939
|
+
* whether the row is in the DOM.
|
|
940
|
+
*/
|
|
941
|
+
const hasHeaderFilterRow =
|
|
942
|
+
filters.inHeader && getGridCapabilities(table, features).canFilterAny;
|
|
943
|
+
/**
|
|
944
|
+
* How many rows deep the header is. In the measurement's dependencies
|
|
945
|
+
* because the rows are found once, by query: a `columns` change that adds or
|
|
946
|
+
* drops a group level changes which elements have to be measured and given
|
|
947
|
+
* their sticky inset, and nothing else in the list moves when it does.
|
|
948
|
+
*/
|
|
949
|
+
const headerDepth = table.getCenterHeaderGroups().length;
|
|
950
|
+
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
|
951
|
+
const [bodyOffsetTop, setBodyOffsetTop] = useState(0);
|
|
952
|
+
const [stickyCoverTop, setStickyCoverTop] = useState(0);
|
|
953
|
+
const [stickyCoverBottom, setStickyCoverBottom] = useState(0);
|
|
852
954
|
useEffect(() => {
|
|
853
|
-
if (newRowCount === 0 && pinnedTopRows.length === 0) return;
|
|
854
955
|
const grid = gridElementRef.current;
|
|
855
956
|
if (grid === null) return;
|
|
856
957
|
const headerRows = grid.querySelectorAll<HTMLElement>(
|
|
857
958
|
"[data-dg-header-row]",
|
|
858
959
|
);
|
|
960
|
+
const stickyBlocks = grid.querySelectorAll<HTMLElement>(
|
|
961
|
+
'[data-dg-entry-block], [data-dg-part="pinned-top"]',
|
|
962
|
+
);
|
|
963
|
+
const flowBlocks = grid.querySelectorAll<HTMLElement>(
|
|
964
|
+
"[data-dg-entry-flow-block]",
|
|
965
|
+
);
|
|
859
966
|
const measure = () => {
|
|
860
|
-
let
|
|
861
|
-
for (const headerRow of headerRows)
|
|
862
|
-
|
|
967
|
+
let header = 0;
|
|
968
|
+
for (const headerRow of headerRows) {
|
|
969
|
+
// Every header row is sticky, so each one needs the height of the rows
|
|
970
|
+
// above it as its own inset - otherwise a stacked group and the filter
|
|
971
|
+
// row all pin to the top edge and paint over each other. Written on the
|
|
972
|
+
// element because only JS knows where the row before it ended; the
|
|
973
|
+
// stylesheet's `top` is the pre-measurement fallback.
|
|
974
|
+
headerRow.style.top = `${header}px`;
|
|
975
|
+
header += headerRow.offsetHeight;
|
|
976
|
+
}
|
|
977
|
+
if (newRowCount > 0 || pinnedTopRows.length > 0) {
|
|
978
|
+
grid.style.setProperty("--dg-header-height", `${header}px`);
|
|
979
|
+
}
|
|
980
|
+
// The whole header's height, on the wrapper rather than on the grid,
|
|
981
|
+
// because the popup is a sibling of the scroll container and never sees
|
|
982
|
+
// a property set inside it. A second name rather than reusing
|
|
983
|
+
// `--dg-header-height`: the header cells read that one for their own
|
|
984
|
+
// `min-height`, so publishing a stacked total there would feed a
|
|
985
|
+
// two-row header back in as the height of each of its rows.
|
|
986
|
+
tableWrapperRef.current?.style.setProperty(
|
|
987
|
+
"--dg-header-stack-height",
|
|
988
|
+
`${header}px`,
|
|
989
|
+
);
|
|
990
|
+
let cover = header;
|
|
991
|
+
for (const block of stickyBlocks) cover += block.offsetHeight;
|
|
992
|
+
let offset = cover;
|
|
993
|
+
for (const block of flowBlocks) offset += block.offsetHeight;
|
|
994
|
+
setStickyCoverTop(cover);
|
|
995
|
+
setBodyOffsetTop(offset);
|
|
863
996
|
};
|
|
864
997
|
measure();
|
|
865
998
|
const observer = new ResizeObserver(measure);
|
|
866
|
-
for (const
|
|
999
|
+
for (const element of [...headerRows, ...stickyBlocks, ...flowBlocks]) {
|
|
1000
|
+
observer.observe(element);
|
|
1001
|
+
}
|
|
867
1002
|
return () => observer.disconnect();
|
|
868
|
-
}, [newRowCount, pinnedTopRows.length]);
|
|
1003
|
+
}, [newRowCount, pinnedTopRows.length, hasHeaderFilterRow, headerDepth]);
|
|
869
1004
|
|
|
870
1005
|
/**
|
|
871
1006
|
* The entry block's height, published as `--dg-entry-height` so the pinned
|
|
@@ -889,32 +1024,6 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
889
1024
|
};
|
|
890
1025
|
}, [newRowCount, pinnedTopRows.length]);
|
|
891
1026
|
|
|
892
|
-
/**
|
|
893
|
-
* The summary row's height, published as `--dg-summary-height` so the
|
|
894
|
-
* pinned bottom block sticks above it rather than underneath it.
|
|
895
|
-
*/
|
|
896
|
-
useEffect(() => {
|
|
897
|
-
if (pinnedBottomRows.length === 0) return;
|
|
898
|
-
const grid = gridElementRef.current;
|
|
899
|
-
if (grid === null) return;
|
|
900
|
-
const summaryRow = grid.querySelector<HTMLElement>(
|
|
901
|
-
'[data-dg-part="summary-row"]',
|
|
902
|
-
);
|
|
903
|
-
if (summaryRow === null) return;
|
|
904
|
-
const measure = () =>
|
|
905
|
-
grid.style.setProperty(
|
|
906
|
-
"--dg-summary-height",
|
|
907
|
-
`${summaryRow.offsetHeight}px`,
|
|
908
|
-
);
|
|
909
|
-
measure();
|
|
910
|
-
const observer = new ResizeObserver(measure);
|
|
911
|
-
observer.observe(summaryRow);
|
|
912
|
-
return () => {
|
|
913
|
-
observer.disconnect();
|
|
914
|
-
grid.style.removeProperty("--dg-summary-height");
|
|
915
|
-
};
|
|
916
|
-
}, [pinnedBottomRows.length]);
|
|
917
|
-
|
|
918
1027
|
// A persisted pageIndex can outlive the data that produced it; TanStack only
|
|
919
1028
|
// auto-resets on live filter/sort/data changes, not on restored state. The
|
|
920
1029
|
// guard makes the dependency-free effect idempotent.
|
|
@@ -959,12 +1068,44 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
959
1068
|
getItemKey: useCallback((index: number) => rows[index]?.id ?? index, [rows]),
|
|
960
1069
|
overscan,
|
|
961
1070
|
initialRect: { height: 600, width: 1200 },
|
|
1071
|
+
/**
|
|
1072
|
+
* The rows are not the first thing in the scroll container - the header,
|
|
1073
|
+
* the entry block and the pinned top block come before them. The margin is
|
|
1074
|
+
* what puts an item's offset in the container's own scroll coordinates;
|
|
1075
|
+
* the two paddings are the sticky blocks at either end, which cover the
|
|
1076
|
+
* viewport without shrinking it, so `align: "auto"` has to scroll past
|
|
1077
|
+
* them rather than stopping underneath.
|
|
1078
|
+
*/
|
|
1079
|
+
scrollMargin: bodyOffsetTop,
|
|
1080
|
+
scrollPaddingStart: stickyCoverTop,
|
|
1081
|
+
scrollPaddingEnd: stickyCoverBottom,
|
|
962
1082
|
});
|
|
963
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* A live `size` (or `meta.rowHeight`) change moves what `estimateSize`
|
|
1086
|
+
* answers, but the virtualizer's measurement cache does not list that option
|
|
1087
|
+
* among its dependencies - by its own docs, changing the estimate means
|
|
1088
|
+
* calling `measure()`. Without this the spacers keep the old height until
|
|
1089
|
+
* something else invalidates the cache, so the scroll range and the window
|
|
1090
|
+
* disagree with the rows actually painted.
|
|
1091
|
+
*/
|
|
1092
|
+
const prevRowHeightRef = useRef(rowHeight);
|
|
1093
|
+
useEffect(() => {
|
|
1094
|
+
if (prevRowHeightRef.current === rowHeight) return;
|
|
1095
|
+
prevRowHeightRef.current = rowHeight;
|
|
1096
|
+
virtualizer.measure();
|
|
1097
|
+
}, [rowHeight, virtualizer]);
|
|
1098
|
+
|
|
1099
|
+
// The spacers stand in for the rows that are not mounted, so they measure
|
|
1100
|
+
// from the first row rather than from the top of the scroll container:
|
|
1101
|
+
// `scrollMargin` is included in every item's `start` and `end` and excluded
|
|
1102
|
+
// from `getTotalSize()`, and taking it back off here is what keeps a spacer
|
|
1103
|
+
// the height of the rows it replaces.
|
|
964
1104
|
const virtualItems = virtualizer.getVirtualItems();
|
|
965
|
-
const paddingTop = virtualItems[0]?.start ??
|
|
1105
|
+
const paddingTop = (virtualItems[0]?.start ?? bodyOffsetTop) - bodyOffsetTop;
|
|
966
1106
|
const paddingBottom =
|
|
967
|
-
virtualizer.getTotalSize() -
|
|
1107
|
+
virtualizer.getTotalSize() -
|
|
1108
|
+
((virtualItems.at(-1)?.end ?? bodyOffsetTop) - bodyOffsetTop);
|
|
968
1109
|
|
|
969
1110
|
/**
|
|
970
1111
|
* `api.scrollToRow`, implemented where the virtualizer is.
|
|
@@ -1021,14 +1162,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1021
1162
|
onReachEnd();
|
|
1022
1163
|
});
|
|
1023
1164
|
|
|
1024
|
-
// The *Visible* variants throughout: `
|
|
1165
|
+
// The *Visible* variants throughout: `getStartLeafColumns()` and friends
|
|
1025
1166
|
// include hidden columns, while the cells below come from
|
|
1026
|
-
// `row.
|
|
1167
|
+
// `row.getStartVisibleCells()`, which does not. Mixing the two would lay down
|
|
1027
1168
|
// a grid track for a column that renders no cell, shifting every column after
|
|
1028
1169
|
// it out of its header. The tree column is hidden exactly this way while
|
|
1029
1170
|
// nothing is grouped.
|
|
1030
|
-
const leftLeafColumns = table.
|
|
1031
|
-
const rightLeafColumns = table.
|
|
1171
|
+
const leftLeafColumns = table.getStartVisibleLeafColumns();
|
|
1172
|
+
const rightLeafColumns = table.getEndVisibleLeafColumns();
|
|
1032
1173
|
const centerLeafColumns = table.getCenterVisibleLeafColumns();
|
|
1033
1174
|
|
|
1034
1175
|
const orderedColumns = [
|
|
@@ -1040,8 +1181,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1040
1181
|
// Headers and cells must follow the same left → center → right order as the
|
|
1041
1182
|
// column tracks, otherwise pinning a column would shuffle it out of its lane.
|
|
1042
1183
|
const centerHeaderGroups = table.getCenterHeaderGroups();
|
|
1043
|
-
const leftHeaderGroups = table.
|
|
1044
|
-
const rightHeaderGroups = table.
|
|
1184
|
+
const leftHeaderGroups = table.getStartHeaderGroups();
|
|
1185
|
+
const rightHeaderGroups = table.getEndHeaderGroups();
|
|
1045
1186
|
const headerGroups = centerHeaderGroups.map((group, index) => ({
|
|
1046
1187
|
id: group.id,
|
|
1047
1188
|
headers: [
|
|
@@ -1058,19 +1199,26 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1058
1199
|
// Pinned columns are always exact: their sticky offsets come from
|
|
1059
1200
|
// `getStart()` / `getAfter()`, which sum `getSize()` and cannot see an `fr`.
|
|
1060
1201
|
const columnSizing = table.store.state.columnSizing;
|
|
1061
|
-
const columnTracks = orderedColumns.map(
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1202
|
+
const columnTracks: Array<TMDataGridColumnTrack> = orderedColumns.map(
|
|
1203
|
+
(column) => {
|
|
1204
|
+
const isFixed = column.columnDef.minSize === column.columnDef.maxSize;
|
|
1205
|
+
const isResized = column.id in columnSizing;
|
|
1206
|
+
const isPinned = column.getIsPinned() !== false;
|
|
1207
|
+
const minWidth = column.columnDef.minSize ?? 80;
|
|
1208
|
+
if (isFixed || isResized || isPinned) {
|
|
1209
|
+
return {
|
|
1210
|
+
id: column.id,
|
|
1211
|
+
track: `${column.getSize()}px`,
|
|
1212
|
+
minWidth: column.getSize(),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
id: column.id,
|
|
1217
|
+
track: `minmax(${minWidth}px, ${column.columnDef.meta?.flex ?? 1}fr)`,
|
|
1218
|
+
minWidth,
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
);
|
|
1074
1222
|
const gridTemplateColumns = columnTracks.map((c) => c.track).join(" ");
|
|
1075
1223
|
const gridMinWidth = columnTracks.reduce((sum, c) => sum + c.minWidth, 0);
|
|
1076
1224
|
|
|
@@ -1118,6 +1266,65 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1118
1266
|
const layoutFor = (columnId: string) =>
|
|
1119
1267
|
columnLayout.get(columnId) ?? UNPINNED_LAYOUT;
|
|
1120
1268
|
|
|
1269
|
+
/**
|
|
1270
|
+
* A running resize drag paints itself, straight onto the grid element.
|
|
1271
|
+
*
|
|
1272
|
+
* Every row sits on the grid's own column tracks through `subgrid`, so the
|
|
1273
|
+
* whole drag is one style write per frame. Letting the drag render instead -
|
|
1274
|
+
* `columnResizeMode: "onChange"`, which publishes a width on every pointer
|
|
1275
|
+
* move - costs a React pass over every mounted cell for each of those moves,
|
|
1276
|
+
* and the gesture turns choppy on a grid wide enough to be worth resizing.
|
|
1277
|
+
*
|
|
1278
|
+
* TanStack still owns the gesture and still commits the width into
|
|
1279
|
+
* `columnSizing` when the pointer is released; the render that follows lands
|
|
1280
|
+
* on the same numbers the last frame painted.
|
|
1281
|
+
*/
|
|
1282
|
+
const previewRef = useRef<{
|
|
1283
|
+
tracks: Array<TMDataGridColumnTrack>;
|
|
1284
|
+
leftLane: Array<string>;
|
|
1285
|
+
rightLane: Array<string>;
|
|
1286
|
+
}>({ tracks: columnTracks, leftLane: [], rightLane: [] });
|
|
1287
|
+
useEffect(() => {
|
|
1288
|
+
previewRef.current = {
|
|
1289
|
+
tracks: columnTracks,
|
|
1290
|
+
leftLane: leftLeafColumns.map((column) => column.id),
|
|
1291
|
+
// Outermost last, the order the offsets above accumulate in.
|
|
1292
|
+
rightLane: [...rightLeafColumns].reverse().map((column) => column.id),
|
|
1293
|
+
};
|
|
1294
|
+
});
|
|
1295
|
+
useEffect(() => {
|
|
1296
|
+
const element = gridElementRef.current;
|
|
1297
|
+
if (!element) return;
|
|
1298
|
+
|
|
1299
|
+
// Painted straight from the store's publish rather than on a frame of its
|
|
1300
|
+
// own: the browser already coalesces pointer moves to one per frame, and a
|
|
1301
|
+
// `requestAnimationFrame` stops firing altogether while the window is
|
|
1302
|
+
// occluded - which would leave the drag frozen under the pointer.
|
|
1303
|
+
let painted = "";
|
|
1304
|
+
const subscription = table.store.subscribe((state) => {
|
|
1305
|
+
const preview = resizePreview({
|
|
1306
|
+
progress: state.columnResizing,
|
|
1307
|
+
boundsOf: (columnId) => table.getColumn(columnId)?.columnDef ?? {},
|
|
1308
|
+
...previewRef.current,
|
|
1309
|
+
});
|
|
1310
|
+
if (!preview || preview.gridTemplateColumns === painted) return;
|
|
1311
|
+
painted = preview.gridTemplateColumns;
|
|
1312
|
+
element.style.gridTemplateColumns = preview.gridTemplateColumns;
|
|
1313
|
+
element.style.minWidth = `${preview.minWidth}px`;
|
|
1314
|
+
// Only the pinned lanes, and only while the drag is resizing one of
|
|
1315
|
+
// them: their sticky offsets are the one measurement that does not
|
|
1316
|
+
// follow from the track list alone.
|
|
1317
|
+
for (const { columnId, side, offset } of preview.offsets) {
|
|
1318
|
+
for (const cell of element.querySelectorAll<HTMLElement>(
|
|
1319
|
+
`[data-column-id="${CSS.escape(columnId)}"]`,
|
|
1320
|
+
)) {
|
|
1321
|
+
cell.style[side] = `${offset}px`;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
return () => subscription.unsubscribe();
|
|
1326
|
+
}, [table]);
|
|
1327
|
+
|
|
1121
1328
|
// Pinned rows count as content: with every visible row pinned to an edge,
|
|
1122
1329
|
// an empty-state message beside them would contradict what is on screen.
|
|
1123
1330
|
const isEmpty = rows.length === 0 && pinnedRowCount === 0;
|
|
@@ -1141,6 +1348,68 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1141
1348
|
const hasSummaryRow = orderedColumns.some(
|
|
1142
1349
|
(column) => column.columnDef.footer !== undefined,
|
|
1143
1350
|
);
|
|
1351
|
+
/**
|
|
1352
|
+
* Rows above the first body row, which is what every `aria-rowindex` in the
|
|
1353
|
+
* body counts from and what `aria-rowcount` adds up. The header groups plus
|
|
1354
|
+
* the filter row, when there is one.
|
|
1355
|
+
*/
|
|
1356
|
+
const headerRowCount = headerGroups.length + (hasHeaderFilterRow ? 1 : 0);
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* The summary row's height, published as `--dg-summary-height`, which two
|
|
1360
|
+
* things read: the pinned bottom block sticks above the summary row rather
|
|
1361
|
+
* than underneath it, and the summary row's own `top` inset holds it on the
|
|
1362
|
+
* bottom edge of a body too short to scroll. A layout effect, so the first
|
|
1363
|
+
* paint already has the measurement - `top` would otherwise resolve to 100%
|
|
1364
|
+
* for a frame and take an overflowing grid's summary row out of view. Down
|
|
1365
|
+
* here rather than beside the other measurements because `hasSummaryRow` is
|
|
1366
|
+
* only known this far into the render.
|
|
1367
|
+
*/
|
|
1368
|
+
useLayoutEffect(() => {
|
|
1369
|
+
if (!hasSummaryRow) return;
|
|
1370
|
+
const grid = gridElementRef.current;
|
|
1371
|
+
if (grid === null) return;
|
|
1372
|
+
const summaryRow = grid.querySelector<HTMLElement>(
|
|
1373
|
+
'[data-dg-part="summary-row"]',
|
|
1374
|
+
);
|
|
1375
|
+
if (summaryRow === null) return;
|
|
1376
|
+
const measure = () =>
|
|
1377
|
+
grid.style.setProperty(
|
|
1378
|
+
"--dg-summary-height",
|
|
1379
|
+
`${summaryRow.offsetHeight}px`,
|
|
1380
|
+
);
|
|
1381
|
+
measure();
|
|
1382
|
+
const observer = new ResizeObserver(measure);
|
|
1383
|
+
observer.observe(summaryRow);
|
|
1384
|
+
return () => {
|
|
1385
|
+
observer.disconnect();
|
|
1386
|
+
grid.style.removeProperty("--dg-summary-height");
|
|
1387
|
+
};
|
|
1388
|
+
}, [hasSummaryRow]);
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* The other end of the same story as `stickyCoverTop`: the pinned bottom
|
|
1392
|
+
* block and the summary row stick to the container's bottom edge, so the
|
|
1393
|
+
* virtualizer takes their height as `scrollPaddingEnd` and stops scrolling a
|
|
1394
|
+
* row to just underneath them. Down here for the same reason as the
|
|
1395
|
+
* measurement above.
|
|
1396
|
+
*/
|
|
1397
|
+
useEffect(() => {
|
|
1398
|
+
const grid = gridElementRef.current;
|
|
1399
|
+
if (grid === null) return;
|
|
1400
|
+
const blocks = grid.querySelectorAll<HTMLElement>(
|
|
1401
|
+
'[data-dg-part="pinned-bottom"], [data-dg-part="summary-row"]',
|
|
1402
|
+
);
|
|
1403
|
+
const measure = () => {
|
|
1404
|
+
let total = 0;
|
|
1405
|
+
for (const block of blocks) total += block.offsetHeight;
|
|
1406
|
+
setStickyCoverBottom(total);
|
|
1407
|
+
};
|
|
1408
|
+
measure();
|
|
1409
|
+
const observer = new ResizeObserver(measure);
|
|
1410
|
+
for (const block of blocks) observer.observe(block);
|
|
1411
|
+
return () => observer.disconnect();
|
|
1412
|
+
}, [pinnedBottomRows.length, hasSummaryRow]);
|
|
1144
1413
|
|
|
1145
1414
|
// "row" mode: the row itself is the selection control, with the modifier
|
|
1146
1415
|
// conventions of any desktop list - see resolveRowSelectionClick.
|
|
@@ -1241,6 +1510,23 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1241
1510
|
/** The last position the effect acted on, so it can tell a move from a re-render. */
|
|
1242
1511
|
const appliedCellRef = useRef<TMDataGridCellPosition | null>(null);
|
|
1243
1512
|
|
|
1513
|
+
/**
|
|
1514
|
+
* The two focusable elements that bracket the body, and the flag that says
|
|
1515
|
+
* which way focus is passing through one.
|
|
1516
|
+
*
|
|
1517
|
+
* They are what makes the body one tab stop without asking anything of the
|
|
1518
|
+
* cell renderers. Tab inside the body parks the focus on the guard past the
|
|
1519
|
+
* edge being left, and the browser's default Tab then continues from there -
|
|
1520
|
+
* so the next stop is whatever follows the grid, however many tabbable
|
|
1521
|
+
* controls the mounted rows happen to hold. Focus arriving at a guard any
|
|
1522
|
+
* other way came from outside the body, and is forwarded to the cursor cell.
|
|
1523
|
+
* `guardHopRef` is what tells the two apart; the grid raises it just before
|
|
1524
|
+
* it moves the focus itself.
|
|
1525
|
+
*/
|
|
1526
|
+
const leadingGuardRef = useRef<HTMLDivElement>(null);
|
|
1527
|
+
const trailingGuardRef = useRef<HTMLDivElement>(null);
|
|
1528
|
+
const guardHopRef = useRef(false);
|
|
1529
|
+
|
|
1244
1530
|
// O(n) per keystroke and per render while a cell is focused. Fine at any row
|
|
1245
1531
|
// count a grid actually holds, and the alternative - an id→index map rebuilt
|
|
1246
1532
|
// whenever sorting, filtering or grouping changes - costs more than it saves.
|
|
@@ -1257,13 +1543,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1257
1543
|
virtualItems.some((item) => item.index === focusedRowIndex);
|
|
1258
1544
|
|
|
1259
1545
|
/**
|
|
1260
|
-
* The
|
|
1546
|
+
* The cell the tab guards hand the focus to when Tab enters the body.
|
|
1261
1547
|
*
|
|
1262
1548
|
* The focused cell when it is on screen. When it is not - the grid has never
|
|
1263
1549
|
* been entered, or the user scrolled the focus away with the wheel - the
|
|
1264
|
-
* first mounted cell stands in, so the body always
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1550
|
+
* first mounted cell stands in, so entering the body always lands somewhere.
|
|
1551
|
+
* No body cell is itself in the tab order: the guards are the body's only
|
|
1552
|
+
* tab stops, which is what keeps a cell from turning up between two of a
|
|
1553
|
+
* row's controls when Tab walks them.
|
|
1267
1554
|
*/
|
|
1268
1555
|
const firstMountedRow = rows[virtualItems[0]?.index ?? -1];
|
|
1269
1556
|
const tabStopCell: TMDataGridCellPosition | null = !cellSelection
|
|
@@ -1274,6 +1561,35 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1274
1561
|
? { rowId: firstMountedRow.id, columnId: orderedColumns[0].id }
|
|
1275
1562
|
: null;
|
|
1276
1563
|
|
|
1564
|
+
/**
|
|
1565
|
+
* Focus landing on either guard.
|
|
1566
|
+
*
|
|
1567
|
+
* A raised hop flag means the grid put it there on its way out, so the
|
|
1568
|
+
* browser's default Tab is about to carry on from the guard - nothing to do
|
|
1569
|
+
* but lower the flag. Anything else is Tab off the last header control onto
|
|
1570
|
+
* the leading guard, or Shift+Tab from below the grid onto the trailing one:
|
|
1571
|
+
* both mean "enter the body", and the body is entered at the cursor cell.
|
|
1572
|
+
*
|
|
1573
|
+
* The state is moved as well as the DOM focus, because the cell may not be
|
|
1574
|
+
* the one the ring is on; the direct focus is so it happens in this event
|
|
1575
|
+
* rather than a frame later.
|
|
1576
|
+
*
|
|
1577
|
+
* With no rows there is no cell to hand it to. The guard keeps the focus and
|
|
1578
|
+
* the next Tab leaves the grid the way it came in.
|
|
1579
|
+
*/
|
|
1580
|
+
const handleGuardFocus = () => {
|
|
1581
|
+
if (guardHopRef.current) {
|
|
1582
|
+
guardHopRef.current = false;
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (tabStopCell === null) return;
|
|
1586
|
+
wantsCellFocusRef.current = true;
|
|
1587
|
+
ui.actions.setFocusedCell(tabStopCell);
|
|
1588
|
+
const container = scrollContainerRef.current;
|
|
1589
|
+
if (container === null) return;
|
|
1590
|
+
findCellElement(container, tabStopCell)?.focus({ preventScroll: true });
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1277
1593
|
const isCellAt = (
|
|
1278
1594
|
position: TMDataGridCellPosition | null,
|
|
1279
1595
|
rowId: string,
|
|
@@ -1340,24 +1656,26 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1340
1656
|
? 0
|
|
1341
1657
|
: orderedColumns
|
|
1342
1658
|
.slice(selectionBounds.left, selectionBounds.right + 1)
|
|
1343
|
-
.filter(
|
|
1344
|
-
|
|
1345
|
-
|
|
1659
|
+
.filter(
|
|
1660
|
+
(column) =>
|
|
1661
|
+
!isGeneratedColumn(column.id) &&
|
|
1662
|
+
column.columnDef.meta?.enableExport !== false,
|
|
1663
|
+
).length;
|
|
1664
|
+
|
|
1665
|
+
const exportOptions = resolveExportOptions(
|
|
1666
|
+
gridExportOptions,
|
|
1667
|
+
cellExport && fromCellExportOptions(cellExport),
|
|
1668
|
+
);
|
|
1346
1669
|
// Sticky across menus, the way a checkbox in a dialog is: a user who exports
|
|
1347
1670
|
// with headers once almost always wants them the next time too.
|
|
1348
1671
|
const [exportIncludesHeaders, setExportIncludesHeaders] = useState(
|
|
1349
1672
|
exportOptions.includeHeaders,
|
|
1350
1673
|
);
|
|
1351
|
-
|
|
1674
|
+
// The rectangle's values, over the displayed rows the bounds index into.
|
|
1675
|
+
const buildSelectionData = () =>
|
|
1352
1676
|
selectionBounds === null
|
|
1353
|
-
?
|
|
1354
|
-
:
|
|
1355
|
-
rows,
|
|
1356
|
-
columns: orderedColumns,
|
|
1357
|
-
bounds: selectionBounds,
|
|
1358
|
-
includeHeaders,
|
|
1359
|
-
decimalComma: exportOptions.decimalComma,
|
|
1360
|
-
});
|
|
1677
|
+
? null
|
|
1678
|
+
: buildExportData({ table, rows, bounds: selectionBounds });
|
|
1361
1679
|
|
|
1362
1680
|
/**
|
|
1363
1681
|
* Ctrl+C. Values only, no header row - Excel's own copy does not include one
|
|
@@ -1366,9 +1684,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1366
1684
|
* because a file is a document and a clipboard is a fragment.
|
|
1367
1685
|
*/
|
|
1368
1686
|
const copySelection = async () => {
|
|
1369
|
-
const
|
|
1370
|
-
if (
|
|
1371
|
-
await writeClipboardText(
|
|
1687
|
+
const data = buildSelectionData();
|
|
1688
|
+
if (data === null || data.columnIds.length === 0) return;
|
|
1689
|
+
await writeClipboardText(
|
|
1690
|
+
toClipboardText(data, {
|
|
1691
|
+
decimalComma: exportOptions.format.decimalComma,
|
|
1692
|
+
}),
|
|
1693
|
+
);
|
|
1372
1694
|
};
|
|
1373
1695
|
|
|
1374
1696
|
/**
|
|
@@ -1382,12 +1704,15 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1382
1704
|
|
|
1383
1705
|
// The drag ends wherever the button comes up, which is routinely outside the
|
|
1384
1706
|
// grid - over the scrollbar, or past the window edge. Only the window hears
|
|
1385
|
-
// that, so only the window can end it.
|
|
1707
|
+
// that, so only the window can end it. The grid's own window, not the
|
|
1708
|
+
// global one: rendered through a portal into a window opened with
|
|
1709
|
+
// `window.open`, the global is the opener and never hears the release.
|
|
1386
1710
|
useEffect(() => {
|
|
1387
1711
|
if (!isDraggingRange) return;
|
|
1388
1712
|
const stop = () => setIsDraggingRange(false);
|
|
1389
|
-
|
|
1390
|
-
|
|
1713
|
+
const view = gridElementRef.current?.ownerDocument.defaultView ?? window;
|
|
1714
|
+
view.addEventListener("mouseup", stop);
|
|
1715
|
+
return () => view.removeEventListener("mouseup", stop);
|
|
1391
1716
|
}, [isDraggingRange]);
|
|
1392
1717
|
|
|
1393
1718
|
const startRangeDrag = (
|
|
@@ -1401,12 +1726,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1401
1726
|
if (!extend) setIsDraggingRange(true);
|
|
1402
1727
|
};
|
|
1403
1728
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1729
|
+
/** The export item: the rectangle, in the grid's export format. */
|
|
1730
|
+
const exportSelection = () => {
|
|
1731
|
+
const data = buildSelectionData();
|
|
1732
|
+
if (data === null || data.columnIds.length === 0) return;
|
|
1733
|
+
void writeExportFile(data, {
|
|
1734
|
+
...exportOptions,
|
|
1735
|
+
includeHeaders: exportIncludesHeaders,
|
|
1410
1736
|
});
|
|
1411
1737
|
};
|
|
1412
1738
|
|
|
@@ -1456,9 +1782,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1456
1782
|
const row = rows.find((candidate) => candidate.id === rowId);
|
|
1457
1783
|
if (row === undefined) return null;
|
|
1458
1784
|
const cells = [
|
|
1459
|
-
...row.
|
|
1785
|
+
...row.getStartVisibleCells(),
|
|
1460
1786
|
...row.getCenterVisibleCells(),
|
|
1461
|
-
...row.
|
|
1787
|
+
...row.getEndVisibleCells(),
|
|
1462
1788
|
];
|
|
1463
1789
|
return (
|
|
1464
1790
|
cells.find((cell) => edit.canEditCell(row, cell.column))?.column.id ??
|
|
@@ -1531,7 +1857,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1531
1857
|
close: TMDataGridCellEditorClose,
|
|
1532
1858
|
) => {
|
|
1533
1859
|
// Where the keyboard goes: Enter moves down, Tab to the next editable
|
|
1534
|
-
// cell (
|
|
1860
|
+
// cell (cellConfirm's deferring variants move the same way, draft kept) -
|
|
1535
1861
|
// everything else, and a saved row, goes back to where the edit was.
|
|
1536
1862
|
const move =
|
|
1537
1863
|
close.via === "enter"
|
|
@@ -1623,14 +1949,109 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1623
1949
|
const handleGridKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
1624
1950
|
const target = event.target as HTMLElement;
|
|
1625
1951
|
|
|
1626
|
-
//
|
|
1627
|
-
//
|
|
1952
|
+
// The body cell the key was pressed in, whether the focus is on the cell
|
|
1953
|
+
// itself or on a control inside it. `null` for a header control or a tab
|
|
1954
|
+
// guard, neither of which is part of cell navigation.
|
|
1955
|
+
const keyCellElement =
|
|
1956
|
+
target.dataset.cell === "true"
|
|
1957
|
+
? target
|
|
1958
|
+
: target.closest<HTMLElement>('[data-cell="true"]');
|
|
1959
|
+
if (keyCellElement === null) return;
|
|
1960
|
+
|
|
1961
|
+
if (event.key === "Tab") {
|
|
1962
|
+
// Inside a row's controls the row is a form, and Tab walks it the way
|
|
1963
|
+
// the browser walks any form: through the row's open editors, the
|
|
1964
|
+
// buttons in its cells and, on an open row, the lane's save and cancel.
|
|
1965
|
+
// That part is left to the browser. What the grid decides is the two
|
|
1966
|
+
// ends: past the row's last control the cursor moves to the next row's
|
|
1967
|
+
// first cell, and before its first control to the previous row's last -
|
|
1968
|
+
// the cell is put on the cursor and left alone, whatever it holds. A cell
|
|
1969
|
+
// editor never gets here, it claims Tab itself to commit and move on.
|
|
1970
|
+
if (target.dataset.cell !== "true") {
|
|
1971
|
+
const rowElement = keyCellElement.closest<HTMLElement>(
|
|
1972
|
+
'[data-dg-part="row"]',
|
|
1973
|
+
);
|
|
1974
|
+
const tabbables =
|
|
1975
|
+
rowElement === null
|
|
1976
|
+
? []
|
|
1977
|
+
: Array.from(
|
|
1978
|
+
rowElement.querySelectorAll<HTMLElement>(FOCUSABLE_IN_CELL),
|
|
1979
|
+
).filter((element) => element.tabIndex >= 0);
|
|
1980
|
+
const index = tabbables.indexOf(target);
|
|
1981
|
+
// A control the walk cannot place is left to the browser rather than
|
|
1982
|
+
// guessed about.
|
|
1983
|
+
if (index < 0) return;
|
|
1984
|
+
// The step itself is taken here rather than left to the browser: a
|
|
1985
|
+
// select editor keeps its option list in the row, and the browser's
|
|
1986
|
+
// Tab lands on that scrollable list, which the editor then closes
|
|
1987
|
+
// under the focus. Walking the controls, and only the controls, is
|
|
1988
|
+
// what the browser would do if the list were not there.
|
|
1989
|
+
const nextControl = tabbables[index + (event.shiftKey ? -1 : 1)];
|
|
1990
|
+
if (nextControl !== undefined) {
|
|
1991
|
+
event.preventDefault();
|
|
1992
|
+
nextControl.focus();
|
|
1993
|
+
// What Tab does on a text field, and what the caret otherwise loses.
|
|
1994
|
+
if (isInputElement(nextControl)) nextControl.select();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const rowIndex = rows.findIndex(
|
|
1998
|
+
(row) => row.id === keyCellElement.dataset.rowId,
|
|
1999
|
+
);
|
|
2000
|
+
const nextRowIndex = rowIndex + (event.shiftKey ? -1 : 1);
|
|
2001
|
+
const nextRow = rowIndex < 0 ? undefined : rows[nextRowIndex];
|
|
2002
|
+
const nextColumn = event.shiftKey
|
|
2003
|
+
? orderedColumns[orderedColumns.length - 1]
|
|
2004
|
+
: orderedColumns[0];
|
|
2005
|
+
if (nextRow !== undefined && nextColumn !== undefined) {
|
|
2006
|
+
event.preventDefault();
|
|
2007
|
+
moveFocusedCell({
|
|
2008
|
+
rowIndex: nextRowIndex,
|
|
2009
|
+
columnIndex: event.shiftKey ? orderedColumns.length - 1 : 0,
|
|
2010
|
+
});
|
|
2011
|
+
// The focus-following effect stands down while the focus sits in an
|
|
2012
|
+
// editor, which is exactly where it sits now: move it by hand. The
|
|
2013
|
+
// adjacent row is mounted whenever there is any overscan; without
|
|
2014
|
+
// one, letting go of the editor is what lets the effect take over.
|
|
2015
|
+
const container = scrollContainerRef.current;
|
|
2016
|
+
const nextCell =
|
|
2017
|
+
container === null
|
|
2018
|
+
? null
|
|
2019
|
+
: findCellElement(container, {
|
|
2020
|
+
rowId: nextRow.id,
|
|
2021
|
+
columnId: nextColumn.id,
|
|
2022
|
+
});
|
|
2023
|
+
if (nextCell !== null) nextCell.focus({ preventScroll: true });
|
|
2024
|
+
else target.blur();
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
// No row past this one: the body is left, below.
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Tab leaves the body in one hop, from a cell or from the last control
|
|
2031
|
+
// of the last row. Not prevented: the focus is moved onto the guard past
|
|
2032
|
+
// the edge being left and the browser's own Tab then runs from there,
|
|
2033
|
+
// which is what puts it on the next thing after the grid - or, going
|
|
2034
|
+
// back, on the last header control, since the leading guard sits below
|
|
2035
|
+
// them.
|
|
2036
|
+
const guard = event.shiftKey
|
|
2037
|
+
? leadingGuardRef.current
|
|
2038
|
+
: trailingGuardRef.current;
|
|
2039
|
+
if (guard === null) return;
|
|
2040
|
+
guardHopRef.current = true;
|
|
2041
|
+
guard.focus({ preventScroll: true });
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// An open editor owns the rest of its keys; the cell editor stops the
|
|
2046
|
+
// ones it acts on before they get here, a row-shaped one lets them pass.
|
|
2047
|
+
if (target.closest('[data-dg-part="editor"]') !== null) return;
|
|
2048
|
+
|
|
2049
|
+
// Focus is on something inside a cell - a checkbox, a link. Its keys are
|
|
2050
|
+
// its own; the one the grid still answers is the way back out.
|
|
1628
2051
|
if (target.dataset.cell !== "true") {
|
|
1629
2052
|
if (event.key !== "Escape") return;
|
|
1630
|
-
const cellElement = target.closest<HTMLElement>('[data-cell="true"]');
|
|
1631
|
-
if (cellElement === null) return;
|
|
1632
2053
|
event.preventDefault();
|
|
1633
|
-
|
|
2054
|
+
keyCellElement.focus();
|
|
1634
2055
|
return;
|
|
1635
2056
|
}
|
|
1636
2057
|
|
|
@@ -1725,9 +2146,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1725
2146
|
// Space selects the row, from any of its cells.
|
|
1726
2147
|
//
|
|
1727
2148
|
// Not routed through `handleRowActivate`, which is the *click* path and only
|
|
1728
|
-
// selects in `"row"` mode:
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
2149
|
+
// selects in `"row"` mode: the checkbox is not in the page's tab order, so
|
|
2150
|
+
// this is the keyboard's way to a selection under every mode that has one -
|
|
2151
|
+
// checkbox included.
|
|
1731
2152
|
if (event.key === " ") {
|
|
1732
2153
|
event.preventDefault();
|
|
1733
2154
|
const row = rows[rowIndex];
|
|
@@ -1785,12 +2206,27 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1785
2206
|
return;
|
|
1786
2207
|
}
|
|
1787
2208
|
|
|
2209
|
+
// The grid's own document, not the global one: in a window opened with
|
|
2210
|
+
// `window.open` the global document is the opener's, and its focus is
|
|
2211
|
+
// never in this grid.
|
|
2212
|
+
const activeElement = container.ownerDocument.activeElement;
|
|
2213
|
+
|
|
1788
2214
|
// Never take the focus from elsewhere on the page. A consumer moving the
|
|
1789
2215
|
// cell while the user is typing in a form somewhere means "move the ring",
|
|
1790
2216
|
// not "move the caret" - the scroll above already did the visible part.
|
|
2217
|
+
if (!wantsCellFocusRef.current && !container.contains(activeElement)) {
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
// Mid-hop: Tab parked the focus on a guard and the browser is about to
|
|
2221
|
+
// carry it out of the grid. This effect can run in between - a discrete
|
|
2222
|
+
// event flushes its renders before the default action - and pulling the
|
|
2223
|
+
// focus back onto the cell here would send the browser's Tab off from
|
|
2224
|
+
// the cell instead. A guard the user *arrived* on has already asked for
|
|
2225
|
+
// the cell, and that intent is honoured.
|
|
1791
2226
|
if (
|
|
1792
2227
|
!wantsCellFocusRef.current &&
|
|
1793
|
-
|
|
2228
|
+
activeElement !== null &&
|
|
2229
|
+
activeElement.closest('[data-dg-part="tab-guard"]') !== null
|
|
1794
2230
|
) {
|
|
1795
2231
|
return;
|
|
1796
2232
|
}
|
|
@@ -1801,7 +2237,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1801
2237
|
wantsCellFocusRef.current = false;
|
|
1802
2238
|
// Focus is already inside - on the control Enter stepped into. The cell is
|
|
1803
2239
|
// where the ring belongs, so there is nothing to move.
|
|
1804
|
-
if (cellElement.contains(
|
|
2240
|
+
if (cellElement.contains(activeElement)) return;
|
|
1805
2241
|
// An open editor outranks the ring, wherever it is. Row mode opens a whole
|
|
1806
2242
|
// row from one gesture, so the caret legitimately sits in a cell other
|
|
1807
2243
|
// than the one that gesture landed the ring on - the lane's pencil is the
|
|
@@ -1809,8 +2245,8 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1809
2245
|
// the caret back onto that cell would leave the row open with the caret in
|
|
1810
2246
|
// none of its editors, and the user with nothing to type into.
|
|
1811
2247
|
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
2248
|
+
activeElement !== null &&
|
|
2249
|
+
activeElement.closest('[data-dg-part="editor"]') !== null
|
|
1814
2250
|
) {
|
|
1815
2251
|
return;
|
|
1816
2252
|
}
|
|
@@ -1909,9 +2345,9 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1909
2345
|
</Menu.Item>
|
|
1910
2346
|
<Menu.Item
|
|
1911
2347
|
disabled={selectedDataColumnCount === 0}
|
|
1912
|
-
onClick={
|
|
2348
|
+
onClick={exportSelection}
|
|
1913
2349
|
>
|
|
1914
|
-
{labels.
|
|
2350
|
+
{labels.exportCells}
|
|
1915
2351
|
</Menu.Item>
|
|
1916
2352
|
{/* Stays open on click: it is the setting the item above reads, and
|
|
1917
2353
|
reopening the menu to change your mind about headers is a worse
|
|
@@ -2032,7 +2468,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2032
2468
|
: undefined;
|
|
2033
2469
|
|
|
2034
2470
|
return (
|
|
2035
|
-
<div
|
|
2471
|
+
<div
|
|
2472
|
+
ref={tableWrapperRef}
|
|
2473
|
+
className={classes.tableWrapper}
|
|
2474
|
+
data-dg-filter-sidebar={filters.surface === "sidebar" || undefined}
|
|
2475
|
+
>
|
|
2036
2476
|
<div
|
|
2037
2477
|
ref={scrollContainerRef}
|
|
2038
2478
|
className={classes.scrollContainer}
|
|
@@ -2070,7 +2510,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2070
2510
|
aria-rowcount={
|
|
2071
2511
|
rows.length +
|
|
2072
2512
|
pinnedRowCount +
|
|
2073
|
-
|
|
2513
|
+
headerRowCount +
|
|
2074
2514
|
(hasSummaryRow ? 1 : 0)
|
|
2075
2515
|
}
|
|
2076
2516
|
aria-colcount={orderedColumns.length}
|
|
@@ -2090,11 +2530,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2090
2530
|
data-dg-header-row
|
|
2091
2531
|
// The scrolled-under shadow belongs to the boundary between
|
|
2092
2532
|
// header and body - the last header row, not every stacked
|
|
2093
|
-
// group row above it
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2533
|
+
// group row above it, and not this one at all when the filter
|
|
2534
|
+
// row is below it. See sticky.module.css.
|
|
2535
|
+
className={[
|
|
2536
|
+
classes.headerRow,
|
|
2537
|
+
!hasHeaderFilterRow && groupIndex === headerGroups.length - 1
|
|
2538
|
+
? sticky.headerBoundary
|
|
2539
|
+
: "",
|
|
2540
|
+
]
|
|
2541
|
+
.filter(Boolean)
|
|
2542
|
+
.join(" ")}
|
|
2098
2543
|
>
|
|
2099
2544
|
{headerGroup.headers.map((header) => (
|
|
2100
2545
|
<TMDataGridHeaderCell
|
|
@@ -2106,8 +2551,34 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2106
2551
|
))}
|
|
2107
2552
|
</div>
|
|
2108
2553
|
))}
|
|
2554
|
+
|
|
2555
|
+
{hasHeaderFilterRow && (
|
|
2556
|
+
<TMDataGridHeaderFilterRow
|
|
2557
|
+
leafHeaders={leafHeaders}
|
|
2558
|
+
layoutFor={layoutFor}
|
|
2559
|
+
ariaRowIndex={headerRowCount}
|
|
2560
|
+
/>
|
|
2561
|
+
)}
|
|
2109
2562
|
</div>
|
|
2110
2563
|
|
|
2564
|
+
{/* The leading tab guard, below the header controls and above the
|
|
2565
|
+
first body row - see `handleGuardFocus`. Not `aria-hidden`: it
|
|
2566
|
+
is focusable, and a focusable node hidden from the tree is what
|
|
2567
|
+
screen readers have no way to describe.
|
|
2568
|
+
Out of the tab order while there is no cell to hand focus to,
|
|
2569
|
+
so an empty body is not two dead stops. */}
|
|
2570
|
+
{cellSelection && (
|
|
2571
|
+
<div
|
|
2572
|
+
ref={leadingGuardRef}
|
|
2573
|
+
tabIndex={tabStopCell === null ? -1 : 0}
|
|
2574
|
+
role="presentation"
|
|
2575
|
+
data-dg-part="tab-guard"
|
|
2576
|
+
data-guard="leading"
|
|
2577
|
+
className={classes.tabGuard}
|
|
2578
|
+
onFocus={handleGuardFocus}
|
|
2579
|
+
/>
|
|
2580
|
+
)}
|
|
2581
|
+
|
|
2111
2582
|
{/* The entry block - new rows being typed, stuck under the header.
|
|
2112
2583
|
Present only while `edit.addRow()` has open entries. */}
|
|
2113
2584
|
{features.editing && newRowCount > 0 && (
|
|
@@ -2146,17 +2617,27 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2146
2617
|
const isGroupRow = row.getIsGrouped();
|
|
2147
2618
|
const isInteractive = rowGesturesFor(row);
|
|
2148
2619
|
const rowCells = [
|
|
2149
|
-
...row.
|
|
2620
|
+
...row.getStartVisibleCells(),
|
|
2150
2621
|
...row.getCenterVisibleCells(),
|
|
2151
|
-
...row.
|
|
2622
|
+
...row.getEndVisibleCells(),
|
|
2152
2623
|
];
|
|
2153
2624
|
// Row mode opens every editable cell of the row at once, and
|
|
2154
2625
|
// rows accumulate: a second row opening leaves the first one
|
|
2155
2626
|
// open, so every row holding a form shows its editors. Which of
|
|
2156
2627
|
// them holds the caret is not decided here - see
|
|
2157
2628
|
// placeEditCaret, which places it once per gesture.
|
|
2629
|
+
//
|
|
2630
|
+
// A parked row is not one of them. It keeps its form, so it is
|
|
2631
|
+
// still "open", but it has been decided: it renders its draft
|
|
2632
|
+
// through the column's own renderer until `begin` reopens it.
|
|
2633
|
+
// A committed entry row in the body has a form too - every
|
|
2634
|
+
// entry row does - but it is a value row until reopened, and
|
|
2635
|
+
// reopening takes it back to the entry block.
|
|
2158
2636
|
const rowEditing =
|
|
2159
|
-
features.editMode === "row" &&
|
|
2637
|
+
features.editMode === "row" &&
|
|
2638
|
+
editOpenRowIdSet.has(row.id) &&
|
|
2639
|
+
!editDraftRowIdSet.has(row.id) &&
|
|
2640
|
+
!newRowIdSet.has(row.id);
|
|
2160
2641
|
// Cell selection takes the body's tab stop off the row and puts
|
|
2161
2642
|
// it on a cell - two stops per row would make Tab a way of
|
|
2162
2643
|
// walking the grid, which is what the arrow keys are for. Space
|
|
@@ -2229,10 +2710,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2229
2710
|
}
|
|
2230
2711
|
// Carrying a dirty draft - the row-level face of the cells'
|
|
2231
2712
|
// own data-dirty markers, for row-scoped styling.
|
|
2232
|
-
data-dirty={
|
|
2233
|
-
|
|
2234
|
-
|
|
2713
|
+
data-dirty={editDirtyRowIdSet.has(row.id)}
|
|
2714
|
+
// Committed into the draft store, waiting for Save. A
|
|
2715
|
+
// committed entry row is one too - it is in the body
|
|
2716
|
+
// because it is committed - and carries `data-new` besides.
|
|
2717
|
+
data-draft={
|
|
2718
|
+
editDraftRowIdSet.has(row.id) || newRowIdSet.has(row.id)
|
|
2235
2719
|
}
|
|
2720
|
+
// An entered row not yet in `data`, committed into the draft
|
|
2721
|
+
// store and sorted, filtered and grouped with the rest.
|
|
2722
|
+
data-new={newRowIdSet.has(row.id)}
|
|
2236
2723
|
// The menu is anchored to the rowgroup, so Mantine's own
|
|
2237
2724
|
// `data-expanded` lands there rather than on a row. This is
|
|
2238
2725
|
// what says which row the open menu is about.
|
|
@@ -2385,13 +2872,10 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2385
2872
|
row.id,
|
|
2386
2873
|
cell.column.id,
|
|
2387
2874
|
),
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
)
|
|
2393
|
-
? 0
|
|
2394
|
-
: -1,
|
|
2875
|
+
// Focusable by click and by the grid, never by
|
|
2876
|
+
// Tab - the guards are the way in. See
|
|
2877
|
+
// `tabStopCell`.
|
|
2878
|
+
tabIndex: -1,
|
|
2395
2879
|
// "Selected" means "in the block Ctrl+C would
|
|
2396
2880
|
// take" - which under `"single"` is the one cell
|
|
2397
2881
|
// the user is standing on.
|
|
@@ -2417,6 +2901,24 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2417
2901
|
// Right-click is the menu's; the middle button is
|
|
2418
2902
|
// the browser's.
|
|
2419
2903
|
if (event.button !== 0) return;
|
|
2904
|
+
// Pressing a control in the cell - a checkbox, a
|
|
2905
|
+
// chevron, a button a renderer put there - is not
|
|
2906
|
+
// a selection gesture, and a block the user built
|
|
2907
|
+
// should not pay for it. The focus still follows
|
|
2908
|
+
// the press, through the bubbling `onFocus`, so
|
|
2909
|
+
// the cursor ends up on the row acted on; only
|
|
2910
|
+
// the range survives.
|
|
2911
|
+
const pressed = event.target as HTMLElement;
|
|
2912
|
+
const control =
|
|
2913
|
+
pressed === event.currentTarget
|
|
2914
|
+
? null
|
|
2915
|
+
: pressed.closest(FOCUSABLE_IN_CELL);
|
|
2916
|
+
if (
|
|
2917
|
+
control !== null &&
|
|
2918
|
+
event.currentTarget.contains(control)
|
|
2919
|
+
) {
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2420
2922
|
// Shift+click would otherwise smear the browser's
|
|
2421
2923
|
// own text selection across everything between
|
|
2422
2924
|
// the two cells.
|
|
@@ -2504,7 +3006,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2504
3006
|
renderBodyRow(
|
|
2505
3007
|
row,
|
|
2506
3008
|
-1,
|
|
2507
|
-
|
|
3009
|
+
headerRowCount + index + 1,
|
|
2508
3010
|
"top",
|
|
2509
3011
|
),
|
|
2510
3012
|
)}
|
|
@@ -2525,7 +3027,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2525
3027
|
row,
|
|
2526
3028
|
virtualItem.index,
|
|
2527
3029
|
virtualItem.index +
|
|
2528
|
-
|
|
3030
|
+
headerRowCount +
|
|
2529
3031
|
pinnedTopRows.length +
|
|
2530
3032
|
1,
|
|
2531
3033
|
);
|
|
@@ -2548,7 +3050,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2548
3050
|
renderBodyRow(
|
|
2549
3051
|
row,
|
|
2550
3052
|
-1,
|
|
2551
|
-
|
|
3053
|
+
headerRowCount +
|
|
2552
3054
|
pinnedTopRows.length +
|
|
2553
3055
|
rows.length +
|
|
2554
3056
|
index +
|
|
@@ -2604,7 +3106,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2604
3106
|
<div
|
|
2605
3107
|
role="row"
|
|
2606
3108
|
aria-rowindex={
|
|
2607
|
-
rows.length + pinnedRowCount +
|
|
3109
|
+
rows.length + pinnedRowCount + headerRowCount + 1
|
|
2608
3110
|
}
|
|
2609
3111
|
// Also what the height measurement above looks for. One
|
|
2610
3112
|
// attribute serves both now that the part name *is* the
|
|
@@ -2657,10 +3159,30 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2657
3159
|
})}
|
|
2658
3160
|
</div>
|
|
2659
3161
|
)}
|
|
3162
|
+
|
|
3163
|
+
{/* The trailing tab guard, last child of the grid so that Shift+Tab
|
|
3164
|
+
from below the grid meets it before any cell. */}
|
|
3165
|
+
{cellSelection && (
|
|
3166
|
+
<div
|
|
3167
|
+
ref={trailingGuardRef}
|
|
3168
|
+
tabIndex={tabStopCell === null ? -1 : 0}
|
|
3169
|
+
role="presentation"
|
|
3170
|
+
data-dg-part="tab-guard"
|
|
3171
|
+
data-guard="trailing"
|
|
3172
|
+
className={classes.tabGuard}
|
|
3173
|
+
onFocus={handleGuardFocus}
|
|
3174
|
+
/>
|
|
3175
|
+
)}
|
|
2660
3176
|
</div>
|
|
2661
3177
|
</div>
|
|
2662
3178
|
|
|
2663
|
-
|
|
3179
|
+
{/* The automatic filter surfaces. The popup floats over the first body
|
|
3180
|
+
rows, anchored to the wrapper; the sidebar is a second column of it,
|
|
3181
|
+
which is why the wrapper turns into a flex row for one. Under
|
|
3182
|
+
`filters.surface: "none"` neither renders and the consumer's own
|
|
3183
|
+
`TMDataGrid.FilterPanel` is the only panel on the page. */}
|
|
3184
|
+
{filters.surface === "popup" && <TMDataGridFilterPopup />}
|
|
3185
|
+
{filters.surface === "sidebar" && <TMDataGridFilterSidebar />}
|
|
2664
3186
|
</div>
|
|
2665
3187
|
);
|
|
2666
3188
|
}
|