@jielga/tmdatagrid 0.3.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 +947 -64
- package/dist/index.js +2474 -1423
- 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 +1 -1
- 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 +10 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +23 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +111 -33
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +5 -4
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +11 -19
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +23 -11
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -3
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +181 -52
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +23 -13
- package/src/tmdatagrid/components/TMDataGridTable.module.css +127 -6
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1003 -30
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +30 -7
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +41 -0
- 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 +176 -15
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +70 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +328 -12
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Button,
|
|
4
|
+
MultiSelect,
|
|
5
|
+
Select,
|
|
6
|
+
Stack,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
} from "@mantine/core";
|
|
2
10
|
import { useSelector } from "@tanstack/react-store";
|
|
3
11
|
import { useEffect, useRef } from "react";
|
|
4
12
|
import classes from "./TMDataGridFilterPanel.module.css";
|
|
5
13
|
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
14
|
+
import {
|
|
15
|
+
optionsToComboboxData,
|
|
16
|
+
resolveColumnOptions,
|
|
17
|
+
} from "../core/columnOptions";
|
|
6
18
|
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
7
19
|
import {
|
|
8
20
|
type TMDataGridFilterOperator,
|
|
9
21
|
type TMDataGridFilterValue,
|
|
10
|
-
FILTER_OPERATOR_LABELS,
|
|
11
22
|
getDefaultOperator,
|
|
12
23
|
getOperatorsForType,
|
|
13
24
|
isTMDataGridFilterValue,
|
|
14
25
|
operatorNeedsValue,
|
|
26
|
+
operatorTakesArrayValue,
|
|
15
27
|
} from "../core/filterOperators";
|
|
16
28
|
import { CloseIcon } from "./icons";
|
|
17
29
|
|
|
@@ -28,7 +40,7 @@ function asFilterValue(value: unknown): TMDataGridFilterValue {
|
|
|
28
40
|
* free — the state is forwarded to the server instead of a row model.
|
|
29
41
|
*/
|
|
30
42
|
export function TMDataGridFilterPanel() {
|
|
31
|
-
const { table, ui, controlSize } = useTMDataGridContext();
|
|
43
|
+
const { table, ui, labels, controlSize } = useTMDataGridContext();
|
|
32
44
|
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
33
45
|
const columnFilters = useSelector(
|
|
34
46
|
table.store,
|
|
@@ -78,6 +90,18 @@ export function TMDataGridFilterPanel() {
|
|
|
78
90
|
columnFilters.find((filter) => filter.id === fromColumnId)?.value,
|
|
79
91
|
);
|
|
80
92
|
const target = table.getColumn(toColumnId);
|
|
93
|
+
const operator = getDefaultOperator(
|
|
94
|
+
target ? getColumnType(target) : "string",
|
|
95
|
+
);
|
|
96
|
+
// The typed value only survives the move while it still fits the new
|
|
97
|
+
// operator — a text needle has no meaning to an `isAnyOf` set, or a set
|
|
98
|
+
// to a text input.
|
|
99
|
+
const value =
|
|
100
|
+
operatorTakesArrayValue(operator) === Array.isArray(current.value)
|
|
101
|
+
? current.value
|
|
102
|
+
: operatorTakesArrayValue(operator)
|
|
103
|
+
? []
|
|
104
|
+
: "";
|
|
81
105
|
table.setColumnFilters(
|
|
82
106
|
columnFilters
|
|
83
107
|
.filter((filter) => filter.id !== toColumnId)
|
|
@@ -85,12 +109,7 @@ export function TMDataGridFilterPanel() {
|
|
|
85
109
|
filter.id === fromColumnId
|
|
86
110
|
? {
|
|
87
111
|
id: toColumnId,
|
|
88
|
-
value: {
|
|
89
|
-
operator: getDefaultOperator(
|
|
90
|
-
target ? getColumnType(target) : "string",
|
|
91
|
-
),
|
|
92
|
-
value: current.value,
|
|
93
|
-
} satisfies TMDataGridFilterValue,
|
|
112
|
+
value: { operator, value } satisfies TMDataGridFilterValue,
|
|
94
113
|
}
|
|
95
114
|
: filter,
|
|
96
115
|
),
|
|
@@ -108,6 +127,21 @@ export function TMDataGridFilterPanel() {
|
|
|
108
127
|
);
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
function changeOperator(columnId: string, operator: TMDataGridFilterOperator) {
|
|
131
|
+
const current = asFilterValue(
|
|
132
|
+
columnFilters.find((filter) => filter.id === columnId)?.value,
|
|
133
|
+
);
|
|
134
|
+
// Same arity rule as changing the column: keep the value across operators
|
|
135
|
+
// of the same shape, reset it across the array/scalar boundary.
|
|
136
|
+
const value =
|
|
137
|
+
operatorTakesArrayValue(operator) === Array.isArray(current.value)
|
|
138
|
+
? current.value
|
|
139
|
+
: operatorTakesArrayValue(operator)
|
|
140
|
+
? []
|
|
141
|
+
: "";
|
|
142
|
+
patchFilter(columnId, { operator, value });
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
/** Clears every filter, including half-typed ones the pills never showed. */
|
|
112
146
|
function clearAllFilters() {
|
|
113
147
|
table.setColumnFilters([]);
|
|
@@ -141,7 +175,7 @@ export function TMDataGridFilterPanel() {
|
|
|
141
175
|
<div
|
|
142
176
|
ref={panelRef}
|
|
143
177
|
role="group"
|
|
144
|
-
aria-label=
|
|
178
|
+
aria-label={labels.filters}
|
|
145
179
|
className={classes.filterPanel}
|
|
146
180
|
// Escape is what closes a floating surface, and every control that can
|
|
147
181
|
// hold focus in here sits inside this element.
|
|
@@ -153,13 +187,13 @@ export function TMDataGridFilterPanel() {
|
|
|
153
187
|
>
|
|
154
188
|
<div className={classes.filterPanelHeader}>
|
|
155
189
|
<Text size={controlSize} fw={600}>
|
|
156
|
-
|
|
190
|
+
{labels.filters}
|
|
157
191
|
</Text>
|
|
158
192
|
<ActionIcon
|
|
159
193
|
variant="subtle"
|
|
160
194
|
color="gray"
|
|
161
195
|
size="sm"
|
|
162
|
-
aria-label=
|
|
196
|
+
aria-label={labels.closeFilters}
|
|
163
197
|
onClick={ui.actions.closeFilterPanel}
|
|
164
198
|
>
|
|
165
199
|
<CloseIcon size={16} stroke={1.6} />
|
|
@@ -172,6 +206,8 @@ export function TMDataGridFilterPanel() {
|
|
|
172
206
|
const value = asFilterValue(filter.value);
|
|
173
207
|
const type = column ? getColumnType(column) : "string";
|
|
174
208
|
const needsValue = operatorNeedsValue(value.operator);
|
|
209
|
+
const takesArray = operatorTakesArrayValue(value.operator);
|
|
210
|
+
const scalarValue = typeof value.value === "string" ? value.value : "";
|
|
175
211
|
|
|
176
212
|
return (
|
|
177
213
|
<div key={filter.id} className={classes.filterRow}>
|
|
@@ -179,14 +215,14 @@ export function TMDataGridFilterPanel() {
|
|
|
179
215
|
variant="subtle"
|
|
180
216
|
color="gray"
|
|
181
217
|
size="lg"
|
|
182
|
-
aria-label=
|
|
218
|
+
aria-label={labels.removeFilter}
|
|
183
219
|
onClick={() => removeFilter(filter.id)}
|
|
184
220
|
>
|
|
185
221
|
<CloseIcon size={18} stroke={1.6} />
|
|
186
222
|
</ActionIcon>
|
|
187
223
|
|
|
188
224
|
<Select
|
|
189
|
-
label=
|
|
225
|
+
label={labels.filterColumn}
|
|
190
226
|
size={controlSize}
|
|
191
227
|
w={160}
|
|
192
228
|
allowDeselect={false}
|
|
@@ -200,36 +236,78 @@ export function TMDataGridFilterPanel() {
|
|
|
200
236
|
/>
|
|
201
237
|
|
|
202
238
|
<Select
|
|
203
|
-
label=
|
|
239
|
+
label={labels.filterOperator}
|
|
204
240
|
size={controlSize}
|
|
205
241
|
w={170}
|
|
206
242
|
allowDeselect={false}
|
|
207
243
|
comboboxProps={{ withinPortal: false }}
|
|
208
244
|
data={getOperatorsForType(type).map((operator) => ({
|
|
209
245
|
value: operator,
|
|
210
|
-
label:
|
|
246
|
+
label: labels.operators[operator],
|
|
211
247
|
}))}
|
|
212
248
|
value={value.operator}
|
|
213
249
|
onChange={(next) =>
|
|
214
250
|
next &&
|
|
215
|
-
|
|
216
|
-
operator: next as TMDataGridFilterOperator,
|
|
217
|
-
})
|
|
251
|
+
changeOperator(filter.id, next as TMDataGridFilterOperator)
|
|
218
252
|
}
|
|
219
253
|
/>
|
|
220
254
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
{takesArray && column ? (
|
|
256
|
+
// The set the cell is tested against. Options come from
|
|
257
|
+
// `meta.options`; a select column that declares none still
|
|
258
|
+
// gets the values present in the data, via the faceted index.
|
|
259
|
+
<MultiSelect
|
|
260
|
+
label={labels.filterValue}
|
|
261
|
+
size={controlSize}
|
|
262
|
+
w={180}
|
|
263
|
+
comboboxProps={{ withinPortal: false }}
|
|
264
|
+
searchable
|
|
265
|
+
data={optionsToComboboxData(
|
|
266
|
+
resolveColumnOptions({ table, column, fallback: "faceted" }),
|
|
267
|
+
)}
|
|
268
|
+
value={Array.isArray(value.value) ? [...value.value] : []}
|
|
269
|
+
onChange={(next) => patchFilter(filter.id, { value: next })}
|
|
270
|
+
/>
|
|
271
|
+
) : type === "boolean" ? (
|
|
272
|
+
<Select
|
|
273
|
+
label={labels.filterValue}
|
|
274
|
+
size={controlSize}
|
|
275
|
+
w={180}
|
|
276
|
+
comboboxProps={{ withinPortal: false }}
|
|
277
|
+
disabled={!needsValue}
|
|
278
|
+
clearable
|
|
279
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
280
|
+
data={[
|
|
281
|
+
{ value: "true", label: labels.booleanTrue },
|
|
282
|
+
{ value: "false", label: labels.booleanFalse },
|
|
283
|
+
]}
|
|
284
|
+
value={needsValue && scalarValue !== "" ? scalarValue : null}
|
|
285
|
+
onChange={(next) =>
|
|
286
|
+
patchFilter(filter.id, { value: next ?? "" })
|
|
287
|
+
}
|
|
288
|
+
/>
|
|
289
|
+
) : (
|
|
290
|
+
<TextInput
|
|
291
|
+
label={labels.filterValue}
|
|
292
|
+
size={controlSize}
|
|
293
|
+
w={180}
|
|
294
|
+
type={
|
|
295
|
+
!needsValue
|
|
296
|
+
? "text"
|
|
297
|
+
: type === "number"
|
|
298
|
+
? "number"
|
|
299
|
+
: type === "date"
|
|
300
|
+
? "date"
|
|
301
|
+
: "text"
|
|
302
|
+
}
|
|
303
|
+
disabled={!needsValue}
|
|
304
|
+
placeholder={needsValue ? labels.filterValuePlaceholder : ""}
|
|
305
|
+
value={needsValue ? scalarValue : ""}
|
|
306
|
+
onChange={(event) =>
|
|
307
|
+
patchFilter(filter.id, { value: event.currentTarget.value })
|
|
308
|
+
}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
233
311
|
</div>
|
|
234
312
|
);
|
|
235
313
|
})}
|
|
@@ -241,7 +319,7 @@ export function TMDataGridFilterPanel() {
|
|
|
241
319
|
disabled={!canAddFilter}
|
|
242
320
|
onClick={addFilter}
|
|
243
321
|
>
|
|
244
|
-
|
|
322
|
+
{labels.addFilter}
|
|
245
323
|
</Button>
|
|
246
324
|
<Button
|
|
247
325
|
variant="subtle"
|
|
@@ -250,7 +328,7 @@ export function TMDataGridFilterPanel() {
|
|
|
250
328
|
disabled={columnFilters.length === 0}
|
|
251
329
|
onClick={clearAllFilters}
|
|
252
330
|
>
|
|
253
|
-
|
|
331
|
+
{labels.clearAllFilters}
|
|
254
332
|
</Button>
|
|
255
333
|
</div>
|
|
256
334
|
</Stack>
|
|
@@ -53,7 +53,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
53
53
|
onPillClick,
|
|
54
54
|
className,
|
|
55
55
|
}: TMDataGridFilterPillsProps<TData>) {
|
|
56
|
-
const { table } = api;
|
|
56
|
+
const { table, labels } = api;
|
|
57
57
|
const columnFilters = useSelector(
|
|
58
58
|
table.store,
|
|
59
59
|
(state) => state.columnFilters,
|
|
@@ -67,7 +67,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
67
67
|
return (
|
|
68
68
|
<div
|
|
69
69
|
role="group"
|
|
70
|
-
aria-label=
|
|
70
|
+
aria-label={labels.activeFilters}
|
|
71
71
|
className={[classes.filterPills, className].filter(Boolean).join(" ")}
|
|
72
72
|
>
|
|
73
73
|
<Pill.Group size={size}>
|
|
@@ -94,7 +94,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
94
94
|
// way out. These pills stand on their own, so the ✕ has to be a
|
|
95
95
|
// real button.
|
|
96
96
|
removeButtonProps={{
|
|
97
|
-
"aria-label":
|
|
97
|
+
"aria-label": labels.clearFilter(label),
|
|
98
98
|
"aria-hidden": false,
|
|
99
99
|
tabIndex: 0,
|
|
100
100
|
}}
|
|
@@ -111,6 +111,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
111
111
|
label,
|
|
112
112
|
type: column ? getColumnType(column) : "string",
|
|
113
113
|
filter: value,
|
|
114
|
+
operatorLabels: labels.operators,
|
|
114
115
|
})}
|
|
115
116
|
</UnstyledButton>
|
|
116
117
|
</Pill>
|
|
@@ -127,7 +128,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
127
128
|
size="compact-xs"
|
|
128
129
|
onClick={() => table.setColumnFilters([])}
|
|
129
130
|
>
|
|
130
|
-
|
|
131
|
+
{labels.clearAllFilters}
|
|
131
132
|
</Button>
|
|
132
133
|
)}
|
|
133
134
|
</div>
|
|
@@ -9,10 +9,6 @@ import { isPagingActive } from "../core/rowSelection";
|
|
|
9
9
|
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
|
|
10
10
|
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
11
11
|
|
|
12
|
-
/** Shown on the greyed-out pager, and the reason it is greyed out. */
|
|
13
|
-
const PAGING_SUSPENDED_HINT =
|
|
14
|
-
"Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.";
|
|
15
|
-
|
|
16
12
|
/** Distilled pagination state and actions for building a custom pager. */
|
|
17
13
|
export type TMDataGridPaginationApi = {
|
|
18
14
|
pageIndex: number;
|
|
@@ -75,7 +71,7 @@ export function TMDataGridFooter({
|
|
|
75
71
|
pageSizeOptions = [10, 25, 50, 100],
|
|
76
72
|
pagination,
|
|
77
73
|
}: TMDataGridFooterProps) {
|
|
78
|
-
const { table, features, controlSize } = useTMDataGridContext();
|
|
74
|
+
const { table, features, labels, controlSize } = useTMDataGridContext();
|
|
79
75
|
useSelector(table.store);
|
|
80
76
|
|
|
81
77
|
if (!getGridCapabilities(table, features).canPaginate) return null;
|
|
@@ -105,7 +101,7 @@ export function TMDataGridFooter({
|
|
|
105
101
|
|
|
106
102
|
return (
|
|
107
103
|
<Tooltip
|
|
108
|
-
label={
|
|
104
|
+
label={labels.pagingSuspendedHint}
|
|
109
105
|
disabled={paging}
|
|
110
106
|
withArrow
|
|
111
107
|
multiline
|
|
@@ -115,7 +111,7 @@ export function TMDataGridFooter({
|
|
|
115
111
|
<div className={classes.footer} data-paging-suspended={!paging}>
|
|
116
112
|
<Group gap="xs" wrap="nowrap">
|
|
117
113
|
<Text size={controlSize} c="dimmed">
|
|
118
|
-
|
|
114
|
+
{labels.rowsPerPage}
|
|
119
115
|
</Text>
|
|
120
116
|
<Select
|
|
121
117
|
size={controlSize}
|
|
@@ -133,23 +129,19 @@ export function TMDataGridFooter({
|
|
|
133
129
|
</Group>
|
|
134
130
|
|
|
135
131
|
<Text size={controlSize} c="dimmed">
|
|
136
|
-
{paging
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Counted before grouping too — `getRowCount()` would be counting
|
|
143
|
-
// the tree, so a collapsed grid would claim to hold eight rows.
|
|
144
|
-
<>Grouped · all {table.getFilteredRowModel().rows.length} rows</>
|
|
145
|
-
)}
|
|
132
|
+
{paging
|
|
133
|
+
? labels.pageRange({ from, to, total })
|
|
134
|
+
: // Not a range: nothing is being sliced, so a range would be a lie.
|
|
135
|
+
// Counted before grouping too — `getRowCount()` would be counting
|
|
136
|
+
// the tree, so a collapsed grid would claim to hold eight rows.
|
|
137
|
+
labels.groupedAllRows(table.getFilteredRowModel().rows.length)}
|
|
146
138
|
</Text>
|
|
147
139
|
|
|
148
140
|
<Group gap={4} wrap="nowrap">
|
|
149
141
|
<ActionIcon
|
|
150
142
|
variant="subtle"
|
|
151
143
|
color="gray"
|
|
152
|
-
aria-label=
|
|
144
|
+
aria-label={labels.previousPage}
|
|
153
145
|
disabled={!paging || !table.getCanPreviousPage()}
|
|
154
146
|
onClick={() => table.previousPage()}
|
|
155
147
|
>
|
|
@@ -158,7 +150,7 @@ export function TMDataGridFooter({
|
|
|
158
150
|
<ActionIcon
|
|
159
151
|
variant="subtle"
|
|
160
152
|
color="gray"
|
|
161
|
-
aria-label=
|
|
153
|
+
aria-label={labels.nextPage}
|
|
162
154
|
disabled={!paging || !table.getCanNextPage()}
|
|
163
155
|
onClick={() => table.nextPage()}
|
|
164
156
|
>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { UnstyledButton } from "@mantine/core";
|
|
2
|
+
import {
|
|
3
|
+
useCellControlTabIndex,
|
|
4
|
+
useTMDataGridContext,
|
|
5
|
+
} from "../TMDataGridContext";
|
|
2
6
|
import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
|
3
7
|
import { useSelector } from "@tanstack/react-store";
|
|
4
8
|
import { shallow } from "@tanstack/store";
|
|
@@ -23,9 +27,12 @@ const BLANK_GROUP_LABEL = "(Blank)";
|
|
|
23
27
|
* written for a data row and is free to reach into `row.original`, which on a
|
|
24
28
|
* group row is the first leaf's record rather than anything about the group.
|
|
25
29
|
*/
|
|
26
|
-
export function formatGroupValue(
|
|
30
|
+
export function formatGroupValue(
|
|
31
|
+
value: unknown,
|
|
32
|
+
blankLabel = BLANK_GROUP_LABEL,
|
|
33
|
+
): string {
|
|
27
34
|
if (value === null || value === undefined || value === "") {
|
|
28
|
-
return
|
|
35
|
+
return blankLabel;
|
|
29
36
|
}
|
|
30
37
|
if (value instanceof Date) return value.toLocaleDateString();
|
|
31
38
|
return String(value);
|
|
@@ -44,21 +51,27 @@ function GroupCell<TData extends RowData>({
|
|
|
44
51
|
}: {
|
|
45
52
|
row: Row<TMDataGridFeatures, TData>;
|
|
46
53
|
}) {
|
|
54
|
+
const { labels } = useTMDataGridContext();
|
|
47
55
|
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
56
|
+
// See useCellControlTabIndex: reached by stepping into the cell, not by Tab.
|
|
57
|
+
const tabIndex = useCellControlTabIndex();
|
|
48
58
|
|
|
49
59
|
// Leaf rows keep the lane empty: their values are in the data columns, and
|
|
50
60
|
// the indent alone is what places them under their group.
|
|
51
61
|
if (!row.getIsGrouped()) return null;
|
|
52
62
|
|
|
53
|
-
const label = formatGroupValue(row.groupingValue);
|
|
63
|
+
const label = formatGroupValue(row.groupingValue, labels.blankGroupValue);
|
|
54
64
|
|
|
55
65
|
return (
|
|
56
66
|
<UnstyledButton
|
|
57
67
|
className={classes.groupToggle}
|
|
58
68
|
// Padding rather than margin, so the whole indented width stays clickable.
|
|
59
69
|
style={{ paddingInlineStart: row.depth * INDENT_STEP }}
|
|
70
|
+
tabIndex={tabIndex}
|
|
60
71
|
aria-expanded={expanded}
|
|
61
|
-
aria-label={
|
|
72
|
+
aria-label={
|
|
73
|
+
expanded ? labels.collapseGroup(label) : labels.expandGroup(label)
|
|
74
|
+
}
|
|
62
75
|
// The row underneath may select or highlight on click; expanding is its
|
|
63
76
|
// own gesture and must not also trigger those.
|
|
64
77
|
onClick={(event) => {
|
|
@@ -88,11 +101,12 @@ function GroupHeader<TData extends RowData>({
|
|
|
88
101
|
}: {
|
|
89
102
|
table: TMDataGridTable<TData>;
|
|
90
103
|
}) {
|
|
104
|
+
const { labels } = useTMDataGridContext();
|
|
91
105
|
const grouping = useSelector(table.store, (state) => state.grouping, {
|
|
92
106
|
compare: shallow,
|
|
93
107
|
});
|
|
94
108
|
|
|
95
|
-
if (grouping.length === 0) return
|
|
109
|
+
if (grouping.length === 0) return labels.groupColumnLabel;
|
|
96
110
|
return grouping
|
|
97
111
|
.map((columnId) => {
|
|
98
112
|
const column = table.getColumn(columnId);
|
|
@@ -114,15 +128,13 @@ function GroupHeader<TData extends RowData>({
|
|
|
114
128
|
* `column.getCanGroup()` requires an `accessorFn`, which a display column has
|
|
115
129
|
* no reason to have.
|
|
116
130
|
*/
|
|
117
|
-
export function createGroupColumn<TData extends RowData>(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
unknown
|
|
121
|
-
> {
|
|
131
|
+
export function createGroupColumn<TData extends RowData>(
|
|
132
|
+
label = "Group",
|
|
133
|
+
): ColumnDef<TMDataGridFeatures, TData, unknown> {
|
|
122
134
|
return {
|
|
123
135
|
id: GROUP_COLUMN_ID,
|
|
124
136
|
meta: {
|
|
125
|
-
label
|
|
137
|
+
label,
|
|
126
138
|
// Structurally the first column after the checkbox lane.
|
|
127
139
|
enableOrdering: false,
|
|
128
140
|
},
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
justify-content: center;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/* Same as the body cells: the select-all box
|
|
25
|
-
|
|
26
|
-
.
|
|
24
|
+
/* Same as the body cells: a control lane's header — the select-all box, the
|
|
25
|
+
expand-all chevron — sits in a fixed 36px track, which the text padding would
|
|
26
|
+
squeeze it out of at the larger sizes. */
|
|
27
|
+
.headerCell[data-control-column="true"] {
|
|
27
28
|
padding: 0;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -84,6 +85,17 @@
|
|
|
84
85
|
cursor: pointer;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/* Sort priority under multi-sort — "1", "2", … beside the arrow. Only rendered
|
|
89
|
+
while more than one column sorts, so a single sort stays as it always was. */
|
|
90
|
+
.sortIndex {
|
|
91
|
+
font-size: 10px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
line-height: 1;
|
|
94
|
+
color: var(--mantine-color-dimmed);
|
|
95
|
+
min-width: 10px;
|
|
96
|
+
text-align: center;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/* Column reordering. The dragged header stays in place and dims — the browser
|
|
88
100
|
already carries a snapshot of it under the pointer — while the header being
|
|
89
101
|
dragged over draws a bar on the edge the column will land against. */
|