@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
@@ -1,10 +1,18 @@
1
- import { ActionIcon, Group, Select, Text, Tooltip } from "@mantine/core";
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ type BoxProps,
5
+ Group,
6
+ Select,
7
+ Text,
8
+ Tooltip,
9
+ } from "@mantine/core";
2
10
  import type { RowData } from "@tanstack/react-table";
3
- import { useSelector } from "@tanstack/react-store";
4
11
  import { useMemo, type ReactNode } from "react";
5
12
  import classes from "./TMDataGridFooter.module.css";
6
13
  import { useTMDataGridContext } from "../TMDataGridContext";
7
14
  import { getGridCapabilities } from "../core/capabilities";
15
+ import { useSettledTableState } from "../core/useSettledTableState";
8
16
  import { isPagingActive } from "../core/rowSelection";
9
17
  import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
10
18
  import type { TMDataGridTable } from "../useTMDataGrid";
@@ -63,6 +71,12 @@ export type TMDataGridPaginationControls = {
63
71
  PageSize: () => ReactNode;
64
72
  /** The "1–25 of 300" range label. */
65
73
  Range: () => ReactNode;
74
+ /**
75
+ * The "Page 3 of 200" label - what a server-paged grid usually shows in
76
+ * place of a row range. Not in the default footer; put it in a
77
+ * `renderPagination` layout.
78
+ */
79
+ PageNumber: () => ReactNode;
66
80
  /** The previous/next buttons. */
67
81
  Pager: () => ReactNode;
68
82
  };
@@ -122,7 +136,7 @@ function PaginationPageSize({
122
136
  pageSizeOptions: ReadonlyArray<number>;
123
137
  }) {
124
138
  const { table, features, labels, controlSize } = useTMDataGridContext();
125
- useSelector(table.store);
139
+ useSettledTableState(table.store);
126
140
  const paging = isPagingActive(table, features);
127
141
  const { pageSize } = table.store.state.pagination;
128
142
  // A page size restored from storage, or set through `initialState`, need not
@@ -163,7 +177,7 @@ function PaginationPageSize({
163
177
 
164
178
  function PaginationRange() {
165
179
  const { table, features, labels, controlSize } = useTMDataGridContext();
166
- useSelector(table.store);
180
+ useSettledTableState(table.store);
167
181
  const paging = isPagingActive(table, features);
168
182
  const { pageIndex, pageSize } = table.store.state.pagination;
169
183
  const total = table.getRowCount();
@@ -182,9 +196,29 @@ function PaginationRange() {
182
196
  );
183
197
  }
184
198
 
199
+ function PaginationPageNumber() {
200
+ const { table, features, labels, controlSize } = useTMDataGridContext();
201
+ useSettledTableState(table.store);
202
+ const paging = isPagingActive(table, features);
203
+ const { pageIndex } = table.store.state.pagination;
204
+
205
+ return (
206
+ <Text span size={controlSize} c="dimmed" data-dg-part="page-number">
207
+ {paging
208
+ ? labels.pageNumber({
209
+ page: pageIndex + 1,
210
+ pageCount: table.getPageCount(),
211
+ })
212
+ : // Same reasoning as the range label: nothing is being sliced, so a
213
+ // page number would be a lie.
214
+ labels.groupedAllRows(table.getFilteredRowModel().rows.length)}
215
+ </Text>
216
+ );
217
+ }
218
+
185
219
  function PaginationPager() {
186
220
  const { table, features, labels } = useTMDataGridContext();
187
- useSelector(table.store);
221
+ useSettledTableState(table.store);
188
222
  const paging = isPagingActive(table, features);
189
223
 
190
224
  return (
@@ -213,7 +247,8 @@ function PaginationPager() {
213
247
  );
214
248
  }
215
249
 
216
- export type TMDataGridFooterProps = {
250
+ /** Mantine's style props (`mt="sm"`, `px="md"`) are set on the footer bar. */
251
+ export type TMDataGridFooterProps = BoxProps & {
217
252
  pageSizeOptions?: ReadonlyArray<number>;
218
253
  /**
219
254
  * Replaces the built-in pager, and is handed the pieces of it.
@@ -223,7 +258,7 @@ export type TMDataGridFooterProps = {
223
258
  * renderPagination={({ state, actions, Controls }) => (
224
259
  * <Group>
225
260
  * <Controls.PageSize />
226
- * <MyJumpToPage page={state.pageIndex} onJump={actions.setPageIndex} />
261
+ * <Controls.PageNumber />
227
262
  * <Controls.Pager />
228
263
  * </Group>
229
264
  * )}
@@ -248,9 +283,12 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
248
283
  export function TMDataGridFooter({
249
284
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
250
285
  renderPagination,
286
+ className,
287
+ ...others
251
288
  }: TMDataGridFooterProps) {
289
+ const footerClassName = [classes.footer, className].filter(Boolean).join(" ");
252
290
  const { table, features, labels } = useTMDataGridContext();
253
- useSelector(table.store);
291
+ useSettledTableState(table.store);
254
292
 
255
293
  // Bound once per option list rather than per render, so the controls keep
256
294
  // their identity and the elements they render keep theirs.
@@ -258,6 +296,7 @@ export function TMDataGridFooter({
258
296
  () => ({
259
297
  PageSize: () => <PaginationPageSize pageSizeOptions={pageSizeOptions} />,
260
298
  Range: PaginationRange,
299
+ PageNumber: PaginationPageNumber,
261
300
  Pager: PaginationPager,
262
301
  }),
263
302
  [pageSizeOptions],
@@ -273,9 +312,9 @@ export function TMDataGridFooter({
273
312
  if (renderPagination) {
274
313
  const { state, actions } = getTMDataGridPaginationApi(table, paging);
275
314
  return (
276
- <div data-dg-part="footer" className={classes.footer}>
315
+ <Box data-dg-part="footer" className={footerClassName} {...others}>
277
316
  {renderPagination({ state, actions, Controls })}
278
- </div>
317
+ </Box>
279
318
  );
280
319
  }
281
320
 
@@ -288,17 +327,18 @@ export function TMDataGridFooter({
288
327
  w={260}
289
328
  position="top"
290
329
  >
291
- <div
330
+ <Box
292
331
  data-dg-part="footer"
293
- className={classes.footer}
332
+ className={footerClassName}
294
333
  data-paging-suspended={!paging}
334
+ {...others}
295
335
  >
296
336
  {/* The default render is the controls in order - which is what makes
297
337
  `renderPagination` a rearrangement rather than a rebuild. */}
298
338
  <Controls.PageSize />
299
339
  <Controls.Range />
300
340
  <Controls.Pager />
301
- </div>
341
+ </Box>
302
342
  </Tooltip>
303
343
  );
304
344
  }
@@ -1,6 +1,6 @@
1
1
  import { UnstyledButton } from "@mantine/core";
2
2
  import {
3
- useCellControlTabIndex,
3
+ useBodyControlTabIndex,
4
4
  useTMDataGridContext,
5
5
  } from "../TMDataGridContext";
6
6
  import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
@@ -52,9 +52,8 @@ function GroupCell<TData extends RowData>({
52
52
  row: Row<TMDataGridFeatures, TData>;
53
53
  }) {
54
54
  const { labels } = useTMDataGridContext();
55
+ const tabIndex = useBodyControlTabIndex();
55
56
  const expanded = useSelector(row.table.store, () => row.getIsExpanded());
56
- // See useCellControlTabIndex: reached by stepping into the cell, not by Tab.
57
- const tabIndex = useCellControlTabIndex();
58
57
 
59
58
  // Leaf rows keep the lane empty: their values are in the data columns, and
60
59
  // the indent alone is what places them under their group.
@@ -67,6 +66,8 @@ function GroupCell<TData extends RowData>({
67
66
  className={classes.groupToggle}
68
67
  // Padding rather than margin, so the whole indented width stays clickable.
69
68
  style={{ paddingInlineStart: row.depth * INDENT_STEP }}
69
+ // See useBodyControlTabIndex: under cell selection, reached by stepping
70
+ // into the cell or by the Tab walk within the row, not by the page.
70
71
  tabIndex={tabIndex}
71
72
  aria-expanded={expanded}
72
73
  aria-label={
@@ -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);
@@ -133,6 +136,9 @@ export function TMDataGridHeaderCell({
133
136
  (filter) => filter.id === column.id && isFilterActive(filter.value),
134
137
  );
135
138
  const isResizing = resizingColumnId === column.id;
139
+ // Any column, not just this one: a resize elsewhere must not leave a
140
+ // neighbouring header ready to start a native drag under the pointer.
141
+ const isAnyResizing = resizingColumnId !== false;
136
142
  const pinnedAt = layout.pinnedAt;
137
143
 
138
144
  // Every affordance below mirrors a TanStack capability check, so switching a
@@ -188,7 +194,7 @@ export function TMDataGridHeaderCell({
188
194
  function handleDragLeave(event: DragEvent<HTMLDivElement>) {
189
195
  // dragleave also fires when the pointer crosses into a child element.
190
196
  const next = event.relatedTarget;
191
- if (next instanceof Node && event.currentTarget.contains(next)) return;
197
+ if (isNode(next) && event.currentTarget.contains(next)) return;
192
198
  setDropSide(null);
193
199
  }
194
200
 
@@ -222,6 +228,43 @@ export function TMDataGridHeaderCell({
222
228
  if (container) autosizeColumn({ table, columnId: column.id, container });
223
229
  }
224
230
 
231
+ /**
232
+ * Writes the width this header's columns are rendered with into
233
+ * `columnSizing`, for the ones that have no width there yet.
234
+ *
235
+ * An unresized column is a fluid (`fr`) track: it renders at whatever share
236
+ * of the grid it is given, while `column.getSize()` keeps reporting the
237
+ * declared `size`. Anything that switches a column to exact pixels has to
238
+ * close that gap first, or the column jumps from the width on screen to the
239
+ * declared one.
240
+ *
241
+ * Under a held pointer that jump is more than cosmetic: it slides the
242
+ * divider out from under the cursor onto the neighbouring header, where the
243
+ * browser starts a native column-move drag instead. That drag swallows the
244
+ * `mouseup` the resize listens for, so the resize keeps running after the
245
+ * button is released.
246
+ */
247
+ function freezeRenderedWidths() {
248
+ const container = cellRef.current?.closest<HTMLElement>(
249
+ "[data-dg-scroll-container]",
250
+ );
251
+ if (!container) return;
252
+ const sizing = table.store.state.columnSizing;
253
+ const frozen: Record<string, number> = {};
254
+ for (const leaf of header.getLeafHeaders()) {
255
+ const id = leaf.column.id;
256
+ if (id in sizing) continue;
257
+ const width = container
258
+ .querySelector<HTMLElement>(
259
+ `[role="columnheader"][data-column-id="${CSS.escape(id)}"]`,
260
+ )
261
+ ?.getBoundingClientRect().width;
262
+ if (width) frozen[id] = Math.round(width);
263
+ }
264
+ if (Object.keys(frozen).length === 0) return;
265
+ table.setColumnSizing((previous) => ({ ...previous, ...frozen }));
266
+ }
267
+
225
268
  /**
226
269
  * Pinned columns are positioned with `getStart()` / `getAfter()`, which sum
227
270
  * `column.getSize()`. Fluid (`fr`) columns don't report their rendered width
@@ -229,17 +272,9 @@ export function TMDataGridHeaderCell({
229
272
  * keeps the column looking the same and keeps the offsets exact.
230
273
  */
231
274
  function setPinned(position: "left" | "right" | false) {
232
- if (position && !(column.id in table.store.state.columnSizing)) {
233
- const width = cellRef.current?.getBoundingClientRect().width;
234
- if (width) {
235
- table.setColumnSizing((prev) => ({
236
- ...prev,
237
- [column.id]: Math.round(width),
238
- }));
239
- }
240
- }
241
- column.pin(position);
242
- // `pin("right")` appends, which would leave the column outside the edit
275
+ if (position) freezeRenderedWidths();
276
+ column.pin(pinningPositionOfSide(position));
277
+ // `pin("end")` appends, which would leave the column outside the edit
243
278
  // lane. Normalising after the pin rather than writing the lane by hand
244
279
  // keeps TanStack's own handling of a header group's leaves.
245
280
  table.setColumnPinning(keepGeneratedColumnsOutermost);
@@ -265,7 +300,9 @@ export function TMDataGridHeaderCell({
265
300
  <Menu.Divider key="sort-divider" />,
266
301
  );
267
302
  }
268
- 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) {
269
306
  menuItems.push(
270
307
  <Menu.Item
271
308
  key="filter"
@@ -444,13 +481,19 @@ export function TMDataGridHeaderCell({
444
481
  }
445
482
  if (canManageColumns) {
446
483
  menuItems.push(
447
- <Menu.Item
448
- key="manage"
449
- leftSection={<ColumnsIcon size={16} stroke={1.6} />}
450
- onClick={() => api.ui.actions.setColumnsPanelOpen(true)}
451
- >
452
- {labels.manageColumns}
453
- </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>,
454
497
  );
455
498
  }
456
499
  // A trailing divider reads as a dropped item - drop the divider instead.
@@ -514,16 +557,23 @@ export function TMDataGridHeaderCell({
514
557
  : undefined
515
558
  }
516
559
  className={cellClass}
517
- // Dropped while a resize is running, so that dragging the separator
518
- // resizes the column instead of starting to move it. The resize handler
519
- // records the column on mousedown, before the first pointer move.
520
- draggable={canReorder && !isResizing}
560
+ // Dropped while any column is resizing, so that dragging the separator
561
+ // resizes the column instead of starting to move it - and so that a
562
+ // header the divider passes under cannot start a move of its own. The
563
+ // resize handler records the column on mousedown, before the first
564
+ // pointer move.
565
+ draggable={canReorder && !isAnyResizing}
521
566
  onDragStart={canReorder ? handleDragStart : undefined}
522
567
  onDragEnd={canReorder ? handleDragEnd : undefined}
523
568
  onDragOver={isDropTarget ? handleDragOver : undefined}
524
569
  onDragLeave={isDropTarget ? handleDragLeave : undefined}
525
570
  onDrop={isDropTarget ? handleDrop : undefined}
526
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,
527
577
  left: pinnedAt === "left" ? layout.offset : undefined,
528
578
  right: pinnedAt === "right" ? layout.offset : undefined,
529
579
  position: pinnedAt ? "sticky" : undefined,
@@ -565,7 +615,9 @@ export function TMDataGridHeaderCell({
565
615
 
566
616
  {isLeaf && (
567
617
  <div className={classes.headerActions}>
568
- {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 && (
569
621
  <ActionIcon
570
622
  className={classes.headerAction}
571
623
  data-pinned-visible="true"
@@ -671,15 +723,35 @@ export function TMDataGridHeaderCell({
671
723
  // Keeps a drag that starts on the separator from being picked up by
672
724
  // the draggable header around it.
673
725
  draggable={false}
726
+ // The drag starts from `column.getSize()`, so the rendered width has
727
+ // to be in `columnSizing` before the handler reads it - otherwise
728
+ // the first pointer move snaps a fluid column to its declared size.
729
+ //
730
+ // The handler listens for the moves and the release on the document
731
+ // it is given - the separator's own, not the global one, which is
732
+ // the opener's when the grid is rendered through a portal into a
733
+ // window opened with `window.open`.
674
734
  onMouseDown={
675
735
  canResize
676
736
  ? (event) => {
677
737
  suppressSortRef.current = true;
678
- header.getResizeHandler()(event);
738
+ freezeRenderedWidths();
739
+ header.getResizeHandler(event.currentTarget.ownerDocument)(
740
+ event,
741
+ );
742
+ }
743
+ : undefined
744
+ }
745
+ onTouchStart={
746
+ canResize
747
+ ? (event) => {
748
+ freezeRenderedWidths();
749
+ header.getResizeHandler(event.currentTarget.ownerDocument)(
750
+ event,
751
+ );
679
752
  }
680
753
  : undefined
681
754
  }
682
- onTouchStart={canResize ? header.getResizeHandler() : undefined}
683
755
  onClick={(event) => event.stopPropagation()}
684
756
  // The spreadsheet gesture: double-click the divider, the column
685
757
  // fits its content. Same measurement as the menu item.
@@ -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
+ }