@opencorestack/opengridx 2.0.4 → 3.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 (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -0,0 +1,38 @@
1
+ import { useMemo } from 'react';
2
+ import { getAriaRowLayout, type GridAriaRowLayout } from '../../utils/aria';
3
+ import { getColumnGroupDepth } from '../../utils/columnGroups';
4
+ import type { GridColumnGroupingModel, GridPaginationModel } from '../../types';
5
+
6
+ export interface UseGridAriaRowsParams {
7
+ columnGroupingModel: GridColumnGroupingModel | undefined;
8
+ pinnedTopCount: number;
9
+ pinnedBottomCount: number;
10
+ pagination: boolean;
11
+ paginationMode: 'client' | 'server' | 'infinite';
12
+ paginationModel: GridPaginationModel;
13
+ /** What the pager pages through (see `getPaginationRowCount`). */
14
+ paginationRowCount: number;
15
+ paginatedUnpinnedRowCount: number;
16
+ sortedUnpinnedRowCount: number;
17
+ }
18
+
19
+ /** `aria-rowcount` and the `aria-rowindex` bases of the pinned and centre rows. */
20
+ export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayout {
21
+ const {
22
+ columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
23
+ paginationRowCount, paginatedUnpinnedRowCount, sortedUnpinnedRowCount,
24
+ } = params;
25
+ const { page, pageSize } = paginationModel;
26
+
27
+ return useMemo(() => getAriaRowLayout({
28
+ headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel),
29
+ pinnedTopCount,
30
+ pinnedBottomCount,
31
+ pageRowCount: pagination ? paginatedUnpinnedRowCount : sortedUnpinnedRowCount,
32
+ totalCenterRowCount: pagination && paginationMode === 'server'
33
+ ? Math.max(paginationRowCount, paginatedUnpinnedRowCount)
34
+ : sortedUnpinnedRowCount,
35
+ pageOffset: pagination ? page * pageSize : 0,
36
+ }), [columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
37
+ sortedUnpinnedRowCount, paginationMode, paginationRowCount, page, pageSize]);
38
+ }
@@ -0,0 +1,38 @@
1
+ import { useCallback, useLayoutEffect } from 'react';
2
+ import type { MutableRefObject, RefObject } from 'react';
3
+ import { useGridClipboard } from '../features/useGridClipboard';
4
+ import type { GridApi, GridColDef, GridRowId, GridRowModel } from '../../types';
5
+
6
+ export interface UseGridClipboardApiParams {
7
+ apiRef: MutableRefObject<GridApi>;
8
+ /** The rendered data columns, in screen order. */
9
+ columns: GridColDef[];
10
+ getRowId: (row: GridRowModel) => GridRowId;
11
+ /** The grid root: Ctrl/Cmd+C only copies while focus is inside it. */
12
+ containerRef: RefObject<HTMLDivElement | null>;
13
+ disableKeyboardShortcut: boolean;
14
+ }
15
+
16
+ /**
17
+ * Copies the selected rows (every page, collapsed groups, pinned rows) with the on-screen columns
18
+ * in screen order, on Ctrl/Cmd+C while focus is inside this grid, and installs `copySelectedRows`
19
+ * on the API for programmatic use.
20
+ */
21
+ export function useGridClipboardApi(params: UseGridClipboardApiParams): void {
22
+ const { apiRef, columns, getRowId, containerRef, disableKeyboardShortcut } = params;
23
+
24
+ const getGridRootElement = useCallback(() => containerRef.current, [containerRef]);
25
+ const { copySelectedRows } = useGridClipboard({
26
+ getSelectedRowIds: () => apiRef.current.getSelectedRows(),
27
+ getColumns: () => columns,
28
+ getRows: () => apiRef.current.getAllFilteredRows(),
29
+ getRowId,
30
+ getRootElement: getGridRootElement,
31
+ disableKeyboardShortcut,
32
+ });
33
+
34
+ // Stable identity.
35
+ useLayoutEffect(() => {
36
+ apiRef.current.copySelectedRows = copySelectedRows;
37
+ }, [copySelectedRows, apiRef]);
38
+ }
@@ -0,0 +1,30 @@
1
+ import { useMemo } from 'react';
2
+ import type { GridColDef, GridRowModel } from '../../types';
3
+ import { buildColumnLookup, type GridColumnLookup } from '../../utils/columnLookup';
4
+
5
+ const SEPARATOR = '\u0000';
6
+
7
+ /**
8
+ * The column lookup used by client-side filtering, sorting and the quick filter.
9
+ *
10
+ * Its identity changes only when the columns change or the set of hidden columns changes, so an
11
+ * inline `columnVisibilityModel` object (a new reference on every parent render) does not re-run
12
+ * the filter and sort passes.
13
+ */
14
+ export function useGridColumnLookup<R extends GridRowModel>(
15
+ columns: GridColDef<R>[],
16
+ columnVisibilityModel: Record<string, boolean>,
17
+ ): GridColumnLookup {
18
+ const hiddenFieldsKey = columns
19
+ .filter(col => columnVisibilityModel[col.field] === false)
20
+ .map(col => col.field)
21
+ .join(SEPARATOR);
22
+
23
+ return useMemo(() => {
24
+ const hidden: Record<string, boolean> = {};
25
+ if (hiddenFieldsKey) {
26
+ for (const field of hiddenFieldsKey.split(SEPARATOR)) hidden[field] = false;
27
+ }
28
+ return buildColumnLookup(columns as unknown as GridColDef[], hidden);
29
+ }, [columns, hiddenFieldsKey]);
30
+ }
@@ -1,12 +1,18 @@
1
1
  import { useState, useMemo, useCallback, useEffect } from 'react';
2
2
  import { useColumnReorder } from '../useColumnReorder';
3
3
  import { ExpandIcon } from '../../components/ui/ExpandIcon';
4
+ import { isColumnPinned } from '../../utils/pinning';
5
+ import { formatAggregateForColumn } from '../../utils/aggregation';
6
+ import { CellErrorBoundary } from '../../components/Cell/CellErrorBoundary';
7
+ import { CHECKBOX_FIELD, EXPAND_FIELD, REORDER_FIELD } from '../../utils/focus';
4
8
  import type {
5
9
  GridColDef,
6
10
  GridRowModel,
7
11
  GridRowId,
8
12
  GridColumnOrderChangeParams,
13
+ GridColumnPinning,
9
14
  GridRenderCellParams,
15
+ GridAggregationModel,
10
16
  } from '../../types';
11
17
  import type { GridInitialState } from '../../state/types';
12
18
 
@@ -14,6 +20,119 @@ interface HierarchyHandlers {
14
20
  toggleExpansion: (id: GridRowId) => void;
15
21
  }
16
22
 
23
+ // A group row carries each column's aggregate in the column's field. It is shown like the footer
24
+ // shows totals: the stored aggregate (a valueGetter would recompute it from fields a group row does
25
+ // not have), formatted with formatAggregateForColumn (so a count is never put in a currency format).
26
+ function withGroupRowAggregates<R extends GridRowModel>(
27
+ col: GridColDef<R>,
28
+ fnName: string | undefined,
29
+ groupingRows: ReadonlyMap<GridRowId, unknown> | undefined,
30
+ ): Pick<GridColDef<R>, 'valueGetter' | 'valueFormatter'> {
31
+ if (!fnName || !groupingRows) return { valueGetter: col.valueGetter, valueFormatter: col.valueFormatter };
32
+ const isGroupRow = (row: R) => groupingRows.has(row.id);
33
+ const ownGetter = col.valueGetter;
34
+ const ownFormatter = col.valueFormatter;
35
+ return {
36
+ valueGetter: ownGetter
37
+ ? (p) => (isGroupRow(p.row) ? p.value : ownGetter(p))
38
+ : undefined,
39
+ valueFormatter: (p) => {
40
+ if (isGroupRow(p.row)) return formatAggregateForColumn(p.value, fnName, col, p.row);
41
+ if (ownFormatter) return ownFormatter(p);
42
+ return p.value == null ? '' : String(p.value);
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Columns in render order: left-pinned in `pinnedColumns.left` order, unpinned in column order,
49
+ * right-pinned in `pinnedColumns.right` order — the order useLayout renders them in.
50
+ */
51
+ function inPinnedRenderOrder<C extends { field: string }>(columns: C[], pinnedColumns?: GridColumnPinning): C[] {
52
+ if (!pinnedColumns) return columns;
53
+ const left = pinnedColumns.left ?? [];
54
+ const right = pinnedColumns.right ?? [];
55
+ const rank = (col: C) => {
56
+ const side = isColumnPinned(col.field, pinnedColumns);
57
+ if (side === 'left') return left.indexOf(col.field) - left.length;
58
+ if (side === 'right') return 1 + right.indexOf(col.field);
59
+ return 0;
60
+ };
61
+ return [...columns].sort((a, b) => rank(a) - rank(b));
62
+ }
63
+
64
+ // Injected hierarchy renderers replace the plain-cell path in Cell, so they must
65
+ // render the formatted value themselves or valueFormatter is silently lost.
66
+ const displayValue = <R extends GridRowModel>(cellParams: GridRenderCellParams<R>): React.ReactNode =>
67
+ cellParams.formattedValue ?? (cellParams.value as React.ReactNode);
68
+
69
+ interface ConsumerCellResult {
70
+ useDefault: boolean;
71
+ content?: React.ReactNode;
72
+ }
73
+
74
+ /**
75
+ * Calls the consumer's renderCell for a hierarchy column cell. For a synthetic row (group, subtotal or
76
+ * auto-created tree parent) a result of `undefined` means "use the grid's default content". In the
77
+ * hierarchy column the output is wrapped in its own error boundary, so a renderCell that fails on a
78
+ * group row shows the error icon next to the expand toggle instead of replacing the toggle.
79
+ */
80
+ function renderConsumerCell<R extends GridRowModel>(
81
+ col: GridColDef<R>,
82
+ cellParams: GridRenderCellParams<R>,
83
+ isolate: boolean,
84
+ ): ConsumerCellResult {
85
+ const renderCell = col.renderCell;
86
+ if (!renderCell) return { useDefault: true };
87
+ const isSynthetic = Boolean(cellParams.rowMeta?.isGroupRow);
88
+ if (!isolate) {
89
+ const output = renderCell(cellParams);
90
+ return isSynthetic && output === undefined ? { useDefault: true } : { useDefault: false, content: output };
91
+ }
92
+ let output: React.ReactNode;
93
+ try {
94
+ output = renderCell(cellParams);
95
+ } catch (error) {
96
+ return {
97
+ useDefault: false,
98
+ content: <CellErrorBoundary field={col.field} resetKey={cellParams.row} renderFn={() => { throw error; }} />,
99
+ };
100
+ }
101
+ if (isSynthetic && output === undefined) return { useDefault: true };
102
+ return { useDefault: false, content: <CellErrorBoundary field={col.field} resetKey={cellParams.row} renderFn={() => output} /> };
103
+ }
104
+
105
+ /** What the hierarchy column shows without a consumer renderCell (or when it returns undefined for a synthetic row). */
106
+ function defaultHierarchyContent<R extends GridRowModel>(
107
+ cellParams: GridRenderCellParams<R>,
108
+ isRowGrouping: boolean,
109
+ isTreeData: boolean,
110
+ ): React.ReactNode {
111
+ const meta = cellParams.rowMeta;
112
+ if (!meta?.isGroupRow) return displayValue(cellParams);
113
+ // A subtotal row carries only aggregates; its label column stays empty.
114
+ if (meta.isGroupFooter) return null;
115
+ const descendantCount = meta.descendantCount;
116
+ if (isRowGrouping && meta.hasChildren) {
117
+ const groupLabel = meta.groupLabel ?? `${meta.groupingField}: ${String(meta.groupingValue)}`;
118
+ return (
119
+ <div className="ogx__group-cell-content">
120
+ {groupLabel}
121
+ {descendantCount !== undefined ? ` (${descendantCount})` : ''}
122
+ </div>
123
+ );
124
+ }
125
+ if (isTreeData) {
126
+ return (
127
+ <div className="ogx__group-cell-content">
128
+ {meta.groupLabel}
129
+ {descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
130
+ </div>
131
+ );
132
+ }
133
+ return displayValue(cellParams);
134
+ }
135
+
17
136
  export interface UseGridColumnsParams<R extends GridRowModel> {
18
137
  activeColumns: GridColDef<R>[];
19
138
  isHierarchyEnabled: boolean;
@@ -23,6 +142,8 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
23
142
  columnVisibilityModel: Record<string, boolean>;
24
143
  columnOrder?: string[];
25
144
  onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
145
+ /** Receives the whole new column order after every reorder and reset (not in pivot mode). */
146
+ onColumnOrderModelChange?: (columnOrder: string[]) => void;
26
147
  disableColumnReorder: boolean;
27
148
  pivotMode: boolean;
28
149
  checkboxSelection: boolean;
@@ -30,17 +151,37 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
30
151
  rowReordering: boolean;
31
152
  initialState?: GridInitialState;
32
153
  setColumns: (cols: GridColDef[]) => void;
154
+ pinnedColumns?: GridColumnPinning;
155
+ /** Resolves a row's id (getRowId) when the injected expand toggle is clicked; defaults to `row.id`. */
156
+ getRowId?: (row: R) => GridRowId;
157
+ /** Formats the aggregates on row-grouping group rows. */
158
+ aggregationModel?: GridAggregationModel;
159
+ /** Row grouping only: the synthetic group rows, keyed by id. */
160
+ groupingRows?: ReadonlyMap<GridRowId, unknown>;
33
161
  }
34
162
 
163
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
164
+
35
165
  export interface UseGridColumnsResult<R extends GridRowModel> {
36
166
  effectiveColumns: GridColDef<R>[];
167
+ /** Tree data / row grouping: the column that shows the expand toggle, indent and group labels. */
168
+ hierarchyField: string | undefined;
37
169
  orderedColumns: GridColDef<R>[];
38
170
  visibleOrderedColumns: GridColDef<R>[];
39
- navigationColumns: Array<GridColDef<R> | { field: string }>;
171
+ /** Visible data columns in render order: left-pinned, unpinned, right-pinned (`apiRef.getVisibleColumns`). */
172
+ renderOrderedColumns: GridColDef<R>[];
173
+ /** Focusable columns in render order: system columns, then the visible data columns (left-pinned, unpinned, right-pinned). */
174
+ navigationColumns: Array<GridColDef<R> | { field: string; sortable: false; editable: false }>;
175
+ /** Position of each visible data column in render order (system columns excluded): the public `colIndex`. */
176
+ columnIndexMap: Map<string, number>;
40
177
  columnWidths: Record<string, number>;
41
178
  effectiveColumnOrder: string[];
42
179
  setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
43
180
  columnReorderHandlers: ReturnType<typeof useColumnReorder>;
181
+ /** Moves `fromField` to the position of `toField` in the column order (Columns panel drag). */
182
+ moveColumn: (fromField: string, toField: string) => void;
183
+ /** Restores the columns' definition order. */
184
+ resetColumnOrder: () => void;
44
185
  handleColumnResize: (field: string, newWidth: number) => void;
45
186
  }
46
187
 
@@ -56,6 +197,7 @@ export function useGridColumns<R extends GridRowModel>(
56
197
  columnVisibilityModel,
57
198
  columnOrder,
58
199
  onColumnOrderChange,
200
+ onColumnOrderModelChange,
59
201
  disableColumnReorder,
60
202
  pivotMode,
61
203
  checkboxSelection,
@@ -63,59 +205,95 @@ export function useGridColumns<R extends GridRowModel>(
63
205
  rowReordering,
64
206
  initialState,
65
207
  setColumns,
208
+ pinnedColumns,
209
+ getRowId = defaultGetRowId,
210
+ aggregationModel,
211
+ groupingRows,
66
212
  } = params;
67
213
 
214
+ // ── Column order ──────────────────────────────────────────────────────────
215
+ const naturalOrder = useMemo(() => activeColumns.map(col => col.field), [activeColumns]);
216
+
217
+ // null = the columns' own order, until the user reorders. Deriving it (instead of snapshotting the
218
+ // columns at mount) keeps it right when the grid mounts in pivot mode or the columns change.
219
+ const [storedColumnOrder, setStoredColumnOrder] = useState<string[] | null>(
220
+ () => initialState?.columns?.columnOrder ?? null
221
+ );
222
+
223
+ // Generated pivot columns keep an order of their own, so pivoting never rewrites the user's column
224
+ // order, and a controlled `columnOrder` (which names source columns) does not apply to them. It
225
+ // resets whenever the generated column set changes.
226
+ const pivotColumnsKey = pivotMode ? naturalOrder.join('\u0000') : '';
227
+ const [pivotOrderState, setPivotOrderState] = useState<{ key: string; order: string[] } | null>(null);
228
+ const pivotColumnOrder = pivotOrderState && pivotOrderState.key === pivotColumnsKey ? pivotOrderState.order : naturalOrder;
229
+
230
+ const internalColumnOrder = storedColumnOrder ?? naturalOrder;
231
+ const effectiveColumnOrder = pivotMode ? pivotColumnOrder : (columnOrder ?? internalColumnOrder);
232
+
233
+ const setInternalColumnOrder = useCallback<React.Dispatch<React.SetStateAction<string[]>>>((action) => {
234
+ if (pivotMode) {
235
+ setPivotOrderState(prev => {
236
+ const base = prev && prev.key === pivotColumnsKey ? prev.order : naturalOrder;
237
+ return { key: pivotColumnsKey, order: typeof action === 'function' ? action(base) : action };
238
+ });
239
+ return;
240
+ }
241
+ setStoredColumnOrder(prev => (typeof action === 'function' ? action(prev ?? naturalOrder) : action));
242
+ }, [pivotMode, pivotColumnsKey, naturalOrder]);
243
+
244
+ // In pivot mode the row-label columns (marked hideable: false) always show: the pivot rows are
245
+ // labelled by them, and they share their field with the source column a visibility model may hide.
246
+ const isColumnShown = useCallback(
247
+ (col: GridColDef<R>) => (pivotMode && col.hideable === false) || columnVisibilityModel[col.field] !== false,
248
+ [pivotMode, columnVisibilityModel],
249
+ );
250
+
251
+ // The expand toggle, indentation and group label go on the leftmost column actually
252
+ // on screen, so hiding, reordering or pinning columns never strips group rows of them.
253
+ // Mirrors the render order: left-pinned, then unpinned, then right-pinned.
254
+ const hierarchyField = useMemo<string | undefined>(() => {
255
+ if (!isHierarchyEnabled) return undefined;
256
+ const orderIndex = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
257
+ const rank = (col: GridColDef<R>) => orderIndex.get(col.field) ?? activeColumns.indexOf(col);
258
+ const ordered = [...activeColumns].sort((a, b) => rank(a) - rank(b));
259
+ const onScreen = inPinnedRenderOrder(ordered.filter(col => columnVisibilityModel[col.field] !== false), pinnedColumns);
260
+ return (onScreen[0] ?? activeColumns[0])?.field;
261
+ }, [isHierarchyEnabled, effectiveColumnOrder, activeColumns, columnVisibilityModel, pinnedColumns]);
262
+
68
263
  // ── Effective columns (hierarchy cell renderer injection) ─────────────────
69
264
  const effectiveColumns = useMemo<GridColDef<R>[]>(() => {
70
265
  if (!isHierarchyEnabled) return activeColumns;
71
266
 
72
- return activeColumns.map((col, index) => {
73
- if (index === 0) {
267
+ const groupRows = isRowGrouping ? groupingRows : undefined;
268
+
269
+ return activeColumns.map((col) => {
270
+ const groupRowAggregates = withGroupRowAggregates(col, aggregationModel?.[col.field], groupRows);
271
+ if (col.field === hierarchyField) {
74
272
  return {
75
273
  ...col,
274
+ ...groupRowAggregates,
76
275
  renderCell: (cellParams: GridRenderCellParams<R>) => {
77
276
  const meta = cellParams.rowMeta;
78
277
  const depth = meta?.treeDepth ?? 0;
79
278
  const hasChildren = Boolean(meta?.hasChildren);
80
279
  const isExpanded = Boolean(meta?.isExpanded);
81
- const groupingField = meta?.groupingField;
82
- const groupingValue = meta?.groupingValue;
83
- const descendantCount = meta?.descendantCount;
84
- const isGroupRow = Boolean(meta?.isGroupRow);
85
-
86
- let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : cellParams.value as React.ReactNode;
87
-
88
- if (isTreeData && hasChildren && isGroupRow) {
89
- content = (
90
- <div className="ogx__group-cell-content">
91
- {cellParams.value as React.ReactNode}
92
- {descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
93
- </div>
94
- );
95
- }
96
-
97
- if (isRowGrouping && hasChildren && groupingField) {
98
- const groupLabel = meta?.groupLabel ?? `${groupingField}: ${String(groupingValue)}`;
99
- content = (
100
- <div className="ogx__group-cell-content">
101
- {groupLabel}
102
- {descendantCount !== undefined ? ` (${descendantCount})` : ''}
103
- </div>
104
- );
105
- }
280
+
281
+ const custom = renderConsumerCell(col, cellParams, true);
282
+ const content = custom.useDefault ? defaultHierarchyContent(cellParams, isRowGrouping, isTreeData) : custom.content;
106
283
 
107
284
  return (
108
285
  <div style={{ display: 'flex', alignItems: 'center', paddingLeft: depth * 24, width: '100%', height: '100%' }}>
109
286
  <div style={{ marginRight: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
110
287
  {hasChildren ? (
111
288
  <div
112
- onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row.id); }}
289
+ onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
113
290
  onMouseDown={(e) => { e.stopPropagation(); }}
114
291
  style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'auto', zIndex: 10, position: 'relative' }}
115
292
  >
116
293
  <ExpandIcon
117
294
  isExpanded={isExpanded}
118
- onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row.id); }}
295
+ tabIndex={-1}
296
+ onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
119
297
  />
120
298
  </div>
121
299
  ) : null}
@@ -129,24 +307,22 @@ export function useGridColumns<R extends GridRowModel>(
129
307
 
130
308
  return {
131
309
  ...col,
310
+ ...groupRowAggregates,
132
311
  renderCell: (cellParams: GridRenderCellParams<R>) => {
133
312
  const meta = cellParams.rowMeta;
134
- const hasChildren = Boolean(meta?.hasChildren);
135
- const groupingField = meta?.groupingField;
136
-
137
- if (isRowGrouping && hasChildren) {
138
- if (col.field === groupingField) return null;
139
- if (cellParams.value !== undefined && cellParams.value !== null) {
140
- return col.renderCell ? col.renderCell(cellParams) : cellParams.value;
141
- }
142
- return null;
313
+ if (!meta?.isGroupRow) {
314
+ return col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
143
315
  }
144
-
145
- return col.renderCell ? col.renderCell(cellParams) : cellParams.value;
316
+ // Synthetic rows: the consumer's renderCell decides; `undefined` keeps the default,
317
+ // which is the aggregate (or nothing) and never the grouping value the label already shows.
318
+ const custom = renderConsumerCell(col, cellParams, false);
319
+ if (!custom.useDefault) return custom.content;
320
+ if (col.field === meta.groupingField && !meta.isGroupFooter) return null;
321
+ return cellParams.value == null ? null : displayValue(cellParams);
146
322
  }
147
323
  };
148
324
  }) as GridColDef<R>[];
149
- }, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers]);
325
+ }, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers, hierarchyField, getRowId, aggregationModel, groupingRows]);
150
326
 
151
327
  // ── Column widths ─────────────────────────────────────────────────────────
152
328
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
@@ -157,72 +333,104 @@ export function useGridColumns<R extends GridRowModel>(
157
333
  setColumnWidths(prev => ({ ...prev, [field]: newWidth }));
158
334
  }, []);
159
335
 
160
- // ── Column order ──────────────────────────────────────────────────────────
161
- const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(
162
- () => initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
163
- );
164
-
165
- const effectiveColumnOrder = columnOrder ?? internalColumnOrder;
166
-
167
336
  useEffect(() => {
168
337
  setColumns(activeColumns as unknown as GridColDef[]);
169
338
  }, [activeColumns, setColumns]);
170
339
 
171
- useEffect(() => {
172
- if (pivotMode) {
173
- setInternalColumnOrder(activeColumns.map(col => col.field));
174
- }
175
- }, [pivotMode, activeColumns]);
176
-
177
340
  // ── Ordered / visible columns ─────────────────────────────────────────────
341
+ // A controlled or initial column order always applies. disableColumnReorder only removes the
342
+ // user's ways to change it (header drag, Columns-panel reorder).
178
343
  const orderedColumns = useMemo<GridColDef<R>[]>(() => {
179
- if (disableColumnReorder) return effectiveColumns;
180
-
181
344
  const orderMap = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
182
345
  return [...effectiveColumns].sort((a, b) => {
183
346
  const ai = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
184
347
  const bi = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
185
348
  return ai - bi;
186
349
  });
187
- }, [effectiveColumns, effectiveColumnOrder, disableColumnReorder]);
350
+ }, [effectiveColumns, effectiveColumnOrder]);
188
351
 
189
352
  const visibleOrderedColumns = useMemo<GridColDef<R>[]>(
190
- () => orderedColumns.filter(col => columnVisibilityModel[col.field] !== false),
191
- [orderedColumns, columnVisibilityModel]
353
+ () => orderedColumns.filter(isColumnShown),
354
+ [orderedColumns, isColumnShown]
192
355
  );
193
356
 
194
- // ── Column reorder handlers ───────────────────────────────────────────────
357
+ // ── Column reorder ────────────────────────────────────────────────────────
358
+ // Every move works on the full current order (orderedColumns: all columns, including ones added
359
+ // after mount or missing from a stored/controlled order, and the synthetic __group__ column),
360
+ // so the reported indices and the stored order always describe the same list.
361
+ const commitColumnOrder = useCallback((newOrder: string[], change?: GridColumnOrderChangeParams) => {
362
+ // A controlled columnOrder names source columns; generated pivot columns keep their own order.
363
+ if (pivotMode || !columnOrder) setInternalColumnOrder(newOrder);
364
+ if (change) onColumnOrderChange?.(change);
365
+ if (!pivotMode) onColumnOrderModelChange?.(newOrder);
366
+ }, [pivotMode, columnOrder, setInternalColumnOrder, onColumnOrderChange, onColumnOrderModelChange]);
367
+
368
+ const applyColumnMove = useCallback((oldIndex: number, targetIndex: number) => {
369
+ const newOrder = orderedColumns.map(col => col.field);
370
+ const [movedField] = newOrder.splice(oldIndex, 1);
371
+ newOrder.splice(targetIndex, 0, movedField);
372
+ commitColumnOrder(newOrder, {
373
+ column: orderedColumns[oldIndex] as unknown as GridColDef,
374
+ oldIndex,
375
+ targetIndex,
376
+ });
377
+ }, [orderedColumns, commitColumnOrder]);
378
+
195
379
  const columnReorderHandlers = useColumnReorder({
196
380
  columns: orderedColumns,
197
- onColumnOrderChange: useCallback((reorderParams: GridColumnOrderChangeParams) => {
198
- const { oldIndex, targetIndex } = reorderParams;
199
- const newOrder = [...effectiveColumnOrder];
200
- const [movedField] = newOrder.splice(oldIndex, 1);
201
- newOrder.splice(targetIndex, 0, movedField);
202
- if (!columnOrder) setInternalColumnOrder(newOrder);
203
- onColumnOrderChange?.(reorderParams);
204
- }, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
381
+ onColumnOrderChange: useCallback(
382
+ ({ oldIndex, targetIndex }: GridColumnOrderChangeParams) => applyColumnMove(oldIndex, targetIndex),
383
+ [applyColumnMove]
384
+ ),
205
385
  disableColumnReorder,
206
386
  });
207
387
 
208
- // ── Navigation columns (system cols + data cols for keyboard nav) ─────────
388
+ const moveColumn = useCallback((fromField: string, toField: string) => {
389
+ const oldIndex = orderedColumns.findIndex(col => col.field === fromField);
390
+ const targetIndex = orderedColumns.findIndex(col => col.field === toField);
391
+ if (oldIndex === -1 || targetIndex === -1 || oldIndex === targetIndex) return;
392
+ applyColumnMove(oldIndex, targetIndex);
393
+ }, [orderedColumns, applyColumnMove]);
394
+
395
+ const resetColumnOrder = useCallback(() => {
396
+ commitColumnOrder(naturalOrder);
397
+ }, [commitColumnOrder, naturalOrder]);
398
+
399
+ // ── Navigation columns (system cols + rendered data cols, in render order) ─
400
+ // Mirrors what Row and Header render, so arrow keys follow the screen and never
401
+ // land on a hidden column. System columns are never sortable or editable.
402
+ const renderedDataColumns = useMemo(
403
+ () => inPinnedRenderOrder(visibleOrderedColumns, pinnedColumns),
404
+ [visibleOrderedColumns, pinnedColumns]
405
+ );
406
+
209
407
  const navigationColumns = useMemo(() => {
210
- const specials: { field: string }[] = [];
211
- if (rowReordering) specials.push({ field: '__reorder_col__' });
212
- if (hasDetailPanel) specials.push({ field: '__expand_col__' });
213
- if (checkboxSelection) specials.push({ field: '__checkbox_col__' });
214
- return [...specials, ...orderedColumns] as Array<GridColDef<R> | { field: string }>;
215
- }, [orderedColumns, checkboxSelection, hasDetailPanel, rowReordering]);
408
+ const specials: { field: string; sortable: false; editable: false }[] = [];
409
+ if (rowReordering) specials.push({ field: REORDER_FIELD, sortable: false, editable: false });
410
+ if (hasDetailPanel) specials.push({ field: EXPAND_FIELD, sortable: false, editable: false });
411
+ if (checkboxSelection) specials.push({ field: CHECKBOX_FIELD, sortable: false, editable: false });
412
+ return [...specials, ...renderedDataColumns];
413
+ }, [renderedDataColumns, checkboxSelection, hasDetailPanel, rowReordering]);
414
+
415
+ const columnIndexMap = useMemo(
416
+ () => new Map(renderedDataColumns.map((col, index) => [col.field, index])),
417
+ [renderedDataColumns]
418
+ );
216
419
 
217
420
  return {
218
421
  effectiveColumns,
422
+ hierarchyField,
219
423
  orderedColumns,
220
424
  visibleOrderedColumns,
425
+ renderOrderedColumns: renderedDataColumns,
221
426
  navigationColumns,
427
+ columnIndexMap,
222
428
  columnWidths,
223
429
  effectiveColumnOrder,
224
430
  setInternalColumnOrder,
225
431
  columnReorderHandlers,
432
+ moveColumn,
433
+ resetColumnOrder,
226
434
  handleColumnResize,
227
435
  };
228
436
  }