@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0

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 (155) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1323 -796
  3. package/dist/index.js +4719 -3193
  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 +268 -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 +69 -78
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +83 -50
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +31 -23
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +8 -8
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  63. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  64. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  65. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
  66. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  68. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  70. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
  71. package/src/components/TMDataGridEntryRows.tsx +354 -0
  72. package/src/components/TMDataGridExportPicker.module.css +77 -0
  73. package/src/components/TMDataGridExportPicker.tsx +234 -0
  74. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  75. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  76. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  77. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  78. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  80. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
  81. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  83. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  84. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  85. package/src/components/TMDataGridMenu.tsx +357 -0
  86. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
  87. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
  89. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  90. package/src/components/TMDataGridToolbar.tsx +181 -0
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  98. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  99. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  100. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  101. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  103. package/src/components/filters/controlLayout.ts +32 -0
  104. package/src/components/filters/filterControlFor.ts +65 -0
  105. package/src/components/generatedColumns.tsx +187 -0
  106. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  107. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  108. package/src/components/useHideableColumns.ts +52 -0
  109. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  110. package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
  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 +1172 -388
  118. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  119. package/src/core/export.ts +704 -0
  120. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  121. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  122. package/src/core/filterSurface.ts +99 -0
  123. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  124. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  125. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  126. package/src/core/pageReset.ts +120 -0
  127. package/src/core/pagination.ts +81 -0
  128. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  129. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  130. package/src/{tmdatagrid/index.ts → index.ts} +70 -36
  131. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
  132. package/src/useTMDataGridExport.ts +78 -0
  133. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
  134. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  135. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  136. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  137. package/src/tmdatagrid/core/cellExport.ts +0 -320
  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}/cellNavigation.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { Button, Pill, UnstyledButton } from "@mantine/core";
1
+ import { Box, type BoxProps, Button, Pill, UnstyledButton } from "@mantine/core";
2
2
  import type { RowData } from "@tanstack/react-table";
3
3
  import { useSelector } from "@tanstack/react-store";
4
4
  import classes from "./TMDataGridFilterPills.module.css";
@@ -11,7 +11,8 @@ import {
11
11
  import type { TMDataGridSize } from "../core/sizes";
12
12
  import { openColumnFilter, type TMDataGridApi } from "../useTMDataGrid";
13
13
 
14
- export type TMDataGridFilterPillsProps<TData extends RowData> = {
14
+ /** Mantine's style props (`mb="sm"`, `hiddenFrom="sm"`) are set on the wrapper. */
15
+ export type TMDataGridFilterPillsProps<TData extends RowData> = BoxProps & {
15
16
  /** The object returned by `useTMDataGrid`. */
16
17
  api: TMDataGridApi<TData>;
17
18
  /** Mantine size of the pills. Defaults to `"sm"`. */
@@ -23,7 +24,6 @@ export type TMDataGridFilterPillsProps<TData extends RowData> = {
23
24
  * filter panel on that column.
24
25
  */
25
26
  onPillClick?: (columnId: string) => void;
26
- className?: string;
27
27
  };
28
28
 
29
29
  /**
@@ -52,6 +52,7 @@ export function TMDataGridFilterPills<TData extends RowData>({
52
52
  showClearAll = true,
53
53
  onPillClick,
54
54
  className,
55
+ ...others
55
56
  }: TMDataGridFilterPillsProps<TData>) {
56
57
  const { table, labels } = api;
57
58
  const columnFilters = useSelector(
@@ -65,11 +66,12 @@ export function TMDataGridFilterPills<TData extends RowData>({
65
66
  if (activeFilters.length === 0) return null;
66
67
 
67
68
  return (
68
- <div
69
+ <Box
69
70
  role="group"
70
71
  aria-label={labels.activeFilters}
71
72
  data-dg-part="filter-pills"
72
73
  className={[classes.filterPills, className].filter(Boolean).join(" ")}
74
+ {...others}
73
75
  >
74
76
  <Pill.Group size={size}>
75
77
  {activeFilters.map((filter) => {
@@ -78,6 +80,13 @@ export function TMDataGridFilterPills<TData extends RowData>({
78
80
  const value = filter.value;
79
81
  // isFilterActive already vouched for the shape; this narrows it.
80
82
  if (!isTMDataGridFilterValue(value)) return null;
83
+ // Mantine types `removeButtonProps` as `CloseButtonProps`, which
84
+ // has no room for `data-*` in an object literal. Spread in from a
85
+ // typed value, they reach the button all the same.
86
+ const removeButtonParts: Record<`data-${string}`, string> = {
87
+ "data-dg-part": "filter-pill-remove",
88
+ "data-column-id": filter.id,
89
+ };
81
90
 
82
91
  return (
83
92
  <Pill
@@ -96,13 +105,11 @@ export function TMDataGridFilterPills<TData extends RowData>({
96
105
  // the tab order, because inside a `PillsInput` Backspace is the
97
106
  // way out. These pills stand on their own, so the ✕ has to be a
98
107
  // real button.
99
- // No test id of its own: Mantine types these props as
100
- // `CloseButtonProps`, which has no room for `data-*`. The pill
101
- // carries one, and the ✕ is the only button inside it.
102
108
  removeButtonProps={{
103
109
  "aria-label": labels.clearFilter(label),
104
110
  "aria-hidden": false,
105
111
  tabIndex: 0,
112
+ ...removeButtonParts,
106
113
  }}
107
114
  >
108
115
  <UnstyledButton
@@ -137,6 +144,6 @@ export function TMDataGridFilterPills<TData extends RowData>({
137
144
  {labels.clearAllFilters}
138
145
  </Button>
139
146
  )}
140
- </div>
147
+ </Box>
141
148
  );
142
149
  }
@@ -0,0 +1,54 @@
1
+ /* Chrome shared by the two automatic surfaces. The panel inside them is a
2
+ plain block; everything that makes one float or sit beside the rows is
3
+ here. */
4
+
5
+ .surfaceHeader {
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: space-between;
9
+ gap: var(--mantine-spacing-md);
10
+ padding-bottom: var(--mantine-spacing-xs);
11
+ }
12
+
13
+ /* Floats over the first body rows, anchored to `.tableWrapper`. Sits below the
14
+ whole header - group rows and the filter row included - which the table
15
+ measures onto the wrapper as `--dg-header-stack-height`. The size scale's
16
+ single-row value is the fallback for the frame before the first
17
+ measurement. */
18
+ .filterPopup {
19
+ position: absolute;
20
+ top: calc(var(--dg-header-stack-height, var(--dg-header-height)) + 2px);
21
+ left: var(--mantine-spacing-md);
22
+ z-index: var(--dg-z-filter-popup, 20);
23
+ padding: var(--mantine-spacing-sm);
24
+ border-radius: var(--mantine-radius-md);
25
+ border: 1px solid var(--mantine-color-default-border);
26
+ background-color: var(--mantine-color-body);
27
+ box-shadow: var(--mantine-shadow-md);
28
+ }
29
+
30
+ /* A column of the table wrapper rather than a layer over it: the rows give up
31
+ the width instead of being covered by it. `flex: none` so the declared
32
+ width holds whatever the panel inside asks for. */
33
+ .filterSidebar {
34
+ display: flex;
35
+ flex-direction: column;
36
+ flex: none;
37
+ min-width: 0;
38
+ padding: var(--mantine-spacing-sm);
39
+ background-color: var(--mantine-color-body);
40
+ border-left: 1px solid var(--mantine-color-default-border);
41
+ }
42
+
43
+ /* On the left it is the same panel with its divider on the other edge, and
44
+ ahead of the scroll container in the flex order. */
45
+ .filterSidebar[data-side="left"] {
46
+ order: -1;
47
+ border-left: none;
48
+ border-right: 1px solid var(--mantine-color-default-border);
49
+ }
50
+
51
+ .filterSidebarBody {
52
+ flex: 1;
53
+ min-height: 0;
54
+ }
@@ -0,0 +1,167 @@
1
+ import { ActionIcon, ScrollArea, Text } from "@mantine/core";
2
+ import { useSelector } from "@tanstack/react-store";
3
+ import { useEffect, useRef } from "react";
4
+ import classes from "./TMDataGridFilterSurface.module.css";
5
+ import { useTMDataGridContext } from "../TMDataGridContext";
6
+ import { isElement } from "../core/dom";
7
+ import { CloseIcon } from "./icons";
8
+ import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
9
+
10
+ /**
11
+ * The title row both automatic surfaces wear: the "Filters" heading and the
12
+ * close button that puts the surface away without touching the filters.
13
+ */
14
+ function FilterSurfaceHeader() {
15
+ const { ui, labels, controlSize } = useTMDataGridContext();
16
+ return (
17
+ <div className={classes.surfaceHeader}>
18
+ <Text span size={controlSize} fw={600}>
19
+ {labels.filters}
20
+ </Text>
21
+ <ActionIcon
22
+ variant="subtle"
23
+ color="gray"
24
+ size="sm"
25
+ aria-label={labels.closeFilters}
26
+ data-dg-part="filter-panel-close"
27
+ onClick={ui.actions.closeFilterPanel}
28
+ >
29
+ <CloseIcon size={16} stroke={1.6} />
30
+ </ActionIcon>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Closes the popup once its last filter row goes, covering both ways that
37
+ * happens - removed by hand, or "Clear all" - without either of them having
38
+ * to know which surface is showing.
39
+ *
40
+ * On the transition to zero, not on being zero: a popup asked to start open
41
+ * with no filters yet would otherwise close itself before its first paint.
42
+ */
43
+ function useCloseWhenEmptied(opened: boolean) {
44
+ const { table, ui } = useTMDataGridContext();
45
+ const filterCount = useSelector(
46
+ table.store,
47
+ (state) => state.columnFilters.length,
48
+ );
49
+ const previousCount = useRef(filterCount);
50
+ useEffect(() => {
51
+ const emptied = previousCount.current > 0 && filterCount === 0;
52
+ previousCount.current = filterCount;
53
+ if (opened && emptied) ui.actions.closeFilterPanel();
54
+ }, [opened, filterCount, ui]);
55
+ }
56
+
57
+ /**
58
+ * `filters.surface: "popup"` - the panel floating over the first body rows,
59
+ * anchored under the header. The default surface, and the only one before
60
+ * `filters` existed.
61
+ *
62
+ * Everything that makes it floating lives here rather than in the panel: a
63
+ * pointerdown outside closes it, Escape closes it, and emptying it closes it.
64
+ *
65
+ * @internal Rendered by `TMDataGrid.Table`.
66
+ */
67
+ export function TMDataGridFilterPopup() {
68
+ const { ui, labels } = useTMDataGridContext();
69
+ const opened = useSelector(ui, (state) => state.filterPanelOpen);
70
+ const popupRef = useRef<HTMLDivElement>(null);
71
+ useCloseWhenEmptied(opened);
72
+
73
+ // Clicking away hides the popup, the way any floating surface behaves. On
74
+ // pointerdown rather than click, so a press that starts outside dismisses it
75
+ // even when the pointer is released somewhere else.
76
+ useEffect(() => {
77
+ if (!opened) return;
78
+
79
+ const handlePointerDown = (event: PointerEvent) => {
80
+ const target = event.target;
81
+ if (!isElement(target)) return;
82
+ if (popupRef.current?.contains(target)) return;
83
+ // The toolbar button toggles the popup itself. Closing from here first
84
+ // would leave its click reopening what the user meant to close.
85
+ if (target.closest('[data-dg-part="filter-button"]')) return;
86
+ // A portalled overlay - a header filter's dropdown, a custom control's
87
+ // portalled picker - is outside the popup in the DOM but not in meaning:
88
+ // a press in one is a press in a control, not a click away. Mantine
89
+ // stamps `data-portal` on every portal node it creates.
90
+ if (target.closest("[data-portal]")) return;
91
+ ui.actions.closeFilterPanel();
92
+ };
93
+
94
+ // The popup's own document, not the global one: rendered through a
95
+ // portal into a window opened with `window.open`, the global `document`
96
+ // is the opener's and never sees a press in the grid's window.
97
+ const doc = popupRef.current?.ownerDocument ?? document;
98
+ doc.addEventListener("pointerdown", handlePointerDown);
99
+ return () => doc.removeEventListener("pointerdown", handlePointerDown);
100
+ }, [opened, ui]);
101
+
102
+ if (!opened) return null;
103
+
104
+ return (
105
+ <div
106
+ ref={popupRef}
107
+ data-dg-part="filter-popup"
108
+ className={classes.filterPopup}
109
+ aria-label={labels.filters}
110
+ // Escape is what closes a floating surface, and every control that can
111
+ // hold focus in here sits inside this element.
112
+ onKeyDown={(event) => {
113
+ if (event.key !== "Escape") return;
114
+ event.stopPropagation();
115
+ ui.actions.closeFilterPanel();
116
+ }}
117
+ >
118
+ <FilterSurfaceHeader />
119
+ <TMDataGridFilterPanel />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * `filters.surface: "sidebar"` - the panel beside the table, inside the grid
126
+ * frame and under the toolbar, taking width from the rows rather than covering
127
+ * them.
128
+ *
129
+ * Not a floating surface: a click in the table is a click on the rows the
130
+ * sidebar is filtering, so nothing about it dismisses the panel, and clearing
131
+ * the filters leaves it standing with its "Add filter" button - a place to
132
+ * work from rather than a thing that appears and goes away. Escape closes it,
133
+ * because a keyboard user inside it needs a way back out to the grid.
134
+ *
135
+ * @internal Rendered by `TMDataGrid.Table`.
136
+ */
137
+ export function TMDataGridFilterSidebar() {
138
+ const { ui, filters, labels } = useTMDataGridContext();
139
+ const opened = useSelector(ui, (state) => state.filterPanelOpen);
140
+
141
+ if (!opened) return null;
142
+
143
+ return (
144
+ <div
145
+ data-dg-part="filter-sidebar"
146
+ data-side={filters.sidebarSide}
147
+ className={classes.filterSidebar}
148
+ style={{ width: filters.sidebarWidth }}
149
+ aria-label={labels.filters}
150
+ onKeyDown={(event) => {
151
+ if (event.key !== "Escape") return;
152
+ event.stopPropagation();
153
+ ui.actions.closeFilterPanel();
154
+ }}
155
+ >
156
+ <FilterSurfaceHeader />
157
+ {/* The panel's rows are as tall as the filters put in them, and the
158
+ sidebar is as tall as the table beside it - so the overflow is the
159
+ sidebar's to scroll, not the page's. */}
160
+ <ScrollArea type="auto" className={classes.filterSidebarBody}>
161
+ {/* Stacked, not side by side: a filter row wants about 550px laid out
162
+ in a line, and the sidebar is 280. */}
163
+ <TMDataGridFilterPanel layout="stacked" />
164
+ </ScrollArea>
165
+ </div>
166
+ );
167
+ }
@@ -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
  }
@@ -1,15 +1,15 @@
1
1
  import { UnstyledButton } from "@mantine/core";
2
2
  import {
3
- useCellControlTabIndex,
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,15 +25,14 @@ 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>;
53
32
  }) {
54
33
  const { labels } = useTMDataGridContext();
34
+ const tabIndex = useBodyControlTabIndex();
55
35
  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
36
 
59
37
  // Leaf rows keep the lane empty: their values are in the data columns, and
60
38
  // the indent alone is what places them under their group.
@@ -67,6 +45,8 @@ function GroupCell<TData extends RowData>({
67
45
  className={classes.groupToggle}
68
46
  // Padding rather than margin, so the whole indented width stays clickable.
69
47
  style={{ paddingInlineStart: row.depth * INDENT_STEP }}
48
+ // See useBodyControlTabIndex: under cell selection, reached by stepping
49
+ // into the cell or by the Tab walk within the row, not by the page.
70
50
  tabIndex={tabIndex}
71
51
  aria-expanded={expanded}
72
52
  aria-label={
@@ -81,7 +61,7 @@ function GroupCell<TData extends RowData>({
81
61
  row.toggleExpanded();
82
62
  }}
83
63
  >
84
- <span className={classes.chevron} data-expanded={expanded}>
64
+ <span className={classes.chevron} data-expanded={expanded || undefined}>
85
65
  <ChevronRightIcon size={16} stroke={1.6} />
86
66
  </span>
87
67
  <span className={classes.groupLabel}>{label}</span>
@@ -98,7 +78,7 @@ function GroupCell<TData extends RowData>({
98
78
  * Names the columns currently grouped on, so the lane says what it is showing
99
79
  * rather than a static "Group".
100
80
  */
101
- function GroupHeader<TData extends RowData>({
81
+ export function GroupHeader<TData extends RowData>({
102
82
  table,
103
83
  }: {
104
84
  table: TMDataGridTable<TData>;
@@ -116,46 +96,3 @@ function GroupHeader<TData extends RowData>({
116
96
  })
117
97
  .join(" / ");
118
98
  }
119
-
120
- /**
121
- * The generated tree column, prepended whenever grouping is enabled and hidden
122
- * again while `grouping` is empty - see the visibility effect in
123
- * `useTMDataGrid`.
124
- *
125
- * It exists because TanStack ships no auto group column: `groupedColumnMode:
126
- * "remove"` takes the grouped column out of the grid, so something has to hold
127
- * the tree. Modelled on the checkbox column, which is generated the same way.
128
- *
129
- * Not groupable itself, and nothing had to be written to make that true -
130
- * `column.getCanGroup()` requires an `accessorFn`, which a display column has
131
- * no reason to have.
132
- */
133
- export function createGroupColumn<TData extends RowData>(
134
- label = "Group",
135
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
136
- return {
137
- id: GROUP_COLUMN_ID,
138
- meta: {
139
- label,
140
- // Structurally the first column after the checkbox lane.
141
- enableOrdering: false,
142
- },
143
- size: 260,
144
- minSize: 180,
145
- enableSorting: false,
146
- enableColumnFilter: false,
147
- enableGlobalFilter: false,
148
- // Keeps it out of the columns panel and out of the header menu: its
149
- // visibility is not the user's to set, it follows the grouping state.
150
- enableHiding: false,
151
- // Structurally pinned to the left; users shouldn't be able to move it.
152
- enablePinning: false,
153
- cell: ({ row }) => <GroupCell row={row} />,
154
- // A group row has subRows, so every cell on it that is not the grouped
155
- // column reports `getIsAggregated()` - this lane included. Without an
156
- // `aggregatedCell` the body would take that as "nothing to summarise" and
157
- // render the tree lane blank on exactly the rows it exists for.
158
- aggregatedCell: ({ row }) => <GroupCell row={row} />,
159
- header: ({ table }) => <GroupHeader table={table} />,
160
- };
161
- }
@@ -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;