@opencorestack/opengridx 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  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 -166
  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 +6 -7
  29. package/docs/components/datagrid.md +15 -13
  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 +90 -38
  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 +126 -39
  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 +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -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 +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
  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 +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. 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,11 +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
+
17
64
  // Injected hierarchy renderers replace the plain-cell path in Cell, so they must
18
65
  // render the formatted value themselves or valueFormatter is silently lost.
19
66
  const displayValue = <R extends GridRowModel>(cellParams: GridRenderCellParams<R>): React.ReactNode =>
20
67
  cellParams.formattedValue ?? (cellParams.value as React.ReactNode);
21
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
+
22
136
  export interface UseGridColumnsParams<R extends GridRowModel> {
23
137
  activeColumns: GridColDef<R>[];
24
138
  isHierarchyEnabled: boolean;
@@ -28,6 +142,8 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
28
142
  columnVisibilityModel: Record<string, boolean>;
29
143
  columnOrder?: string[];
30
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;
31
147
  disableColumnReorder: boolean;
32
148
  pivotMode: boolean;
33
149
  checkboxSelection: boolean;
@@ -35,17 +151,37 @@ export interface UseGridColumnsParams<R extends GridRowModel> {
35
151
  rowReordering: boolean;
36
152
  initialState?: GridInitialState;
37
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>;
38
161
  }
39
162
 
163
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
164
+
40
165
  export interface UseGridColumnsResult<R extends GridRowModel> {
41
166
  effectiveColumns: GridColDef<R>[];
167
+ /** Tree data / row grouping: the column that shows the expand toggle, indent and group labels. */
168
+ hierarchyField: string | undefined;
42
169
  orderedColumns: GridColDef<R>[];
43
170
  visibleOrderedColumns: GridColDef<R>[];
44
- 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>;
45
177
  columnWidths: Record<string, number>;
46
178
  effectiveColumnOrder: string[];
47
179
  setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
48
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;
49
185
  handleColumnResize: (field: string, newWidth: number) => void;
50
186
  }
51
187
 
@@ -61,6 +197,7 @@ export function useGridColumns<R extends GridRowModel>(
61
197
  columnVisibilityModel,
62
198
  columnOrder,
63
199
  onColumnOrderChange,
200
+ onColumnOrderModelChange,
64
201
  disableColumnReorder,
65
202
  pivotMode,
66
203
  checkboxSelection,
@@ -68,59 +205,95 @@ export function useGridColumns<R extends GridRowModel>(
68
205
  rowReordering,
69
206
  initialState,
70
207
  setColumns,
208
+ pinnedColumns,
209
+ getRowId = defaultGetRowId,
210
+ aggregationModel,
211
+ groupingRows,
71
212
  } = params;
72
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
+
73
263
  // ── Effective columns (hierarchy cell renderer injection) ─────────────────
74
264
  const effectiveColumns = useMemo<GridColDef<R>[]>(() => {
75
265
  if (!isHierarchyEnabled) return activeColumns;
76
266
 
77
- return activeColumns.map((col, index) => {
78
- 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) {
79
272
  return {
80
273
  ...col,
274
+ ...groupRowAggregates,
81
275
  renderCell: (cellParams: GridRenderCellParams<R>) => {
82
276
  const meta = cellParams.rowMeta;
83
277
  const depth = meta?.treeDepth ?? 0;
84
278
  const hasChildren = Boolean(meta?.hasChildren);
85
279
  const isExpanded = Boolean(meta?.isExpanded);
86
- const groupingField = meta?.groupingField;
87
- const groupingValue = meta?.groupingValue;
88
- const descendantCount = meta?.descendantCount;
89
- const isGroupRow = Boolean(meta?.isGroupRow);
90
-
91
- let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
92
-
93
- if (isTreeData && hasChildren && isGroupRow) {
94
- content = (
95
- <div className="ogx__group-cell-content">
96
- {cellParams.value as React.ReactNode}
97
- {descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
98
- </div>
99
- );
100
- }
101
-
102
- if (isRowGrouping && hasChildren && groupingField) {
103
- const groupLabel = meta?.groupLabel ?? `${groupingField}: ${String(groupingValue)}`;
104
- content = (
105
- <div className="ogx__group-cell-content">
106
- {groupLabel}
107
- {descendantCount !== undefined ? ` (${descendantCount})` : ''}
108
- </div>
109
- );
110
- }
280
+
281
+ const custom = renderConsumerCell(col, cellParams, true);
282
+ const content = custom.useDefault ? defaultHierarchyContent(cellParams, isRowGrouping, isTreeData) : custom.content;
111
283
 
112
284
  return (
113
285
  <div style={{ display: 'flex', alignItems: 'center', paddingLeft: depth * 24, width: '100%', height: '100%' }}>
114
286
  <div style={{ marginRight: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
115
287
  {hasChildren ? (
116
288
  <div
117
- onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row.id); }}
289
+ onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
118
290
  onMouseDown={(e) => { e.stopPropagation(); }}
119
291
  style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'auto', zIndex: 10, position: 'relative' }}
120
292
  >
121
293
  <ExpandIcon
122
294
  isExpanded={isExpanded}
123
- 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)); }}
124
297
  />
125
298
  </div>
126
299
  ) : null}
@@ -134,24 +307,22 @@ export function useGridColumns<R extends GridRowModel>(
134
307
 
135
308
  return {
136
309
  ...col,
310
+ ...groupRowAggregates,
137
311
  renderCell: (cellParams: GridRenderCellParams<R>) => {
138
312
  const meta = cellParams.rowMeta;
139
- const hasChildren = Boolean(meta?.hasChildren);
140
- const groupingField = meta?.groupingField;
141
-
142
- if (isRowGrouping && hasChildren) {
143
- if (col.field === groupingField) return null;
144
- if (cellParams.value !== undefined && cellParams.value !== null) {
145
- return col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
146
- }
147
- return null;
313
+ if (!meta?.isGroupRow) {
314
+ return col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
148
315
  }
149
-
150
- return col.renderCell ? col.renderCell(cellParams) : displayValue(cellParams);
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);
151
322
  }
152
323
  };
153
324
  }) as GridColDef<R>[];
154
- }, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers]);
325
+ }, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers, hierarchyField, getRowId, aggregationModel, groupingRows]);
155
326
 
156
327
  // ── Column widths ─────────────────────────────────────────────────────────
157
328
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
@@ -162,72 +333,104 @@ export function useGridColumns<R extends GridRowModel>(
162
333
  setColumnWidths(prev => ({ ...prev, [field]: newWidth }));
163
334
  }, []);
164
335
 
165
- // ── Column order ──────────────────────────────────────────────────────────
166
- const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(
167
- () => initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
168
- );
169
-
170
- const effectiveColumnOrder = columnOrder ?? internalColumnOrder;
171
-
172
336
  useEffect(() => {
173
337
  setColumns(activeColumns as unknown as GridColDef[]);
174
338
  }, [activeColumns, setColumns]);
175
339
 
176
- useEffect(() => {
177
- if (pivotMode) {
178
- setInternalColumnOrder(activeColumns.map(col => col.field));
179
- }
180
- }, [pivotMode, activeColumns]);
181
-
182
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).
183
343
  const orderedColumns = useMemo<GridColDef<R>[]>(() => {
184
- if (disableColumnReorder) return effectiveColumns;
185
-
186
344
  const orderMap = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
187
345
  return [...effectiveColumns].sort((a, b) => {
188
346
  const ai = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
189
347
  const bi = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
190
348
  return ai - bi;
191
349
  });
192
- }, [effectiveColumns, effectiveColumnOrder, disableColumnReorder]);
350
+ }, [effectiveColumns, effectiveColumnOrder]);
193
351
 
194
352
  const visibleOrderedColumns = useMemo<GridColDef<R>[]>(
195
- () => orderedColumns.filter(col => columnVisibilityModel[col.field] !== false),
196
- [orderedColumns, columnVisibilityModel]
353
+ () => orderedColumns.filter(isColumnShown),
354
+ [orderedColumns, isColumnShown]
197
355
  );
198
356
 
199
- // ── 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
+
200
379
  const columnReorderHandlers = useColumnReorder({
201
380
  columns: orderedColumns,
202
- onColumnOrderChange: useCallback((reorderParams: GridColumnOrderChangeParams) => {
203
- const { oldIndex, targetIndex } = reorderParams;
204
- const newOrder = [...effectiveColumnOrder];
205
- const [movedField] = newOrder.splice(oldIndex, 1);
206
- newOrder.splice(targetIndex, 0, movedField);
207
- if (!columnOrder) setInternalColumnOrder(newOrder);
208
- onColumnOrderChange?.(reorderParams);
209
- }, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
381
+ onColumnOrderChange: useCallback(
382
+ ({ oldIndex, targetIndex }: GridColumnOrderChangeParams) => applyColumnMove(oldIndex, targetIndex),
383
+ [applyColumnMove]
384
+ ),
210
385
  disableColumnReorder,
211
386
  });
212
387
 
213
- // ── 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
+
214
407
  const navigationColumns = useMemo(() => {
215
- const specials: { field: string }[] = [];
216
- if (rowReordering) specials.push({ field: '__reorder_col__' });
217
- if (hasDetailPanel) specials.push({ field: '__expand_col__' });
218
- if (checkboxSelection) specials.push({ field: '__checkbox_col__' });
219
- return [...specials, ...orderedColumns] as Array<GridColDef<R> | { field: string }>;
220
- }, [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
+ );
221
419
 
222
420
  return {
223
421
  effectiveColumns,
422
+ hierarchyField,
224
423
  orderedColumns,
225
424
  visibleOrderedColumns,
425
+ renderOrderedColumns: renderedDataColumns,
226
426
  navigationColumns,
427
+ columnIndexMap,
227
428
  columnWidths,
228
429
  effectiveColumnOrder,
229
430
  setInternalColumnOrder,
230
431
  columnReorderHandlers,
432
+ moveColumn,
433
+ resetColumnOrder,
231
434
  handleColumnResize,
232
435
  };
233
436
  }