@jielga/tmdatagrid 1.0.2 → 1.1.1
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 +17 -14
- package/dist/index.d.ts +193 -21
- package/dist/index.js +1914 -1802
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +4 -4
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +33 -20
- package/skills/editing/SKILL.md +100 -103
- package/skills/editing/references/editing-api.md +5 -2
- package/skills/editing/references/editors-and-validation.md +7 -1
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +23 -17
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/rows/SKILL.md +50 -15
- package/skills/rows/references/rows-api.md +3 -2
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +132 -32
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +223 -95
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +41 -4
- package/src/tmdatagrid/components/TMDataGridTable.tsx +218 -67
- package/src/tmdatagrid/core/editEngine.ts +28 -20
- package/src/tmdatagrid/index.ts +17 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActionIcon, Group, Select, Text, Tooltip } from "@mantine/core";
|
|
2
2
|
import type { RowData } from "@tanstack/react-table";
|
|
3
3
|
import { useSelector } from "@tanstack/react-store";
|
|
4
|
-
import type
|
|
4
|
+
import { useMemo, type ReactNode } from "react";
|
|
5
5
|
import classes from "./TMDataGridFooter.module.css";
|
|
6
6
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
7
7
|
import { getGridCapabilities } from "../core/capabilities";
|
|
@@ -9,8 +9,8 @@ import { isPagingActive } from "../core/rowSelection";
|
|
|
9
9
|
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
|
|
10
10
|
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
export type
|
|
12
|
+
/** What the pager is showing. The read half of {@link TMDataGridPaginationApi}. */
|
|
13
|
+
export type TMDataGridPaginationState = {
|
|
14
14
|
pageIndex: number;
|
|
15
15
|
pageSize: number;
|
|
16
16
|
/** `-1` when a manual grid declares `pageCount: -1` (unknown total). */
|
|
@@ -18,6 +18,18 @@ export type TMDataGridPaginationApi = {
|
|
|
18
18
|
rowCount: number;
|
|
19
19
|
canPreviousPage: boolean;
|
|
20
20
|
canNextPage: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the pager is slicing anything right now. `false` while a grouping
|
|
23
|
+
* is active, which suspends paging - see `isPagingActive`.
|
|
24
|
+
*/
|
|
25
|
+
isPagingActive: boolean;
|
|
26
|
+
/** First and last row number on this page, 1-based, for a range label. */
|
|
27
|
+
from: number;
|
|
28
|
+
to: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** What the pager can do. The write half of {@link TMDataGridPaginationApi}. */
|
|
32
|
+
export type TMDataGridPaginationActions = {
|
|
21
33
|
setPageIndex: (pageIndex: number) => void;
|
|
22
34
|
setPageSize: (pageSize: number) => void;
|
|
23
35
|
previousPage: () => void;
|
|
@@ -26,37 +38,203 @@ export type TMDataGridPaginationApi = {
|
|
|
26
38
|
lastPage: () => void;
|
|
27
39
|
};
|
|
28
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Pagination state and actions, split into the half you read and the half you
|
|
43
|
+
* call.
|
|
44
|
+
*
|
|
45
|
+
* The split is what makes a partial override possible: a consumer replacing
|
|
46
|
+
* only the range label reads `state` and never touches `actions`, and one
|
|
47
|
+
* replacing only the buttons does the opposite.
|
|
48
|
+
*/
|
|
49
|
+
export type TMDataGridPaginationApi = {
|
|
50
|
+
state: TMDataGridPaginationState;
|
|
51
|
+
actions: TMDataGridPaginationActions;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The pre-bound pieces of the built-in pager.
|
|
56
|
+
*
|
|
57
|
+
* Each renders exactly what the default footer renders, already wired to the
|
|
58
|
+
* table and already respecting the suspended-paging state - so a custom layout
|
|
59
|
+
* keeps the parts it likes rather than rebuilding them to look the same.
|
|
60
|
+
*/
|
|
61
|
+
export type TMDataGridPaginationControls = {
|
|
62
|
+
/** "Rows per page" and its select. */
|
|
63
|
+
PageSize: () => ReactNode;
|
|
64
|
+
/** The "1–25 of 300" range label. */
|
|
65
|
+
Range: () => ReactNode;
|
|
66
|
+
/** The previous/next buttons. */
|
|
67
|
+
Pager: () => ReactNode;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** What {@link TMDataGridFooterProps.renderPagination} is handed. */
|
|
71
|
+
export type TMDataGridPaginationSlotArgs = TMDataGridPaginationApi & {
|
|
72
|
+
Controls: TMDataGridPaginationControls;
|
|
73
|
+
};
|
|
74
|
+
|
|
29
75
|
/**
|
|
30
76
|
* Reads {@link TMDataGridPaginationApi} off a table. The Footer feeds it to the
|
|
31
|
-
* `
|
|
77
|
+
* `renderPagination` slot; a pager living outside the Footer can call it with
|
|
32
78
|
* the table from `useTMDataGrid` directly.
|
|
79
|
+
*
|
|
80
|
+
* `Controls` are not here: they are components bound to the grid context, and
|
|
81
|
+
* this function takes only a table.
|
|
33
82
|
*/
|
|
34
83
|
export function getTMDataGridPaginationApi<TData extends RowData>(
|
|
35
84
|
table: TMDataGridTable<TData>,
|
|
85
|
+
isPaging = true,
|
|
36
86
|
): TMDataGridPaginationApi {
|
|
37
87
|
const { pageIndex, pageSize } = table.store.state.pagination;
|
|
88
|
+
const rowCount = table.getRowCount();
|
|
38
89
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
state: {
|
|
91
|
+
pageIndex,
|
|
92
|
+
pageSize,
|
|
93
|
+
pageCount: table.getPageCount(),
|
|
94
|
+
rowCount,
|
|
95
|
+
canPreviousPage: table.getCanPreviousPage(),
|
|
96
|
+
canNextPage: table.getCanNextPage(),
|
|
97
|
+
isPagingActive: isPaging,
|
|
98
|
+
from: rowCount === 0 ? 0 : pageIndex * pageSize + 1,
|
|
99
|
+
to: Math.min(rowCount, (pageIndex + 1) * pageSize),
|
|
100
|
+
},
|
|
101
|
+
actions: {
|
|
102
|
+
setPageIndex: (index) => table.setPageIndex(index),
|
|
103
|
+
setPageSize: (size) => table.setPageSize(size),
|
|
104
|
+
previousPage: () => table.previousPage(),
|
|
105
|
+
nextPage: () => table.nextPage(),
|
|
106
|
+
firstPage: () => table.firstPage(),
|
|
107
|
+
lastPage: () => table.lastPage(),
|
|
108
|
+
},
|
|
51
109
|
};
|
|
52
110
|
}
|
|
53
111
|
|
|
112
|
+
/*
|
|
113
|
+
* The three controls are module-scope components on purpose. Defined inside
|
|
114
|
+
* the Footer they would be a new component type on every render, so React
|
|
115
|
+
* would unmount and remount the pager between one render and the next - which
|
|
116
|
+
* detaches the button a click is landing on mid-interaction.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
function PaginationPageSize({
|
|
120
|
+
pageSizeOptions,
|
|
121
|
+
}: {
|
|
122
|
+
pageSizeOptions: ReadonlyArray<number>;
|
|
123
|
+
}) {
|
|
124
|
+
const { table, features, labels, controlSize } = useTMDataGridContext();
|
|
125
|
+
useSelector(table.store);
|
|
126
|
+
const paging = isPagingActive(table, features);
|
|
127
|
+
const { pageSize } = table.store.state.pagination;
|
|
128
|
+
// A page size restored from storage, or set through `initialState`, need not
|
|
129
|
+
// be one of the offered sizes. Without it in the list the Select renders
|
|
130
|
+
// blank, so the current size is folded in rather than shown as nothing.
|
|
131
|
+
const sizeOptions = pageSizeOptions.includes(pageSize)
|
|
132
|
+
? pageSizeOptions
|
|
133
|
+
: [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Group gap="xs" wrap="nowrap">
|
|
137
|
+
{/* `span`, here and in every other label the grid renders: Mantine's
|
|
138
|
+
Text is a `<p>` by default, and a grid dropped into prose - a docs
|
|
139
|
+
page, a CMS body, Mantine's own Typography - picks up that context's
|
|
140
|
+
`p` margins. A bottom margin on a flex item shifts its border box up
|
|
141
|
+
off centre, which is how the pager came to sit a half-line below its
|
|
142
|
+
own label. Chrome labels are not prose. */}
|
|
143
|
+
<Text span size={controlSize} c="dimmed">
|
|
144
|
+
{labels.rowsPerPage}
|
|
145
|
+
</Text>
|
|
146
|
+
<Select
|
|
147
|
+
size={controlSize}
|
|
148
|
+
w={78}
|
|
149
|
+
allowDeselect={false}
|
|
150
|
+
variant="unstyled"
|
|
151
|
+
disabled={!paging}
|
|
152
|
+
data-dg-part="page-size"
|
|
153
|
+
data={sizeOptions.map(String)}
|
|
154
|
+
value={String(pageSize)}
|
|
155
|
+
onChange={(value) => {
|
|
156
|
+
table.setPageSize(Number(value) || pageSizeOptions[0]);
|
|
157
|
+
table.setPageIndex(0);
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
</Group>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function PaginationRange() {
|
|
165
|
+
const { table, features, labels, controlSize } = useTMDataGridContext();
|
|
166
|
+
useSelector(table.store);
|
|
167
|
+
const paging = isPagingActive(table, features);
|
|
168
|
+
const { pageIndex, pageSize } = table.store.state.pagination;
|
|
169
|
+
const total = table.getRowCount();
|
|
170
|
+
const from = total === 0 ? 0 : pageIndex * pageSize + 1;
|
|
171
|
+
const to = Math.min(total, (pageIndex + 1) * pageSize);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Text span size={controlSize} c="dimmed" data-dg-part="page-range">
|
|
175
|
+
{paging
|
|
176
|
+
? labels.pageRange({ from, to, total })
|
|
177
|
+
: // Not a range: nothing is being sliced, so a range would be a lie.
|
|
178
|
+
// Counted before grouping too - `getRowCount()` would be counting
|
|
179
|
+
// the tree, so a collapsed grid would claim to hold eight rows.
|
|
180
|
+
labels.groupedAllRows(table.getFilteredRowModel().rows.length)}
|
|
181
|
+
</Text>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function PaginationPager() {
|
|
186
|
+
const { table, features, labels } = useTMDataGridContext();
|
|
187
|
+
useSelector(table.store);
|
|
188
|
+
const paging = isPagingActive(table, features);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Group gap={4} wrap="nowrap">
|
|
192
|
+
<ActionIcon
|
|
193
|
+
variant="subtle"
|
|
194
|
+
color="gray"
|
|
195
|
+
aria-label={labels.previousPage}
|
|
196
|
+
data-dg-part="page-prev"
|
|
197
|
+
disabled={!paging || !table.getCanPreviousPage()}
|
|
198
|
+
onClick={() => table.previousPage()}
|
|
199
|
+
>
|
|
200
|
+
<ChevronLeftIcon size={18} stroke={1.6} />
|
|
201
|
+
</ActionIcon>
|
|
202
|
+
<ActionIcon
|
|
203
|
+
variant="subtle"
|
|
204
|
+
color="gray"
|
|
205
|
+
aria-label={labels.nextPage}
|
|
206
|
+
data-dg-part="page-next"
|
|
207
|
+
disabled={!paging || !table.getCanNextPage()}
|
|
208
|
+
onClick={() => table.nextPage()}
|
|
209
|
+
>
|
|
210
|
+
<ChevronRightIcon size={18} stroke={1.6} />
|
|
211
|
+
</ActionIcon>
|
|
212
|
+
</Group>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
54
216
|
export type TMDataGridFooterProps = {
|
|
55
217
|
pageSizeOptions?: ReadonlyArray<number>;
|
|
56
|
-
/**
|
|
57
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Replaces the built-in pager, and is handed the pieces of it.
|
|
220
|
+
*
|
|
221
|
+
* ```tsx
|
|
222
|
+
* <TMDataGrid.Footer
|
|
223
|
+
* renderPagination={({ state, actions, Controls }) => (
|
|
224
|
+
* <Group>
|
|
225
|
+
* <Controls.PageSize />
|
|
226
|
+
* <MyJumpToPage page={state.pageIndex} onJump={actions.setPageIndex} />
|
|
227
|
+
* <Controls.Pager />
|
|
228
|
+
* </Group>
|
|
229
|
+
* )}
|
|
230
|
+
* />
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
renderPagination?: (args: TMDataGridPaginationSlotArgs) => ReactNode;
|
|
58
234
|
};
|
|
59
235
|
|
|
236
|
+
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
|
237
|
+
|
|
60
238
|
/**
|
|
61
239
|
* MUI-style pager: "Rows per page · from–to of total · ‹ ›".
|
|
62
240
|
*
|
|
@@ -68,37 +246,39 @@ export type TMDataGridFooterProps = {
|
|
|
68
246
|
* - so a server-paged grid shows the server's total without changes here.
|
|
69
247
|
*/
|
|
70
248
|
export function TMDataGridFooter({
|
|
71
|
-
pageSizeOptions =
|
|
72
|
-
|
|
249
|
+
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
250
|
+
renderPagination,
|
|
73
251
|
}: TMDataGridFooterProps) {
|
|
74
|
-
const { table, features, labels
|
|
252
|
+
const { table, features, labels } = useTMDataGridContext();
|
|
75
253
|
useSelector(table.store);
|
|
76
254
|
|
|
255
|
+
// Bound once per option list rather than per render, so the controls keep
|
|
256
|
+
// their identity and the elements they render keep theirs.
|
|
257
|
+
const Controls = useMemo<TMDataGridPaginationControls>(
|
|
258
|
+
() => ({
|
|
259
|
+
PageSize: () => <PaginationPageSize pageSizeOptions={pageSizeOptions} />,
|
|
260
|
+
Range: PaginationRange,
|
|
261
|
+
Pager: PaginationPager,
|
|
262
|
+
}),
|
|
263
|
+
[pageSizeOptions],
|
|
264
|
+
);
|
|
265
|
+
|
|
77
266
|
if (!getGridCapabilities(table, features).canPaginate) return null;
|
|
78
267
|
|
|
79
|
-
|
|
268
|
+
// Grouping suspends paging - see isPagingActive. The pager is greyed out
|
|
269
|
+
// rather than dropped, so that a footer going quiet reads as a state the grid
|
|
270
|
+
// is in rather than as something that broke.
|
|
271
|
+
const paging = isPagingActive(table, features);
|
|
272
|
+
|
|
273
|
+
if (renderPagination) {
|
|
274
|
+
const { state, actions } = getTMDataGridPaginationApi(table, paging);
|
|
80
275
|
return (
|
|
81
276
|
<div data-dg-part="footer" className={classes.footer}>
|
|
82
|
-
{
|
|
277
|
+
{renderPagination({ state, actions, Controls })}
|
|
83
278
|
</div>
|
|
84
279
|
);
|
|
85
280
|
}
|
|
86
281
|
|
|
87
|
-
// Grouping suspends paging - see isPagingActive. The pager is greyed out
|
|
88
|
-
// rather than dropped, so that a footer going quiet reads as a state the grid
|
|
89
|
-
// is in rather than as something that broke.
|
|
90
|
-
const paging = isPagingActive(table, features);
|
|
91
|
-
const { pageIndex, pageSize } = table.store.state.pagination;
|
|
92
|
-
const total = table.getRowCount();
|
|
93
|
-
const from = total === 0 ? 0 : pageIndex * pageSize + 1;
|
|
94
|
-
const to = Math.min(total, (pageIndex + 1) * pageSize);
|
|
95
|
-
// A page size restored from storage, or set through `initialState`, need not
|
|
96
|
-
// be one of the offered sizes. Without it in the list the Select renders
|
|
97
|
-
// blank, so the current size is folded in rather than shown as nothing.
|
|
98
|
-
const sizeOptions = pageSizeOptions.includes(pageSize)
|
|
99
|
-
? pageSizeOptions
|
|
100
|
-
: [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
101
|
-
|
|
102
282
|
return (
|
|
103
283
|
<Tooltip
|
|
104
284
|
label={labels.pagingSuspendedHint}
|
|
@@ -113,63 +293,11 @@ export function TMDataGridFooter({
|
|
|
113
293
|
className={classes.footer}
|
|
114
294
|
data-paging-suspended={!paging}
|
|
115
295
|
>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
border box up off centre, which is how the pager came to sit a
|
|
122
|
-
half-line below its own label. Chrome labels are not prose. */}
|
|
123
|
-
<Text span size={controlSize} c="dimmed">
|
|
124
|
-
{labels.rowsPerPage}
|
|
125
|
-
</Text>
|
|
126
|
-
<Select
|
|
127
|
-
size={controlSize}
|
|
128
|
-
w={78}
|
|
129
|
-
allowDeselect={false}
|
|
130
|
-
variant="unstyled"
|
|
131
|
-
disabled={!paging}
|
|
132
|
-
data-dg-part="page-size"
|
|
133
|
-
data={sizeOptions.map(String)}
|
|
134
|
-
value={String(pageSize)}
|
|
135
|
-
onChange={(value) => {
|
|
136
|
-
table.setPageSize(Number(value) || pageSizeOptions[0]);
|
|
137
|
-
table.setPageIndex(0);
|
|
138
|
-
}}
|
|
139
|
-
/>
|
|
140
|
-
</Group>
|
|
141
|
-
|
|
142
|
-
<Text span size={controlSize} c="dimmed" data-dg-part="page-range">
|
|
143
|
-
{paging
|
|
144
|
-
? labels.pageRange({ from, to, total })
|
|
145
|
-
: // Not a range: nothing is being sliced, so a range would be a lie.
|
|
146
|
-
// Counted before grouping too - `getRowCount()` would be counting
|
|
147
|
-
// the tree, so a collapsed grid would claim to hold eight rows.
|
|
148
|
-
labels.groupedAllRows(table.getFilteredRowModel().rows.length)}
|
|
149
|
-
</Text>
|
|
150
|
-
|
|
151
|
-
<Group gap={4} wrap="nowrap">
|
|
152
|
-
<ActionIcon
|
|
153
|
-
variant="subtle"
|
|
154
|
-
color="gray"
|
|
155
|
-
aria-label={labels.previousPage}
|
|
156
|
-
data-dg-part="page-prev"
|
|
157
|
-
disabled={!paging || !table.getCanPreviousPage()}
|
|
158
|
-
onClick={() => table.previousPage()}
|
|
159
|
-
>
|
|
160
|
-
<ChevronLeftIcon size={18} stroke={1.6} />
|
|
161
|
-
</ActionIcon>
|
|
162
|
-
<ActionIcon
|
|
163
|
-
variant="subtle"
|
|
164
|
-
color="gray"
|
|
165
|
-
aria-label={labels.nextPage}
|
|
166
|
-
data-dg-part="page-next"
|
|
167
|
-
disabled={!paging || !table.getCanNextPage()}
|
|
168
|
-
onClick={() => table.nextPage()}
|
|
169
|
-
>
|
|
170
|
-
<ChevronRightIcon size={18} stroke={1.6} />
|
|
171
|
-
</ActionIcon>
|
|
172
|
-
</Group>
|
|
296
|
+
{/* The default render is the controls in order - which is what makes
|
|
297
|
+
`renderPagination` a rearrangement rather than a rebuild. */}
|
|
298
|
+
<Controls.PageSize />
|
|
299
|
+
<Controls.Range />
|
|
300
|
+
<Controls.Pager />
|
|
173
301
|
</div>
|
|
174
302
|
</Tooltip>
|
|
175
303
|
);
|
|
@@ -47,18 +47,45 @@ import {
|
|
|
47
47
|
import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
|
|
48
48
|
import { type TMDataGridFeatures, openColumnFilter } from "../useTMDataGrid";
|
|
49
49
|
|
|
50
|
+
/** The table as the header cell sees it, after the context type erasure. */
|
|
51
|
+
type TMDataGridTableInstance = ReturnType<
|
|
52
|
+
typeof useTMDataGridContext
|
|
53
|
+
>["table"];
|
|
54
|
+
|
|
50
55
|
export type TMDataGridHeader = Header<
|
|
51
56
|
TMDataGridFeatures,
|
|
52
57
|
TMDataGridRowData,
|
|
53
58
|
unknown
|
|
54
59
|
>;
|
|
55
60
|
|
|
61
|
+
/** What {@link TMDataGridColumnMenuItemsRenderer} is handed. */
|
|
62
|
+
export type TMDataGridColumnMenuItemsArgs = {
|
|
63
|
+
column: TMDataGridHeader["column"];
|
|
64
|
+
table: TMDataGridTableInstance;
|
|
65
|
+
/**
|
|
66
|
+
* The items the grid would have rendered, in order, dividers included.
|
|
67
|
+
* Return them to keep the built-in menu, splice around them to extend it,
|
|
68
|
+
* or drop them to replace it.
|
|
69
|
+
*/
|
|
70
|
+
internalItems: Array<ReactNode>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Replaces a column's menu contents. Returning an empty list leaves the column
|
|
75
|
+
* with no menu button at all, the same as a column whose every feature is off.
|
|
76
|
+
*/
|
|
77
|
+
export type TMDataGridColumnMenuItemsRenderer = (
|
|
78
|
+
args: TMDataGridColumnMenuItemsArgs,
|
|
79
|
+
) => Array<ReactNode>;
|
|
80
|
+
|
|
56
81
|
export function TMDataGridHeaderCell({
|
|
57
82
|
header,
|
|
58
83
|
layout,
|
|
84
|
+
renderColumnMenuItems,
|
|
59
85
|
}: {
|
|
60
86
|
header: TMDataGridHeader;
|
|
61
87
|
layout: TMDataGridColumnLayout;
|
|
88
|
+
renderColumnMenuItems?: TMDataGridColumnMenuItemsRenderer;
|
|
62
89
|
}) {
|
|
63
90
|
const api = useTMDataGridContext();
|
|
64
91
|
const { table, features, labels } = api;
|
|
@@ -426,11 +453,21 @@ export function TMDataGridHeaderCell({
|
|
|
426
453
|
menuItems.pop();
|
|
427
454
|
}
|
|
428
455
|
|
|
456
|
+
// The handback: the consumer is given what the grid would have rendered and
|
|
457
|
+
// returns what should be. Trimmed again afterwards, because a consumer that
|
|
458
|
+
// drops the last group can leave a divider stranded the same way.
|
|
459
|
+
const finalItems = renderColumnMenuItems
|
|
460
|
+
? [...renderColumnMenuItems({ column, table, internalItems: menuItems })]
|
|
461
|
+
: menuItems;
|
|
462
|
+
while (finalItems.length > 0 && isDivider(finalItems.at(-1))) {
|
|
463
|
+
finalItems.pop();
|
|
464
|
+
}
|
|
465
|
+
|
|
429
466
|
// One gate for both ways into the menu - the button and the right-click -
|
|
430
467
|
// so the two can never disagree about which headers have one. A control
|
|
431
468
|
// lane's header is itself a control (select-all, expand-all), so it carries
|
|
432
469
|
// no column menu, and a right-click there falls through to the browser's.
|
|
433
|
-
const hasMenu = isLeaf &&
|
|
470
|
+
const hasMenu = isLeaf && finalItems.length > 0 && !isControlColumn(column.id);
|
|
434
471
|
|
|
435
472
|
const cellClass = [
|
|
436
473
|
classes.headerCell,
|
|
@@ -607,7 +644,7 @@ export function TMDataGridHeaderCell({
|
|
|
607
644
|
</ActionIcon>
|
|
608
645
|
</Menu.Target>
|
|
609
646
|
<Menu.Dropdown onClick={(event) => event.stopPropagation()}>
|
|
610
|
-
{
|
|
647
|
+
{finalItems}
|
|
611
648
|
</Menu.Dropdown>
|
|
612
649
|
</Menu>
|
|
613
650
|
)}
|
|
@@ -658,7 +695,7 @@ export function TMDataGridHeaderCell({
|
|
|
658
695
|
|
|
659
696
|
// A second Menu for the same items, rather than one Menu serving both ways
|
|
660
697
|
// in: the button's dropdown hangs off the button, while a context menu
|
|
661
|
-
// belongs at the pointer, and a Mantine Menu has one target. `
|
|
698
|
+
// belongs at the pointer, and a Mantine Menu has one target. `finalItems` is
|
|
662
699
|
// built once and rendered by whichever dropdown is open (never both), so
|
|
663
700
|
// the two can't drift apart.
|
|
664
701
|
//
|
|
@@ -681,7 +718,7 @@ export function TMDataGridHeaderCell({
|
|
|
681
718
|
>
|
|
682
719
|
<Menu.ContextMenu>{cell}</Menu.ContextMenu>
|
|
683
720
|
<Menu.Dropdown onClick={(event) => event.stopPropagation()}>
|
|
684
|
-
{
|
|
721
|
+
{finalItems}
|
|
685
722
|
</Menu.Dropdown>
|
|
686
723
|
</Menu>
|
|
687
724
|
);
|