@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.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /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-panel row - what renders when a
13
- * column declares no `meta.filter.control`. Shaped by the operator: a
14
- * multi-select for the set operators, a From/To pair for `between`, a Yes/No
15
- * dropdown for booleans, a typed input otherwise.
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
- operator,
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
- label={labels.filterFrom}
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
- label={labels.filterTo}
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={labels.filterValue}
94
+ {...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
76
95
  size={size}
77
- w={180}
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={labels.filterValue}
109
+ {...filterFieldProps(args, { label: labels.filterValue, width: 180 })}
92
110
  size={size}
93
- w={180}
94
- comboboxProps={{ withinPortal: false }}
111
+ comboboxProps={comboboxProps}
95
112
  data-dg-part="filter-value"
96
113
  disabled={!needsValue}
97
114
  clearable
98
- placeholder={needsValue ? labels.filterValuePlaceholder : ""}
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={labels.filterValue}
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={needsValue ? labels.filterValuePlaceholder : ""}
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
+ }
@@ -16,6 +16,7 @@ export {
16
16
  IconChevronRight as ChevronRightIcon,
17
17
  IconColumns3 as ColumnsIcon,
18
18
  IconDotsVertical as DotsVerticalIcon,
19
+ IconDownload as DownloadIcon,
19
20
  IconEyeOff as EyeOffIcon,
20
21
  IconFilter as FilterIcon,
21
22
  IconFoldDown as ExpandAllIcon,
@@ -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
- const range = document.createRange();
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 instanceof HTMLElement)) continue;
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
- /** The commit policy, or `null` while editing is off. */
123
+ /** What counts as a commit, or `null` while editing is off. */
124
124
  editMode: TMDataGridEditMode | null;
125
125
  /**
126
- * Whether a confirmed entry row stays pinned in the sticky entry block
127
- * until Save all - `editing.newRowsSticky`, draft mode only. Off by
128
- * default: a confirmed row joins the scrolling flow instead, so entering
129
- * many rows cannot fill the viewport with sticky chrome.
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?.mode === "draft" &&
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 { Column, ColumnPinningState } from "@tanstack/react-table";
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.left` / `.right` order the pinned
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 = "left" | "center" | "right";
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.left.includes(columnId)) return "left";
27
- if (columnPinning.right.includes(columnId)) return "right";
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("right")` appends, so pinning a column right would otherwise drop
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
- left: [
105
- ...pinning.left.filter(generated),
106
- ...pinning.left.filter((id) => !generated(id)),
123
+ start: [
124
+ ...pinning.start.filter(generated),
125
+ ...pinning.start.filter((id) => !generated(id)),
107
126
  ],
108
- right: [
109
- ...pinning.right.filter((id) => !generated(id)),
110
- ...pinning.right.filter(generated),
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 === "left") return table.getLeftVisibleLeafColumns();
121
- if (region === "right") return table.getRightVisibleLeafColumns();
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
- return (
48
- column.columnDef.meta?.filter?.defaultOperator ??
49
- getDefaultOperator(getColumnType(column))
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
+ }