@jielga/tmdatagrid 2.2.0 → 2.4.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 +302 -11
- package/dist/index.js +2338 -2097
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/columns.md +9 -1
- package/docs/components.md +1 -0
- package/docs/filtering.md +5 -1
- package/docs/grouping.md +1 -1
- package/docs/localization.md +3 -3
- package/docs/persistence.md +189 -2
- package/docs/row-details.md +5 -0
- package/docs/row-interaction.md +1 -1
- package/docs/row-styling.md +17 -0
- package/docs/typed-deep-links.md +169 -0
- package/docs/use-tm-data-grid.md +6 -2
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/appearance/references/labels.md +3 -3
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +6 -3
- package/skills/columns/references/columns-api.md +1 -0
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +26 -13
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/grouping/SKILL.md +2 -2
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +105 -10
- package/skills/options/references/typed-links.md +134 -0
- package/skills/rows/SKILL.md +18 -9
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridFilterPills.module.css +9 -7
- package/src/components/TMDataGridFilterPills.tsx +15 -1
- package/src/components/TMDataGridGroupColumn.tsx +19 -4
- package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
- package/src/components/TMDataGridTable.module.css +15 -6
- package/src/components/TMDataGridTable.tsx +23 -11
- package/src/components/sticky.module.css +27 -7
- package/src/core/filterControls.ts +8 -0
- package/src/core/filterOperators.ts +54 -18
- package/src/core/grouping.ts +11 -3
- package/src/core/labels.ts +4 -3
- package/src/core/persistence.ts +330 -26
- package/src/core/urlState.ts +439 -0
- package/src/core/valueText.ts +46 -0
- package/src/index.ts +14 -0
- package/src/useTMDataGrid.tsx +145 -21
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Typed links
|
|
2
|
+
|
|
3
|
+
Reference for the `options` skill: `createTMDataGridUrlState`, what a link
|
|
4
|
+
may name, the definition's members, and the TanStack Router setup.
|
|
5
|
+
|
|
6
|
+
`createTMDataGridUrlState({ key, columns, slices? })` defines a grid's deep
|
|
7
|
+
links from its columns. `columns` is the array the grid renders, built with
|
|
8
|
+
`createTMDataGridColumnHelper`; each helper column carries its id, `meta.type`,
|
|
9
|
+
value type, `meta.filter.operators` and its `enableColumnFilter`,
|
|
10
|
+
`enableSorting` and `meta.filter.url` switches in its type only
|
|
11
|
+
(`TMDataGridColumnInfo`, read with `TMDataGridColumnInfoOf`; the brand type is
|
|
12
|
+
`TMDataGridTypedColumn`).
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
const columns = columnHelper.columns([
|
|
16
|
+
columnHelper.accessor("department", { header: "Department", meta: { type: "select" } }),
|
|
17
|
+
columnHelper.accessor("salary", { header: "Salary", meta: { type: "number" } }),
|
|
18
|
+
columnHelper.accessor("email", { header: "Email", enableColumnFilter: false }),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const employeesUrl = createTMDataGridUrlState({
|
|
22
|
+
key: "employees",
|
|
23
|
+
columns,
|
|
24
|
+
slices: ["columnFilters", "sorting"],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const persist = { urlKey: employeesUrl.urlKey } satisfies TMDataGridPersistence;
|
|
28
|
+
|
|
29
|
+
const href = `/employees?${employeesUrl.searchParams({
|
|
30
|
+
filters: { department: { operator: "isAnyOf", value: ["Design"] } },
|
|
31
|
+
sorting: [{ id: "salary", desc: true }],
|
|
32
|
+
})}`;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
What a link may name (`TMDataGridUrlInput`); anything else is a type error:
|
|
36
|
+
|
|
37
|
+
- `filters` (`TMDataGridUrlFiltersInput`): only columns whose
|
|
38
|
+
`enableColumnFilter` is not `false` and `meta.filter.url` is not `false`.
|
|
39
|
+
With no such column, `filters` accepts no key at all.
|
|
40
|
+
- The operator: one the column's `meta.type` offers, narrowed by
|
|
41
|
+
`meta.filter.operators`.
|
|
42
|
+
- The value: a `number` for a number column (`between` takes
|
|
43
|
+
`[number | null, number | null]`, `null` leaving that end open); a `Date` or a
|
|
44
|
+
`YYYY-MM-DD` string for a date column (`TMDataGridDateInput`; a `Date` is
|
|
45
|
+
written as its local calendar day, a longer string is cut to 10 characters);
|
|
46
|
+
a `boolean` for a boolean column; a `string` for a string column; an array of
|
|
47
|
+
the column's own values (the element type for an array value) for `isAnyOf` /
|
|
48
|
+
`isNoneOf`; no `value` for `isEmpty` / `isNotEmpty`.
|
|
49
|
+
- `sorting` (`TMDataGridUrlSortableId`): only columns whose `enableSorting` is
|
|
50
|
+
not `false`. `desc` defaults to `false`.
|
|
51
|
+
- `globalFilter` and `pagination` (`{ pageIndex, pageSize }`): only when the
|
|
52
|
+
definition carries those slices. `slices` defaults to every data slice.
|
|
53
|
+
- A column with an accessor function is named by its `id`; without an `id` it
|
|
54
|
+
cannot be named. A dotted accessor key is named with underscores
|
|
55
|
+
(`address_city`).
|
|
56
|
+
- Only literal values count: `enableSorting: isAdmin` reads as not sortable.
|
|
57
|
+
Columns built without the helper offer nothing. A table-level
|
|
58
|
+
`enableColumnFilters` or `enableSorting` is not seen.
|
|
59
|
+
|
|
60
|
+
The definition (`TMDataGridUrlState`):
|
|
61
|
+
|
|
62
|
+
| Member | Type | Description |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `key` | `string` | The URL search param. |
|
|
65
|
+
| `slices` | `TMDataGridDataSlice[]` | The data slices the param carries. Default: every data slice. |
|
|
66
|
+
| `urlKey` | `TMDataGridPersistKey` | For `persist.urlKey`: the key, or `[key, slices]` when `slices` is set. |
|
|
67
|
+
| `searchParams(input, { serialize? })` | `URLSearchParams` | The link as search params, encoded with `serialize` (default `JSON.stringify`, as the grid's default). Empty when the input sets nothing. Pass the `serialize` given to `persist`, if any. |
|
|
68
|
+
| `search(input)` | `{ [key]: TMDataGridUrlPayload \| undefined }` | The link as a router search object. |
|
|
69
|
+
| `payload(input)` | `TMDataGridUrlPayload \| undefined` | The param's value in the grid's state shape (filters as `{ id, value: { operator, value } }` with string values), or `undefined` when the input sets nothing. |
|
|
70
|
+
| `validateSearch(search)` | `{ [key]?: TMDataGridUrlPayload }` | A route search validator: keeps the param when it is a plain object, drops anything else. The grid checks each slice itself on read. |
|
|
71
|
+
| `persist(storage?)` | `{ urlKey, urlStorage? }` | Spread into `persist`. Without `storage`, only `urlKey`, and the grid uses the browser's URL. |
|
|
72
|
+
|
|
73
|
+
`persist(storage)` takes a `TMDataGridUrlSearchStorage`,
|
|
74
|
+
`{ get(name): unknown; set(name, value: TMDataGridUrlPayload | undefined): void }`,
|
|
75
|
+
for a router that keeps objects in its search and encodes them itself, such as
|
|
76
|
+
TanStack Router. `get` returns the param's value or `undefined` (a string value
|
|
77
|
+
is passed to the grid unchanged); `set` receives the payload object, or
|
|
78
|
+
`undefined` to remove the param. The bridge converts with JSON, so leave
|
|
79
|
+
`serialize` and `deserialize` at their defaults with it.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// TanStack Router: the route validates, the grid writes through the router.
|
|
83
|
+
const employeesRoute = createRoute({
|
|
84
|
+
getParentRoute: () => rootRoute,
|
|
85
|
+
path: "/employees",
|
|
86
|
+
validateSearch: employeesUrl.validateSearch,
|
|
87
|
+
component: EmployeesPage,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function EmployeesPage() {
|
|
91
|
+
const router = useRouter();
|
|
92
|
+
const persist = useMemo(
|
|
93
|
+
() =>
|
|
94
|
+
employeesUrl.persist({
|
|
95
|
+
get: (name) => (router.state.location.search as Record<string, unknown>)[name],
|
|
96
|
+
set: (name, value) =>
|
|
97
|
+
void router.navigate({
|
|
98
|
+
to: ".",
|
|
99
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, [name]: value }),
|
|
100
|
+
replace: true,
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
[router],
|
|
104
|
+
);
|
|
105
|
+
const grid = useTMDataGrid({ data, columns, persist });
|
|
106
|
+
// ...
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
<Link
|
|
110
|
+
to="/employees"
|
|
111
|
+
search={employeesUrl.search({
|
|
112
|
+
filters: { salary: { operator: "between", value: [40_000, 60_000] } },
|
|
113
|
+
})}
|
|
114
|
+
/>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- A route with its own search params spreads the validator:
|
|
118
|
+
`validateSearch: (s) => ({ ...employeesUrl.validateSearch(s), tab: ... })`.
|
|
119
|
+
- With `createHashHistory()` the param sits after the `#` with the rest of the
|
|
120
|
+
router's search.
|
|
121
|
+
- For URLs without percent escapes, give the router `parseSearch:
|
|
122
|
+
parseSearchWith(parse)` from `jsurl2`, and a `stringifySearch` that encodes
|
|
123
|
+
each object value with JSURL2's `stringify` and joins the pairs with
|
|
124
|
+
`encodeURIComponent`. TanStack Router's own `stringifySearchWith(stringify)`
|
|
125
|
+
joins them with `URLSearchParams`, which escapes JSURL2's `(`, `)`, `~` and
|
|
126
|
+
`!` again. JSURL2's `parse` also reads JSON, so older links still open. The
|
|
127
|
+
grid is unaffected. The full `stringifySearch` is on the
|
|
128
|
+
`typed-deep-links.md` docs page.
|
|
129
|
+
- The grid reads its param once, on mount. A link to the route the grid is
|
|
130
|
+
already on changes the URL but not the grid; link from another route, or
|
|
131
|
+
remount the grid on that navigation.
|
|
132
|
+
|
|
133
|
+
Source: `packages/tmdatagrid/docs/persistence.md` (Typed links) and
|
|
134
|
+
`packages/tmdatagrid/docs/typed-deep-links.md`.
|
package/skills/rows/SKILL.md
CHANGED
|
@@ -8,16 +8,16 @@ description: >
|
|
|
8
8
|
reading a selection through the table store, onRowClick / onCellClick /
|
|
9
9
|
onCellDoubleClick / onCellContextMenu, the renderRowContextMenu slot with its
|
|
10
10
|
internalItems handback, renderColumnMenuItems and rowContextMenuProps, per-row
|
|
11
|
-
styling with rowStyle, rowClassName, striped and the
|
|
12
|
-
details panel through renderDetails,
|
|
13
|
-
DETAILS_COLUMN_ID, row pinning with enableRowPinning
|
|
14
|
-
row-number gutter through enableRowNumbers. Load when
|
|
15
|
-
to a click, opening a detail panel, colouring rows by
|
|
16
|
-
to an edge, or numbering them.
|
|
11
|
+
styling with rowStyle, rowClassName, striped, highlightOnHover and the
|
|
12
|
+
--row-bg rule, the row details panel through renderDetails,
|
|
13
|
+
detailsColumnPosition and DETAILS_COLUMN_ID, row pinning with enableRowPinning
|
|
14
|
+
and row.pin, and the row-number gutter through enableRowNumbers. Load when
|
|
15
|
+
selecting rows, reacting to a click, opening a detail panel, colouring rows by
|
|
16
|
+
their data, pinning rows to an edge, or numbering them.
|
|
17
17
|
metadata:
|
|
18
18
|
type: core
|
|
19
19
|
library: '@jielga/tmdatagrid'
|
|
20
|
-
library_version: '2.
|
|
20
|
+
library_version: '2.4.0'
|
|
21
21
|
sources:
|
|
22
22
|
- 'Jielga/react:packages/tmdatagrid/docs/row-selection.md'
|
|
23
23
|
- 'Jielga/react:packages/tmdatagrid/docs/row-interaction.md'
|
|
@@ -213,8 +213,15 @@ them and they stop showing. `striped` follows position in the view, so it
|
|
|
213
213
|
survives sorting and filtering rather than sticking to records, and pinned rows
|
|
214
214
|
are not striped.
|
|
215
215
|
|
|
216
|
+
The hover tint follows the gestures a row answers: a row takes it under
|
|
217
|
+
`selectionMode: "row"` or a highlight mode, `onRowClick` or
|
|
218
|
+
`renderRowContextMenu`, and a group row never does. A grid with none of those
|
|
219
|
+
shows no hover tint. `highlightOnHover` on the Table overrides that: `true`
|
|
220
|
+
tints every row, `false` none.
|
|
221
|
+
|
|
216
222
|
Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
|
|
217
|
-
`data-grouped`, `data-depth`, `data-context-menu
|
|
223
|
+
`data-grouped`, `data-depth`, `data-context-menu`, `data-highlight-on-hover`
|
|
224
|
+
and `data-row-id`, so a
|
|
218
225
|
stylesheet can target any of it without a callback. The boolean attributes are
|
|
219
226
|
present, with the value `"true"`, only while they apply - `data-grouped` only
|
|
220
227
|
on group rows - so `[data-grouped]` and `[data-grouped="true"]` are
|
|
@@ -247,7 +254,9 @@ Which rows are open is TanStack's own `expanded` state, so anything can open
|
|
|
247
254
|
one - `row.toggleExpanded()` from a menu item, `initialState.expanded`,
|
|
248
255
|
`table.toggleAllRowsExpanded()` - and it persists as a `data` slice. The panel
|
|
249
256
|
is a cell spanning the row, not a row: `aria-rowcount` still counts records, and
|
|
250
|
-
a click inside it does not select the row underneath.
|
|
257
|
+
a click inside it does not select the row underneath. It has no padding: content
|
|
258
|
+
fills the row edge to edge, and content that should line up with the cell text
|
|
259
|
+
pads itself with `padding: var(--dg-padding)`. Group rows have no panel;
|
|
251
260
|
expanding one opens its children.
|
|
252
261
|
|
|
253
262
|
Rows are virtualized. A row that leaves the viewport and the `overscan`
|
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
metadata:
|
|
16
16
|
type: core
|
|
17
17
|
library: '@jielga/tmdatagrid'
|
|
18
|
-
library_version: '2.
|
|
18
|
+
library_version: '2.4.0'
|
|
19
19
|
sources:
|
|
20
20
|
- 'Jielga/react:packages/tmdatagrid/docs/server-side.md'
|
|
21
21
|
- 'Jielga/react:packages/tmdatagrid/docs/server-query.md'
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ description: >
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: core
|
|
14
14
|
library: '@jielga/tmdatagrid'
|
|
15
|
-
library_version: '2.
|
|
15
|
+
library_version: '2.4.0'
|
|
16
16
|
sources:
|
|
17
17
|
- 'Jielga/react:packages/tmdatagrid/docs/testing.md'
|
|
18
18
|
- 'Jielga/react:packages/tmdatagrid/test/gallery/main.tsx'
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
shape readable against any surface. Scoped through `.filterPills` for the
|
|
14
14
|
specificity to beat Mantine's own rule whichever stylesheet loads last. */
|
|
15
15
|
.filterPills .pill {
|
|
16
|
-
background-color:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
background-color: var(--mantine-color-gray-1);
|
|
17
|
+
color: var(--mantine-color-black);
|
|
18
|
+
border: 1px solid var(--mantine-color-gray-3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:where([data-mantine-color-scheme="dark"]) .filterPills .pill {
|
|
22
|
+
background-color: var(--mantine-color-dark-5);
|
|
23
|
+
color: var(--mantine-color-dark-0);
|
|
24
|
+
border-color: var(--mantine-color-dark-3);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* The label is a button so a pill can be clicked back open, but it must read as
|
|
@@ -2,6 +2,7 @@ import { Box, type BoxProps, Button, Pill, UnstyledButton } from "@mantine/core"
|
|
|
2
2
|
import type { RowData } from "@tanstack/react-table";
|
|
3
3
|
import { useSelector } from "@tanstack/react-store";
|
|
4
4
|
import classes from "./TMDataGridFilterPills.module.css";
|
|
5
|
+
import { resolveColumnOptions } from "../core/columnOptions";
|
|
5
6
|
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
6
7
|
import {
|
|
7
8
|
formatFilterLabel,
|
|
@@ -9,7 +10,12 @@ import {
|
|
|
9
10
|
isTMDataGridFilterValue,
|
|
10
11
|
} from "../core/filterOperators";
|
|
11
12
|
import type { TMDataGridSize } from "../core/sizes";
|
|
12
|
-
import {
|
|
13
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
14
|
+
import {
|
|
15
|
+
openColumnFilter,
|
|
16
|
+
type TMDataGridApi,
|
|
17
|
+
type TMDataGridTable,
|
|
18
|
+
} from "../useTMDataGrid";
|
|
13
19
|
|
|
14
20
|
/** Mantine's style props (`mb="sm"`, `hiddenFrom="sm"`) are set on the wrapper. */
|
|
15
21
|
export type TMDataGridFilterPillsProps<TData extends RowData> = BoxProps & {
|
|
@@ -55,6 +61,9 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
55
61
|
...others
|
|
56
62
|
}: TMDataGridFilterPillsProps<TData>) {
|
|
57
63
|
const { table, labels } = api;
|
|
64
|
+
// The options readers take the row-type-erased table, like the rest of the
|
|
65
|
+
// chrome does through context.
|
|
66
|
+
const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
|
|
58
67
|
const columnFilters = useSelector(
|
|
59
68
|
table.store,
|
|
60
69
|
(state) => state.columnFilters,
|
|
@@ -76,6 +85,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
76
85
|
<Pill.Group size={size}>
|
|
77
86
|
{activeFilters.map((filter) => {
|
|
78
87
|
const column = table.getColumn(filter.id);
|
|
88
|
+
const erasedColumn = erased.getColumn(filter.id);
|
|
79
89
|
const label = column ? getColumnLabel(column) : filter.id;
|
|
80
90
|
const value = filter.value;
|
|
81
91
|
// isFilterActive already vouched for the shape; this narrows it.
|
|
@@ -125,6 +135,10 @@ export function TMDataGridFilterPills<TData extends RowData>({
|
|
|
125
135
|
type: column ? getColumnType(column) : "string",
|
|
126
136
|
filter: value,
|
|
127
137
|
operatorLabels: labels.operators,
|
|
138
|
+
options: erasedColumn
|
|
139
|
+
? resolveColumnOptions({ table: erased, column: erasedColumn })
|
|
140
|
+
: undefined,
|
|
141
|
+
labels,
|
|
128
142
|
})}
|
|
129
143
|
</UnstyledButton>
|
|
130
144
|
</Pill>
|
|
@@ -8,7 +8,8 @@ import { useSelector } from "@tanstack/react-store";
|
|
|
8
8
|
import { shallow } from "@tanstack/store";
|
|
9
9
|
import classes from "./TMDataGridGroupColumn.module.css";
|
|
10
10
|
import { ChevronRightIcon } from "./icons";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveColumnOptions } from "../core/columnOptions";
|
|
12
|
+
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
12
13
|
import { formatGroupValue, getGroupDataRows } from "../core/grouping";
|
|
13
14
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
14
15
|
|
|
@@ -30,7 +31,7 @@ export function GroupCell<TData extends RowData>({
|
|
|
30
31
|
}: {
|
|
31
32
|
row: Row<TMDataGridFeatures, TData>;
|
|
32
33
|
}) {
|
|
33
|
-
const { labels } = useTMDataGridContext();
|
|
34
|
+
const { table, labels } = useTMDataGridContext();
|
|
34
35
|
const tabIndex = useBodyControlTabIndex();
|
|
35
36
|
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
36
37
|
|
|
@@ -38,7 +39,21 @@ export function GroupCell<TData extends RowData>({
|
|
|
38
39
|
// the indent alone is what places them under their group.
|
|
39
40
|
if (!row.getIsGrouped()) return null;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
// `groupingValue` is TanStack's map key, the value already turned into a
|
|
43
|
+
// string: `true` arrives as "true", a Date as its English timestamp, `null`
|
|
44
|
+
// as "null". The first record's grouping value is the value itself.
|
|
45
|
+
const columnId = row.groupingColumnId;
|
|
46
|
+
const column = columnId === undefined ? undefined : table.getColumn(columnId);
|
|
47
|
+
const dataRows = getGroupDataRows(row);
|
|
48
|
+
const value =
|
|
49
|
+
columnId === undefined || dataRows[0] === undefined
|
|
50
|
+
? row.groupingValue
|
|
51
|
+
: dataRows[0].getGroupingValue(columnId);
|
|
52
|
+
const label = formatGroupValue(value, labels.blankGroupValue, {
|
|
53
|
+
type: column ? getColumnType(column) : undefined,
|
|
54
|
+
options: column ? resolveColumnOptions({ table, column }) : undefined,
|
|
55
|
+
labels,
|
|
56
|
+
});
|
|
42
57
|
|
|
43
58
|
return (
|
|
44
59
|
<UnstyledButton
|
|
@@ -68,7 +83,7 @@ export function GroupCell<TData extends RowData>({
|
|
|
68
83
|
{/* Data rows, not direct children and not `getLeafRows()`: a nested group
|
|
69
84
|
counts the records under it, never the sub-groups in between. */}
|
|
70
85
|
<span className={classes.groupCount}>
|
|
71
|
-
({
|
|
86
|
+
({dataRows.length})
|
|
72
87
|
</span>
|
|
73
88
|
</UnstyledButton>
|
|
74
89
|
);
|
|
@@ -268,7 +268,13 @@ export function TMDataGridHeaderFilterRow({
|
|
|
268
268
|
<div
|
|
269
269
|
key={header.id}
|
|
270
270
|
role="columnheader"
|
|
271
|
-
|
|
271
|
+
// Named only where there is a filter: a generated lane or a column
|
|
272
|
+
// that does not filter holds no control to name.
|
|
273
|
+
aria-label={
|
|
274
|
+
column.getCanFilter()
|
|
275
|
+
? labels.filterOn(getColumnLabel(column))
|
|
276
|
+
: undefined
|
|
277
|
+
}
|
|
272
278
|
data-dg-part="header-filter-cell"
|
|
273
279
|
data-column-id={column.id}
|
|
274
280
|
data-control-column={isControlColumn(column.id) || undefined}
|
|
@@ -84,7 +84,13 @@
|
|
|
84
84
|
--row-bg: var(--dg-row-new-bg, var(--mantine-color-body));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
/* The hover tint, on the rows that carry `data-highlight-on-hover` - by default
|
|
88
|
+
the rows that answer a click or a right-click, see the Table's
|
|
89
|
+
`highlightOnHover`. `:where()` adds no specificity, so the rule stays at the
|
|
90
|
+
weight of the state rules around it: declared after the stripe and the entry
|
|
91
|
+
tint, which it wins, and before the selection and highlight rules, which win
|
|
92
|
+
over it. */
|
|
93
|
+
.bodyRow:where([data-highlight-on-hover="true"]):hover {
|
|
88
94
|
--row-bg: var(--mantine-color-default-hover);
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -320,7 +326,9 @@
|
|
|
320
326
|
/* Base and tint, exactly as .bodyCell paints them. */
|
|
321
327
|
background-color: var(--mantine-color-body);
|
|
322
328
|
background-image: linear-gradient(var(--row-bg), var(--row-bg));
|
|
323
|
-
padding:
|
|
329
|
+
/* No padding: the panel is the user's content, and a nested grid or a tab
|
|
330
|
+
strip wants the full width. Content that should line up with the cell
|
|
331
|
+
text pads itself with var(--dg-padding). */
|
|
324
332
|
/* The row above may be the selection control, which turns text selection off
|
|
325
333
|
for everything inside it. A panel is content, so it takes both back. */
|
|
326
334
|
user-select: text;
|
|
@@ -477,14 +485,15 @@
|
|
|
477
485
|
the browser's default so the colour is the grid's to state - override with
|
|
478
486
|
`--dg-match-highlight-bg` on the grid. */
|
|
479
487
|
.matchHighlight {
|
|
480
|
-
background-color: var(
|
|
481
|
-
--dg-match-highlight-bg,
|
|
482
|
-
light-dark(var(--mantine-color-yellow-2), rgba(250, 176, 5, 0.35))
|
|
483
|
-
);
|
|
488
|
+
background-color: var(--dg-match-highlight-bg, var(--mantine-color-yellow-2));
|
|
484
489
|
color: inherit;
|
|
485
490
|
border-radius: 2px;
|
|
486
491
|
}
|
|
487
492
|
|
|
493
|
+
:where([data-mantine-color-scheme="dark"]) .matchHighlight {
|
|
494
|
+
background-color: var(--dg-match-highlight-bg, rgba(250, 176, 5, 0.35));
|
|
495
|
+
}
|
|
496
|
+
|
|
488
497
|
/* The summary row - one sticky row of column footers along the bottom edge,
|
|
489
498
|
rendered only while at least one column defines `footer`. Same subgrid
|
|
490
499
|
placement as the header, so its cells sit exactly on the body's tracks.
|
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
resolveRowSelectionClick,
|
|
91
91
|
} from "../core/rowSelection";
|
|
92
92
|
import { readPinnedRows } from "../core/rowPinning";
|
|
93
|
-
import {
|
|
93
|
+
import { filterNarrowsRows } from "../core/filterOperators";
|
|
94
94
|
import { buildMatchNeedles, findMatchRanges } from "../core/matchHighlight";
|
|
95
95
|
import { ROW_NUMBER_COLUMN_ID } from "./TMDataGridRowNumberColumn";
|
|
96
96
|
import { SearchIcon } from "./icons";
|
|
@@ -631,6 +631,14 @@ export type TMDataGridTableProps<TData extends RowData> = {
|
|
|
631
631
|
* stay put while the virtualizer mounts and unmounts rows.
|
|
632
632
|
*/
|
|
633
633
|
striped?: boolean;
|
|
634
|
+
/**
|
|
635
|
+
* Whether a body row takes the hover tint under the pointer. Unset, a row
|
|
636
|
+
* takes it when the row itself answers a gesture: `selectionMode: "row"`,
|
|
637
|
+
* a highlight mode, `onRowClick` or `renderRowContextMenu`. Group rows sit
|
|
638
|
+
* out those gestures and so sit out the tint too. `true` tints every row,
|
|
639
|
+
* `false` none. The row carries `data-highlight-on-hover` while it applies.
|
|
640
|
+
*/
|
|
641
|
+
highlightOnHover?: boolean;
|
|
634
642
|
/**
|
|
635
643
|
* Called when the scroll *arrives* at an edge - once per arrival, not per
|
|
636
644
|
* scroll event, and not on mount (the grid starts at the top-left edge).
|
|
@@ -751,6 +759,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
751
759
|
rowClassName,
|
|
752
760
|
rowStyle,
|
|
753
761
|
striped = false,
|
|
762
|
+
highlightOnHover,
|
|
754
763
|
onScrollToTop,
|
|
755
764
|
onScrollToBottom,
|
|
756
765
|
onScrollToLeft,
|
|
@@ -1480,18 +1489,12 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1480
1489
|
const isEmpty = rows.length === 0 && pinnedRowCount === 0;
|
|
1481
1490
|
// Which emptiness this is. A column filter counts by the rule the pills, the
|
|
1482
1491
|
// header indicator and the page reset use, so the grid has one definition
|
|
1483
|
-
// of "active"
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
// value written by consumer code narrows the rows as far as the grid can
|
|
1487
|
-
// tell. The body already subscribes to the whole table store, so these
|
|
1488
|
-
// reads are reactive.
|
|
1492
|
+
// of "active" - see `filterNarrowsRows`. The half-typed
|
|
1493
|
+
// `{ operator, value: "" }` the filter panel seeds filters nothing. The body
|
|
1494
|
+
// already subscribes to the whole table store, so these reads are reactive.
|
|
1489
1495
|
const emptyGlobalFilter = table.store.state.globalFilter;
|
|
1490
1496
|
const hasActiveFilters =
|
|
1491
|
-
table.store.state.columnFilters.some(
|
|
1492
|
-
(entry) =>
|
|
1493
|
-
!isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value),
|
|
1494
|
-
) ||
|
|
1497
|
+
table.store.state.columnFilters.some(filterNarrowsRows) ||
|
|
1495
1498
|
(typeof emptyGlobalFilter === "string"
|
|
1496
1499
|
? emptyGlobalFilter.trim() !== ""
|
|
1497
1500
|
: emptyGlobalFilter != null);
|
|
@@ -2833,6 +2836,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2833
2836
|
) => {
|
|
2834
2837
|
const isGroupRow = row.getIsGrouped();
|
|
2835
2838
|
const isInteractive = rowGesturesFor(row);
|
|
2839
|
+
// The hover tint promises that the row answers the pointer, so
|
|
2840
|
+
// by default it follows the gestures the row takes: the ones
|
|
2841
|
+
// behind `isInteractive`, and the context menu - which a group
|
|
2842
|
+
// row sits out as well, see the row's onContextMenu below.
|
|
2843
|
+
const highlightsOnHover =
|
|
2844
|
+
highlightOnHover ??
|
|
2845
|
+
(isInteractive ||
|
|
2846
|
+
(renderRowContextMenu !== undefined && !isGroupRow));
|
|
2836
2847
|
const rowCells = [
|
|
2837
2848
|
...row.getStartVisibleCells(),
|
|
2838
2849
|
...row.getCenterVisibleCells(),
|
|
@@ -2940,6 +2951,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2940
2951
|
data-selects-on-click={
|
|
2941
2952
|
(selectsOnRowClick && !isGroupRow) || undefined
|
|
2942
2953
|
}
|
|
2954
|
+
data-highlight-on-hover={highlightsOnHover || undefined}
|
|
2943
2955
|
aria-selected={
|
|
2944
2956
|
features.rowSelection ? row.getIsSelected() : undefined
|
|
2945
2957
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
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
7
|
seam. Instead: a gradient pseudo element pinned to `top: 0; bottom: 0` so
|
|
8
|
-
consecutive cells butt together into one unbroken band.
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
consecutive cells butt together into one unbroken band.
|
|
9
|
+
|
|
10
|
+
The dark scheme is a second rule under Mantine's `data-mantine-color-scheme`
|
|
11
|
+
attribute, not the `light-dark` function: scripts/package-stylesheets.test.mjs
|
|
12
|
+
says why.
|
|
11
13
|
|
|
12
14
|
No border. A 1px line the colour of the grid's own borders drew a hard rule
|
|
13
15
|
between the pinned lanes and the data, so the pinned columns read as a
|
|
@@ -31,8 +33,8 @@
|
|
|
31
33
|
left: 100%;
|
|
32
34
|
background: linear-gradient(
|
|
33
35
|
to right,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
rgba(0, 0, 0, 0.07),
|
|
37
|
+
rgba(0, 0, 0, 0.025) 45%,
|
|
36
38
|
transparent
|
|
37
39
|
);
|
|
38
40
|
}
|
|
@@ -41,8 +43,26 @@
|
|
|
41
43
|
right: 100%;
|
|
42
44
|
background: linear-gradient(
|
|
43
45
|
to left,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
rgba(0, 0, 0, 0.07),
|
|
47
|
+
rgba(0, 0, 0, 0.025) 45%,
|
|
48
|
+
transparent
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:where([data-mantine-color-scheme="dark"]) .stickyLeft::after {
|
|
53
|
+
background: linear-gradient(
|
|
54
|
+
to right,
|
|
55
|
+
rgba(0, 0, 0, 0.26),
|
|
56
|
+
rgba(0, 0, 0, 0.09) 45%,
|
|
57
|
+
transparent
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:where([data-mantine-color-scheme="dark"]) .stickyRight::after {
|
|
62
|
+
background: linear-gradient(
|
|
63
|
+
to left,
|
|
64
|
+
rgba(0, 0, 0, 0.26),
|
|
65
|
+
rgba(0, 0, 0, 0.09) 45%,
|
|
46
66
|
transparent
|
|
47
67
|
);
|
|
48
68
|
}
|
|
@@ -106,4 +106,12 @@ export type TMDataGridColumnFilterOptions = {
|
|
|
106
106
|
* See {@link TMDataGridFilterControlComponent}.
|
|
107
107
|
*/
|
|
108
108
|
control?: TMDataGridFilterControlComponent;
|
|
109
|
+
/**
|
|
110
|
+
* Whether this column's filter goes into the `persist.urlKey` search param.
|
|
111
|
+
* Defaults to `true`. `false` keeps it out of the param, on read and on
|
|
112
|
+
* write. Only has an effect when `urlKey` carries `columnFilters`, and since such
|
|
113
|
+
* a slice is not stored under `dataKey` either, the filter is then not
|
|
114
|
+
* persisted anywhere.
|
|
115
|
+
*/
|
|
116
|
+
url?: boolean;
|
|
109
117
|
};
|