@jielga/tmdatagrid 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1337 -94
- package/dist/index.js +2812 -1189
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +61 -11
- package/skills/getting-started/SKILL.md +41 -6
- package/skills/options/SKILL.md +12 -2
- package/skills/server-side/SKILL.md +1 -1
- package/src/tmdatagrid/TMDataGridContext.ts +26 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +21 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +29 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +12 -6
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +192 -37
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +136 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +63 -43
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +159 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +249 -44
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +103 -16
- package/src/tmdatagrid/components/TMDataGridTable.module.css +160 -5
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1380 -43
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +33 -7
- package/src/tmdatagrid/components/icons.ts +6 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +122 -11
- package/src/tmdatagrid/core/cellExport.ts +321 -0
- package/src/tmdatagrid/core/cellNavigation.ts +103 -0
- package/src/tmdatagrid/core/cellRange.ts +118 -0
- package/src/tmdatagrid/core/columnOptions.ts +123 -0
- package/src/tmdatagrid/core/columnUtils.ts +18 -0
- package/src/tmdatagrid/core/expanding.ts +93 -0
- package/src/tmdatagrid/core/filterOperators.ts +198 -12
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/persistence.ts +12 -0
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +96 -2
- package/src/tmdatagrid/useTMDataGrid.tsx +558 -37
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionIcon, Popover, Text, Tooltip } from "@mantine/core";
|
|
1
|
+
import { ActionIcon, Loader, Popover, Text, Tooltip } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import classes from "./TMDataGridToolbar.module.css";
|
|
@@ -19,6 +19,29 @@ export function TMDataGridToolbarSpacer() {
|
|
|
19
19
|
return <div className={classes.toolbarSpacer} />;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A small spinner shown while `meta.loading` is true, and nothing otherwise.
|
|
24
|
+
*
|
|
25
|
+
* The body only shows its loading state while the grid is *empty* — a
|
|
26
|
+
* server-driven grid refetching with rows on screen keeps showing them, which
|
|
27
|
+
* is right, but leaves nothing saying a fetch is running. This is that signal,
|
|
28
|
+
* and the consumer decides where it sits by placing it in the toolbar:
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <TMDataGrid.Toolbar>
|
|
32
|
+
* <TMDataGrid.SummaryCount />
|
|
33
|
+
* <TMDataGrid.Spacer />
|
|
34
|
+
* <TMDataGrid.LoadingIndicator />
|
|
35
|
+
* <TMDataGrid.FilterButton />
|
|
36
|
+
* </TMDataGrid.Toolbar>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function TMDataGridLoadingIndicator() {
|
|
40
|
+
const { table, labels } = useTMDataGridContext();
|
|
41
|
+
if (table.options.meta?.loading !== true) return null;
|
|
42
|
+
return <Loader size="xs" aria-label={labels.loading} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Visible rows over total rows. On a server-driven grid, set
|
|
24
47
|
* `meta.totalRowCount` for the denominator — the client never sees every row.
|
|
@@ -52,7 +75,7 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
|
52
75
|
* Renders nothing when no column can be hidden (`enableHiding: false`).
|
|
53
76
|
*/
|
|
54
77
|
export function TMDataGridColumnsButton() {
|
|
55
|
-
const { table, ui, features, controlSize } = useTMDataGridContext();
|
|
78
|
+
const { table, ui, features, labels, controlSize } = useTMDataGridContext();
|
|
56
79
|
const opened = useSelector(ui, (state) => state.columnsPanelOpen);
|
|
57
80
|
|
|
58
81
|
if (!getGridCapabilities(table, features).canHideAny) return null;
|
|
@@ -68,12 +91,12 @@ export function TMDataGridColumnsButton() {
|
|
|
68
91
|
trapFocus
|
|
69
92
|
>
|
|
70
93
|
<Popover.Target>
|
|
71
|
-
<Tooltip label=
|
|
94
|
+
<Tooltip label={labels.manageColumns} openDelay={400}>
|
|
72
95
|
<ActionIcon
|
|
73
96
|
variant="subtle"
|
|
74
97
|
color="gray"
|
|
75
98
|
size={controlSize}
|
|
76
|
-
aria-label=
|
|
99
|
+
aria-label={labels.manageColumns}
|
|
77
100
|
onClick={ui.actions.toggleColumnsPanel}
|
|
78
101
|
>
|
|
79
102
|
<BurgerIcon size={18} stroke={1.6} />
|
|
@@ -93,7 +116,7 @@ export function TMDataGridColumnsButton() {
|
|
|
93
116
|
*/
|
|
94
117
|
export function TMDataGridFilterButton() {
|
|
95
118
|
const api = useTMDataGridContext();
|
|
96
|
-
const { table, ui, features, controlSize } = api;
|
|
119
|
+
const { table, ui, features, labels, controlSize } = api;
|
|
97
120
|
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
98
121
|
const columnFilters = useSelector(
|
|
99
122
|
table.store,
|
|
@@ -106,12 +129,15 @@ export function TMDataGridFilterButton() {
|
|
|
106
129
|
if (!getGridCapabilities(table, features).canFilterAny) return null;
|
|
107
130
|
|
|
108
131
|
return (
|
|
109
|
-
<Tooltip label=
|
|
132
|
+
<Tooltip label={labels.filters} openDelay={400}>
|
|
110
133
|
<ActionIcon
|
|
111
134
|
variant={opened || activeCount > 0 ? "light" : "subtle"}
|
|
112
135
|
color={activeCount > 0 ? undefined : "gray"}
|
|
113
136
|
size={controlSize}
|
|
114
|
-
aria-label=
|
|
137
|
+
aria-label={labels.filters}
|
|
138
|
+
// Marks this as the panel's own toggle, so the panel's click-away
|
|
139
|
+
// handler leaves it alone and the button stays a toggle.
|
|
140
|
+
data-dg-filter-toggle
|
|
115
141
|
onClick={() => {
|
|
116
142
|
if (opened) {
|
|
117
143
|
ui.actions.closeFilterPanel();
|
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
* `sideEffects: false`, so only the icons re-exported here reach the bundle.
|
|
7
7
|
*/
|
|
8
8
|
export {
|
|
9
|
+
IconArrowAutofitWidth as AutosizeIcon,
|
|
9
10
|
IconArrowDown as ArrowDownIcon,
|
|
10
11
|
IconArrowLeft as MoveLeftIcon,
|
|
11
12
|
IconArrowRight as MoveRightIcon,
|
|
12
13
|
IconArrowUp as ArrowUpIcon,
|
|
14
|
+
IconChevronDown as ChevronDownIcon,
|
|
13
15
|
IconChevronLeft as ChevronLeftIcon,
|
|
14
16
|
IconChevronRight as ChevronRightIcon,
|
|
15
17
|
IconColumns3 as ColumnsIcon,
|
|
16
18
|
IconDotsVertical as DotsVerticalIcon,
|
|
17
19
|
IconEyeOff as EyeOffIcon,
|
|
18
20
|
IconFilter as FilterIcon,
|
|
21
|
+
IconFoldDown as ExpandAllIcon,
|
|
22
|
+
IconFoldUp as CollapseAllIcon,
|
|
23
|
+
IconIndentDecrease as UngroupIcon,
|
|
24
|
+
IconIndentIncrease as GroupIcon,
|
|
19
25
|
IconArrowBarToLeft as PinLeftIcon,
|
|
20
26
|
IconArrowBarToRight as PinRightIcon,
|
|
21
27
|
IconMenu2 as BurgerIcon,
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
/* Boundary of a pinned column group. Shared, because the edge is cast by both
|
|
2
|
-
the header cell and every body cell in the same column — the two have to
|
|
3
|
-
|
|
2
|
+
the header cell and every body cell in the same column — the two have to draw
|
|
3
|
+
an identical band or the seam shows at the header.
|
|
4
4
|
|
|
5
5
|
A box-shadow is drawn per cell, and its blur fades in at each cell's own top
|
|
6
6
|
and bottom edge — stacked down a column that reads as banding at every row
|
|
7
|
-
seam. Instead: a
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.stickyLeft {
|
|
12
|
-
border-right: 1px solid var(--mantine-color-default-border);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.stickyRight {
|
|
16
|
-
border-left: 1px solid var(--mantine-color-default-border);
|
|
17
|
-
}
|
|
7
|
+
seam. Instead: a gradient pseudo element pinned to `top: 0; bottom: 0` so
|
|
8
|
+
consecutive cells butt together into one unbroken band. Mantine sets
|
|
9
|
+
`color-scheme` on the root, so light-dark() resolves without a second
|
|
10
|
+
selector.
|
|
18
11
|
|
|
12
|
+
No border. A 1px line the colour of the grid's own borders drew a hard rule
|
|
13
|
+
between the pinned lanes and the data, so the pinned columns read as a
|
|
14
|
+
separate table rather than as part of this one — and at the system lanes,
|
|
15
|
+
which are only a control wide, it framed them like a sidebar. */
|
|
19
16
|
.stickyLeft::after,
|
|
20
17
|
.stickyRight::after {
|
|
21
18
|
content: "";
|
|
22
19
|
position: absolute;
|
|
23
20
|
top: 0;
|
|
24
21
|
bottom: 0;
|
|
25
|
-
width:
|
|
22
|
+
width: 8px;
|
|
26
23
|
pointer-events: none;
|
|
27
24
|
}
|
|
28
25
|
|
|
26
|
+
/* Three stops rather than two. A straight linear ramp to `transparent` fades
|
|
27
|
+
fastest at the start, so its own outer edge stays visible as a faint line —
|
|
28
|
+
the sharpness a hard border was blamed for. Front-loading the falloff puts
|
|
29
|
+
the density against the pinned cell and lets the tail reach zero unnoticed. */
|
|
29
30
|
.stickyLeft::after {
|
|
30
31
|
left: 100%;
|
|
31
32
|
background: linear-gradient(
|
|
32
33
|
to right,
|
|
33
|
-
light-dark(rgba(0, 0, 0, 0.
|
|
34
|
+
light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
|
|
35
|
+
light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
|
|
34
36
|
transparent
|
|
35
37
|
);
|
|
36
38
|
}
|
|
@@ -39,7 +41,66 @@
|
|
|
39
41
|
right: 100%;
|
|
40
42
|
background: linear-gradient(
|
|
41
43
|
to left,
|
|
42
|
-
light-dark(rgba(0, 0, 0, 0.
|
|
44
|
+
light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.26)),
|
|
45
|
+
light-dark(rgba(0, 0, 0, 0.025), rgba(0, 0, 0, 0.09)) 45%,
|
|
43
46
|
transparent
|
|
44
47
|
);
|
|
45
48
|
}
|
|
49
|
+
|
|
50
|
+
/* The band says "there is more table under this lane". So it should only be
|
|
51
|
+
there while that is true: nothing is hidden at either end of the scroll, and
|
|
52
|
+
nothing at all is hidden by a grid narrow enough not to scroll.
|
|
53
|
+
|
|
54
|
+
Driven by the scroll position itself rather than by a listener writing a
|
|
55
|
+
class. A scroll-driven animation is evaluated on the compositor, so the band
|
|
56
|
+
tracks the scroll frame for frame without a single React render — and the
|
|
57
|
+
body re-renders on every scroll frame already, from virtualization.
|
|
58
|
+
|
|
59
|
+
`scroll(nearest inline)` resolves to `.scrollContainer`, the one horizontal
|
|
60
|
+
scroller above these cells. An inactive timeline — which is what a container
|
|
61
|
+
with nothing to scroll gives — leaves the base style in place, so `opacity:
|
|
62
|
+
0` inside this block is also the answer for a grid that does not scroll. */
|
|
63
|
+
@supports (animation-timeline: scroll()) {
|
|
64
|
+
.stickyLeft::after,
|
|
65
|
+
.stickyRight::after {
|
|
66
|
+
/* Distance over which the band arrives. Short enough to read as an edge
|
|
67
|
+
appearing under the lane rather than as a fade. */
|
|
68
|
+
--dg-sticky-edge-range: 20px;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
animation-timeline: scroll(nearest inline);
|
|
71
|
+
animation-timing-function: linear;
|
|
72
|
+
animation-fill-mode: both;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stickyLeft::after {
|
|
76
|
+
animation-name: stickyEdgeIn;
|
|
77
|
+
animation-range: 0 var(--dg-sticky-edge-range);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The mirror: on at rest, gone as the last column arrives. The range is
|
|
81
|
+
measured from the end, and `both` holds the first value everywhere before
|
|
82
|
+
it — so the band stays fully on until the scroller is within 20px of home.
|
|
83
|
+
*/
|
|
84
|
+
.stickyRight::after {
|
|
85
|
+
animation-name: stickyEdgeOut;
|
|
86
|
+
animation-range: calc(100% - var(--dg-sticky-edge-range)) 100%;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@keyframes stickyEdgeIn {
|
|
91
|
+
from {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
}
|
|
94
|
+
to {
|
|
95
|
+
opacity: 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@keyframes stickyEdgeOut {
|
|
100
|
+
from {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
}
|
|
103
|
+
to {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
|
+
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Room the header keeps for its hover-revealed actions (sort arrow, menu) and
|
|
7
|
+
* the resize divider, which measure zero while idle. Without it a column sized
|
|
8
|
+
* exactly to its header text puts the actions on top of the title.
|
|
9
|
+
*/
|
|
10
|
+
const HEADER_ACTIONS_ALLOWANCE = 30;
|
|
11
|
+
|
|
12
|
+
/** Breathing room so the widest value does not touch the next divider. */
|
|
13
|
+
const CONTENT_ALLOWANCE = 2;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What the content inside one cell actually spans, in px.
|
|
17
|
+
*
|
|
18
|
+
* A Range over the children rather than the span's `scrollWidth`, because the
|
|
19
|
+
* two disagree in opposite directions: a left-aligned cell stretches its
|
|
20
|
+
* content span to fill the cell (`flex: 1 1 auto`), so `scrollWidth` reports
|
|
21
|
+
* the stretched box and autosize could never *shrink* a column — while for
|
|
22
|
+
* clipped content both see past the `overflow: hidden`. The Range measures
|
|
23
|
+
* the laid-out text and elements themselves, whatever box they sit in.
|
|
24
|
+
*
|
|
25
|
+
* `scrollWidth` stays as the fallback for environments whose Ranges do not
|
|
26
|
+
* measure (jsdom reports 0 there).
|
|
27
|
+
*/
|
|
28
|
+
function contentSpanWidth(content: HTMLElement): number {
|
|
29
|
+
const range = document.createRange();
|
|
30
|
+
range.selectNodeContents(content);
|
|
31
|
+
// jsdom's Range has no getBoundingClientRect at all, hence the guard
|
|
32
|
+
// rather than a zero-check alone.
|
|
33
|
+
const width =
|
|
34
|
+
typeof range.getBoundingClientRect === "function"
|
|
35
|
+
? range.getBoundingClientRect().width
|
|
36
|
+
: 0;
|
|
37
|
+
return width > 0 ? width : content.scrollWidth;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Widest rendered content of one column, in px, measured from the DOM.
|
|
42
|
+
*
|
|
43
|
+
* Mounted cells only: under virtualization the unmounted rows do not exist to
|
|
44
|
+
* be measured, so this reads the visible window plus overscan — the same
|
|
45
|
+
* trade AG Grid's autosize makes by default.
|
|
46
|
+
*/
|
|
47
|
+
export function measureColumnContentWidth({
|
|
48
|
+
container,
|
|
49
|
+
columnId,
|
|
50
|
+
}: {
|
|
51
|
+
/** The grid's scroll container — anything enclosing the column's cells. */
|
|
52
|
+
container: HTMLElement;
|
|
53
|
+
columnId: string;
|
|
54
|
+
}): number {
|
|
55
|
+
let widest = 0;
|
|
56
|
+
|
|
57
|
+
const cells = container.querySelectorAll<HTMLElement>(
|
|
58
|
+
`[role="cell"][data-column-id="${CSS.escape(columnId)}"],` +
|
|
59
|
+
`[role="gridcell"][data-column-id="${CSS.escape(columnId)}"]`,
|
|
60
|
+
);
|
|
61
|
+
for (const cell of cells) {
|
|
62
|
+
const content = cell.firstElementChild;
|
|
63
|
+
if (!(content instanceof HTMLElement)) continue;
|
|
64
|
+
const styles = getComputedStyle(cell);
|
|
65
|
+
const padding =
|
|
66
|
+
(Number.parseFloat(styles.paddingLeft) || 0) +
|
|
67
|
+
(Number.parseFloat(styles.paddingRight) || 0);
|
|
68
|
+
widest = Math.max(
|
|
69
|
+
widest,
|
|
70
|
+
contentSpanWidth(content) + padding + CONTENT_ALLOWANCE,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const title = container.querySelector<HTMLElement>(
|
|
75
|
+
`[data-testid="dg-header-${CSS.escape(columnId)}"] [data-dg-header-title]`,
|
|
76
|
+
);
|
|
77
|
+
if (title) {
|
|
78
|
+
const header = title.closest<HTMLElement>('[role="columnheader"]');
|
|
79
|
+
const styles = header ? getComputedStyle(header) : null;
|
|
80
|
+
const padding = styles
|
|
81
|
+
? (Number.parseFloat(styles.paddingLeft) || 0) +
|
|
82
|
+
(Number.parseFloat(styles.paddingRight) || 0)
|
|
83
|
+
: 0;
|
|
84
|
+
widest = Math.max(
|
|
85
|
+
widest,
|
|
86
|
+
contentSpanWidth(title) + padding + HEADER_ACTIONS_ALLOWANCE,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return widest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sizes a column to its widest mounted content — what double-clicking the
|
|
95
|
+
* resize divider does, exported so a menu item or consumer code can trigger
|
|
96
|
+
* the same thing.
|
|
97
|
+
*
|
|
98
|
+
* The width lands in `columnSizing`, so it persists with the other widths and
|
|
99
|
+
* a later resize drag takes over from it. Clamped to the column's
|
|
100
|
+
* `minSize`/`maxSize`; a no-op (returning `false`) for a column that cannot
|
|
101
|
+
* be resized or has nothing mounted to measure.
|
|
102
|
+
*/
|
|
103
|
+
export function autosizeColumn<TData extends RowData>({
|
|
104
|
+
table,
|
|
105
|
+
columnId,
|
|
106
|
+
container,
|
|
107
|
+
}: {
|
|
108
|
+
table: TMDataGridTable<TData>;
|
|
109
|
+
columnId: string;
|
|
110
|
+
container: HTMLElement;
|
|
111
|
+
}): boolean {
|
|
112
|
+
const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
|
|
113
|
+
const column = erased.getColumn(columnId);
|
|
114
|
+
if (!column || !column.getCanResize()) return false;
|
|
115
|
+
|
|
116
|
+
const measured = measureColumnContentWidth({ container, columnId });
|
|
117
|
+
if (measured <= 0) return false;
|
|
118
|
+
|
|
119
|
+
const minSize = column.columnDef.minSize ?? 80;
|
|
120
|
+
const maxSize = column.columnDef.maxSize ?? Number.POSITIVE_INFINITY;
|
|
121
|
+
const width = Math.round(Math.min(Math.max(measured, minSize), maxSize));
|
|
122
|
+
|
|
123
|
+
erased.setColumnSizing((previous) => ({ ...previous, [columnId]: width }));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
@@ -20,28 +20,103 @@ import type {
|
|
|
20
20
|
* what applies the per-column overrides.
|
|
21
21
|
*/
|
|
22
22
|
/**
|
|
23
|
-
* How rows are selected — see `
|
|
23
|
+
* How rows are selected — see `selectionMode` on {@link UseTMDataGridOptions}.
|
|
24
|
+
*
|
|
25
|
+
* One axis, because the two things a config could set independently — which
|
|
26
|
+
* chrome selects, and what a bare row click does — cannot both be free: a click
|
|
27
|
+
* can either toggle a multi-selection or move the highlight, never both. Folding
|
|
28
|
+
* them into one value makes that conflict unrepresentable rather than something
|
|
29
|
+
* to warn about.
|
|
24
30
|
*/
|
|
25
|
-
export type
|
|
31
|
+
export type TMDataGridSelectionMode =
|
|
32
|
+
/** Checkbox column, multi-select. Clicking a row elsewhere does nothing. */
|
|
33
|
+
| "checkbox"
|
|
34
|
+
/** No checkbox column; row click multi-selects, with Ctrl/Shift modifiers. */
|
|
35
|
+
| "row"
|
|
36
|
+
/** Checkbox column multi-selects, and a row click highlights one row. */
|
|
37
|
+
| "checkboxAndHighlight"
|
|
38
|
+
/** No selection at all — a row click only highlights. Master-detail. */
|
|
39
|
+
| "highlight";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How cells are selected — see `cellSelection` on {@link UseTMDataGridOptions}.
|
|
43
|
+
*
|
|
44
|
+
* A mode rather than a pair of booleans, for the reason
|
|
45
|
+
* {@link TMDataGridSelectionMode} is one: "cells can be selected" and "more
|
|
46
|
+
* than one at a time" are not independent, and a range that cannot be anchored
|
|
47
|
+
* anywhere is not a state worth being able to write down.
|
|
48
|
+
*/
|
|
49
|
+
export type TMDataGridCellSelectionMode =
|
|
50
|
+
/** No cell cursor at all. The body's tab stop stays on the row. */
|
|
51
|
+
| "none"
|
|
52
|
+
/** One cell takes the focus and the arrow keys move it. */
|
|
53
|
+
| "single"
|
|
54
|
+
/** As `"single"`, plus a rectangle: drag, Shift+click, Shift+arrows, Ctrl+C. */
|
|
55
|
+
| "range";
|
|
26
56
|
|
|
27
57
|
export type TMDataGridFeatureFlags = {
|
|
28
58
|
sorting: boolean;
|
|
29
59
|
filtering: boolean;
|
|
60
|
+
/** Whether the quick search may filter — `enableGlobalFilter`. */
|
|
61
|
+
globalFiltering: boolean;
|
|
30
62
|
hiding: boolean;
|
|
31
63
|
pinning: boolean;
|
|
32
64
|
resizing: boolean;
|
|
33
65
|
ordering: boolean;
|
|
66
|
+
/** Whether any row selection is possible. Off outright under `"highlight"`. */
|
|
34
67
|
rowSelection: boolean;
|
|
35
|
-
/**
|
|
36
|
-
|
|
68
|
+
/** Defaults to `"checkbox"`. */
|
|
69
|
+
selectionMode: TMDataGridSelectionMode;
|
|
70
|
+
/** Whether the generated checkbox column is prepended. */
|
|
71
|
+
selectColumn: boolean;
|
|
72
|
+
/** Whether a bare row click toggles the selection. Only under `"row"`. */
|
|
73
|
+
rowClickSelects: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Whether more than one row can be selected at once — TanStack's
|
|
76
|
+
* `enableMultiRowSelection`. Off, the grid drops the select-all header
|
|
77
|
+
* checkbox: TanStack's `toggleAllRowsSelected` only consults `getCanSelect`,
|
|
78
|
+
* so that control would select every row and walk straight past the limit.
|
|
79
|
+
*
|
|
80
|
+
* A per-row predicate counts as on, since some rows may still multi-select.
|
|
81
|
+
*/
|
|
82
|
+
multiRowSelection: boolean;
|
|
37
83
|
/** Whether a selected row takes the highlight colour. Follows the mode. */
|
|
38
|
-
|
|
84
|
+
showSelectedBackground: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The single highlighted row — clicking a row highlights it, for a detail
|
|
87
|
+
* panel to follow. State of its own, not a slice of `rowSelection`, which is
|
|
88
|
+
* what lets it coexist with a checkbox multi-selection.
|
|
89
|
+
*/
|
|
90
|
+
highlightRow: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Whether cells can be selected at all. Off unless `cellSelection` asks for
|
|
93
|
+
* it — it takes the body's tab stop off the row and puts it on a cell, and
|
|
94
|
+
* reports the grid to assistive technology as a `grid` rather than a `table`,
|
|
95
|
+
* which is a different promise about what the keyboard does.
|
|
96
|
+
*/
|
|
97
|
+
cellSelection: boolean;
|
|
98
|
+
/** Defaults to `"none"`. */
|
|
99
|
+
cellSelectionMode: TMDataGridCellSelectionMode;
|
|
100
|
+
/** Whether a rectangle of cells can be selected. Only under `"range"`. */
|
|
101
|
+
cellRangeSelection: boolean;
|
|
39
102
|
/**
|
|
40
103
|
* The one default-off flag: pagination must be asked for, either with the
|
|
41
104
|
* grid's `enablePagination` or implicitly by declaring `manualPagination`.
|
|
42
105
|
* Off, the grid renders every filtered row and relies on virtualization.
|
|
43
106
|
*/
|
|
44
107
|
pagination: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Row grouping: the "Group by" items in the header menu and the generated
|
|
110
|
+
* tree column. On unless `enableGrouping: false`, since an ungrouped grid
|
|
111
|
+
* looks and behaves exactly as it did before — `grouping` starts empty, and
|
|
112
|
+
* an empty grouping state passes straight through the row model.
|
|
113
|
+
*
|
|
114
|
+
* The exception is `manualPagination`, where the client holds one page rather
|
|
115
|
+
* than the whole set: grouping that page would build groups out of an
|
|
116
|
+
* arbitrary slice and quietly show wrong counts. A server-side grid that does
|
|
117
|
+
* its own grouping can still say `enableGrouping: true` to override.
|
|
118
|
+
*/
|
|
119
|
+
grouping: boolean;
|
|
45
120
|
};
|
|
46
121
|
|
|
47
122
|
export function readFeatureFlags<TData extends RowData>(
|
|
@@ -49,32 +124,56 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
49
124
|
UseTMDataGridOptions<TData>,
|
|
50
125
|
| "enableSorting"
|
|
51
126
|
| "enableColumnFilters"
|
|
127
|
+
| "enableGlobalFilter"
|
|
52
128
|
| "enableHiding"
|
|
53
129
|
| "enableColumnPinning"
|
|
54
130
|
| "enableColumnResizing"
|
|
55
131
|
| "enableColumnOrdering"
|
|
56
132
|
| "enableRowSelection"
|
|
57
|
-
| "
|
|
58
|
-
| "
|
|
133
|
+
| "enableMultiRowSelection"
|
|
134
|
+
| "selectionMode"
|
|
135
|
+
| "showSelectedBackground"
|
|
136
|
+
| "cellSelection"
|
|
59
137
|
| "enablePagination"
|
|
60
138
|
| "manualPagination"
|
|
139
|
+
| "enableGrouping"
|
|
61
140
|
>,
|
|
62
141
|
): TMDataGridFeatureFlags {
|
|
142
|
+
const selectionMode = options.selectionMode ?? "checkbox";
|
|
143
|
+
const cellSelectionMode = options.cellSelection ?? "none";
|
|
144
|
+
// `"highlight"` is the master-detail mode: no selection to speak of, so the
|
|
145
|
+
// checkbox column and the click-to-select behaviour both fall away. Otherwise
|
|
146
|
+
// `enableRowSelection` still has the final say, including its predicate form.
|
|
147
|
+
const rowSelection =
|
|
148
|
+
selectionMode !== "highlight" && options.enableRowSelection !== false;
|
|
149
|
+
|
|
63
150
|
return {
|
|
64
151
|
sorting: options.enableSorting !== false,
|
|
65
152
|
filtering: options.enableColumnFilters !== false,
|
|
153
|
+
globalFiltering: options.enableGlobalFilter !== false,
|
|
66
154
|
hiding: options.enableHiding !== false,
|
|
67
155
|
pinning: options.enableColumnPinning !== false,
|
|
68
156
|
resizing: options.enableColumnResizing !== false,
|
|
69
157
|
ordering: options.enableColumnOrdering !== false,
|
|
70
|
-
rowSelection
|
|
71
|
-
|
|
158
|
+
rowSelection,
|
|
159
|
+
selectionMode,
|
|
160
|
+
selectColumn:
|
|
161
|
+
rowSelection &&
|
|
162
|
+
(selectionMode === "checkbox" || selectionMode === "checkboxAndHighlight"),
|
|
163
|
+
rowClickSelects: rowSelection && selectionMode === "row",
|
|
164
|
+
multiRowSelection: options.enableMultiRowSelection !== false,
|
|
72
165
|
// In "row" mode the highlight *is* the feedback for a click, so it is on.
|
|
73
166
|
// With checkboxes the box already says it, so it is off unless asked for.
|
|
74
|
-
|
|
75
|
-
options.
|
|
167
|
+
showSelectedBackground:
|
|
168
|
+
options.showSelectedBackground ?? selectionMode === "row",
|
|
169
|
+
highlightRow:
|
|
170
|
+
selectionMode === "checkboxAndHighlight" || selectionMode === "highlight",
|
|
171
|
+
cellSelection: cellSelectionMode !== "none",
|
|
172
|
+
cellSelectionMode,
|
|
173
|
+
cellRangeSelection: cellSelectionMode === "range",
|
|
76
174
|
pagination:
|
|
77
175
|
options.enablePagination === true || options.manualPagination === true,
|
|
176
|
+
grouping: options.enableGrouping ?? options.manualPagination !== true,
|
|
78
177
|
};
|
|
79
178
|
}
|
|
80
179
|
|
|
@@ -89,6 +188,7 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
89
188
|
* | Pin to left / right | `enableColumnPinning` / `enablePinning` |
|
|
90
189
|
* | Resize dragging | `enableColumnResizing` / `enableResizing` |
|
|
91
190
|
* | Header dragging, Move left / right | `enableColumnOrdering` / `meta.enableOrdering` |
|
|
191
|
+
* | Group by / Ungroup | `enableGrouping` (table or column) |
|
|
92
192
|
*/
|
|
93
193
|
export type TMDataGridColumnCapabilities = {
|
|
94
194
|
canSort: boolean;
|
|
@@ -97,6 +197,7 @@ export type TMDataGridColumnCapabilities = {
|
|
|
97
197
|
canPin: boolean;
|
|
98
198
|
canResize: boolean;
|
|
99
199
|
canReorder: boolean;
|
|
200
|
+
canGroup: boolean;
|
|
100
201
|
};
|
|
101
202
|
|
|
102
203
|
export function getColumnCapabilities(
|
|
@@ -111,6 +212,9 @@ export function getColumnCapabilities(
|
|
|
111
212
|
canResize: features.resizing && column.getCanResize(),
|
|
112
213
|
// Ordering has no TanStack capability method — see isColumnReorderable.
|
|
113
214
|
canReorder: features.ordering && isColumnReorderable(column),
|
|
215
|
+
// `getCanGroup()` also insists on an accessor, which is what keeps the
|
|
216
|
+
// generated checkbox and tree columns from offering to group on themselves.
|
|
217
|
+
canGroup: features.grouping && column.getCanGroup(),
|
|
114
218
|
};
|
|
115
219
|
}
|
|
116
220
|
|
|
@@ -121,8 +225,11 @@ export type TMDataGridCapabilities = {
|
|
|
121
225
|
canHideAny: boolean;
|
|
122
226
|
canPinAny: boolean;
|
|
123
227
|
canReorderAny: boolean;
|
|
228
|
+
canGroupAny: boolean;
|
|
124
229
|
canSelectRows: boolean;
|
|
125
230
|
canPaginate: boolean;
|
|
231
|
+
/** At least one leaf column takes part in the global quick search. */
|
|
232
|
+
canSearch: boolean;
|
|
126
233
|
};
|
|
127
234
|
|
|
128
235
|
export function getGridCapabilities(
|
|
@@ -139,7 +246,11 @@ export function getGridCapabilities(
|
|
|
139
246
|
canHideAny: any((c) => c.canHide),
|
|
140
247
|
canPinAny: any((c) => c.canPin),
|
|
141
248
|
canReorderAny: any((c) => c.canReorder),
|
|
249
|
+
canGroupAny: any((c) => c.canGroup),
|
|
142
250
|
canSelectRows: features.rowSelection,
|
|
143
251
|
canPaginate: features.pagination,
|
|
252
|
+
canSearch:
|
|
253
|
+
features.globalFiltering &&
|
|
254
|
+
columns.some((column) => column.getCanGlobalFilter()),
|
|
144
255
|
};
|
|
145
256
|
}
|