@jielga/tmdatagrid 0.1.0 → 0.3.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/README.md +35 -2
- package/dist/index.d.ts +792 -327
- package/dist/index.js +1581 -892
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +15 -7
- package/skills/columns/SKILL.md +8 -7
- package/skills/features/SKILL.md +104 -16
- package/skills/getting-started/SKILL.md +78 -20
- package/skills/options/SKILL.md +12 -7
- package/skills/server-side/SKILL.md +5 -2
- package/src/tmdatagrid/TMDataGridContext.ts +1 -1
- package/src/tmdatagrid/components/TMDataGrid.module.css +73 -0
- package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +15 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
- package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +10 -4
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +35 -0
- package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +91 -14
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +171 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +159 -0
- package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +99 -14
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +177 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +137 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +720 -0
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
- package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +8 -5
- package/src/tmdatagrid/{icons.ts → components/icons.ts} +5 -0
- package/src/tmdatagrid/components/sticky.module.css +45 -0
- package/src/tmdatagrid/core/capabilities.ts +215 -0
- package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
- package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
- package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +25 -0
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +92 -2
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/index.ts +39 -9
- package/src/tmdatagrid/useTMDataGrid.tsx +318 -66
- package/src/tmdatagrid/TMDataGrid.module.css +0 -410
- package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
- package/src/tmdatagrid/TMDataGridTable.tsx +0 -297
- package/src/tmdatagrid/capabilities.ts +0 -117
- /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* The grid root: the size scale every other module reads, and the frame.
|
|
2
|
+
Each component owns its own module; these variables cascade into all of
|
|
3
|
+
them, and the pinned-column boundary shared by header and body cells lives
|
|
4
|
+
in sticky.module.css. */
|
|
5
|
+
|
|
6
|
+
/* Size scale. Row heights mirror SIZE_ROW_HEIGHT in core/sizes.ts — the
|
|
7
|
+
virtualizer needs the number in JS, so the two have to agree. */
|
|
8
|
+
.root[data-size="xs"] {
|
|
9
|
+
--dg-row-height: 34px;
|
|
10
|
+
--dg-header-height: 32px;
|
|
11
|
+
--dg-font-size: var(--mantine-font-size-xs);
|
|
12
|
+
--dg-padding: 6px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.root[data-size="sm"] {
|
|
16
|
+
--dg-row-height: 42px;
|
|
17
|
+
--dg-header-height: 38px;
|
|
18
|
+
--dg-font-size: var(--mantine-font-size-sm);
|
|
19
|
+
--dg-padding: 8px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.root[data-size="md"] {
|
|
23
|
+
--dg-row-height: 52px;
|
|
24
|
+
--dg-header-height: 44px;
|
|
25
|
+
--dg-font-size: var(--mantine-font-size-sm);
|
|
26
|
+
--dg-padding: 10px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.root[data-size="lg"] {
|
|
30
|
+
--dg-row-height: 62px;
|
|
31
|
+
--dg-header-height: 52px;
|
|
32
|
+
--dg-font-size: var(--mantine-font-size-md);
|
|
33
|
+
--dg-padding: 14px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.root[data-size="xl"] {
|
|
37
|
+
--dg-row-height: 72px;
|
|
38
|
+
--dg-header-height: 60px;
|
|
39
|
+
--dg-font-size: var(--mantine-font-size-lg);
|
|
40
|
+
--dg-padding: 18px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.root {
|
|
44
|
+
/* Fallbacks, so the parts still work if rendered outside a sized root. */
|
|
45
|
+
--dg-row-height: 52px;
|
|
46
|
+
--dg-header-height: 44px;
|
|
47
|
+
--dg-font-size: var(--mantine-font-size-sm);
|
|
48
|
+
--dg-padding: 10px;
|
|
49
|
+
/* Background of a selected row, when `showSelectedBackground` is on. Override
|
|
50
|
+
it on the grid:
|
|
51
|
+
<TMDataGrid style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }} /> */
|
|
52
|
+
--dg-row-selected-bg: var(--mantine-primary-color-light);
|
|
53
|
+
/* Background of the highlighted row, under a selectionMode that has one.
|
|
54
|
+
Stronger than the selected background by default, since under
|
|
55
|
+
"checkboxAndHighlight" the two show up together. */
|
|
56
|
+
--dg-row-highlight-bg: var(--mantine-primary-color-light-hover);
|
|
57
|
+
/* Background of a group row. Transparent by default — the bold label and the
|
|
58
|
+
chevron are enough, and with the tree collapsed every row on screen is a
|
|
59
|
+
group row, so a tint would grey out the whole grid. Set it to give groups a
|
|
60
|
+
band of their own. */
|
|
61
|
+
--dg-row-group-bg: var(--mantine-color-body);
|
|
62
|
+
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
min-height: 0;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
/* Header text is a stretched flex item, so an inherited `text-align: center`
|
|
68
|
+
from the host page would centre headers away from their cell content. */
|
|
69
|
+
text-align: left;
|
|
70
|
+
border: 1px solid var(--mantine-color-default-border);
|
|
71
|
+
border-radius: var(--mantine-radius-md);
|
|
72
|
+
background-color: var(--mantine-color-body);
|
|
73
|
+
}
|
|
@@ -2,8 +2,9 @@ import type { RowData } from "@tanstack/react-table";
|
|
|
2
2
|
import { type CSSProperties, type ReactNode, useMemo } from "react";
|
|
3
3
|
import classes from "./TMDataGrid.module.css";
|
|
4
4
|
import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
|
|
5
|
-
import { TMDataGridContextProvider } from "
|
|
5
|
+
import { TMDataGridContextProvider } from "../TMDataGridContext";
|
|
6
6
|
import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
|
|
7
|
+
import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
|
|
7
8
|
import { TMDataGridFooter } from "./TMDataGridFooter";
|
|
8
9
|
import { TMDataGridTable } from "./TMDataGridTable";
|
|
9
10
|
import {
|
|
@@ -18,8 +19,8 @@ import {
|
|
|
18
19
|
SIZE_CONTROL_SIZE,
|
|
19
20
|
SIZE_ROW_HEIGHT,
|
|
20
21
|
type TMDataGridSize,
|
|
21
|
-
} from "
|
|
22
|
-
import type { TMDataGridApi } from "
|
|
22
|
+
} from "../core/sizes";
|
|
23
|
+
import type { TMDataGridApi } from "../useTMDataGrid";
|
|
23
24
|
|
|
24
25
|
export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
|
|
25
26
|
children: ReactNode;
|
|
@@ -29,7 +30,12 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
|
|
|
29
30
|
*/
|
|
30
31
|
size?: TMDataGridSize;
|
|
31
32
|
className?: string;
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Inline styles. Widened to accept CSS variables, which is how the grid's
|
|
35
|
+
* colours are themed — `--dg-row-selected-bg` for the selected row
|
|
36
|
+
* background. Plain `CSSProperties` rejects `--*` keys.
|
|
37
|
+
*/
|
|
38
|
+
style?: CSSProperties & Record<`--${string}`, string | number>;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -96,6 +102,11 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
|
|
|
96
102
|
Footer: TMDataGridFooter,
|
|
97
103
|
/** Rendered by `TMDataGrid.Table`; exported for custom layouts. */
|
|
98
104
|
FilterPanel: TMDataGridFilterPanel,
|
|
105
|
+
/**
|
|
106
|
+
* Takes the grid as an `api` prop rather than from context, so it can be
|
|
107
|
+
* rendered outside `<TMDataGrid>` — a page header, for instance.
|
|
108
|
+
*/
|
|
109
|
+
FilterPills: TMDataGridFilterPills,
|
|
99
110
|
/** Rendered by `TMDataGrid.ColumnsButton`; exported for custom layouts. */
|
|
100
111
|
ColumnsPanel: TMDataGridColumnsPanel,
|
|
101
112
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.columnsPanel {
|
|
2
|
+
width: 280px;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.columnsPanelSearch {
|
|
8
|
+
padding: var(--mantine-spacing-sm);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.columnsPanelList {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--mantine-spacing-sm);
|
|
15
|
+
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-sm);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.columnsPanelFooter {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
gap: var(--mantine-spacing-sm);
|
|
23
|
+
padding: var(--mantine-spacing-sm);
|
|
24
|
+
border-top: 1px solid var(--mantine-color-default-border);
|
|
25
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Button, Checkbox, ScrollArea, Text, TextInput } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import classes from "./
|
|
5
|
-
import { useTMDataGridContext } from "
|
|
6
|
-
import { getColumnLabel } from "
|
|
4
|
+
import classes from "./TMDataGridColumnsPanel.module.css";
|
|
5
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
6
|
+
import { getColumnLabel } from "../core/columnUtils";
|
|
7
|
+
import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
|
|
7
8
|
import { SearchIcon } from "./icons";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -20,7 +21,12 @@ export function TMDataGridColumnsPanel() {
|
|
|
20
21
|
(state) => state.columnVisibility,
|
|
21
22
|
);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// The tree column is left out rather than listed and disabled: its visibility
|
|
25
|
+
// is not a setting at all, it follows the grouping state, so an unchecked box
|
|
26
|
+
// that cannot be ticked would only invite the question.
|
|
27
|
+
const columns = table
|
|
28
|
+
.getAllLeafColumns()
|
|
29
|
+
.filter((column) => column.id !== GROUP_COLUMN_ID);
|
|
24
30
|
const needle = search.trim().toLowerCase();
|
|
25
31
|
const visibleInPanel = needle
|
|
26
32
|
? columns.filter((column) =>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Floats over the first body rows, anchored to `.tableWrapper`. Sits just
|
|
2
|
+
below the header row, so its offset follows the size scale rather than a
|
|
3
|
+
fixed pixel count. */
|
|
4
|
+
.filterPanel {
|
|
5
|
+
position: absolute;
|
|
6
|
+
top: calc(var(--dg-header-height) + 2px);
|
|
7
|
+
left: var(--mantine-spacing-md);
|
|
8
|
+
z-index: 5;
|
|
9
|
+
padding: var(--mantine-spacing-sm);
|
|
10
|
+
border-radius: var(--mantine-radius-md);
|
|
11
|
+
border: 1px solid var(--mantine-color-default-border);
|
|
12
|
+
background-color: var(--mantine-color-body);
|
|
13
|
+
box-shadow: var(--mantine-shadow-md);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.filterRow {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: flex-end;
|
|
19
|
+
gap: var(--mantine-spacing-xs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.filterPanelHeader {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
gap: var(--mantine-spacing-md);
|
|
27
|
+
padding-bottom: var(--mantine-spacing-xs);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.filterPanelFooter {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
gap: var(--mantine-spacing-md);
|
|
35
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ActionIcon, Button, Select, Stack, TextInput } from "@mantine/core";
|
|
1
|
+
import { ActionIcon, Button, Select, Stack, Text, TextInput } from "@mantine/core";
|
|
2
2
|
import { useSelector } from "@tanstack/react-store";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import classes from "./TMDataGridFilterPanel.module.css";
|
|
5
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
6
|
+
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
6
7
|
import {
|
|
7
8
|
type TMDataGridFilterOperator,
|
|
8
9
|
type TMDataGridFilterValue,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
getOperatorsForType,
|
|
12
13
|
isTMDataGridFilterValue,
|
|
13
14
|
operatorNeedsValue,
|
|
14
|
-
} from "
|
|
15
|
+
} from "../core/filterOperators";
|
|
15
16
|
import { CloseIcon } from "./icons";
|
|
16
17
|
|
|
17
18
|
const FALLBACK_FILTER: TMDataGridFilterValue = { operator: "contains", value: "" };
|
|
@@ -33,6 +34,27 @@ export function TMDataGridFilterPanel() {
|
|
|
33
34
|
table.store,
|
|
34
35
|
(state) => state.columnFilters,
|
|
35
36
|
);
|
|
37
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
|
|
39
|
+
// Clicking away hides the panel, the way any floating surface behaves. On
|
|
40
|
+
// pointerdown rather than click, so a press that starts outside dismisses it
|
|
41
|
+
// even when the pointer is released somewhere else.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!opened) return;
|
|
44
|
+
|
|
45
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
46
|
+
const target = event.target;
|
|
47
|
+
if (!(target instanceof Element)) return;
|
|
48
|
+
if (panelRef.current?.contains(target)) return;
|
|
49
|
+
// The toolbar button toggles the panel itself. Closing from here first
|
|
50
|
+
// would leave its click reopening what the user meant to close.
|
|
51
|
+
if (target.closest("[data-dg-filter-toggle]")) return;
|
|
52
|
+
ui.actions.closeFilterPanel();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
56
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
57
|
+
}, [opened, ui]);
|
|
36
58
|
|
|
37
59
|
if (!opened) return null;
|
|
38
60
|
|
|
@@ -86,6 +108,14 @@ export function TMDataGridFilterPanel() {
|
|
|
86
108
|
);
|
|
87
109
|
}
|
|
88
110
|
|
|
111
|
+
/** Clears every filter, including half-typed ones the pills never showed. */
|
|
112
|
+
function clearAllFilters() {
|
|
113
|
+
table.setColumnFilters([]);
|
|
114
|
+
// Same exit as removing the last filter row by hand: an empty panel has
|
|
115
|
+
// nothing to show but its own buttons.
|
|
116
|
+
ui.actions.closeFilterPanel();
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
function addFilter() {
|
|
90
120
|
const nextColumn = filterableColumns.find(
|
|
91
121
|
(column) => !columnFilters.some((filter) => filter.id === column.id),
|
|
@@ -103,8 +133,39 @@ export function TMDataGridFilterPanel() {
|
|
|
103
133
|
]);
|
|
104
134
|
}
|
|
105
135
|
|
|
136
|
+
const canAddFilter = filterableColumns.some(
|
|
137
|
+
(column) => !columnFilters.some((filter) => filter.id === column.id),
|
|
138
|
+
);
|
|
139
|
+
|
|
106
140
|
return (
|
|
107
|
-
<div
|
|
141
|
+
<div
|
|
142
|
+
ref={panelRef}
|
|
143
|
+
role="group"
|
|
144
|
+
aria-label="Filters"
|
|
145
|
+
className={classes.filterPanel}
|
|
146
|
+
// Escape is what closes a floating surface, and every control that can
|
|
147
|
+
// hold focus in here sits inside this element.
|
|
148
|
+
onKeyDown={(event) => {
|
|
149
|
+
if (event.key !== "Escape") return;
|
|
150
|
+
event.stopPropagation();
|
|
151
|
+
ui.actions.closeFilterPanel();
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<div className={classes.filterPanelHeader}>
|
|
155
|
+
<Text size={controlSize} fw={600}>
|
|
156
|
+
Filters
|
|
157
|
+
</Text>
|
|
158
|
+
<ActionIcon
|
|
159
|
+
variant="subtle"
|
|
160
|
+
color="gray"
|
|
161
|
+
size="sm"
|
|
162
|
+
aria-label="Close filters"
|
|
163
|
+
onClick={ui.actions.closeFilterPanel}
|
|
164
|
+
>
|
|
165
|
+
<CloseIcon size={16} stroke={1.6} />
|
|
166
|
+
</ActionIcon>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
108
169
|
<Stack gap="xs">
|
|
109
170
|
{columnFilters.map((filter) => {
|
|
110
171
|
const column = table.getColumn(filter.id);
|
|
@@ -129,6 +190,10 @@ export function TMDataGridFilterPanel() {
|
|
|
129
190
|
size={controlSize}
|
|
130
191
|
w={160}
|
|
131
192
|
allowDeselect={false}
|
|
193
|
+
// Both dropdowns render inside the panel: a portalled one is
|
|
194
|
+
// outside it in the DOM, and picking an option would read as a
|
|
195
|
+
// click away and close the panel under the user.
|
|
196
|
+
comboboxProps={{ withinPortal: false }}
|
|
132
197
|
data={columnOptions}
|
|
133
198
|
value={filter.id}
|
|
134
199
|
onChange={(next) => next && changeFilterColumn(filter.id, next)}
|
|
@@ -139,6 +204,7 @@ export function TMDataGridFilterPanel() {
|
|
|
139
204
|
size={controlSize}
|
|
140
205
|
w={170}
|
|
141
206
|
allowDeselect={false}
|
|
207
|
+
comboboxProps={{ withinPortal: false }}
|
|
142
208
|
data={getOperatorsForType(type).map((operator) => ({
|
|
143
209
|
value: operator,
|
|
144
210
|
label: FILTER_OPERATOR_LABELS[operator],
|
|
@@ -168,14 +234,25 @@ export function TMDataGridFilterPanel() {
|
|
|
168
234
|
);
|
|
169
235
|
})}
|
|
170
236
|
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
237
|
+
<div className={classes.filterPanelFooter}>
|
|
238
|
+
<Button
|
|
239
|
+
variant="subtle"
|
|
240
|
+
size="compact-sm"
|
|
241
|
+
disabled={!canAddFilter}
|
|
242
|
+
onClick={addFilter}
|
|
243
|
+
>
|
|
244
|
+
Add filter
|
|
245
|
+
</Button>
|
|
246
|
+
<Button
|
|
247
|
+
variant="subtle"
|
|
248
|
+
color="gray"
|
|
249
|
+
size="compact-sm"
|
|
250
|
+
disabled={columnFilters.length === 0}
|
|
251
|
+
onClick={clearAllFilters}
|
|
252
|
+
>
|
|
253
|
+
Clear all
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
179
256
|
</Stack>
|
|
180
257
|
</div>
|
|
181
258
|
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Wraps rather than scrolls: the pills are a summary, and a summary that goes
|
|
2
|
+
off-screen is not one. */
|
|
3
|
+
.filterPills {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
gap: var(--mantine-spacing-xs);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Mantine's dark-scheme pill is `dark-7` — the same colour as the body — so a
|
|
11
|
+
pill sitting on the page rather than inside a `PillsInput` disappears. Both
|
|
12
|
+
schemes are restated here so the two stay a pair, with a border to keep the
|
|
13
|
+
shape readable against any surface. Scoped through `.filterPills` for the
|
|
14
|
+
specificity to beat Mantine's own rule whichever stylesheet loads last. */
|
|
15
|
+
.filterPills .pill {
|
|
16
|
+
background-color: light-dark(
|
|
17
|
+
var(--mantine-color-gray-1),
|
|
18
|
+
var(--mantine-color-dark-5)
|
|
19
|
+
);
|
|
20
|
+
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
|
21
|
+
border: 1px solid
|
|
22
|
+
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* The label is a button so a pill can be clicked back open, but it must read as
|
|
26
|
+
part of the pill — Mantine's own label styles carry the font. */
|
|
27
|
+
.pillLabel {
|
|
28
|
+
font: inherit;
|
|
29
|
+
color: inherit;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pillLabel:hover {
|
|
34
|
+
text-decoration: underline;
|
|
35
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Button, Pill, UnstyledButton } from "@mantine/core";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
import { useSelector } from "@tanstack/react-store";
|
|
4
|
+
import classes from "./TMDataGridFilterPills.module.css";
|
|
5
|
+
import { getColumnLabel, getColumnType } from "../core/columnUtils";
|
|
6
|
+
import {
|
|
7
|
+
formatFilterLabel,
|
|
8
|
+
isFilterActive,
|
|
9
|
+
isTMDataGridFilterValue,
|
|
10
|
+
} from "../core/filterOperators";
|
|
11
|
+
import type { TMDataGridSize } from "../core/sizes";
|
|
12
|
+
import { openColumnFilter, type TMDataGridApi } from "../useTMDataGrid";
|
|
13
|
+
|
|
14
|
+
export type TMDataGridFilterPillsProps<TData extends RowData> = {
|
|
15
|
+
/** The object returned by `useTMDataGrid`. */
|
|
16
|
+
api: TMDataGridApi<TData>;
|
|
17
|
+
/** Mantine size of the pills. Defaults to `"sm"`. */
|
|
18
|
+
size?: TMDataGridSize;
|
|
19
|
+
/** "Clear all" button, shown once two filters are active. Defaults to `true`. */
|
|
20
|
+
showClearAll?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Replaces what a click on a pill's label does. The default opens the grid's
|
|
23
|
+
* filter panel on that column.
|
|
24
|
+
*/
|
|
25
|
+
onPillClick?: (columnId: string) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One pill per active filter — `First name: Sofia ✕` — with the ✕ clearing that
|
|
31
|
+
* filter and a click on the label opening the filter panel on its column.
|
|
32
|
+
*
|
|
33
|
+
* The one grid component that takes the api as a prop instead of reading
|
|
34
|
+
* context, so it can live anywhere: a page header, a card title, a breadcrumb
|
|
35
|
+
* row. Renders nothing while no filter is active.
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const grid = useTMDataGrid({ data, columns });
|
|
39
|
+
*
|
|
40
|
+
* <Group>
|
|
41
|
+
* <Title order={3}>Employees</Title>
|
|
42
|
+
* <TMDataGridFilterPills api={grid} />
|
|
43
|
+
* </Group>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Half-typed filters are left out: a filter that is not narrowing the rows yet
|
|
47
|
+
* has nothing to report, which is the same rule the funnel indicator uses.
|
|
48
|
+
*/
|
|
49
|
+
export function TMDataGridFilterPills<TData extends RowData>({
|
|
50
|
+
api,
|
|
51
|
+
size = "sm",
|
|
52
|
+
showClearAll = true,
|
|
53
|
+
onPillClick,
|
|
54
|
+
className,
|
|
55
|
+
}: TMDataGridFilterPillsProps<TData>) {
|
|
56
|
+
const { table } = api;
|
|
57
|
+
const columnFilters = useSelector(
|
|
58
|
+
table.store,
|
|
59
|
+
(state) => state.columnFilters,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const activeFilters = columnFilters.filter((filter) =>
|
|
63
|
+
isFilterActive(filter.value),
|
|
64
|
+
);
|
|
65
|
+
if (activeFilters.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
role="group"
|
|
70
|
+
aria-label="Active filters"
|
|
71
|
+
className={[classes.filterPills, className].filter(Boolean).join(" ")}
|
|
72
|
+
>
|
|
73
|
+
<Pill.Group size={size}>
|
|
74
|
+
{activeFilters.map((filter) => {
|
|
75
|
+
const column = table.getColumn(filter.id);
|
|
76
|
+
const label = column ? getColumnLabel(column) : filter.id;
|
|
77
|
+
const value = filter.value;
|
|
78
|
+
// isFilterActive already vouched for the shape; this narrows it.
|
|
79
|
+
if (!isTMDataGridFilterValue(value)) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Pill
|
|
83
|
+
key={filter.id}
|
|
84
|
+
size={size}
|
|
85
|
+
className={classes.pill}
|
|
86
|
+
withRemoveButton
|
|
87
|
+
onRemove={() =>
|
|
88
|
+
table.setColumnFilters(
|
|
89
|
+
columnFilters.filter((other) => other.id !== filter.id),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
// Mantine hides its remove button from assistive tech and from
|
|
93
|
+
// the tab order, because inside a `PillsInput` Backspace is the
|
|
94
|
+
// way out. These pills stand on their own, so the ✕ has to be a
|
|
95
|
+
// real button.
|
|
96
|
+
removeButtonProps={{
|
|
97
|
+
"aria-label": `Clear ${label} filter`,
|
|
98
|
+
"aria-hidden": false,
|
|
99
|
+
tabIndex: 0,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<UnstyledButton
|
|
103
|
+
className={classes.pillLabel}
|
|
104
|
+
onClick={() =>
|
|
105
|
+
onPillClick
|
|
106
|
+
? onPillClick(filter.id)
|
|
107
|
+
: openColumnFilter(api, filter.id)
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
{formatFilterLabel({
|
|
111
|
+
label,
|
|
112
|
+
type: column ? getColumnType(column) : "string",
|
|
113
|
+
filter: value,
|
|
114
|
+
})}
|
|
115
|
+
</UnstyledButton>
|
|
116
|
+
</Pill>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</Pill.Group>
|
|
120
|
+
|
|
121
|
+
{showClearAll && activeFilters.length > 1 && (
|
|
122
|
+
// Below two pills their own ✕ is the shorter path, so the button would
|
|
123
|
+
// only be a second way to do the same thing.
|
|
124
|
+
<Button
|
|
125
|
+
variant="subtle"
|
|
126
|
+
color="gray"
|
|
127
|
+
size="compact-xs"
|
|
128
|
+
onClick={() => table.setColumnFilters([])}
|
|
129
|
+
>
|
|
130
|
+
Clear all
|
|
131
|
+
</Button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.footer {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: flex-end;
|
|
5
|
+
gap: var(--mantine-spacing-lg);
|
|
6
|
+
padding: 4px var(--mantine-spacing-sm);
|
|
7
|
+
min-height: 48px;
|
|
8
|
+
flex-shrink: 0;
|
|
9
|
+
border-top: 1px solid var(--mantine-color-default-border);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Grouping suspends paging. The pager stays put and fades rather than
|
|
13
|
+
disappearing, so the footer reads as switched off rather than as having
|
|
14
|
+
vanished — and the row it holds does not change height under the grid. Its
|
|
15
|
+
controls carry their own `disabled`; this is the label half. */
|
|
16
|
+
.footer[data-paging-suspended="true"] {
|
|
17
|
+
opacity: 0.55;
|
|
18
|
+
}
|