@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1

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 (157) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +269 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +49 -48
  47. package/skills/columns/SKILL.md +125 -70
  48. package/skills/columns/references/columns-api.md +59 -0
  49. package/skills/data/SKILL.md +112 -18
  50. package/skills/editing/SKILL.md +76 -42
  51. package/skills/editing/references/common-mistakes.md +77 -69
  52. package/skills/editing/references/editing-api.md +25 -20
  53. package/skills/editing/references/editors-and-validation.md +80 -18
  54. package/skills/filtering/SKILL.md +155 -41
  55. package/skills/getting-started/SKILL.md +116 -16
  56. package/skills/grouping/SKILL.md +31 -16
  57. package/skills/migrating-to-2/SKILL.md +244 -0
  58. package/skills/options/SKILL.md +24 -12
  59. package/skills/rows/SKILL.md +22 -18
  60. package/skills/rows/references/rows-api.md +10 -6
  61. package/skills/server-side/SKILL.md +170 -17
  62. package/skills/testing/SKILL.md +150 -32
  63. package/skills/testing-components/SKILL.md +230 -0
  64. package/skills/testing-editing/SKILL.md +240 -0
  65. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  66. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  68. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  70. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  71. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  72. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  73. package/src/components/TMDataGridExportPicker.module.css +77 -0
  74. package/src/components/TMDataGridExportPicker.tsx +234 -0
  75. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  76. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  78. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  79. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  80. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  81. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  83. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  84. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  85. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  86. package/src/components/TMDataGridMenu.tsx +357 -0
  87. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  89. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  90. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  91. package/src/components/TMDataGridToolbar.tsx +181 -0
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  98. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  99. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  100. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  101. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  103. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  104. package/src/components/filters/controlLayout.ts +32 -0
  105. package/src/components/filters/filterControlFor.ts +65 -0
  106. package/src/components/generatedColumns.tsx +187 -0
  107. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  108. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  109. package/src/components/useHideableColumns.ts +52 -0
  110. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  118. package/src/core/export.ts +704 -0
  119. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  120. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  121. package/src/core/filterSurface.ts +99 -0
  122. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  123. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  124. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  125. package/src/core/pageReset.ts +120 -0
  126. package/src/core/pagination.ts +81 -0
  127. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  128. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  129. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  130. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  131. package/src/useTMDataGridExport.ts +78 -0
  132. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  133. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  134. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  135. package/src/tmdatagrid/core/cellExport.ts +0 -320
  136. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  156. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  157. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -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
+ }
@@ -0,0 +1,187 @@
1
+ import type { ColumnDef, RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridFeatures } from "../useTMDataGrid";
3
+ import {
4
+ DETAILS_COLUMN_ID,
5
+ DetailsCell,
6
+ DetailsHeader,
7
+ } from "./TMDataGridDetailsColumn";
8
+ import { EDIT_COLUMN_ID, EditLaneCell } from "./TMDataGridEditColumn";
9
+ import { GROUP_COLUMN_ID, GroupCell, GroupHeader } from "./TMDataGridGroupColumn";
10
+ import {
11
+ SELECT_COLUMN_ID,
12
+ SelectAllHeader,
13
+ SelectRowCheckbox,
14
+ } from "./TMDataGridSelectColumn";
15
+
16
+ /**
17
+ * The generated checkbox column, prepended under
18
+ * `selectionMode: "checkbox"` (the default) or `"checkboxAndHighlight"`.
19
+ */
20
+ export function createSelectColumn<TData extends RowData>(
21
+ label = "Checkbox selection",
22
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
23
+ return {
24
+ id: SELECT_COLUMN_ID,
25
+ meta: {
26
+ label,
27
+ align: "center",
28
+ // Structurally the first column; it also anchors the left pinned lane, so
29
+ // no other column can be moved in front of it.
30
+ enableOrdering: false,
31
+ },
32
+ // A system lane: as wide as the control it holds and no wider. Fixed at
33
+ // every scale - the control does not grow with the font size, so neither
34
+ // should its track.
35
+ size: 36,
36
+ minSize: 36,
37
+ maxSize: 36,
38
+ enableResizing: false,
39
+ enableSorting: false,
40
+ enableColumnFilter: false,
41
+ enableGlobalFilter: false,
42
+ // Not a column the user chose, so not one they can switch off: hiding the
43
+ // lane would take the grid's only way to select a row with it, with the
44
+ // row-selection state left behind and no way back to it. Keeping it out of
45
+ // "Manage columns" follows from this - the panel lists what can be hidden.
46
+ enableHiding: false,
47
+ // Structurally pinned to the left; users shouldn't be able to move it.
48
+ enablePinning: false,
49
+ header: ({ table }) => <SelectAllHeader table={table} />,
50
+ cell: ({ row }) => <SelectRowCheckbox row={row} />,
51
+ // Every cell on a group row that is not the grouped column counts as
52
+ // aggregated, this lane included, and an aggregated cell with nothing
53
+ // declared renders blank. Without this the checkbox would disappear from
54
+ // exactly the rows that select a whole group. See renderCellContent.
55
+ aggregatedCell: ({ row }) => <SelectRowCheckbox row={row} />,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The generated tree column, prepended whenever grouping is enabled and hidden
61
+ * again while `grouping` is empty - see the visibility effect in
62
+ * `useTMDataGrid`.
63
+ *
64
+ * It exists because TanStack ships no auto group column: `groupedColumnMode:
65
+ * "remove"` takes the grouped column out of the grid, so something has to hold
66
+ * the tree. Modelled on the checkbox column, which is generated the same way.
67
+ *
68
+ * Not groupable itself, and nothing had to be written to make that true -
69
+ * `column.getCanGroup()` requires an `accessorFn`, which a display column has
70
+ * no reason to have.
71
+ */
72
+ export function createGroupColumn<TData extends RowData>(
73
+ label = "Group",
74
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
75
+ return {
76
+ id: GROUP_COLUMN_ID,
77
+ meta: {
78
+ label,
79
+ // Structurally the first column after the checkbox lane.
80
+ enableOrdering: false,
81
+ },
82
+ size: 260,
83
+ minSize: 180,
84
+ enableSorting: false,
85
+ enableColumnFilter: false,
86
+ enableGlobalFilter: false,
87
+ // Keeps it out of the columns panel and out of the header menu: its
88
+ // visibility is not the user's to set, it follows the grouping state.
89
+ enableHiding: false,
90
+ // Structurally pinned to the left; users shouldn't be able to move it.
91
+ enablePinning: false,
92
+ cell: ({ row }) => <GroupCell row={row} />,
93
+ // A group row has subRows, so every cell on it that is not the grouped
94
+ // column reports `getIsAggregated()` - this lane included. Without an
95
+ // `aggregatedCell` the body would take that as "nothing to summarise" and
96
+ // render the tree lane blank on exactly the rows it exists for.
97
+ aggregatedCell: ({ row }) => <GroupCell row={row} />,
98
+ header: ({ table }) => <GroupHeader table={table} />,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * The generated details lane, prepended whenever `renderDetails` is set.
104
+ *
105
+ * Structural, like the checkbox and tree columns: fixed width, pinned to the
106
+ * left after both of them, not hideable, not movable and not resizable. Moving
107
+ * or hiding the toggle would leave rows with panels that cannot be opened.
108
+ *
109
+ * Last of the three because it acts on a single record.
110
+ *
111
+ * A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
112
+ * interface, and this lane is only the control the grid ships.
113
+ */
114
+ export function createDetailsColumn<TData extends RowData>(
115
+ label = "Details",
116
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
117
+ return {
118
+ id: DETAILS_COLUMN_ID,
119
+ meta: {
120
+ label,
121
+ align: "center",
122
+ // Structurally the last of the generated lanes.
123
+ enableOrdering: false,
124
+ },
125
+ // A system lane: as wide as the control it holds and no wider. Fixed at
126
+ // every scale - the control does not grow with the font size, so neither
127
+ // should its track.
128
+ size: 36,
129
+ minSize: 36,
130
+ maxSize: 36,
131
+ enableResizing: false,
132
+ enableSorting: false,
133
+ enableColumnFilter: false,
134
+ enableGlobalFilter: false,
135
+ // Its visibility is not the user's to set: hiding it would strand every
136
+ // panel behind a control that is no longer there.
137
+ enableHiding: false,
138
+ // Structurally pinned to the left; users shouldn't be able to move it.
139
+ enablePinning: false,
140
+ header: ({ table }) => <DetailsHeader table={table} />,
141
+ cell: ({ row }) => <DetailsCell row={row} />,
142
+ // Deliberately no `aggregatedCell`: on a group row every cell outside the
143
+ // grouped column counts as aggregated, and blank is the right answer here -
144
+ // groups expand into their rows, not into a panel.
145
+ };
146
+ }
147
+
148
+ /**
149
+ * The generated edit lane, appended and pinned right - the row's Save at the
150
+ * end of the row under `mode: "row"`, the state marker and revert under
151
+ * `editing.draft`, mirroring the checkbox lane's build on the left.
152
+ */
153
+ export function createEditColumn<TData extends RowData>(
154
+ label = "Edit",
155
+ /** A draft lane holds three controls where the rest hold two. */
156
+ wide = false,
157
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
158
+ const width = wide ? 88 : 64;
159
+ return {
160
+ id: EDIT_COLUMN_ID,
161
+ meta: {
162
+ label,
163
+ align: "center",
164
+ enableOrdering: false,
165
+ },
166
+ // Wide enough for the pair (or draft's trio) it holds while editing.
167
+ size: width,
168
+ minSize: width,
169
+ maxSize: width,
170
+ enableResizing: false,
171
+ enableSorting: false,
172
+ enableColumnFilter: false,
173
+ enableGlobalFilter: false,
174
+ // The row's Save, Cancel and Delete live here, so hiding the lane would
175
+ // strand an open row with no way to commit or discard it. Same rule as the
176
+ // checkbox lane: chrome the grid generates is not a user setting.
177
+ enableHiding: false,
178
+ // Structurally pinned to the right; not movable.
179
+ enablePinning: false,
180
+ header: () => null,
181
+ cell: ({ row }) => <EditLaneCell row={row} />,
182
+ // Group rows: same reasoning as the checkbox lane - without this the
183
+ // cell renders blank on aggregated rows, but here blank is also correct,
184
+ // so the aggregated cell renders the same (null for groups).
185
+ aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
186
+ };
187
+ }
@@ -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) +
@@ -1,4 +1,4 @@
1
- import type { Column, Row } from "@tanstack/react-table";
1
+ import type { Column, Row, RowData } from "@tanstack/react-table";
2
2
  import type { TMDataGridRowData } from "../TMDataGridContext";
3
3
  import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
4
4
 
@@ -17,17 +17,21 @@ export type TMDataGridOption = {
17
17
  group?: string;
18
18
  };
19
19
 
20
- export type TMDataGridOptionsArgs = {
21
- table: TMDataGridTable<TMDataGridRowData>;
22
- column: Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
20
+ /**
21
+ * What an options function is called with. `TData` is the row type of the
22
+ * column helper the column was declared with.
23
+ */
24
+ export type TMDataGridOptionsArgs<TData extends RowData = TMDataGridRowData> = {
25
+ table: TMDataGridTable<TData>;
26
+ column: Column<TMDataGridFeatures, TData, unknown>;
23
27
  /** The row an editor is standing on. Absent when the filter panel asks. */
24
- row?: Row<TMDataGridFeatures, TMDataGridRowData>;
28
+ row?: Row<TMDataGridFeatures, TData>;
25
29
  /**
26
30
  * Source to use when the column declares none. The filter panel passes
27
31
  * `"faceted"` so a select column with no `meta.options` still offers the
28
32
  * values that exist, rather than an empty dropdown.
29
33
  */
30
- fallback?: TMDataGridOptionsSource;
34
+ fallback?: TMDataGridOptionsSource<TData>;
31
35
  };
32
36
 
33
37
  /**
@@ -40,10 +44,57 @@ export type TMDataGridOptionsArgs = {
40
44
  * | `"faceted"` | low-cardinality data columns: the distinct values present, via `getFacetedUniqueValues` |
41
45
  * | function | large or contextual sets - `row` is set when an editor asks, so options can depend on the record (city given country) |
42
46
  */
43
- export type TMDataGridOptionsSource =
47
+ export type TMDataGridOptionsSource<TData extends RowData = TMDataGridRowData> =
44
48
  | ReadonlyArray<TMDataGridOption | string>
45
49
  | "faceted"
46
- | ((args: TMDataGridOptionsArgs) => ReadonlyArray<TMDataGridOption | string>);
50
+ | ((
51
+ args: TMDataGridOptionsArgs<TData>,
52
+ ) => ReadonlyArray<TMDataGridOption | string>);
53
+
54
+ /**
55
+ * Columns already warned about, per grid - so a warning fires once and a
56
+ * test's grid is not silenced by another test's. Keyed on `table.store`, not
57
+ * on the table: `useTable` returns a fresh table object every render, while
58
+ * the store is created once and shared by every render's copy.
59
+ */
60
+ const warnedFaceted = new WeakMap<object, Set<string>>();
61
+
62
+ /**
63
+ * Faceted options read the distinct values in `data`, which under
64
+ * `manualFiltering` or `manualPagination` is whatever the server sent for the
65
+ * current page. The dropdown then offers the values that happen to be on the
66
+ * page the user is looking at, and looks correct while being wrong - so it is
67
+ * said out loud, once per column.
68
+ *
69
+ * Fires from render, unlike the library's other warnings: the fallback form
70
+ * of `"faceted"` only exists at resolve time, which is render. The guard
71
+ * makes it once per grid regardless - a StrictMode double render or a
72
+ * discarded concurrent render marks the set the same way a committed one
73
+ * does. H3 in the backlog folds it into the diagnostics mechanism with the
74
+ * rest.
75
+ */
76
+ function warnFacetedUnderManualMode(
77
+ table: TMDataGridTable<TMDataGridRowData>,
78
+ columnId: string,
79
+ ): void {
80
+ if (
81
+ table.options.manualFiltering !== true &&
82
+ table.options.manualPagination !== true
83
+ ) {
84
+ return;
85
+ }
86
+ const store = table.store as object;
87
+ let warned = warnedFaceted.get(store);
88
+ if (warned === undefined) {
89
+ warned = new Set();
90
+ warnedFaceted.set(store, warned);
91
+ }
92
+ if (warned.has(columnId)) return;
93
+ warned.add(columnId);
94
+ console.warn(
95
+ `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.`,
96
+ );
97
+ }
47
98
 
48
99
  function addFacetValue(target: Set<string>, value: unknown): void {
49
100
  if (value === null || value === undefined || value === "") return;
@@ -68,6 +119,7 @@ export function resolveColumnOptions({
68
119
  if (!source) return [];
69
120
 
70
121
  if (source === "faceted") {
122
+ warnFacetedUnderManualMode(table, column.id);
71
123
  const values = new Set<string>();
72
124
  for (const key of column.getFacetedUniqueValues().keys()) {
73
125
  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