@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
@@ -1,5 +1,12 @@
1
- import { ActionIcon, Group, Select, Text, Tooltip } from "@mantine/core";
2
- import type { RowData } from "@tanstack/react-table";
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ type BoxProps,
5
+ Group,
6
+ Select,
7
+ Text,
8
+ Tooltip,
9
+ } from "@mantine/core";
3
10
  import { useMemo, type ReactNode } from "react";
4
11
  import classes from "./TMDataGridFooter.module.css";
5
12
  import { useTMDataGridContext } from "../TMDataGridContext";
@@ -7,49 +14,10 @@ import { getGridCapabilities } from "../core/capabilities";
7
14
  import { useSettledTableState } from "../core/useSettledTableState";
8
15
  import { isPagingActive } from "../core/rowSelection";
9
16
  import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
10
- import type { TMDataGridTable } from "../useTMDataGrid";
11
-
12
- /** What the pager is showing. The read half of {@link TMDataGridPaginationApi}. */
13
- export type TMDataGridPaginationState = {
14
- pageIndex: number;
15
- pageSize: number;
16
- /** `-1` when a manual grid declares `pageCount: -1` (unknown total). */
17
- pageCount: number;
18
- rowCount: number;
19
- canPreviousPage: boolean;
20
- canNextPage: boolean;
21
- /**
22
- * Whether the pager is slicing anything right now. `false` while a grouping
23
- * is active, which suspends paging - see `isPagingActive`.
24
- */
25
- isPagingActive: boolean;
26
- /** First and last row number on this page, 1-based, for a range label. */
27
- from: number;
28
- to: number;
29
- };
30
-
31
- /** What the pager can do. The write half of {@link TMDataGridPaginationApi}. */
32
- export type TMDataGridPaginationActions = {
33
- setPageIndex: (pageIndex: number) => void;
34
- setPageSize: (pageSize: number) => void;
35
- previousPage: () => void;
36
- nextPage: () => void;
37
- firstPage: () => void;
38
- lastPage: () => void;
39
- };
40
-
41
- /**
42
- * Pagination state and actions, split into the half you read and the half you
43
- * call.
44
- *
45
- * The split is what makes a partial override possible: a consumer replacing
46
- * only the range label reads `state` and never touches `actions`, and one
47
- * replacing only the buttons does the opposite.
48
- */
49
- export type TMDataGridPaginationApi = {
50
- state: TMDataGridPaginationState;
51
- actions: TMDataGridPaginationActions;
52
- };
17
+ import {
18
+ getTMDataGridPaginationApi,
19
+ type TMDataGridPaginationApi,
20
+ } from "../core/pagination";
53
21
 
54
22
  /**
55
23
  * The pre-bound pieces of the built-in pager.
@@ -63,6 +31,12 @@ export type TMDataGridPaginationControls = {
63
31
  PageSize: () => ReactNode;
64
32
  /** The "1–25 of 300" range label. */
65
33
  Range: () => ReactNode;
34
+ /**
35
+ * The "Page 3 of 200" label - what a server-paged grid usually shows in
36
+ * place of a row range. Not in the default footer; put it in a
37
+ * `renderPagination` layout.
38
+ */
39
+ PageNumber: () => ReactNode;
66
40
  /** The previous/next buttons. */
67
41
  Pager: () => ReactNode;
68
42
  };
@@ -72,43 +46,6 @@ export type TMDataGridPaginationSlotArgs = TMDataGridPaginationApi & {
72
46
  Controls: TMDataGridPaginationControls;
73
47
  };
74
48
 
75
- /**
76
- * Reads {@link TMDataGridPaginationApi} off a table. The Footer feeds it to the
77
- * `renderPagination` slot; a pager living outside the Footer can call it with
78
- * the table from `useTMDataGrid` directly.
79
- *
80
- * `Controls` are not here: they are components bound to the grid context, and
81
- * this function takes only a table.
82
- */
83
- export function getTMDataGridPaginationApi<TData extends RowData>(
84
- table: TMDataGridTable<TData>,
85
- isPaging = true,
86
- ): TMDataGridPaginationApi {
87
- const { pageIndex, pageSize } = table.store.state.pagination;
88
- const rowCount = table.getRowCount();
89
- return {
90
- state: {
91
- pageIndex,
92
- pageSize,
93
- pageCount: table.getPageCount(),
94
- rowCount,
95
- canPreviousPage: table.getCanPreviousPage(),
96
- canNextPage: table.getCanNextPage(),
97
- isPagingActive: isPaging,
98
- from: rowCount === 0 ? 0 : pageIndex * pageSize + 1,
99
- to: Math.min(rowCount, (pageIndex + 1) * pageSize),
100
- },
101
- actions: {
102
- setPageIndex: (index) => table.setPageIndex(index),
103
- setPageSize: (size) => table.setPageSize(size),
104
- previousPage: () => table.previousPage(),
105
- nextPage: () => table.nextPage(),
106
- firstPage: () => table.firstPage(),
107
- lastPage: () => table.lastPage(),
108
- },
109
- };
110
- }
111
-
112
49
  /*
113
50
  * The three controls are module-scope components on purpose. Defined inside
114
51
  * the Footer they would be a new component type on every render, so React
@@ -182,6 +119,26 @@ function PaginationRange() {
182
119
  );
183
120
  }
184
121
 
122
+ function PaginationPageNumber() {
123
+ const { table, features, labels, controlSize } = useTMDataGridContext();
124
+ useSettledTableState(table.store);
125
+ const paging = isPagingActive(table, features);
126
+ const { pageIndex } = table.store.state.pagination;
127
+
128
+ return (
129
+ <Text span size={controlSize} c="dimmed" data-dg-part="page-number">
130
+ {paging
131
+ ? labels.pageNumber({
132
+ page: pageIndex + 1,
133
+ pageCount: table.getPageCount(),
134
+ })
135
+ : // Same reasoning as the range label: nothing is being sliced, so a
136
+ // page number would be a lie.
137
+ labels.groupedAllRows(table.getFilteredRowModel().rows.length)}
138
+ </Text>
139
+ );
140
+ }
141
+
185
142
  function PaginationPager() {
186
143
  const { table, features, labels } = useTMDataGridContext();
187
144
  useSettledTableState(table.store);
@@ -213,7 +170,8 @@ function PaginationPager() {
213
170
  );
214
171
  }
215
172
 
216
- export type TMDataGridFooterProps = {
173
+ /** Mantine's style props (`mt="sm"`, `px="md"`) are set on the footer bar. */
174
+ export type TMDataGridFooterProps = BoxProps & {
217
175
  pageSizeOptions?: ReadonlyArray<number>;
218
176
  /**
219
177
  * Replaces the built-in pager, and is handed the pieces of it.
@@ -223,7 +181,7 @@ export type TMDataGridFooterProps = {
223
181
  * renderPagination={({ state, actions, Controls }) => (
224
182
  * <Group>
225
183
  * <Controls.PageSize />
226
- * <MyJumpToPage page={state.pageIndex} onJump={actions.setPageIndex} />
184
+ * <Controls.PageNumber />
227
185
  * <Controls.Pager />
228
186
  * </Group>
229
187
  * )}
@@ -248,7 +206,10 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
248
206
  export function TMDataGridFooter({
249
207
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
250
208
  renderPagination,
209
+ className,
210
+ ...others
251
211
  }: TMDataGridFooterProps) {
212
+ const footerClassName = [classes.footer, className].filter(Boolean).join(" ");
252
213
  const { table, features, labels } = useTMDataGridContext();
253
214
  useSettledTableState(table.store);
254
215
 
@@ -258,6 +219,7 @@ export function TMDataGridFooter({
258
219
  () => ({
259
220
  PageSize: () => <PaginationPageSize pageSizeOptions={pageSizeOptions} />,
260
221
  Range: PaginationRange,
222
+ PageNumber: PaginationPageNumber,
261
223
  Pager: PaginationPager,
262
224
  }),
263
225
  [pageSizeOptions],
@@ -273,9 +235,9 @@ export function TMDataGridFooter({
273
235
  if (renderPagination) {
274
236
  const { state, actions } = getTMDataGridPaginationApi(table, paging);
275
237
  return (
276
- <div data-dg-part="footer" className={classes.footer}>
238
+ <Box data-dg-part="footer" className={footerClassName} {...others}>
277
239
  {renderPagination({ state, actions, Controls })}
278
- </div>
240
+ </Box>
279
241
  );
280
242
  }
281
243
 
@@ -288,17 +250,18 @@ export function TMDataGridFooter({
288
250
  w={260}
289
251
  position="top"
290
252
  >
291
- <div
253
+ <Box
292
254
  data-dg-part="footer"
293
- className={classes.footer}
294
- data-paging-suspended={!paging}
255
+ className={footerClassName}
256
+ data-paging-suspended={!paging || undefined}
257
+ {...others}
295
258
  >
296
259
  {/* The default render is the controls in order - which is what makes
297
260
  `renderPagination` a rearrangement rather than a rebuild. */}
298
261
  <Controls.PageSize />
299
262
  <Controls.Range />
300
263
  <Controls.Pager />
301
- </div>
264
+ </Box>
302
265
  </Tooltip>
303
266
  );
304
267
  }
@@ -3,13 +3,13 @@ import {
3
3
  useBodyControlTabIndex,
4
4
  useTMDataGridContext,
5
5
  } from "../TMDataGridContext";
6
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
6
+ import type { Row, RowData } from "@tanstack/react-table";
7
7
  import { useSelector } from "@tanstack/react-store";
8
8
  import { shallow } from "@tanstack/store";
9
9
  import classes from "./TMDataGridGroupColumn.module.css";
10
10
  import { ChevronRightIcon } from "./icons";
11
11
  import { getColumnLabel } from "../core/columnUtils";
12
- import { getGroupDataRows } from "../core/grouping";
12
+ import { formatGroupValue, getGroupDataRows } from "../core/grouping";
13
13
  import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
14
14
 
15
15
  export const GROUP_COLUMN_ID = "__group__";
@@ -17,27 +17,6 @@ export const GROUP_COLUMN_ID = "__group__";
17
17
  /** Indent added per level of nesting, in px. */
18
18
  const INDENT_STEP = 16;
19
19
 
20
- /** Shown for a group whose value is empty - `String(null)` would read as "null". */
21
- const BLANK_GROUP_LABEL = "(Blank)";
22
-
23
- /**
24
- * How a grouping value is written into the tree cell.
25
- *
26
- * Deliberately not the grouped column's own `cell` renderer: that renderer is
27
- * written for a data row and is free to reach into `row.original`, which on a
28
- * group row is the first leaf's record rather than anything about the group.
29
- */
30
- export function formatGroupValue(
31
- value: unknown,
32
- blankLabel = BLANK_GROUP_LABEL,
33
- ): string {
34
- if (value === null || value === undefined || value === "") {
35
- return blankLabel;
36
- }
37
- if (value instanceof Date) return value.toLocaleDateString();
38
- return String(value);
39
- }
40
-
41
20
  /**
42
21
  * The tree cell: chevron, group value and leaf count, indented by depth.
43
22
  *
@@ -46,7 +25,7 @@ export function formatGroupValue(
46
25
  * `row` identity survives an expand, so the React Compiler would cache the call
47
26
  * along with it and the chevron would never turn. See TMDataGridSelectColumn.
48
27
  */
49
- function GroupCell<TData extends RowData>({
28
+ export function GroupCell<TData extends RowData>({
50
29
  row,
51
30
  }: {
52
31
  row: Row<TMDataGridFeatures, TData>;
@@ -82,7 +61,7 @@ function GroupCell<TData extends RowData>({
82
61
  row.toggleExpanded();
83
62
  }}
84
63
  >
85
- <span className={classes.chevron} data-expanded={expanded}>
64
+ <span className={classes.chevron} data-expanded={expanded || undefined}>
86
65
  <ChevronRightIcon size={16} stroke={1.6} />
87
66
  </span>
88
67
  <span className={classes.groupLabel}>{label}</span>
@@ -99,7 +78,7 @@ function GroupCell<TData extends RowData>({
99
78
  * Names the columns currently grouped on, so the lane says what it is showing
100
79
  * rather than a static "Group".
101
80
  */
102
- function GroupHeader<TData extends RowData>({
81
+ export function GroupHeader<TData extends RowData>({
103
82
  table,
104
83
  }: {
105
84
  table: TMDataGridTable<TData>;
@@ -117,46 +96,3 @@ function GroupHeader<TData extends RowData>({
117
96
  })
118
97
  .join(" / ");
119
98
  }
120
-
121
- /**
122
- * The generated tree column, prepended whenever grouping is enabled and hidden
123
- * again while `grouping` is empty - see the visibility effect in
124
- * `useTMDataGrid`.
125
- *
126
- * It exists because TanStack ships no auto group column: `groupedColumnMode:
127
- * "remove"` takes the grouped column out of the grid, so something has to hold
128
- * the tree. Modelled on the checkbox column, which is generated the same way.
129
- *
130
- * Not groupable itself, and nothing had to be written to make that true -
131
- * `column.getCanGroup()` requires an `accessorFn`, which a display column has
132
- * no reason to have.
133
- */
134
- export function createGroupColumn<TData extends RowData>(
135
- label = "Group",
136
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
137
- return {
138
- id: GROUP_COLUMN_ID,
139
- meta: {
140
- label,
141
- // Structurally the first column after the checkbox lane.
142
- enableOrdering: false,
143
- },
144
- size: 260,
145
- minSize: 180,
146
- enableSorting: false,
147
- enableColumnFilter: false,
148
- enableGlobalFilter: false,
149
- // Keeps it out of the columns panel and out of the header menu: its
150
- // visibility is not the user's to set, it follows the grouping state.
151
- enableHiding: false,
152
- // Structurally pinned to the left; users shouldn't be able to move it.
153
- enablePinning: false,
154
- cell: ({ row }) => <GroupCell row={row} />,
155
- // A group row has subRows, so every cell on it that is not the grouped
156
- // column reports `getIsAggregated()` - this lane included. Without an
157
- // `aggregatedCell` the body would take that as "nothing to summarise" and
158
- // render the tree lane blank on exactly the rows it exists for.
159
- aggregatedCell: ({ row }) => <GroupCell row={row} />,
160
- header: ({ table }) => <GroupHeader table={table} />,
161
- };
162
- }
@@ -90,11 +90,11 @@
90
90
 
91
91
  /* See the sort ActionIcon: an unsorted column's arrow is an invitation, not a
92
92
  state, so it sits back until the pointer is on it. */
93
- .sortAction[data-sorted="false"] {
93
+ .sortAction:not([data-sorted]) {
94
94
  opacity: 0.55;
95
95
  }
96
96
 
97
- .sortAction[data-sorted="false"]:hover {
97
+ .sortAction:not([data-sorted]):hover {
98
98
  opacity: 1;
99
99
  }
100
100
 
@@ -149,6 +149,16 @@
149
149
  z-index: 1;
150
150
  }
151
151
 
152
+ /* The last column's divider straddles inward instead. Half of a 10px box
153
+ hanging past the last track is 5px of scrollable overflow, which is a
154
+ permanent horizontal scrollbar under a grid whose columns fit exactly. The
155
+ grab area keeps its width, wholly inside the cell, and flex-end keeps the
156
+ visible line on the column edge where every other divider draws it. */
157
+ .headerCell:last-child .columnSeparator {
158
+ transform: none;
159
+ justify-content: flex-end;
160
+ }
161
+
152
162
  .columnSeparatorResizable {
153
163
  cursor: col-resize;
154
164
  touch-action: none;
@@ -12,12 +12,14 @@ import {
12
12
  import type { TMDataGridColumnLayout } from "./TMDataGridTable";
13
13
  import { getColumnCapabilities, getGridCapabilities } from "../core/capabilities";
14
14
  import { autosizeColumn } from "../core/autosize";
15
+ import { isNode } from "../core/dom";
15
16
  import {
16
17
  getColumnRegion,
17
18
  getStepTargetColumn,
18
19
  keepGeneratedColumnsOutermost,
19
20
  moveColumn,
20
21
  moveColumnByStep,
22
+ pinningPositionOfSide,
21
23
  type TMDataGridDropSide,
22
24
  } from "../core/columnOrdering";
23
25
  import {
@@ -46,6 +48,7 @@ import {
46
48
  UngroupIcon,
47
49
  } from "./icons";
48
50
  import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
51
+ import { TMDataGridMenuColumns } from "./TMDataGridMenu";
49
52
  import { type TMDataGridFeatures, openColumnFilter } from "../useTMDataGrid";
50
53
 
51
54
  /** The table as the header cell sees it, after the context type erasure. */
@@ -89,7 +92,7 @@ export function TMDataGridHeaderCell({
89
92
  renderColumnMenuItems?: TMDataGridColumnMenuItemsRenderer;
90
93
  }) {
91
94
  const api = useTMDataGridContext();
92
- const { table, features, labels } = api;
95
+ const { table, features, filters, labels } = api;
93
96
  const column = header.column;
94
97
  const [menuOpened, setMenuOpened] = useState(false);
95
98
  const [contextMenuOpened, setContextMenuOpened] = useState(false);
@@ -191,7 +194,7 @@ export function TMDataGridHeaderCell({
191
194
  function handleDragLeave(event: DragEvent<HTMLDivElement>) {
192
195
  // dragleave also fires when the pointer crosses into a child element.
193
196
  const next = event.relatedTarget;
194
- if (next instanceof Node && event.currentTarget.contains(next)) return;
197
+ if (isNode(next) && event.currentTarget.contains(next)) return;
195
198
  setDropSide(null);
196
199
  }
197
200
 
@@ -270,8 +273,8 @@ export function TMDataGridHeaderCell({
270
273
  */
271
274
  function setPinned(position: "left" | "right" | false) {
272
275
  if (position) freezeRenderedWidths();
273
- column.pin(position);
274
- // `pin("right")` appends, which would leave the column outside the edit
276
+ column.pin(pinningPositionOfSide(position));
277
+ // `pin("end")` appends, which would leave the column outside the edit
275
278
  // lane. Normalising after the pin rather than writing the lane by hand
276
279
  // keeps TanStack's own handling of a header group's leaves.
277
280
  table.setColumnPinning(keepGeneratedColumnsOutermost);
@@ -297,7 +300,9 @@ export function TMDataGridHeaderCell({
297
300
  <Menu.Divider key="sort-divider" />,
298
301
  );
299
302
  }
300
- if (canFilter) {
303
+ // Not under header filters: the item's only job is to reveal a control that
304
+ // is already on screen, one row below this menu.
305
+ if (canFilter && !filters.inHeader) {
301
306
  menuItems.push(
302
307
  <Menu.Item
303
308
  key="filter"
@@ -476,13 +481,19 @@ export function TMDataGridHeaderCell({
476
481
  }
477
482
  if (canManageColumns) {
478
483
  menuItems.push(
479
- <Menu.Item
480
- key="manage"
481
- leftSection={<ColumnsIcon size={16} stroke={1.6} />}
482
- onClick={() => api.ui.actions.setColumnsPanelOpen(true)}
483
- >
484
- {labels.manageColumns}
485
- </Menu.Item>,
484
+ <Menu.Sub key="manage">
485
+ <Menu.Sub.Target>
486
+ <Menu.Sub.Item leftSection={<ColumnsIcon size={16} stroke={1.6} />}>
487
+ {labels.manageColumns}
488
+ </Menu.Sub.Item>
489
+ </Menu.Sub.Target>
490
+ <Menu.Sub.Dropdown>
491
+ {/* No search box: `Menu.Search` registers on the root menu context,
492
+ which switches off type-ahead and the arrow-key handling of every
493
+ dropdown of this menu - the column menu's included. */}
494
+ <TMDataGridMenuColumns searchable={false} />
495
+ </Menu.Sub.Dropdown>
496
+ </Menu.Sub>,
486
497
  );
487
498
  }
488
499
  // A trailing divider reads as a dropped item - drop the divider instead.
@@ -529,11 +540,11 @@ export function TMDataGridHeaderCell({
529
540
  // The same coordinate the body, summary and entry cells carry, so one
530
541
  // selector reaches a column's header and its cells alike.
531
542
  data-column-id={column.id}
532
- data-active={isSorted || isFiltered}
543
+ data-active={isSorted || isFiltered || undefined}
533
544
  data-align={align}
534
545
  // A control lane is a fixed track, so it cannot take the cell padding the
535
546
  // scale grows for text. See isControlColumn.
536
- data-control-column={isControlColumn(column.id)}
547
+ data-control-column={isControlColumn(column.id) || undefined}
537
548
  // Only meaningful on a sortable column: "none" announces that this
538
549
  // header sorts, which is wrong on one that does not.
539
550
  aria-sort={
@@ -558,6 +569,11 @@ export function TMDataGridHeaderCell({
558
569
  onDragLeave={isDropTarget ? handleDragLeave : undefined}
559
570
  onDrop={isDropTarget ? handleDrop : undefined}
560
571
  style={{
572
+ // A group header covers the tracks of the leaves under it. Without the
573
+ // span it would sit in one track and leave the rest of its width with
574
+ // no cell at all - a gap the body rows scroll up through, since the
575
+ // header rows above the body are what hides them.
576
+ gridColumn: header.colSpan > 1 ? `span ${header.colSpan}` : undefined,
561
577
  left: pinnedAt === "left" ? layout.offset : undefined,
562
578
  right: pinnedAt === "right" ? layout.offset : undefined,
563
579
  position: pinnedAt ? "sticky" : undefined,
@@ -599,7 +615,9 @@ export function TMDataGridHeaderCell({
599
615
 
600
616
  {isLeaf && (
601
617
  <div className={classes.headerActions}>
602
- {isFiltered && (
618
+ {/* Dropped under header filters, where the filled-in control below
619
+ is both the indicator and the way in. */}
620
+ {isFiltered && !filters.inHeader && (
603
621
  <ActionIcon
604
622
  className={classes.headerAction}
605
623
  data-pinned-visible="true"
@@ -621,19 +639,24 @@ export function TMDataGridHeaderCell({
621
639
  {canSort && (
622
640
  <ActionIcon
623
641
  className={`${classes.headerAction} ${classes.sortAction}`}
624
- data-pinned-visible={isSorted}
642
+ data-pinned-visible={isSorted || undefined}
625
643
  // The arrow is the only sort indicator now that the title no
626
644
  // longer tints, so it takes the colour while the sort holds. On
627
645
  // an unsorted column it is only the hover affordance, and stays a
628
646
  // faded grey so the two are not confused.
629
- data-sorted={isSorted}
647
+ data-sorted={isSorted || undefined}
630
648
  variant="subtle"
631
649
  color={isSorted ? undefined : "gray"}
632
650
  size="xs"
633
651
  aria-label={labels.sortColumn(label)}
634
652
  data-dg-part="header-sort"
635
653
  data-column-id={column.id}
636
- onClick={column.getToggleSortingHandler()}
654
+ // Stopped here: the header cell around it sorts on click too, and
655
+ // letting the click reach it would advance the sort twice.
656
+ onClick={(event) => {
657
+ event.stopPropagation();
658
+ column.getToggleSortingHandler()?.(event);
659
+ }}
637
660
  >
638
661
  {sortDirection ? (
639
662
  <ArrowDownIcon size={14} stroke={1.6} />
@@ -667,7 +690,7 @@ export function TMDataGridHeaderCell({
667
690
  // Held visible for the right-click menu too: that one opens at
668
691
  // the pointer and is portaled, so the hover that revealed the
669
692
  // actions is lost the moment the pointer enters the dropdown.
670
- data-pinned-visible={menuOpened || contextMenuOpened}
693
+ data-pinned-visible={menuOpened || contextMenuOpened || undefined}
671
694
  variant="subtle"
672
695
  color="gray"
673
696
  size="xs"
@@ -702,18 +725,30 @@ export function TMDataGridHeaderCell({
702
725
  ]
703
726
  .filter(Boolean)
704
727
  .join(" ")}
728
+ // A part only where it is a handle: on a fixed-width column the
729
+ // separator is a plain divider, and a test that finds it would be
730
+ // dragging something that does nothing.
731
+ data-dg-part={canResize ? "header-resize" : undefined}
732
+ data-column-id={canResize ? column.id : undefined}
705
733
  // Keeps a drag that starts on the separator from being picked up by
706
734
  // the draggable header around it.
707
735
  draggable={false}
708
736
  // The drag starts from `column.getSize()`, so the rendered width has
709
737
  // to be in `columnSizing` before the handler reads it - otherwise
710
738
  // the first pointer move snaps a fluid column to its declared size.
739
+ //
740
+ // The handler listens for the moves and the release on the document
741
+ // it is given - the separator's own, not the global one, which is
742
+ // the opener's when the grid is rendered through a portal into a
743
+ // window opened with `window.open`.
711
744
  onMouseDown={
712
745
  canResize
713
746
  ? (event) => {
714
747
  suppressSortRef.current = true;
715
748
  freezeRenderedWidths();
716
- header.getResizeHandler()(event);
749
+ header.getResizeHandler(event.currentTarget.ownerDocument)(
750
+ event,
751
+ );
717
752
  }
718
753
  : undefined
719
754
  }
@@ -721,7 +756,9 @@ export function TMDataGridHeaderCell({
721
756
  canResize
722
757
  ? (event) => {
723
758
  freezeRenderedWidths();
724
- header.getResizeHandler()(event);
759
+ header.getResizeHandler(event.currentTarget.ownerDocument)(
760
+ event,
761
+ );
725
762
  }
726
763
  : undefined
727
764
  }
@@ -0,0 +1,51 @@
1
+ /* The second header row, under the group rows and above the body. Same column
2
+ tracks as every other row, through subgrid, so it follows resizing,
3
+ reordering and pinning without knowing about any of them.
4
+
5
+ Sticky like the header rows above it, and stacked under them by the same
6
+ measurement: the table writes every header row's `top` inline once it knows
7
+ how tall the rows before it are. This is the pre-measurement fallback, and
8
+ it is right for the common case of one header row. */
9
+ .headerFilterRow {
10
+ display: grid;
11
+ grid-column: 1 / -1;
12
+ grid-template-columns: subgrid;
13
+ position: sticky;
14
+ top: var(--dg-header-height);
15
+ z-index: var(--dg-z-header, 6);
16
+ }
17
+
18
+ .headerFilterCell {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 2px;
22
+ min-width: 0;
23
+ /* A 60px column has room for the input and nothing beside it. Clipping is
24
+ what keeps the control from spilling over its neighbour; the operator
25
+ button is the first thing to go. */
26
+ overflow: hidden;
27
+ padding: calc(var(--dg-padding) / 2);
28
+ /* Opaque: the body scrolls underneath it. */
29
+ background-color: var(--mantine-color-body);
30
+ border-bottom: 1px solid var(--mantine-color-default-border);
31
+ }
32
+
33
+ /* The generated lanes - checkbox, details, row number - hold no filter, so
34
+ they keep the header's own padding rather than a control's. */
35
+ .headerFilterCell[data-control-column="true"] {
36
+ padding: 0;
37
+ }
38
+
39
+ .headerFilterValue {
40
+ flex: 1;
41
+ min-width: 0;
42
+ }
43
+
44
+ .headerFilterOperator {
45
+ flex: none;
46
+ }
47
+
48
+ .operatorItemActive {
49
+ font-weight: 600;
50
+ color: var(--mantine-primary-color-filled);
51
+ }