@jielga/tmdatagrid 2.0.0-beta.11 → 2.0.0-beta.13
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 +285 -22
- package/dist/index.js +2857 -2376
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -2
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/data/SKILL.md +5 -2
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +112 -26
- package/skills/getting-started/SKILL.md +4 -4
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +3 -1
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +44 -15
- package/skills/testing/SKILL.md +6 -3
- package/src/tmdatagrid/components/TMDataGrid.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +37 -18
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +138 -160
- package/src/tmdatagrid/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/tmdatagrid/components/TMDataGridFilterSurface.tsx +162 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +28 -1
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +10 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -3
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.tsx +303 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +9 -41
- package/src/tmdatagrid/components/TMDataGridTable.tsx +84 -15
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +37 -11
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +22 -5
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/tmdatagrid/components/filters/controlLayout.ts +32 -0
- package/src/tmdatagrid/components/filters/filterControlFor.ts +65 -0
- package/src/tmdatagrid/components/sticky.module.css +44 -0
- package/src/tmdatagrid/core/columnOptions.ts +46 -0
- package/src/tmdatagrid/core/controlledStateSync.ts +108 -0
- package/src/tmdatagrid/core/editEngine.ts +68 -12
- package/src/tmdatagrid/core/filterControls.ts +28 -0
- package/src/tmdatagrid/core/filterOperators.ts +64 -1
- package/src/tmdatagrid/core/filterSurface.ts +99 -0
- package/src/tmdatagrid/core/labels.ts +7 -0
- package/src/tmdatagrid/core/labelsSv.ts +3 -0
- package/src/tmdatagrid/core/pageReset.ts +115 -0
- package/src/tmdatagrid/index.ts +13 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +191 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Anchors the filter
|
|
1
|
+
/* Anchors the filter popup, which floats over the first body rows. */
|
|
2
2
|
.tableWrapper {
|
|
3
3
|
position: relative;
|
|
4
4
|
flex: 1;
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
flex-direction: column;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/* The sidebar surface is a second column of the wrapper rather than a layer
|
|
11
|
+
over the rows, so the wrapper lays its children out side by side. The
|
|
12
|
+
sidebar's own module decides which side it lands on. */
|
|
13
|
+
.tableWrapper[data-dg-filter-sidebar] {
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
.scrollContainer {
|
|
11
18
|
position: relative;
|
|
12
19
|
flex: 1;
|
|
20
|
+
min-width: 0;
|
|
13
21
|
min-height: 0;
|
|
14
22
|
overflow: auto;
|
|
15
23
|
/* Lets .messageRow size itself to the scrollport (100cqi) instead of the
|
|
@@ -52,46 +60,6 @@
|
|
|
52
60
|
z-index: var(--dg-z-header, 6);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
/* The scrolled-under shadow: a soft band under the header, only while body
|
|
56
|
-
rows are actually beneath it. On the last header row - the header/body
|
|
57
|
-
boundary - not on every stacked group row. A scroll-driven animation, like
|
|
58
|
-
the pinned-lane gradients: the shadow tracks the scroll on the compositor
|
|
59
|
-
with no listener and no render, and an inactive timeline (nothing to
|
|
60
|
-
scroll) leaves `opacity: 0` standing, so a grid that fits shows nothing.
|
|
61
|
-
Where unsupported there is simply no shadow - the header's border already
|
|
62
|
-
draws the boundary. */
|
|
63
|
-
.headerRow[data-dg-header-last]::after {
|
|
64
|
-
content: "";
|
|
65
|
-
position: absolute;
|
|
66
|
-
inset: 100% 0 auto 0;
|
|
67
|
-
height: 6px;
|
|
68
|
-
pointer-events: none;
|
|
69
|
-
background: linear-gradient(
|
|
70
|
-
to bottom,
|
|
71
|
-
var(--dg-header-shadow-color, rgba(0, 0, 0, 0.14)),
|
|
72
|
-
transparent
|
|
73
|
-
);
|
|
74
|
-
opacity: 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@supports (animation-timeline: scroll()) {
|
|
78
|
-
.headerRow[data-dg-header-last]::after {
|
|
79
|
-
animation: dg-header-shadow linear both;
|
|
80
|
-
animation-timeline: scroll(nearest block);
|
|
81
|
-
/* Arrives over the first rows leaving, reads as depth not as a fade. */
|
|
82
|
-
animation-range: 0px 24px;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@keyframes dg-header-shadow {
|
|
87
|
-
from {
|
|
88
|
-
opacity: 0;
|
|
89
|
-
}
|
|
90
|
-
to {
|
|
91
|
-
opacity: 1;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
63
|
.bodyRow {
|
|
96
64
|
--row-bg: var(--mantine-color-body);
|
|
97
65
|
display: grid;
|
|
@@ -24,7 +24,12 @@ import {
|
|
|
24
24
|
import classes from "./TMDataGridTable.module.css";
|
|
25
25
|
import sticky from "./sticky.module.css";
|
|
26
26
|
import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
TMDataGridFilterPopup,
|
|
29
|
+
TMDataGridFilterSidebar,
|
|
30
|
+
} from "./TMDataGridFilterSurface";
|
|
31
|
+
import { TMDataGridHeaderFilterRow } from "./TMDataGridHeaderFilterRow";
|
|
32
|
+
import { getGridCapabilities } from "../core/capabilities";
|
|
28
33
|
import {
|
|
29
34
|
TMDataGridHeaderCell,
|
|
30
35
|
type TMDataGridColumnMenuItemsRenderer,
|
|
@@ -737,6 +742,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
737
742
|
ui,
|
|
738
743
|
edit,
|
|
739
744
|
features,
|
|
745
|
+
filters,
|
|
740
746
|
labels,
|
|
741
747
|
rowHeight,
|
|
742
748
|
controlSize,
|
|
@@ -900,6 +906,22 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
900
906
|
* virtualizer needs its height from the first paint.
|
|
901
907
|
*/
|
|
902
908
|
const gridElementRef = useRef<HTMLDivElement>(null);
|
|
909
|
+
/**
|
|
910
|
+
* Header filters only render where something can be filtered - the row would
|
|
911
|
+
* otherwise be a band of empty cells on a grid with `enableColumnFilters:
|
|
912
|
+
* false`. Derived up here because the header measurement below depends on
|
|
913
|
+
* whether the row is in the DOM.
|
|
914
|
+
*/
|
|
915
|
+
const hasHeaderFilterRow =
|
|
916
|
+
filters.inHeader && getGridCapabilities(table, features).canFilterAny;
|
|
917
|
+
/**
|
|
918
|
+
* How many rows deep the header is. In the measurement's dependencies
|
|
919
|
+
* because the rows are found once, by query: a `columns` change that adds or
|
|
920
|
+
* drops a group level changes which elements have to be measured and given
|
|
921
|
+
* their sticky inset, and nothing else in the list moves when it does.
|
|
922
|
+
*/
|
|
923
|
+
const headerDepth = table.getCenterHeaderGroups().length;
|
|
924
|
+
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
|
903
925
|
const [bodyOffsetTop, setBodyOffsetTop] = useState(0);
|
|
904
926
|
const [stickyCoverTop, setStickyCoverTop] = useState(0);
|
|
905
927
|
const [stickyCoverBottom, setStickyCoverBottom] = useState(0);
|
|
@@ -917,10 +939,28 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
917
939
|
);
|
|
918
940
|
const measure = () => {
|
|
919
941
|
let header = 0;
|
|
920
|
-
for (const headerRow of headerRows)
|
|
942
|
+
for (const headerRow of headerRows) {
|
|
943
|
+
// Every header row is sticky, so each one needs the height of the rows
|
|
944
|
+
// above it as its own inset - otherwise a stacked group and the filter
|
|
945
|
+
// row all pin to the top edge and paint over each other. Written on the
|
|
946
|
+
// element because only JS knows where the row before it ended; the
|
|
947
|
+
// stylesheet's `top` is the pre-measurement fallback.
|
|
948
|
+
headerRow.style.top = `${header}px`;
|
|
949
|
+
header += headerRow.offsetHeight;
|
|
950
|
+
}
|
|
921
951
|
if (newRowCount > 0 || pinnedTopRows.length > 0) {
|
|
922
952
|
grid.style.setProperty("--dg-header-height", `${header}px`);
|
|
923
953
|
}
|
|
954
|
+
// The whole header's height, on the wrapper rather than on the grid,
|
|
955
|
+
// because the popup is a sibling of the scroll container and never sees
|
|
956
|
+
// a property set inside it. A second name rather than reusing
|
|
957
|
+
// `--dg-header-height`: the header cells read that one for their own
|
|
958
|
+
// `min-height`, so publishing a stacked total there would feed a
|
|
959
|
+
// two-row header back in as the height of each of its rows.
|
|
960
|
+
tableWrapperRef.current?.style.setProperty(
|
|
961
|
+
"--dg-header-stack-height",
|
|
962
|
+
`${header}px`,
|
|
963
|
+
);
|
|
924
964
|
let cover = header;
|
|
925
965
|
for (const block of stickyBlocks) cover += block.offsetHeight;
|
|
926
966
|
let offset = cover;
|
|
@@ -934,7 +974,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
934
974
|
observer.observe(element);
|
|
935
975
|
}
|
|
936
976
|
return () => observer.disconnect();
|
|
937
|
-
}, [newRowCount, pinnedTopRows.length]);
|
|
977
|
+
}, [newRowCount, pinnedTopRows.length, hasHeaderFilterRow, headerDepth]);
|
|
938
978
|
|
|
939
979
|
/**
|
|
940
980
|
* The entry block's height, published as `--dg-entry-height` so the pinned
|
|
@@ -1282,6 +1322,12 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
1282
1322
|
const hasSummaryRow = orderedColumns.some(
|
|
1283
1323
|
(column) => column.columnDef.footer !== undefined,
|
|
1284
1324
|
);
|
|
1325
|
+
/**
|
|
1326
|
+
* Rows above the first body row, which is what every `aria-rowindex` in the
|
|
1327
|
+
* body counts from and what `aria-rowcount` adds up. The header groups plus
|
|
1328
|
+
* the filter row, when there is one.
|
|
1329
|
+
*/
|
|
1330
|
+
const headerRowCount = headerGroups.length + (hasHeaderFilterRow ? 1 : 0);
|
|
1285
1331
|
|
|
1286
1332
|
/**
|
|
1287
1333
|
* The summary row's height, published as `--dg-summary-height`, which two
|
|
@@ -2384,7 +2430,11 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2384
2430
|
: undefined;
|
|
2385
2431
|
|
|
2386
2432
|
return (
|
|
2387
|
-
<div
|
|
2433
|
+
<div
|
|
2434
|
+
ref={tableWrapperRef}
|
|
2435
|
+
className={classes.tableWrapper}
|
|
2436
|
+
data-dg-filter-sidebar={filters.surface === "sidebar" || undefined}
|
|
2437
|
+
>
|
|
2388
2438
|
<div
|
|
2389
2439
|
ref={scrollContainerRef}
|
|
2390
2440
|
className={classes.scrollContainer}
|
|
@@ -2422,7 +2472,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2422
2472
|
aria-rowcount={
|
|
2423
2473
|
rows.length +
|
|
2424
2474
|
pinnedRowCount +
|
|
2425
|
-
|
|
2475
|
+
headerRowCount +
|
|
2426
2476
|
(hasSummaryRow ? 1 : 0)
|
|
2427
2477
|
}
|
|
2428
2478
|
aria-colcount={orderedColumns.length}
|
|
@@ -2442,11 +2492,16 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2442
2492
|
data-dg-header-row
|
|
2443
2493
|
// The scrolled-under shadow belongs to the boundary between
|
|
2444
2494
|
// header and body - the last header row, not every stacked
|
|
2445
|
-
// group row above it
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2495
|
+
// group row above it, and not this one at all when the filter
|
|
2496
|
+
// row is below it. See sticky.module.css.
|
|
2497
|
+
className={[
|
|
2498
|
+
classes.headerRow,
|
|
2499
|
+
!hasHeaderFilterRow && groupIndex === headerGroups.length - 1
|
|
2500
|
+
? sticky.headerBoundary
|
|
2501
|
+
: "",
|
|
2502
|
+
]
|
|
2503
|
+
.filter(Boolean)
|
|
2504
|
+
.join(" ")}
|
|
2450
2505
|
>
|
|
2451
2506
|
{headerGroup.headers.map((header) => (
|
|
2452
2507
|
<TMDataGridHeaderCell
|
|
@@ -2458,6 +2513,14 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2458
2513
|
))}
|
|
2459
2514
|
</div>
|
|
2460
2515
|
))}
|
|
2516
|
+
|
|
2517
|
+
{hasHeaderFilterRow && (
|
|
2518
|
+
<TMDataGridHeaderFilterRow
|
|
2519
|
+
leafHeaders={leafHeaders}
|
|
2520
|
+
layoutFor={layoutFor}
|
|
2521
|
+
ariaRowIndex={headerRowCount}
|
|
2522
|
+
/>
|
|
2523
|
+
)}
|
|
2461
2524
|
</div>
|
|
2462
2525
|
|
|
2463
2526
|
{/* The leading tab guard, below the header controls and above the
|
|
@@ -2914,7 +2977,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2914
2977
|
renderBodyRow(
|
|
2915
2978
|
row,
|
|
2916
2979
|
-1,
|
|
2917
|
-
|
|
2980
|
+
headerRowCount + index + 1,
|
|
2918
2981
|
"top",
|
|
2919
2982
|
),
|
|
2920
2983
|
)}
|
|
@@ -2935,7 +2998,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2935
2998
|
row,
|
|
2936
2999
|
virtualItem.index,
|
|
2937
3000
|
virtualItem.index +
|
|
2938
|
-
|
|
3001
|
+
headerRowCount +
|
|
2939
3002
|
pinnedTopRows.length +
|
|
2940
3003
|
1,
|
|
2941
3004
|
);
|
|
@@ -2958,7 +3021,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
2958
3021
|
renderBodyRow(
|
|
2959
3022
|
row,
|
|
2960
3023
|
-1,
|
|
2961
|
-
|
|
3024
|
+
headerRowCount +
|
|
2962
3025
|
pinnedTopRows.length +
|
|
2963
3026
|
rows.length +
|
|
2964
3027
|
index +
|
|
@@ -3014,7 +3077,7 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
3014
3077
|
<div
|
|
3015
3078
|
role="row"
|
|
3016
3079
|
aria-rowindex={
|
|
3017
|
-
rows.length + pinnedRowCount +
|
|
3080
|
+
rows.length + pinnedRowCount + headerRowCount + 1
|
|
3018
3081
|
}
|
|
3019
3082
|
// Also what the height measurement above looks for. One
|
|
3020
3083
|
// attribute serves both now that the part name *is* the
|
|
@@ -3084,7 +3147,13 @@ export function TMDataGridTable<TData extends RowData = TMDataGridRowData>({
|
|
|
3084
3147
|
</div>
|
|
3085
3148
|
</div>
|
|
3086
3149
|
|
|
3087
|
-
|
|
3150
|
+
{/* The automatic filter surfaces. The popup floats over the first body
|
|
3151
|
+
rows, anchored to the wrapper; the sidebar is a second column of it,
|
|
3152
|
+
which is why the wrapper turns into a flex row for one. Under
|
|
3153
|
+
`filters.surface: "none"` neither renders and the consumer's own
|
|
3154
|
+
`TMDataGrid.FilterPanel` is the only panel on the page. */}
|
|
3155
|
+
{filters.surface === "popup" && <TMDataGridFilterPopup />}
|
|
3156
|
+
{filters.surface === "sidebar" && <TMDataGridFilterSidebar />}
|
|
3088
3157
|
</div>
|
|
3089
3158
|
);
|
|
3090
3159
|
}
|
|
@@ -7,7 +7,7 @@ import { getGridCapabilities } from "../core/capabilities";
|
|
|
7
7
|
import { useSettledTableState } from "../core/useSettledTableState";
|
|
8
8
|
import { isFilterActive } from "../core/filterOperators";
|
|
9
9
|
import { FilterIcon } from "./icons";
|
|
10
|
-
import {
|
|
10
|
+
import { seedColumnFilter } from "../useTMDataGrid";
|
|
11
11
|
|
|
12
12
|
/** Row above the grid. Compose it from the pieces below, or anything else. */
|
|
13
13
|
export function TMDataGridToolbar({ children }: { children?: ReactNode }) {
|
|
@@ -49,15 +49,21 @@ export function TMDataGridLoadingIndicator() {
|
|
|
49
49
|
/**
|
|
50
50
|
* Visible rows over total rows. On a server-driven grid, set
|
|
51
51
|
* `meta.totalRowCount` for the denominator - the client never sees every row.
|
|
52
|
+
* Without it the count is rendered alone, since the fallback denominator would
|
|
53
|
+
* be the rows the server sent for the current page: a plausible-looking wrong
|
|
54
|
+
* total, and on an unfiltered first page the same number twice.
|
|
52
55
|
*/
|
|
53
56
|
export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
54
57
|
const { table, controlSize } = useTMDataGridContext();
|
|
55
58
|
useSettledTableState(table.store);
|
|
56
59
|
|
|
60
|
+
const serverDriven =
|
|
61
|
+
table.options.manualFiltering === true ||
|
|
62
|
+
table.options.manualPagination === true;
|
|
57
63
|
const shown = table.getRowCount();
|
|
58
64
|
const total =
|
|
59
65
|
table.options.meta?.totalRowCount ??
|
|
60
|
-
table.getPreFilteredRowModel().rows.length;
|
|
66
|
+
(serverDriven ? undefined : table.getPreFilteredRowModel().rows.length);
|
|
61
67
|
|
|
62
68
|
return (
|
|
63
69
|
<Text
|
|
@@ -67,18 +73,28 @@ export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
|
67
73
|
className={classes.summaryCount}
|
|
68
74
|
data-dg-part="summary-count"
|
|
69
75
|
>
|
|
70
|
-
{children !== undefined
|
|
76
|
+
{children !== undefined
|
|
77
|
+
? children
|
|
78
|
+
: total === undefined
|
|
79
|
+
? `${shown}`
|
|
80
|
+
: `${shown} / ${total}`}
|
|
71
81
|
</Text>
|
|
72
82
|
);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
76
|
-
* Toggles the filter
|
|
77
|
-
*
|
|
86
|
+
* Toggles the grid's filter surface - the popup or the sidebar, whichever
|
|
87
|
+
* `filters.surface` names - seeding a filter row on the first filterable
|
|
88
|
+
* column. The count of active filters tints it.
|
|
89
|
+
*
|
|
90
|
+
* Renders nothing when no column can be filtered (`enableColumnFilters:
|
|
91
|
+
* false`), and nothing under `filters.surface: "none"`, where there is no
|
|
92
|
+
* automatic surface for it to toggle. Read `ui.state.filterPanelOpen` and
|
|
93
|
+
* render your own control if a hand-placed panel wants one.
|
|
78
94
|
*/
|
|
79
95
|
export function TMDataGridFilterButton() {
|
|
80
96
|
const api = useTMDataGridContext();
|
|
81
|
-
const { table, ui, features, labels, controlSize } = api;
|
|
97
|
+
const { table, ui, features, filters, labels, controlSize } = api;
|
|
82
98
|
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
83
99
|
const columnFilters = useSelector(
|
|
84
100
|
table.store,
|
|
@@ -89,6 +105,7 @@ export function TMDataGridFilterButton() {
|
|
|
89
105
|
).length;
|
|
90
106
|
|
|
91
107
|
if (!getGridCapabilities(table, features).canFilterAny) return null;
|
|
108
|
+
if (filters.surface === "none") return null;
|
|
92
109
|
|
|
93
110
|
return (
|
|
94
111
|
<Tooltip label={labels.filters} openDelay={400}>
|
|
@@ -105,11 +122,20 @@ export function TMDataGridFilterButton() {
|
|
|
105
122
|
ui.actions.closeFilterPanel();
|
|
106
123
|
return;
|
|
107
124
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
// Not `openColumnFilter`: that one honours header filters and
|
|
126
|
+
// focuses the header control instead of opening anything. This
|
|
127
|
+
// button's whole job is to open the surface it belongs to, which a
|
|
128
|
+
// grid may well have alongside header filters.
|
|
129
|
+
//
|
|
130
|
+
// Seeded only when the panel would otherwise be empty: with filters
|
|
131
|
+
// already in state it has rows to show, and seeding would stack a
|
|
132
|
+
// blank row on the first filterable column on top of them.
|
|
133
|
+
const seedColumn =
|
|
134
|
+
columnFilters.length === 0
|
|
135
|
+
? table.getAllLeafColumns().find((column) => column.getCanFilter())
|
|
136
|
+
: undefined;
|
|
137
|
+
if (seedColumn) seedColumnFilter(api, seedColumn.id);
|
|
138
|
+
ui.actions.openFilterPanel(seedColumn?.id ?? null);
|
|
113
139
|
}}
|
|
114
140
|
>
|
|
115
141
|
<FilterIcon size={16} stroke={1.6} />
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
operatorTakesArrayValue,
|
|
7
7
|
operatorTakesRangeValue,
|
|
8
8
|
} from "../../core/filterOperators";
|
|
9
|
+
import { filterFieldProps } from "./controlLayout";
|
|
9
10
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -31,12 +32,12 @@ export function DgAutocompleteFilter(args: TMDataGridFilterControlArgs) {
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<Autocomplete
|
|
34
|
-
label
|
|
35
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
35
36
|
size={size}
|
|
36
|
-
w={180}
|
|
37
|
-
comboboxProps={{ withinPortal: false }}
|
|
38
37
|
disabled={!needsValue}
|
|
39
|
-
placeholder={
|
|
38
|
+
placeholder={
|
|
39
|
+
needsValue && args.layout !== "header" ? labels.filterValuePlaceholder : ""
|
|
40
|
+
}
|
|
40
41
|
data={suggestions.map((option) => option.value)}
|
|
41
42
|
limit={20}
|
|
42
43
|
value={needsValue && typeof value === "string" ? value : ""}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Group, TextInput } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
3
|
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { filterFieldProps } from "./controlLayout";
|
|
4
5
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -16,25 +17,41 @@ export function DgDateRangeFilter(args: TMDataGridFilterControlArgs) {
|
|
|
16
17
|
return <TMDataGridFilterValueInput {...args} />;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
const fills = args.layout !== "row";
|
|
19
21
|
const pair: [string, string] = Array.isArray(value)
|
|
20
22
|
? [String(value[0] ?? ""), String(value[1] ?? "")]
|
|
21
23
|
: ["", ""];
|
|
22
24
|
|
|
23
25
|
return (
|
|
24
|
-
<Group
|
|
26
|
+
<Group
|
|
27
|
+
gap={4}
|
|
28
|
+
wrap="nowrap"
|
|
29
|
+
align="flex-start"
|
|
30
|
+
w={fills ? "100%" : undefined}
|
|
31
|
+
>
|
|
25
32
|
<TextInput
|
|
26
|
-
|
|
33
|
+
{...filterFieldProps(
|
|
34
|
+
args,
|
|
35
|
+
{ label: labels.filterFrom, width: 88 },
|
|
36
|
+
labels.filterFrom,
|
|
37
|
+
)}
|
|
27
38
|
size={size}
|
|
28
|
-
w={88}
|
|
29
39
|
type="date"
|
|
40
|
+
flex={fills ? 1 : undefined}
|
|
41
|
+
miw={0}
|
|
30
42
|
value={pair[0]}
|
|
31
43
|
onChange={(event) => onChange([event.currentTarget.value, pair[1]])}
|
|
32
44
|
/>
|
|
33
45
|
<TextInput
|
|
34
|
-
|
|
46
|
+
{...filterFieldProps(
|
|
47
|
+
args,
|
|
48
|
+
{ label: labels.filterTo, width: 88 },
|
|
49
|
+
labels.filterTo,
|
|
50
|
+
)}
|
|
35
51
|
size={size}
|
|
36
|
-
w={88}
|
|
37
52
|
type="date"
|
|
53
|
+
flex={fills ? 1 : undefined}
|
|
54
|
+
miw={0}
|
|
38
55
|
value={pair[1]}
|
|
39
56
|
onChange={(event) => onChange([pair[0], event.currentTarget.value])}
|
|
40
57
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Input, RangeSlider } from "@mantine/core";
|
|
2
2
|
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
3
|
import { operatorTakesRangeValue } from "../../core/filterOperators";
|
|
4
|
+
import { filterFieldProps } from "./controlLayout";
|
|
4
5
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -30,7 +31,10 @@ export function DgRangeSliderFilter(args: TMDataGridFilterControlArgs) {
|
|
|
30
31
|
const upper = pair[1] === undefined || pair[1] === "" ? max : Number(pair[1]);
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
|
-
<Input.Wrapper
|
|
34
|
+
<Input.Wrapper
|
|
35
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
36
|
+
size={size}
|
|
37
|
+
>
|
|
34
38
|
<RangeSlider
|
|
35
39
|
size={size}
|
|
36
40
|
mt={6}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
operatorTakesArrayValue,
|
|
6
6
|
operatorTakesRangeValue,
|
|
7
7
|
} from "../../core/filterOperators";
|
|
8
|
+
import { filterFieldProps } from "./controlLayout";
|
|
8
9
|
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -21,7 +22,10 @@ export function DgTriStateFilter(args: TMDataGridFilterControlArgs) {
|
|
|
21
22
|
const scalar = typeof value === "string" ? value : "";
|
|
22
23
|
|
|
23
24
|
return (
|
|
24
|
-
<Input.Wrapper
|
|
25
|
+
<Input.Wrapper
|
|
26
|
+
{...filterFieldProps(args, { label: labels.filterValue })}
|
|
27
|
+
size={size}
|
|
28
|
+
>
|
|
25
29
|
<SegmentedControl
|
|
26
30
|
size={size}
|
|
27
31
|
disabled={!needsValue}
|
|
@@ -7,30 +7,39 @@ import {
|
|
|
7
7
|
operatorTakesArrayValue,
|
|
8
8
|
operatorTakesRangeValue,
|
|
9
9
|
} from "../../core/filterOperators";
|
|
10
|
+
import { filterFieldProps } from "./controlLayout";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* The built-in value control of a filter
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* The built-in value control of a filter row - what renders when a column
|
|
14
|
+
* declares no `meta.filter.control`. Shaped by the operator: a multi-select
|
|
15
|
+
* for the set operators, a From/To pair for `between`, a Yes/No dropdown for
|
|
16
|
+
* booleans, a typed input otherwise.
|
|
17
|
+
*
|
|
18
|
+
* Shaped by `layout` as well: in a header cell the fields drop their labels,
|
|
19
|
+
* fill the column's width and name themselves through `aria-label`.
|
|
16
20
|
*
|
|
17
21
|
* Exported so a custom control can fall back to it for the operators it does
|
|
18
22
|
* not care about, instead of rebuilding them.
|
|
19
23
|
*/
|
|
20
|
-
export function TMDataGridFilterValueInput({
|
|
21
|
-
column,
|
|
22
|
-
|
|
23
|
-
value,
|
|
24
|
-
onChange,
|
|
25
|
-
options,
|
|
26
|
-
size,
|
|
27
|
-
labels,
|
|
28
|
-
}: TMDataGridFilterControlArgs) {
|
|
24
|
+
export function TMDataGridFilterValueInput(args: TMDataGridFilterControlArgs) {
|
|
25
|
+
const { column, operator, value, onChange, options, size, labels, layout } =
|
|
26
|
+
args;
|
|
29
27
|
const type = getColumnType(column);
|
|
30
28
|
const needsValue = operatorNeedsValue(operator);
|
|
31
29
|
const scalarValue = typeof value === "string" ? value : "";
|
|
32
30
|
const inputType =
|
|
33
31
|
type === "number" ? "number" : type === "date" ? "date" : "text";
|
|
32
|
+
const inHeader = layout === "header";
|
|
33
|
+
const fills = layout !== "row";
|
|
34
|
+
// Portalled everywhere: drawn inline, the dropdown is clipped - by the
|
|
35
|
+
// header cell, by the grid frame's `overflow: hidden` (its corner radius)
|
|
36
|
+
// under the popup, or by the sidebar's scroller. The popup's click-away
|
|
37
|
+
// exempts portal nodes, so picking an option does not read as a click away.
|
|
38
|
+
const comboboxProps = { withinPortal: true };
|
|
39
|
+
// No placeholder in a header cell: the column name is directly above the
|
|
40
|
+
// field, and "Filter value" only ever fits a wide column anyway.
|
|
41
|
+
const placeholder =
|
|
42
|
+
needsValue && !inHeader ? labels.filterValuePlaceholder : "";
|
|
34
43
|
|
|
35
44
|
if (operatorTakesRangeValue(operator)) {
|
|
36
45
|
const rangeValue: [string, string] = Array.isArray(value)
|
|
@@ -39,12 +48,17 @@ export function TMDataGridFilterValueInput({
|
|
|
39
48
|
return (
|
|
40
49
|
// The interval's two ends. Either may stay empty (an open end), and
|
|
41
50
|
// each writes its slot of the `[min, max]` pair.
|
|
42
|
-
<Group gap={4} wrap="nowrap" align="flex-start">
|
|
51
|
+
<Group gap={4} wrap="nowrap" align="flex-start" w={fills ? "100%" : undefined}>
|
|
43
52
|
<TextInput
|
|
44
|
-
|
|
53
|
+
{...filterFieldProps(
|
|
54
|
+
args,
|
|
55
|
+
{ label: labels.filterFrom, width: 88 },
|
|
56
|
+
labels.filterFrom,
|
|
57
|
+
)}
|
|
45
58
|
size={size}
|
|
46
|
-
w={88}
|
|
47
59
|
type={inputType}
|
|
60
|
+
flex={fills ? 1 : undefined}
|
|
61
|
+
miw={0}
|
|
48
62
|
data-dg-part="filter-value-from"
|
|
49
63
|
value={rangeValue[0]}
|
|
50
64
|
onChange={(event) =>
|
|
@@ -52,10 +66,15 @@ export function TMDataGridFilterValueInput({
|
|
|
52
66
|
}
|
|
53
67
|
/>
|
|
54
68
|
<TextInput
|
|
55
|
-
|
|
69
|
+
{...filterFieldProps(
|
|
70
|
+
args,
|
|
71
|
+
{ label: labels.filterTo, width: 88 },
|
|
72
|
+
labels.filterTo,
|
|
73
|
+
)}
|
|
56
74
|
size={size}
|
|
57
|
-
w={88}
|
|
58
75
|
type={inputType}
|
|
76
|
+
flex={fills ? 1 : undefined}
|
|
77
|
+
miw={0}
|
|
59
78
|
data-dg-part="filter-value-to"
|
|
60
79
|
value={rangeValue[1]}
|
|
61
80
|
onChange={(event) =>
|
|
@@ -72,10 +91,9 @@ export function TMDataGridFilterValueInput({
|
|
|
72
91
|
// the data, via the faceted index.
|
|
73
92
|
return (
|
|
74
93
|
<MultiSelect
|
|
75
|
-
label
|
|
94
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
76
95
|
size={size}
|
|
77
|
-
|
|
78
|
-
comboboxProps={{ withinPortal: false }}
|
|
96
|
+
comboboxProps={comboboxProps}
|
|
79
97
|
searchable
|
|
80
98
|
data-dg-part="filter-value"
|
|
81
99
|
data={optionsToComboboxData(options)}
|
|
@@ -88,14 +106,13 @@ export function TMDataGridFilterValueInput({
|
|
|
88
106
|
if (type === "boolean") {
|
|
89
107
|
return (
|
|
90
108
|
<Select
|
|
91
|
-
label
|
|
109
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
92
110
|
size={size}
|
|
93
|
-
|
|
94
|
-
comboboxProps={{ withinPortal: false }}
|
|
111
|
+
comboboxProps={comboboxProps}
|
|
95
112
|
data-dg-part="filter-value"
|
|
96
113
|
disabled={!needsValue}
|
|
97
114
|
clearable
|
|
98
|
-
placeholder={
|
|
115
|
+
placeholder={placeholder}
|
|
99
116
|
data={[
|
|
100
117
|
{ value: "true", label: labels.booleanTrue },
|
|
101
118
|
{ value: "false", label: labels.booleanFalse },
|
|
@@ -108,13 +125,12 @@ export function TMDataGridFilterValueInput({
|
|
|
108
125
|
|
|
109
126
|
return (
|
|
110
127
|
<TextInput
|
|
111
|
-
label
|
|
128
|
+
{...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
|
|
112
129
|
size={size}
|
|
113
|
-
w={180}
|
|
114
130
|
type={needsValue ? inputType : "text"}
|
|
115
131
|
data-dg-part="filter-value"
|
|
116
132
|
disabled={!needsValue}
|
|
117
|
-
placeholder={
|
|
133
|
+
placeholder={placeholder}
|
|
118
134
|
value={needsValue ? scalarValue : ""}
|
|
119
135
|
onChange={(event) => onChange(event.currentTarget.value)}
|
|
120
136
|
/>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getColumnLabel } from "../../core/columnUtils";
|
|
2
|
+
import type { TMDataGridFilterControlArgs } from "../../core/filterControls";
|
|
3
|
+
|
|
4
|
+
type LayoutArgs = Pick<
|
|
5
|
+
TMDataGridFilterControlArgs,
|
|
6
|
+
"column" | "layout" | "labels"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The label and width props a filter control's field takes, by layout.
|
|
11
|
+
*
|
|
12
|
+
* Side by side in a panel row a field is a labelled control of a fixed
|
|
13
|
+
* comfortable width. Stacked in a narrow host it keeps the label and fills the
|
|
14
|
+
* host instead. In a header cell there is no room for a label above it at all,
|
|
15
|
+
* so the field names itself to assistive tech and fills the column.
|
|
16
|
+
*
|
|
17
|
+
* `qualifier` distinguishes the two ends of a pair, which would otherwise both
|
|
18
|
+
* read as the column's name.
|
|
19
|
+
*/
|
|
20
|
+
export function filterFieldProps(
|
|
21
|
+
args: LayoutArgs,
|
|
22
|
+
panel: { label: string; width?: number | string },
|
|
23
|
+
qualifier?: string,
|
|
24
|
+
): { label?: string; "aria-label"?: string; w?: number | string } {
|
|
25
|
+
if (args.layout === "row") return { label: panel.label, w: panel.width };
|
|
26
|
+
if (args.layout === "stacked") return { label: panel.label, w: "100%" };
|
|
27
|
+
const name = args.labels.filterOn(getColumnLabel(args.column));
|
|
28
|
+
return {
|
|
29
|
+
"aria-label": qualifier === undefined ? name : `${name} ${qualifier}`,
|
|
30
|
+
w: "100%",
|
|
31
|
+
};
|
|
32
|
+
}
|