@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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 +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Column } from "@tanstack/react-table";
|
|
2
|
+
import { resolveColumnOptions, type TMDataGridOption } from "../../core/columnOptions";
|
|
3
|
+
import { getColumnFilterControl, getColumnType } from "../../core/columnUtils";
|
|
4
|
+
import type { TMDataGridFilterControlComponent } from "../../core/filterControls";
|
|
5
|
+
import type { TMDataGridRowData } from "../../TMDataGridContext";
|
|
6
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../../useTMDataGrid";
|
|
7
|
+
import { TMDataGridFilterValueInput } from "./TMDataGridFilterValueInput";
|
|
8
|
+
|
|
9
|
+
type FilterColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
10
|
+
|
|
11
|
+
/** One array, so "this column has no options" never changes identity. */
|
|
12
|
+
const NO_OPTIONS: ReadonlyArray<TMDataGridOption> = [];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether resolving this column's options would read the faceted index -
|
|
16
|
+
* which is what makes the resolution worth memoizing, and what it goes stale
|
|
17
|
+
* against.
|
|
18
|
+
*/
|
|
19
|
+
export function filterOptionsUseFacets(column: FilterColumn): boolean {
|
|
20
|
+
const declared = column.columnDef.meta?.options;
|
|
21
|
+
return (
|
|
22
|
+
columnNeedsFilterOptions(column) &&
|
|
23
|
+
(declared === undefined || declared === "faceted")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a column's filter control is offered a list of options at all.
|
|
29
|
+
*
|
|
30
|
+
* Only where options mean something out of the box - a declared set, or a
|
|
31
|
+
* select-shaped column's faceted values. A custom control wanting faceted
|
|
32
|
+
* values on some other column resolves them itself; resolving here would build
|
|
33
|
+
* the faceted index for every filtered column.
|
|
34
|
+
*/
|
|
35
|
+
function columnNeedsFilterOptions(column: FilterColumn): boolean {
|
|
36
|
+
const type = getColumnType(column);
|
|
37
|
+
return (
|
|
38
|
+
column.columnDef.meta?.options !== undefined ||
|
|
39
|
+
type === "select" ||
|
|
40
|
+
type === "multiSelect"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a column's filter control is, and what options it is handed - the one
|
|
46
|
+
* decision the panel row and the header cell make identically.
|
|
47
|
+
*
|
|
48
|
+
* Not a hook: the panel resolves this inside a `map` over its rows, where a
|
|
49
|
+
* hook cannot go. The header row memoizes the call itself, because it
|
|
50
|
+
* re-renders with the table on every scroll frame.
|
|
51
|
+
*/
|
|
52
|
+
export function filterControlFor(
|
|
53
|
+
table: TMDataGridTable<TMDataGridRowData>,
|
|
54
|
+
column: FilterColumn,
|
|
55
|
+
): {
|
|
56
|
+
options: ReadonlyArray<TMDataGridOption>;
|
|
57
|
+
ValueControl: TMDataGridFilterControlComponent;
|
|
58
|
+
} {
|
|
59
|
+
return {
|
|
60
|
+
options: columnNeedsFilterOptions(column)
|
|
61
|
+
? resolveColumnOptions({ table, column, fallback: "faceted" })
|
|
62
|
+
: NO_OPTIONS,
|
|
63
|
+
ValueControl: getColumnFilterControl(column) ?? TMDataGridFilterValueInput,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -104,3 +104,47 @@
|
|
|
104
104
|
opacity: 0;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/* The scrolled-under shadow: a soft band under the header, only while body
|
|
109
|
+
rows are actually beneath it. Worn by the last header row - the header/body
|
|
110
|
+
boundary - which is the filter row under `filters.inHeader` and the last
|
|
111
|
+
group row otherwise. Lives here rather than in either row's own module
|
|
112
|
+
because the two are in different modules and the boundary moves between
|
|
113
|
+
them.
|
|
114
|
+
|
|
115
|
+
A scroll-driven animation, like the pinned-lane gradients: the shadow tracks
|
|
116
|
+
the scroll on the compositor with no listener and no render, and an inactive
|
|
117
|
+
timeline (nothing to scroll) leaves `opacity: 0` standing, so a grid that
|
|
118
|
+
fits shows nothing. Where unsupported there is simply no shadow - the
|
|
119
|
+
header's border already draws the boundary. */
|
|
120
|
+
.headerBoundary::after {
|
|
121
|
+
content: "";
|
|
122
|
+
position: absolute;
|
|
123
|
+
inset: 100% 0 auto 0;
|
|
124
|
+
height: 6px;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
background: linear-gradient(
|
|
127
|
+
to bottom,
|
|
128
|
+
var(--dg-header-shadow-color, rgba(0, 0, 0, 0.14)),
|
|
129
|
+
transparent
|
|
130
|
+
);
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@supports (animation-timeline: scroll()) {
|
|
135
|
+
.headerBoundary::after {
|
|
136
|
+
animation: dgHeaderShadow linear both;
|
|
137
|
+
animation-timeline: scroll(nearest block);
|
|
138
|
+
/* Arrives over the first rows leaving, reads as depth not as a fade. */
|
|
139
|
+
animation-range: 0px 24px;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes dgHeaderShadow {
|
|
144
|
+
from {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
}
|
|
147
|
+
to {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSelector } from "@tanstack/react-store";
|
|
2
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The columns a column chooser may list, and the two writes it makes.
|
|
6
|
+
*
|
|
7
|
+
* Shared by `TMDataGridColumnsPanel` (plain controls) and
|
|
8
|
+
* `TMDataGrid.Menu.Columns` (menu items), so the two always list the same
|
|
9
|
+
* columns and hide them the same way.
|
|
10
|
+
*/
|
|
11
|
+
export function useHideableColumns() {
|
|
12
|
+
const { table } = useTMDataGridContext();
|
|
13
|
+
|
|
14
|
+
const columnVisibility = useSelector(
|
|
15
|
+
table.store,
|
|
16
|
+
(state) => state.columnVisibility,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Only what can actually be hidden. A column with `enableHiding: false` is
|
|
20
|
+
// left out rather than listed and disabled: a box that cannot be ticked only
|
|
21
|
+
// invites the question, and every generated lane is one - the checkbox and
|
|
22
|
+
// edit lanes hold the controls the grid needs, the tree column follows the
|
|
23
|
+
// grouping state, the row-number gutter follows `enableRowNumbers`. None of
|
|
24
|
+
// them is a setting.
|
|
25
|
+
const columns = table
|
|
26
|
+
.getAllLeafColumns()
|
|
27
|
+
.filter((column) => column.getCanHide());
|
|
28
|
+
|
|
29
|
+
const shownCount = columns.filter(
|
|
30
|
+
(column) => columnVisibility[column.id] !== false,
|
|
31
|
+
).length;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show or hide every listed column.
|
|
35
|
+
*
|
|
36
|
+
* Not `table.toggleAllColumnsVisible`, which writes a visibility entry for
|
|
37
|
+
* *every* leaf column: showing all would publish the tree column - hidden
|
|
38
|
+
* because nothing is grouped, not because the user hid it - and hiding all
|
|
39
|
+
* would force the same column visible, since it writes `!getCanHide()` for
|
|
40
|
+
* the columns it will not touch. Either way a lane the panel never listed
|
|
41
|
+
* changes state, and persistence then keeps it that way.
|
|
42
|
+
*/
|
|
43
|
+
const setAllVisible = (visible: boolean) => {
|
|
44
|
+
table.setColumnVisibility((previous) => {
|
|
45
|
+
const next = { ...previous };
|
|
46
|
+
for (const column of columns) next[column.id] = visible;
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return { columns, columnVisibility, shownCount, setAllVisible };
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RowData } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
+
import { isHTMLElement } from "./dom";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Room the header keeps for its hover-revealed actions (sort arrow, menu) and
|
|
@@ -26,7 +27,9 @@ const CONTENT_ALLOWANCE = 2;
|
|
|
26
27
|
* measure (jsdom reports 0 there).
|
|
27
28
|
*/
|
|
28
29
|
function contentSpanWidth(content: HTMLElement): number {
|
|
29
|
-
|
|
30
|
+
// The cell's own document, not the global one: a Range from the opener's
|
|
31
|
+
// document cannot select nodes in a window opened with `window.open`.
|
|
32
|
+
const range = content.ownerDocument.createRange();
|
|
30
33
|
range.selectNodeContents(content);
|
|
31
34
|
// jsdom's Range has no getBoundingClientRect at all, hence the guard
|
|
32
35
|
// rather than a zero-check alone.
|
|
@@ -84,7 +87,7 @@ export function measureColumnContentWidth({
|
|
|
84
87
|
|
|
85
88
|
for (const cell of mountedCells(container, columnId)) {
|
|
86
89
|
const content = cell.firstElementChild;
|
|
87
|
-
if (!(content
|
|
90
|
+
if (!isHTMLElement(content)) continue;
|
|
88
91
|
const styles = getComputedStyle(cell);
|
|
89
92
|
const padding =
|
|
90
93
|
(Number.parseFloat(styles.paddingLeft) || 0) +
|
|
@@ -120,13 +120,20 @@ export type TMDataGridFeatureFlags = {
|
|
|
120
120
|
grouping: boolean;
|
|
121
121
|
/** Whether cells can be edited at all - the `editing` option was set. */
|
|
122
122
|
editing: boolean;
|
|
123
|
-
/**
|
|
123
|
+
/** What counts as a commit, or `null` while editing is off. */
|
|
124
124
|
editMode: TMDataGridEditMode | null;
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
126
|
+
* Where a commit goes - `editing.draft`. On, commits park in the grid's
|
|
127
|
+
* draft store and leave through `edit.saveDrafts()`; off, each one reaches
|
|
128
|
+
* the consumer as it happens.
|
|
129
|
+
*/
|
|
130
|
+
editDraft: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Whether a committed entry row stays pinned in the sticky entry block
|
|
133
|
+
* until the draft store is saved - `editing.newRowsSticky`, which needs
|
|
134
|
+
* `editing.draft`. Off by default: a committed row joins the body rows
|
|
135
|
+
* instead, sorted and filtered with them, so entering many rows cannot
|
|
136
|
+
* fill the viewport with sticky chrome.
|
|
130
137
|
*/
|
|
131
138
|
editNewRowsSticky: boolean;
|
|
132
139
|
/** The generated row-number gutter - `enableRowNumbers`. Off by default. */
|
|
@@ -211,8 +218,9 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
211
218
|
grouping: options.enableGrouping ?? options.manualPagination !== true,
|
|
212
219
|
editing: options.editing !== undefined,
|
|
213
220
|
editMode: options.editing?.mode ?? null,
|
|
221
|
+
editDraft: options.editing?.draft === true,
|
|
214
222
|
editNewRowsSticky:
|
|
215
|
-
options.editing?.
|
|
223
|
+
options.editing?.draft === true &&
|
|
216
224
|
options.editing.newRowsSticky === true,
|
|
217
225
|
rowNumbers: options.enableRowNumbers === true,
|
|
218
226
|
// A predicate counts as on - some rows may still pin.
|
|
@@ -45,6 +45,51 @@ export type TMDataGridOptionsSource =
|
|
|
45
45
|
| "faceted"
|
|
46
46
|
| ((args: TMDataGridOptionsArgs) => ReadonlyArray<TMDataGridOption | string>);
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Columns already warned about, per grid - so a warning fires once and a
|
|
50
|
+
* test's grid is not silenced by another test's. Keyed on `table.store`, not
|
|
51
|
+
* on the table: `useTable` returns a fresh table object every render, while
|
|
52
|
+
* the store is created once and shared by every render's copy.
|
|
53
|
+
*/
|
|
54
|
+
const warnedFaceted = new WeakMap<object, Set<string>>();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Faceted options read the distinct values in `data`, which under
|
|
58
|
+
* `manualFiltering` or `manualPagination` is whatever the server sent for the
|
|
59
|
+
* current page. The dropdown then offers the values that happen to be on the
|
|
60
|
+
* page the user is looking at, and looks correct while being wrong - so it is
|
|
61
|
+
* said out loud, once per column.
|
|
62
|
+
*
|
|
63
|
+
* Fires from render, unlike the library's other warnings: the fallback form
|
|
64
|
+
* of `"faceted"` only exists at resolve time, which is render. The guard
|
|
65
|
+
* makes it once per grid regardless - a StrictMode double render or a
|
|
66
|
+
* discarded concurrent render marks the set the same way a committed one
|
|
67
|
+
* does. H3 in the backlog folds it into the diagnostics mechanism with the
|
|
68
|
+
* rest.
|
|
69
|
+
*/
|
|
70
|
+
function warnFacetedUnderManualMode(
|
|
71
|
+
table: TMDataGridTable<TMDataGridRowData>,
|
|
72
|
+
columnId: string,
|
|
73
|
+
): void {
|
|
74
|
+
if (
|
|
75
|
+
table.options.manualFiltering !== true &&
|
|
76
|
+
table.options.manualPagination !== true
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const store = table.store as object;
|
|
81
|
+
let warned = warnedFaceted.get(store);
|
|
82
|
+
if (warned === undefined) {
|
|
83
|
+
warned = new Set();
|
|
84
|
+
warnedFaceted.set(store, warned);
|
|
85
|
+
}
|
|
86
|
+
if (warned.has(columnId)) return;
|
|
87
|
+
warned.add(columnId);
|
|
88
|
+
console.warn(
|
|
89
|
+
`TMDataGrid: column "${columnId}" resolves faceted options while the server owns the rows - the distinct values of one page are not the distinct values of the result set. Pass meta.options as a list or a function instead.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
48
93
|
function addFacetValue(target: Set<string>, value: unknown): void {
|
|
49
94
|
if (value === null || value === undefined || value === "") return;
|
|
50
95
|
target.add(String(value));
|
|
@@ -68,6 +113,7 @@ export function resolveColumnOptions({
|
|
|
68
113
|
if (!source) return [];
|
|
69
114
|
|
|
70
115
|
if (source === "faceted") {
|
|
116
|
+
warnFacetedUnderManualMode(table, column.id);
|
|
71
117
|
const values = new Set<string>();
|
|
72
118
|
for (const key of column.getFacetedUniqueValues().keys()) {
|
|
73
119
|
if (Array.isArray(key)) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Column,
|
|
3
|
+
ColumnPinningPosition,
|
|
4
|
+
ColumnPinningState,
|
|
5
|
+
} from "@tanstack/react-table";
|
|
2
6
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
7
|
import { isColumnReorderable, isGeneratedColumn } from "./columnUtils";
|
|
4
8
|
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
@@ -10,11 +14,11 @@ type GridTable = TMDataGridTable<TMDataGridRowData>;
|
|
|
10
14
|
* The lane a column renders in.
|
|
11
15
|
*
|
|
12
16
|
* Pinning splits the grid into three, and TanStack sequences them from two
|
|
13
|
-
* different state slices: `columnPinning.
|
|
17
|
+
* different state slices: `columnPinning.start` / `.end` order the pinned
|
|
14
18
|
* lanes, `columnOrder` orders the centre. A move therefore always stays inside
|
|
15
19
|
* one lane - moving a column into another one would be a pin, not a reorder.
|
|
16
20
|
*/
|
|
17
|
-
export type TMDataGridColumnRegion = "
|
|
21
|
+
export type TMDataGridColumnRegion = "start" | "center" | "end";
|
|
18
22
|
|
|
19
23
|
/** Which edge of the column under the pointer a dragged column lands on. */
|
|
20
24
|
export type TMDataGridDropSide = "before" | "after";
|
|
@@ -23,11 +27,26 @@ export function getColumnRegion(
|
|
|
23
27
|
columnPinning: ColumnPinningState,
|
|
24
28
|
columnId: string,
|
|
25
29
|
): TMDataGridColumnRegion {
|
|
26
|
-
if (columnPinning.
|
|
27
|
-
if (columnPinning.
|
|
30
|
+
if (columnPinning.start.includes(columnId)) return "start";
|
|
31
|
+
if (columnPinning.end.includes(columnId)) return "end";
|
|
28
32
|
return "center";
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* TanStack's pinned regions are logical: `start` and `end` follow the writing
|
|
37
|
+
* direction. The grid's own vocabulary - its sticky CSS, the `data-pinned`
|
|
38
|
+
* attribute, the column menu's "Pin left" and "Pin right" - is physical, and
|
|
39
|
+
* the grid renders left to right only. This translates between the pair:
|
|
40
|
+
* `left` maps to `start` and `right` to `end`.
|
|
41
|
+
*/
|
|
42
|
+
export function pinningPositionOfSide(
|
|
43
|
+
side: "left" | "right" | false,
|
|
44
|
+
): ColumnPinningPosition {
|
|
45
|
+
if (side === "left") return "start";
|
|
46
|
+
if (side === "right") return "end";
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/** Moves one id next to another, leaving every other id in place. */
|
|
32
51
|
function moveInList(
|
|
33
52
|
ids: ReadonlyArray<string>,
|
|
@@ -87,7 +106,7 @@ export function moveColumn({
|
|
|
87
106
|
* Puts the generated lanes back on the outside of both pinned lanes: the ones
|
|
88
107
|
* on the left before every consumer column, the edit lane after all of them.
|
|
89
108
|
*
|
|
90
|
-
* `column.pin("
|
|
109
|
+
* `column.pin("end")` appends, so pinning a column right would otherwise drop
|
|
91
110
|
* it outside the edit lane, so the row's Save and Delete would no longer be
|
|
92
111
|
* last in the row. Pinning left appends too, which is already correct there,
|
|
93
112
|
* but the same pass keeps both lanes in place whatever a consumer writes into
|
|
@@ -101,13 +120,13 @@ export function keepGeneratedColumnsOutermost(
|
|
|
101
120
|
): ColumnPinningState {
|
|
102
121
|
const generated = (id: string) => isGeneratedColumn(id);
|
|
103
122
|
return {
|
|
104
|
-
|
|
105
|
-
...pinning.
|
|
106
|
-
...pinning.
|
|
123
|
+
start: [
|
|
124
|
+
...pinning.start.filter(generated),
|
|
125
|
+
...pinning.start.filter((id) => !generated(id)),
|
|
107
126
|
],
|
|
108
|
-
|
|
109
|
-
...pinning.
|
|
110
|
-
...pinning.
|
|
127
|
+
end: [
|
|
128
|
+
...pinning.end.filter((id) => !generated(id)),
|
|
129
|
+
...pinning.end.filter(generated),
|
|
111
130
|
],
|
|
112
131
|
};
|
|
113
132
|
}
|
|
@@ -117,8 +136,8 @@ function getRegionColumns(
|
|
|
117
136
|
table: GridTable,
|
|
118
137
|
region: TMDataGridColumnRegion,
|
|
119
138
|
): Array<GridColumn> {
|
|
120
|
-
if (region === "
|
|
121
|
-
if (region === "
|
|
139
|
+
if (region === "start") return table.getStartVisibleLeafColumns();
|
|
140
|
+
if (region === "end") return table.getEndVisibleLeafColumns();
|
|
122
141
|
return table.getCenterVisibleLeafColumns();
|
|
123
142
|
}
|
|
124
143
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Row } from "@tanstack/react-table";
|
|
2
2
|
import {
|
|
3
3
|
getDefaultOperator,
|
|
4
|
+
getOperatorsForType,
|
|
4
5
|
type TMDataGridColumnType,
|
|
5
6
|
type TMDataGridFilterOperator,
|
|
6
7
|
} from "./filterOperators";
|
|
@@ -37,17 +38,36 @@ export function getColumnType(column: ColumnLike): TMDataGridColumnType {
|
|
|
37
38
|
return column.columnDef.meta?.type ?? "string";
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The operators this column offers: the type's list, narrowed to
|
|
43
|
+
* `meta.filter.operators` when the column declares one. The type's order is
|
|
44
|
+
* kept so the menu reads the same on every column; an operator the type does
|
|
45
|
+
* not offer is dropped, and an allowlist that leaves nothing falls back to the
|
|
46
|
+
* type's full list rather than an empty menu.
|
|
47
|
+
*/
|
|
48
|
+
export function getColumnOperators(
|
|
49
|
+
column: ColumnLike,
|
|
50
|
+
): readonly TMDataGridFilterOperator[] {
|
|
51
|
+
const offered = getOperatorsForType(getColumnType(column));
|
|
52
|
+
const allowed = column.columnDef.meta?.filter?.operators;
|
|
53
|
+
if (!allowed) return offered;
|
|
54
|
+
const narrowed = offered.filter((operator) => allowed.includes(operator));
|
|
55
|
+
return narrowed.length > 0 ? narrowed : offered;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
/**
|
|
41
59
|
* The operator a fresh filter on this column starts with -
|
|
42
|
-
* `meta.filter.defaultOperator`, else the type's default
|
|
60
|
+
* `meta.filter.defaultOperator`, else the type's default where the column
|
|
61
|
+
* offers it, else the first operator it does offer.
|
|
43
62
|
*/
|
|
44
63
|
export function getColumnDefaultOperator(
|
|
45
64
|
column: ColumnLike,
|
|
46
65
|
): TMDataGridFilterOperator {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
66
|
+
const declared = column.columnDef.meta?.filter?.defaultOperator;
|
|
67
|
+
if (declared) return declared;
|
|
68
|
+
const offered = getColumnOperators(column);
|
|
69
|
+
const typeDefault = getDefaultOperator(getColumnType(column));
|
|
70
|
+
return offered.includes(typeDefault) ? typeDefault : offered[0];
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
/**
|
|
@@ -139,3 +159,22 @@ export function isColumnReorderable(column: ColumnLike): boolean {
|
|
|
139
159
|
if (column.columnDef.meta?.enableOrdering === false) return false;
|
|
140
160
|
return column.parent === undefined;
|
|
141
161
|
}
|
|
162
|
+
|
|
163
|
+
/** The list length from which a column chooser shows its search box. */
|
|
164
|
+
export const COLUMN_SEARCH_MIN = 6;
|
|
165
|
+
|
|
166
|
+
/** `"auto"` shows the search box from `COLUMN_SEARCH_MIN` columns. */
|
|
167
|
+
export type TMDataGridColumnSearchable = boolean | "auto";
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Whether a column list shows a search box. A handful of columns is read at
|
|
171
|
+
* a glance, so `"auto"` keeps the input out of the way until the list is long
|
|
172
|
+
* enough to need one.
|
|
173
|
+
*/
|
|
174
|
+
export function showColumnSearch(
|
|
175
|
+
searchable: TMDataGridColumnSearchable,
|
|
176
|
+
count: number,
|
|
177
|
+
): boolean {
|
|
178
|
+
if (searchable === "auto") return count >= COLUMN_SEARCH_MIN;
|
|
179
|
+
return searchable;
|
|
180
|
+
}
|