@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
@@ -1,50 +1,86 @@
1
- import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
1
+ import React, { useRef, useMemo } from 'react';
2
2
  import { useLayout } from '../../hooks/core/useLayout';
3
3
  import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavigation';
4
4
  import { useGridControlledState } from '../../hooks/core/useGridControlledState';
5
- import { useGridRowPipeline } from '../../hooks/core/useGridRowPipeline';
5
+ import { useGridThemeDimensions } from '../../hooks/core/useGridThemeDimensions';
6
+ import { useGridRowPipeline, type GridRowPipelineResult } from '../../hooks/core/useGridRowPipeline';
6
7
  import { useGridVirtualization } from '../../hooks/core/useGridVirtualization';
7
8
  import { useGridColumns } from '../../hooks/core/useGridColumns';
8
9
  import { useGridVisibleRows } from '../../hooks/core/useGridVisibleRows';
9
- import { useGridScrollSync } from '../../hooks/core/useGridScrollSync';
10
+ import { useGridViewport } from '../../hooks/core/useGridViewport';
11
+ import { useGridDetailPanel } from '../../hooks/core/useGridDetailPanel';
10
12
  import { useGridStateSnapshot } from '../../hooks/core/useGridStateSnapshot';
11
- import { useGridDevWarnings } from '../../hooks/core/useGridDevWarnings';
12
- import { scrollRowIntoView } from '../../utils/scroll';
13
+ import { useGridDevWarnings, useGridStylesheetWarning } from '../../hooks/core/useGridDevWarnings';
14
+ import { useGridRowInteractions } from '../../hooks/core/useGridRowInteractions';
15
+ import { useGridLiveRowSelection } from '../../hooks/core/useGridLiveRowSelection';
16
+ import { useGridApiMethods } from '../../hooks/core/useGridApiMethods';
17
+ import { useGridColumnLookup } from '../../hooks/core/useGridColumnLookup';
18
+ import { useGridPageCorrection } from '../../hooks/core/useGridPageCorrection';
19
+ import { useGridColumnsPanel } from '../../hooks/core/useGridColumnsPanel';
20
+ import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
21
+ import { GridToolbarSlot } from './GridToolbarSlot';
13
22
  import { GridAggregationFooter } from './GridAggregationFooter';
14
23
  import { GridEmptyState } from './GridEmptyState';
15
24
  import { GridErrorOverlay } from './GridErrorOverlay';
16
25
  import { Header } from '../Header/Header';
17
- import { Pagination } from '../Pagination/Pagination';
18
26
  import { useDataGrid } from '../../hooks/core/useDataGrid';
19
27
  import { useRowReorder } from '../../hooks/useRowReorder';
20
- import { useTreeData } from '../../hooks/useTreeData';
21
- import { useRowGrouping } from '../../hooks/useRowGrouping';
28
+ import { useGridHierarchy } from '../../hooks/core/useGridHierarchy';
22
29
  import { useGridEditing } from '../../hooks/features/useGridEditing';
23
30
  import { useGridSpanning } from '../../hooks/features/useGridSpanning';
31
+ import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
32
+ import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
24
33
  import { useGridDataSource } from '../../hooks/features/useGridDataSource';
25
- import { useAggregation } from '../../hooks/features/useAggregation';
26
- import { usePivot } from '../../hooks/features/usePivot';
27
- import { useGridClipboard } from '../../hooks/features/useGridClipboard';
34
+ import { useServerTreeChildren } from '../../hooks/features/useServerTreeChildren';
35
+ import { useAggregation, useServerAggregationResults } from '../../hooks/features/useAggregation';
36
+ import { useGridPivot } from '../../hooks/features/useGridPivot';
37
+ import { isServerDrivenDataSource } from '../../utils/dataSource';
28
38
  import { GridListView } from './GridListView';
39
+ import { GridLoadingOverlay } from './GridLoadingOverlay';
29
40
  import { GridPinnedRows } from './GridPinnedRows';
30
41
  import { GridVirtualRows } from './GridVirtualRows';
31
42
  import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
32
- import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem, GridRowMeta, GridGroupedExportRow } from '../../types';
33
-
34
- const EMPTY_ROW_META_MAP: Map<GridRowId, GridRowMeta> = new Map();
35
-
36
- export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
43
+ import { resolveGridModes, EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
44
+ import { useGridGroupingColumn } from '../../hooks/core/useGridGroupingColumn';
45
+ import { useGridRowIdOf, getDefaultRowId } from '../../hooks/core/useGridRowIdOf';
46
+ import { useGridApiRefBinding } from '../../hooks/core/useGridApiRefBinding';
47
+ import { collectAllFilteredRows } from '../../utils/gridApiRows';
48
+ import { NO_AGGREGATION_MODEL } from '../../utils/aggregation';
49
+ import { isSyntheticRowId } from '../../utils/syntheticRows';
50
+ import { useStableColumns } from '../../hooks/core/useStableColumns';
51
+ import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
52
+ import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
53
+ import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
54
+ import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
55
+ import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
56
+ import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
57
+ import { GridPaginationArea } from './GridPaginationArea';
58
+ import { GridLiveRegion } from './GridLiveRegion';
59
+ import { useGridAriaRows } from '../../hooks/core/useGridAriaRows';
60
+ import { useGridToolbarProps } from '../../hooks/core/useGridToolbarProps';
61
+ import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
62
+
63
+ /**
64
+ * The grid. `R` is your row type: any object type (an interface works), inferred from `rows`. `columns`
65
+ * can be typed for it (`GridColDef<R>[]`, so callbacks see `params.row` as `R`) or untyped (`GridColDef[]`).
66
+ */
67
+ export function DataGrid<R extends GridValidRowModel = GridRowModel>(props: DataGridProps<R>): React.JSX.Element;
68
+ export function DataGrid<R extends GridValidRowModel = GridRowModel>(props: DataGridUntypedColumnsProps<R>): React.JSX.Element;
69
+ // Internally every row is read as a GridRowModel (a record keyed by field): the overloads above are the
70
+ // public signatures, and a consumer's row objects are passed through untouched.
71
+ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>): React.JSX.Element {
37
72
  const {
38
73
  rows,
39
- columns,
74
+ columns: columnsProp,
40
75
  getRowId,
41
- rowHeight = 52,
42
- headerHeight = 56,
76
+ rowHeight: rowHeightProp,
77
+ headerHeight: headerHeightProp,
43
78
  autoHeight = false,
44
79
  density,
45
80
  checkboxSelection = false,
46
81
  disableRowSelectionOnClick = false,
47
82
  disableMultipleRowSelection = false,
83
+ disableClipboardCopy = false,
48
84
  rowSelectionModel: propRowSelectionModel,
49
85
  onRowSelectionModelChange: propOnRowSelectionModelChange,
50
86
  onRowClick,
@@ -72,8 +108,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
72
108
  disableColumnReorder = false,
73
109
  columnOrder,
74
110
  onColumnOrderChange,
111
+ onColumnOrderModelChange,
75
112
  height,
76
- rowReordering = false,
113
+ rowReordering: rowReorderingProp = false,
77
114
  onRowOrderChange,
78
115
  loading = false,
79
116
 
@@ -115,7 +152,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
115
152
  // Column Visibility
116
153
  columnVisibilityModel: propColumnVisibilityModel,
117
154
  onColumnVisibilityModelChange,
118
- listView = false,
155
+ listView: listViewRequested = false,
119
156
  listViewColumn,
120
157
  columnGroupingModel,
121
158
  noRowsLabel = 'No Data',
@@ -123,46 +160,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
123
160
  apiRef: propApiRef,
124
161
  overscanRowCount = 3,
125
162
  } = props;
163
+ // Equal inline column definitions keep one identity, so the row passes memoised on it do not re-run.
164
+ const columns = useStableColumns(columnsProp);
126
165
 
127
166
  const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
128
-
129
- // Stable defaults
130
- const defaultFilterModel: GridFilterModel = useMemo(() => ({ items: [] }), []);
131
- const filterModel = (propFilterModel || defaultFilterModel) as GridFilterModel;
132
-
133
- const defaultRowGroupingModel = useMemo(() => [], []);
134
- const rowGroupingModel = propRowGroupingModel || defaultRowGroupingModel;
135
-
136
- // ─── Stabilize toolbar component identity ────────────────────────────────
137
- // Problem: demos/users often define their toolbar as an inline function
138
- // INSIDE their component body, e.g.:
139
- // const MyToolbar = (props) => <GridToolbar {...props} /> // inside render!
140
- // This creates a NEW function reference on every parent re-render. React
141
- // reconciles by component *identity*, so a new reference = unmount old
142
- // toolbar + mount fresh one = all toolbar state (search expansion, filter
143
- // open, typed text) is destroyed on every keystroke.
144
- //
145
- // Fix: a stable wrapper component created ONCE via useRef. Its identity
146
- // never changes, so React keeps it mounted. It reads the latest toolbar
147
- // from a separate ref that is updated on every render, so the rendered
148
- // output is always current — zero stale closures.
149
- const latestToolbarRef = useRef(slots?.toolbar);
150
- latestToolbarRef.current = slots?.toolbar;
151
- // The wrapper itself has a stable identity (created once via useRef).
152
- // We call latestToolbarRef.current(props) as a PLAIN FUNCTION — not via
153
- // React.createElement — so React never sees a changing component type.
154
- // The elements the toolbar function returns are reconciled normally, so
155
- // GridToolbar's internal state (filterOpen, search expansion) is preserved
156
- // even when the toolbar is defined as an inline function inside the parent.
157
- const StableToolbar = useRef((props: Record<string, unknown>) => {
158
- const Toolbar = latestToolbarRef.current;
159
- return Toolbar ? (Toolbar as (p: typeof props) => React.ReactElement | null)(props) : null;
160
- }).current;
167
+ const rowGroupingModel = propRowGroupingModel || EMPTY_ROW_GROUPING_MODEL;
161
168
 
162
169
  const controlledState = useGridControlledState({
163
170
  initialState,
164
171
  sortModel: propSortModel,
165
172
  onSortModelChange,
173
+ filterModel: propFilterModel,
174
+ onFilterModelChange,
166
175
  aggregationModel: propAggregationModel,
167
176
  onAggregationModelChange,
168
177
  columnVisibilityModel: propColumnVisibilityModel,
@@ -170,161 +179,93 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
170
179
  pinnedColumns: propPinnedColumns,
171
180
  onPinnedColumnsChange,
172
181
  pivotModel: propPivotModel,
173
- onPivotModelChange,
174
- paginationModel,
175
- onPaginationModelChange,
182
+ onPivotModelChange, paginationModel, onPaginationModelChange, pageSizeOptions,
176
183
  rowSelectionModel: propRowSelectionModel,
177
184
  onRowSelectionModelChange: propOnRowSelectionModelChange,
185
+ density,
178
186
  });
179
187
 
180
188
  const {
181
- sortModel,
182
- isSortControlled,
183
- setInternalSortModel,
184
- aggregationModel,
185
- handleAggregationModelChange,
186
- columnVisibilityModel,
187
- handleColumnVisibilityModelChange,
188
- pinnedColumns,
189
- handlePinnedColumnsChange,
190
- currentPivotModel,
191
- handlePivotModelChange,
192
- effectivePaginationModel,
193
- handlePaginationModelChange,
194
- selectedRowIds,
195
- isSelectionControlled,
196
- setInternalRowSelectionModel,
189
+ sortModel, handleSortModelChange, filterModel,
190
+ handleFilterModelChange, aggregationModel, handleAggregationModelChange, columnVisibilityModel,
191
+ handleColumnVisibilityModelChange, pinnedColumns, handlePinnedColumnsChange, currentPivotModel,
192
+ handlePivotModelChange, effectivePaginationModel, handlePaginationModelChange,
193
+ selectedRowIds: selectionModelRowIds,
194
+ handleRowSelectionModelChange,
195
+ density: effectiveDensity,
197
196
  } = controlledState;
198
197
 
199
- const onRowSelectionModelChange = propOnRowSelectionModelChange;
200
-
201
- const pivot = usePivot(rows as GridRowModel[], columns as unknown as GridColDef[], currentPivotModel, pivotMode);
202
-
203
- const effectiveRowHeight = density === 'compact' ? 32 : density === 'comfortable' ? 72 : rowHeight;
204
-
205
- const activeRows = (pivotMode && pivot.isValid ? pivot.pivotRows : rows) as unknown as R[];
206
- const baseColumns = (pivotMode && pivot.isValid ? pivot.pivotColumns : columns) as unknown as GridColDef<R>[];
207
-
208
- // When groupingColDef is provided and row grouping is active, prepend a
209
- // dedicated synthetic __group__ column at position 0 (pinned-left).
210
- const isRowGroupingActive = Boolean(propRowGroupingModel && propRowGroupingModel.length > 0);
211
-
212
- // Auto-pin __group__ column to the left when groupingColDef is active.
213
- // Both values MUST be memoized: without useMemo they produce a new array/object
214
- // reference every render, which cascades through useRowGrouping's memos and
215
- // effects into an infinite setState loop (Maximum update depth exceeded).
216
- const effectivePinnedColumns = useMemo(() => (
217
- (groupingColDef && isRowGroupingActive && !pivotMode)
218
- ? { ...pinnedColumns, left: ['__group__', ...((pinnedColumns?.left ?? []).filter(f => f !== '__group__'))] }
219
- : pinnedColumns
220
- ), [groupingColDef, isRowGroupingActive, pivotMode, pinnedColumns]);
221
-
222
- const activeColumns = useMemo(() => (
223
- (groupingColDef && isRowGroupingActive && !pivotMode)
224
- ? [
225
- {
226
- headerName: 'Group',
227
- width: 220,
228
- ...groupingColDef,
229
- field: '__group__',
230
- hideable: false,
231
- sortable: false,
232
- pinnable: false,
233
- exportable: false,
234
- } as unknown as GridColDef<R>,
235
- ...baseColumns,
236
- ]
237
- : baseColumns
238
- ), [groupingColDef, isRowGroupingActive, pivotMode, baseColumns]);
239
-
240
- const [serverAggregationResults, setServerAggregationResults] = useState<GridAggregationResult | null>(null);
241
-
242
- useEffect(() => {
243
- setServerAggregationResults(null);
244
- }, [aggregationModel]);
198
+ const pivot = useGridPivot({
199
+ rows: rows as GridRowModel[],
200
+ columns: columns as unknown as GridColDef[],
201
+ pivotMode,
202
+ pivotModel: currentPivotModel,
203
+ filterModel,
204
+ sortModel,
205
+ hasDataSource: Boolean(dataSource),
206
+ hierarchyRequested: treeData || rowGroupingModel.length > 0,
207
+ });
208
+ const isPivotActive = pivot.isActive;
209
+ const {
210
+ rowReordering, hierarchyRowGroupingModel, isTreeDataRequested, isTreeData,
211
+ isRowGrouping, isHierarchyEnabled, hasGroupingColumn, pagination, listView,
212
+ } = resolveGridModes({
213
+ rowGroupingModel,
214
+ treeData,
215
+ hasTreeDataPath: Boolean(getTreeDataPath),
216
+ isPivotActive,
217
+ rowReordering: rowReorderingProp,
218
+ hasGroupingColDef: Boolean(groupingColDef),
219
+ pagination: propPagination,
220
+ paginationMode,
221
+ listView: listViewRequested,
222
+ hasListViewColumn: Boolean(listViewColumn),
223
+ });
245
224
 
246
- const viewportRef = useRef<HTMLDivElement>(null);
247
- const contentRef = useRef<HTMLDivElement>(null);
248
- const gridRef = useRef<HTMLDivElement>(null);
249
-
250
- const defaultGetRowId = useCallback((row: R) => row.id, []);
251
- const effectiveGetRowId = getRowId || defaultGetRowId;
252
-
253
- // Normalize rows so every row has `id === getRowId(row)`.
254
- // createInitialState and SET_ROWS both key the internal store by row.id,
255
- // so rows without a native id field collide on undefined without this.
256
- // When getRowId is the default (row) => row.id this is a no-op per row.
257
- const normalizedRows = useMemo(
258
- () => activeRows.map(row => {
259
- const id = effectiveGetRowId(row);
260
- return id === row.id ? row : ({ ...row, id } as R);
261
- }),
262
- [activeRows, effectiveGetRowId]
263
- );
225
+ // Props, then the enclosing DataGridThemeProvider's heights, then the defaults (52 / 56).
226
+ const { rowHeight: effectiveRowHeight, headerHeight } = useGridThemeDimensions({
227
+ rowHeight: rowHeightProp, headerHeight: headerHeightProp, density: effectiveDensity,
228
+ });
264
229
 
265
- // Keyboard-mode flag: toggled via DOM classname — no React state needed
266
- // so the ring appears instantly without a re-render cycle.
267
- const setKeyboardMode = useCallback((on: boolean) => {
268
- containerRef.current?.classList.toggle('ogx--kb', on);
269
- }, []);
230
+ const activeRows = pivot.rows as unknown as R[];
231
+ const baseColumns = pivot.columns as unknown as GridColDef<R>[];
270
232
 
271
- const dataSourceRef = useRef<GridDataSource<R> | undefined>(dataSource);
272
- dataSourceRef.current = dataSource;
233
+ const { activeColumns, effectivePinnedColumns } = useGridGroupingColumn<R>({
234
+ baseColumns, pinnedColumns, hasGroupingColumn, groupingColDef, isTreeData, getTreeDataPath,
235
+ });
236
+
237
+ const columnLookup = useGridColumnLookup(activeColumns, columnVisibilityModel);
273
238
 
274
- const fetchChildrenRef = useRef<((parentId: GridRowId, groupKeys: string[]) => Promise<void>) | null>(null);
275
- const treeDataRef = useRef<ReturnType<typeof useTreeData> | null>(null);
239
+ // getRows-provided totals, dropped as soon as the aggregation model they were computed for changes.
240
+ const [serverAggregationResults, setServerAggregationResults] = useServerAggregationResults(aggregationModel);
276
241
 
277
- const handleNodeExpansion = useCallback((node: GridTreeNode) => {
278
- if (dataSourceRef.current && treeData) {
242
+ const containerRef = useRef<HTMLDivElement>(null);
243
+ const viewportRef = useRef<HTMLDivElement>(null);
279
244
 
280
- if ((node.serverChildrenCount ?? 0) > 0 && (node.children ?? []).length === 0) {
245
+ // Rows are stored and handed to consumers untouched: getRowId only decides the internal
246
+ // key. Pivot rows are the grid's own and carry their own ids, so a getRowId written for
247
+ // the source rows does not apply to them.
248
+ const effectiveGetRowId = (!isPivotActive && getRowId) || getDefaultRowId;
281
249
 
282
- const groupKeys = treeDataRef.current?.getNodePath(node.id) || [node.groupingKey];
283
- fetchChildrenRef.current?.(node.id, groupKeys);
284
- }
285
- }
286
- }, [treeData]);
250
+ const setKeyboardMode = useGridKeyboardMode(containerRef);
287
251
 
288
252
  const gridData = useDataGrid({
289
- rows: normalizedRows,
253
+ rows: activeRows,
254
+ getRowId: effectiveGetRowId,
290
255
  columns: activeColumns,
291
256
  rowHeight: effectiveRowHeight,
292
257
  headerHeight,
293
- rowCount: propRowCount,
294
258
  columnVisibilityModel,
295
- initialState: props.initialState
259
+ initialState: props.initialState,
260
+ syncRows: !dataSource,
296
261
  });
297
262
  const {
298
- state,
299
- apiRef,
300
- setRows,
301
- setColumns,
302
- setDimensions,
303
- setDataSourceLoading,
304
- setDataSourceError,
263
+ state, apiRef, setRows, replaceRow, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
305
264
  setRowCount
306
265
  } = gridData;
307
266
 
308
- const { scrollTop, scrollLeft, overscanRows, handleScroll } = useGridScrollSync({ onRowsScrollEnd, overscanRowCount });
309
-
310
- useEffect(() => {
311
- if (propApiRef) {
312
- propApiRef.current = apiRef.current;
313
- }
314
- }, [propApiRef, apiRef]);
315
-
316
- const { copySelectedRows } = useGridClipboard({
317
- selectedRowIds,
318
- columns: activeColumns as unknown as GridColDef[],
319
- getVisibleRows: () => apiRef.current.getVisibleRows(),
320
- getRowId: effectiveGetRowId as unknown as (row: GridRowModel) => GridRowId,
321
- });
322
-
323
- // Expose on apiRef for programmatic use
324
- useEffect(() => {
325
- apiRef.current.copySelectedRows = copySelectedRows;
326
- apiRef.current.getSelectedRows = () => Array.from(selectedRowIds);
327
- }, [copySelectedRows, selectedRowIds, apiRef]);
267
+ const { getRowIdOf, getRowIdOfInEvents } = useGridRowIdOf(state.rows.idByRow);
268
+ useGridApiRefBinding(propApiRef, apiRef);
328
269
 
329
270
  const isInternalLoading = state.dataSource.loading;
330
271
  const effectiveLoading = loading || isInternalLoading;
@@ -333,479 +274,315 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
333
274
  return state.rows.allRows.map(id => state.rows.idRowsLookup.get(id)!) as R[];
334
275
  }, [state.rows]);
335
276
 
336
- const treeDataHandlers = useTreeData({
277
+ const { treeDataHandlers, rowGroupingHandlers, activeHierarchyHandlers, rowMetaMap } = useGridHierarchy<R>({
337
278
  rows: effectiveRows,
338
- getRowId: effectiveGetRowId,
339
- getTreeDataPath,
340
- treeData,
341
- defaultGroupingExpansionDepth,
342
- filterModel,
343
-
344
- sortModel,
345
- onRowExpansionChange: handleNodeExpansion
346
- });
347
-
348
- useEffect(() => {
349
- treeDataRef.current = treeDataHandlers;
350
- }, [treeDataHandlers]);
351
-
352
- const rowGroupingHandlers = useRowGrouping({
353
- rows: effectiveRows,
354
- getRowId: effectiveGetRowId,
279
+ getRowId: getRowIdOf,
280
+ getTreeDataPath, isTreeDataRequested, isTreeData,
355
281
  columns: activeColumns,
356
- rowGroupingModel,
357
- aggregationModel,
358
- defaultGroupingExpansionDepth,
359
- filterModel,
360
- sortModel,
361
- getAggregationPosition
282
+ rowGroupingModel: hierarchyRowGroupingModel,
283
+ isRowGrouping, aggregationModel, defaultGroupingExpansionDepth, filterModel, sortModel,
284
+ getAggregationPosition, columnLookup, filterMode, sortingMode,
362
285
  });
363
286
 
364
287
  const editingHandlers = useGridEditing({
365
288
  rows: effectiveRows,
366
- getRowId: effectiveGetRowId,
289
+ getRowId: getRowIdOf,
290
+ columns: activeColumns,
367
291
  processRowUpdate,
368
292
  onProcessRowUpdateError,
369
- onRowChange: (updatedRow) => {
370
- // Dispatch a proper state update so React re-renders with the new row value
371
- const { dispatch } = gridData;
372
- const currentRows = Array.from(gridData.state.rows.idRowsLookup.values());
373
- const nextRows = currentRows.map(r =>
374
- r.id === updatedRow.id ? updatedRow : r
375
- );
376
- dispatch({ type: 'SET_ROWS', payload: nextRows });
293
+ onRowChange: (updatedRow, rowId) => {
294
+ // Replaces the edited row in the *current* store, under the edited row's id: this runs
295
+ // after an awaited processRowUpdate, by which time the rows prop (or a dataSource
296
+ // fetch) may have replaced the rows this render saw. The returned row is stored as is
297
+ // (the grid never writes an id onto it), even if it does not carry its key itself.
298
+ replaceRow(rowId, updatedRow);
377
299
  },
378
300
  });
379
301
 
380
- const isTreeData = treeData;
381
- const isRowGrouping = !!(rowGroupingModel && rowGroupingModel.length > 0);
382
- const isHierarchyEnabled = isTreeData || isRowGrouping;
383
- const activeHierarchyHandlers = isTreeData ? treeDataHandlers : (isRowGrouping ? rowGroupingHandlers : null);
384
-
385
- const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
386
- if (isTreeData) return treeDataHandlers.rowMetaMap;
387
- if (isRowGrouping) return rowGroupingHandlers.rowMetaMap;
388
- return EMPTY_ROW_META_MAP;
389
- }, [isTreeData, treeDataHandlers.rowMetaMap, isRowGrouping, rowGroupingHandlers.rowMetaMap]);
390
-
391
- const pagination = propPagination && !isRowGrouping;
392
-
393
- const handleRowClick = useCallback((params: GridRowParams<R>) => {
394
- const { row, id } = params;
395
-
396
- if (isHierarchyEnabled && rowMetaMap.get(row.id)?.hasChildren) {
397
- activeHierarchyHandlers?.toggleExpansion(id);
398
- return;
399
- }
400
-
401
- onRowClick?.(params);
402
-
403
- if (!disableRowSelectionOnClick) {
404
- let next: GridRowId[];
405
- if (disableMultipleRowSelection) {
406
- const alreadySelected = selectedRowIds.has(id as GridRowId);
407
- next = alreadySelected ? [] : [id as GridRowId];
408
- } else {
409
- const newSelection = new Set(selectedRowIds);
410
- if (newSelection.has(id as GridRowId)) {
411
- newSelection.delete(id as GridRowId);
412
- } else {
413
- newSelection.add(id as GridRowId);
414
- }
415
- next = Array.from(newSelection);
416
- }
417
- if (!isSelectionControlled) {
418
- setInternalRowSelectionModel(next);
419
- }
420
- onRowSelectionModelChange?.(next);
421
- }
422
- }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap, disableRowSelectionOnClick, disableMultipleRowSelection, selectedRowIds, isSelectionControlled, setInternalRowSelectionModel, onRowSelectionModelChange]);
302
+ // The selection without ids of removed rows. Only client-owned rows are pruned: a server page
303
+ // or a pivot of the rows does not hold every row a selection may name.
304
+ const selectedRowIds = useGridLiveRowSelection({
305
+ selectedRowIds: selectionModelRowIds,
306
+ rowsLookup: state.rows.idRowsLookup,
307
+ rowMetaMap,
308
+ enabled: !dataSource && !isPivotActive && paginationMode === 'client' && filterMode === 'client',
309
+ onSelectionModelChange: handleRowSelectionModelChange,
310
+ });
423
311
 
424
312
  const dataSourceHandlers = useGridDataSource({
425
- dataSource,
426
- sortModel,
427
- filterModel,
313
+ dataSource, sortModel, filterModel,
428
314
  paginationModel: effectivePaginationModel,
429
- paginationMode,
430
- sortingMode,
431
- filterMode,
432
- aggregationModel,
433
- setRows,
434
- setRowCount,
435
- setDataSourceLoading,
436
- setDataSourceError,
315
+ paginationMode, sortingMode, filterMode, aggregationModel,
316
+ rowGroupingActive: isRowGrouping,
317
+ getRowId: effectiveGetRowId as unknown as (row: GridRowModel) => GridRowId,
318
+ setRows, setRowCount, setDataSourceLoading, setDataSourceError,
437
319
  onAggregationResults: setServerAggregationResults,
320
+ onPaginationModelChange: handlePaginationModelChange,
438
321
  });
439
322
 
440
- useEffect(() => {
441
- if (dataSourceHandlers.fetchChildren) {
442
- fetchChildrenRef.current = dataSourceHandlers.fetchChildren;
443
- }
444
- }, [dataSourceHandlers.fetchChildren]);
445
-
446
- useEffect(() => {
447
- if (!dataSource) {
448
- setRows(normalizedRows);
449
- }
450
- }, [normalizedRows, setRows, dataSource]);
323
+ useServerTreeChildren({
324
+ enabled: isTreeData && Boolean(dataSource),
325
+ treeNodes: treeDataHandlers.treeNodes,
326
+ isGroupExpanded: treeDataHandlers.isGroupExpanded,
327
+ getNodePath: treeDataHandlers.getNodePath,
328
+ toggleExpansion: treeDataHandlers.toggleExpansion,
329
+ fetchChildren: dataSourceHandlers.fetchChildren,
330
+ });
451
331
 
452
332
  // ── Detail panel (hoisted — hasDetailPanel feeds into useGridColumns) ──────
453
- const hasDetailPanel = Boolean(getDetailPanelContent);
454
- const [internalExpandedRowIds, setInternalExpandedRowIds] = useState<Set<GridRowId>>(new Set());
455
- const expandedRowIds = controlledExpandedRowIds ?? internalExpandedRowIds;
456
-
457
- const handleDetailPanelToggle = useCallback((rowId: GridRowId) => {
458
- const newExpandedRowIds = new Set(expandedRowIds);
459
- if (newExpandedRowIds.has(rowId)) {
460
- newExpandedRowIds.delete(rowId);
461
- } else {
462
- newExpandedRowIds.add(rowId);
463
- }
464
- if (controlledExpandedRowIds === undefined) {
465
- setInternalExpandedRowIds(newExpandedRowIds);
466
- }
467
- onDetailPanelExpandedRowIdsChange?.(newExpandedRowIds);
468
- }, [expandedRowIds, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange]);
333
+ const {
334
+ hasDetailPanel, expandedRowIds, handleDetailPanelToggle, detailPanelHeights, reportDetailPanelHeight,
335
+ } = useGridDetailPanel({
336
+ hasDetailPanel: Boolean(getDetailPanelContent),
337
+ controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange, rowMetaMap,
338
+ });
469
339
 
470
340
  // ── Column management ─────────────────────────────────────────────────────
471
341
  const {
472
- effectiveColumns,
473
- orderedColumns,
474
- visibleOrderedColumns,
475
- navigationColumns,
476
- columnWidths,
477
- effectiveColumnOrder,
478
- setInternalColumnOrder,
479
- columnReorderHandlers,
480
- handleColumnResize,
342
+ effectiveColumns, hierarchyField, orderedColumns, visibleOrderedColumns, renderOrderedColumns, navigationColumns, columnIndexMap,
343
+ columnWidths, effectiveColumnOrder, columnReorderHandlers, moveColumn, resetColumnOrder, handleColumnResize,
481
344
  } = useGridColumns<R>({
482
- activeColumns,
483
- isHierarchyEnabled,
484
- isRowGrouping,
485
- isTreeData,
486
- activeHierarchyHandlers,
487
- columnVisibilityModel,
488
- columnOrder,
489
- onColumnOrderChange,
490
- disableColumnReorder,
491
- pivotMode,
492
- checkboxSelection,
493
- hasDetailPanel,
494
- rowReordering,
495
- initialState,
496
- setColumns,
345
+ activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers,
346
+ columnVisibilityModel, columnOrder, onColumnOrderChange, onColumnOrderModelChange, disableColumnReorder,
347
+ pivotMode: isPivotActive,
348
+ checkboxSelection, hasDetailPanel, rowReordering, initialState, setColumns,
349
+ pinnedColumns: effectivePinnedColumns,
350
+ getRowId: getRowIdOfInEvents,
351
+ aggregationModel,
352
+ groupingRows: isRowGrouping ? rowGroupingHandlers.groupingRows : undefined,
497
353
  });
498
354
 
499
355
  useGridStateSnapshot({
500
- onStateChange,
501
- sortModel,
502
- filterModel,
503
- effectivePaginationModel,
504
- columnWidths,
505
- effectiveColumnOrder,
506
- columnVisibilityModel,
507
- pinnedColumns: effectivePinnedColumns,
356
+ onStateChange, sortModel, filterModel, effectivePaginationModel, columnWidths, effectiveColumnOrder,
357
+ columnVisibilityModel, pinnedColumns,
358
+ density: effectiveDensity,
508
359
  });
509
360
 
510
361
  const rowPipeline = useGridRowPipeline<GridRowModel>({
511
362
  effectiveRows: effectiveRows as GridRowModel[],
512
- activeHierarchyHandlers: activeHierarchyHandlers as { getVisibleRows: () => GridRowModel[] } | null,
363
+ activeHierarchyHandlers,
513
364
  filterMode,
514
- filterModel,
515
- dataSource: dataSource as GridDataSource<GridRowModel> | undefined,
516
- sortModel,
517
- sortingMode,
518
- pagination,
365
+ filterModel: pivot.pipelineFilterModel,
366
+ sortModel: pivot.pipelineSortModel,
367
+ sortingMode, pagination, paginationMode, effectivePaginationModel, pinnedRows,
368
+ getRowId: getRowIdOf,
369
+ columnLookup,
370
+ hierarchyField,
371
+ });
372
+ const {
373
+ filteredRows, dataRows, pinnedTopRows, pinnedBottomRows, sortedUnpinnedRows, paginatedUnpinnedRows, allRenderableRows,
374
+ } = rowPipeline as unknown as GridRowPipelineResult<R>;
375
+ // The unpinned rows on screen: the current page, or every row without pagination.
376
+ const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
377
+
378
+ useGridPageCorrection({
379
+ paginationModel: effectivePaginationModel,
380
+ currentPage: rowPipeline.currentPage,
381
+ enabled: pagination && !effectiveLoading && sortedUnpinnedRows.length > 0,
382
+ onPaginationModelChange: handlePaginationModelChange,
383
+ });
384
+
385
+ const paginationRowCount = getPaginationRowCount({
519
386
  paginationMode,
520
- effectivePaginationModel,
521
- pinnedRows,
522
- isLoading: state.dataSource.loading,
523
- pageSize: effectivePaginationModel.pageSize,
387
+ hasDataSource: Boolean(dataSource),
388
+ dataSourceRowCount: state.pagination.rowCount,
389
+ rowCountProp: propRowCount,
390
+ clientRowCount: sortedUnpinnedRows.length,
391
+ });
392
+
393
+ const { rowSelection, handleRowClick, handleSelectionChange } = useGridRowInteractions<R>({
394
+ dataRows,
395
+ getRowId: getRowIdOf,
396
+ isPivotActive,
397
+ selectedRowIds,
398
+ onSelectionModelChange: handleRowSelectionModelChange,
399
+ disableMultipleRowSelection, disableRowSelectionOnClick, isHierarchyEnabled, rowMetaMap,
400
+ activeHierarchyHandlers, onRowClick,
401
+ });
402
+
403
+ useGridApiMethods({
404
+ apiRef: gridData.apiRef,
405
+ sortModel,
406
+ multiSort,
407
+ onSortModelChange: handleSortModelChange,
408
+ filterModel,
409
+ onFilterModelChange: handleFilterModelChange,
410
+ paginationModel: effectivePaginationModel,
411
+ onPaginationModelChange: handlePaginationModelChange,
412
+ selectedRowIds,
413
+ getPendingRowSelectionModel: controlledState.getPendingRowSelectionModel,
414
+ onRowSelectionModelChange: handleRowSelectionModelChange,
415
+ disableMultipleRowSelection,
416
+ isSyntheticRowId: (id) => isSyntheticRowId(id, rowMetaMap),
417
+ getVisibleRows: () => [...pinnedTopRows, ...centerRows, ...pinnedBottomRows],
418
+ getAllFilteredRows: () => collectAllFilteredRows<GridRowModel>({
419
+ hierarchyHandlers: activeHierarchyHandlers,
420
+ pinnedTopRows,
421
+ sortedUnpinnedRows,
422
+ pinnedBottomRows,
423
+ rowMetaMap,
424
+ getRowId: getRowIdOf,
425
+ hierarchyField,
426
+ dataRows,
427
+ }),
428
+ visibleColumns: renderOrderedColumns as unknown as GridColDef[],
524
429
  });
525
- const filteredRows = rowPipeline.filteredRows as R[];
526
- const pinnedTopRows = rowPipeline.pinnedTopRows as R[];
527
- const pinnedBottomRows = rowPipeline.pinnedBottomRows as R[];
528
- const sortedUnpinnedRows = rowPipeline.sortedUnpinnedRows as R[];
529
- const paginatedUnpinnedRows = rowPipeline.paginatedUnpinnedRows as R[];
530
- const allRenderableRows = rowPipeline.allRenderableRows as R[];
531
-
532
- useEffect(() => {
533
- gridData.apiRef.current.getVisibleRows = () => pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
534
- gridData.apiRef.current.getAllFilteredRows = () => sortedUnpinnedRows;
535
- gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as unknown as GridColDef[];
536
- }, [sortedUnpinnedRows, paginatedUnpinnedRows, pagination, effectiveColumns, gridData.apiRef]);
537
430
 
431
+ // Indices address the consumer's own rows, whatever the sort, filter, page or pinning.
538
432
  const rowReorderHandlers = useRowReorder({
539
- rows: pagination ? paginatedUnpinnedRows : sortedUnpinnedRows,
540
- getRowId: (row) => row.id,
433
+ rows: effectiveRows,
434
+ getRowId: getRowIdOf,
541
435
  onRowOrderChange,
542
436
  rowReordering
543
437
  });
544
438
 
545
- const spanning = useGridSpanning(
546
- allRenderableRows,
547
- navigationColumns,
548
- columnWidths
549
- );
550
-
551
- const { aggregationResult } = useAggregation({
552
- rows: filteredRows,
439
+ const { aggregationResult, isLoading: isAggregationLoading } = useAggregation({
440
+ rows: dataRows,
553
441
  columns: activeColumns as unknown as GridColDef[],
554
- aggregationModel,
555
- isServerSide: !!(dataSource && (paginationMode === 'server' || sortingMode === 'server')),
556
- dataSource,
557
- filterModel,
558
- sortModel,
559
- serverAggregationResults,
442
+ // The pivot's Grand Total row is the aggregate in pivot mode: nothing to total here.
443
+ aggregationModel: isPivotActive ? NO_AGGREGATION_MODEL : aggregationModel,
444
+ // When the server drives the rows (paginates, sorts or filters them), the grid only holds
445
+ // the rows it returned, so client totals would be partial.
446
+ isServerSide: isServerDrivenDataSource({ dataSource, paginationMode, sortingMode, filterMode }),
447
+ dataSource, filterModel, sortModel, serverAggregationResults,
560
448
  });
561
449
 
562
- const hasAggregation = Object.keys(aggregationModel).length > 0;
563
-
564
- useEffect(() => {
565
- gridData.apiRef.current.getAggregationResult = () => hasAggregation ? aggregationResult : null;
566
- gridData.apiRef.current.getAggregationModel = () => hasAggregation ? aggregationModel : null;
567
- gridData.apiRef.current.getGroupedExportRows = (): GridGroupedExportRow[] | null => {
568
- if (!isRowGrouping) return null;
569
- const { treeNodes, groupingRows } = rowGroupingHandlers;
570
- const rowLookup = new Map<GridRowId, GridRowModel>();
571
- sortedUnpinnedRows.forEach(r => rowLookup.set((r as GridRowModel).id as GridRowId, r as GridRowModel));
572
-
573
- const result: GridGroupedExportRow[] = [];
574
-
575
- const traverse = (ids: GridRowId[]) => {
576
- ids.forEach(id => {
577
- const node = treeNodes.get(id);
578
- if (!node) return;
579
- const isGroup = Boolean(node.children && node.children.length > 0);
580
- if (isGroup) {
581
- result.push({
582
- type: 'group-header',
583
- depth: node.depth,
584
- groupField: node.groupingField,
585
- groupValue: node.groupingValue,
586
- });
587
- traverse(node.children!);
588
- if (hasAggregation && node.aggregatedValues) {
589
- result.push({
590
- type: 'group-subtotal',
591
- depth: node.depth,
592
- groupField: node.groupingField,
593
- groupValue: node.groupingValue,
594
- aggregatedValues: node.aggregatedValues,
595
- });
596
- }
597
- } else {
598
- const row = rowLookup.get(id) ?? groupingRows.get(id);
599
- if (row) {
600
- result.push({ type: 'leaf', depth: node.depth, row });
601
- }
602
- }
603
- });
604
- };
605
-
606
- traverse(Array.from(treeNodes.keys()).filter(id => treeNodes.get(id)?.parentId === null));
607
-
608
- if (hasAggregation && aggregationResult) {
609
- result.push({ type: 'grand-total', depth: 0, aggregatedValues: aggregationResult });
610
- }
611
- return result;
612
- };
613
- }, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef, isRowGrouping, rowGroupingHandlers, sortedUnpinnedRows]);
450
+ // In pivot mode the pivot's own Grand Total row is the aggregate; aggregationModel keys name source
451
+ // fields that the pivot rows do not have.
452
+ const hasAggregation = Object.keys(aggregationModel).length > 0 && !isPivotActive;
453
+
454
+ useGridAggregationApi<R>({
455
+ apiRef, hasAggregation, aggregationResult, aggregationModel, isRowGrouping, rowGroupingHandlers, rowMetaMap,
456
+ columns: activeColumns,
457
+ getRowId: getRowIdOf,
458
+ hierarchyField,
459
+ });
614
460
 
615
461
  const layout = useLayout({
616
462
  rowHeight: effectiveRowHeight,
617
- pagination,
618
- paginatedUnpinnedRows,
619
- sortedUnpinnedRows,
620
- expandedRowIds,
621
- getDetailPanelHeight,
463
+ pagination, paginatedUnpinnedRows, sortedUnpinnedRows, expandedRowIds, getDetailPanelHeight,
464
+ detailPanelHeights,
622
465
  pinnedTopRowsLength: pinnedTopRows.length,
623
466
  pinnedBottomRowsLength: pinnedBottomRows.length,
624
- visibleOrderedColumns,
467
+ pinnedTopRows, pinnedBottomRows, visibleOrderedColumns,
625
468
  pinnedColumns: effectivePinnedColumns,
626
469
  columnWidths,
627
470
  viewportWidth: state.dimensions.viewportWidth || 1000,
628
- checkboxSelection,
629
- hasDetailPanel,
630
- rowReordering,
631
- pinCheckboxColumn,
632
- pinExpandColumn,
633
- autoHeight,
471
+ checkboxSelection, hasDetailPanel, rowReordering, pinCheckboxColumn, pinExpandColumn, autoHeight,
634
472
  paginationMode,
635
473
  isLoading: state.dataSource.loading,
636
474
  pageSize: effectivePaginationModel.pageSize,
475
+ getRowId: getRowIdOf,
637
476
  });
638
477
 
639
- // Merge layout-computed widths (which include flex resolution) with user-resize
640
- // overrides. The footer and other consumers that receive `columnWidths` only get
641
- // the resize-override map, which has no entry for flex columns that haven't been
642
- // manually resized — causing them to fall back to the raw `col.width` prop instead
643
- // of the actual rendered width.
644
- const resolvedColumnWidths = useMemo(() => {
645
- const result: Record<string, number> = {};
646
- for (const col of [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols]) {
647
- if (!col.isSpacer) result[col.field] = col.width;
648
- }
649
- return { ...result, ...columnWidths };
650
- }, [layout.leftPinnedCols, layout.unpinnedColsWithWidth, layout.rightPinnedCols, columnWidths]);
651
-
652
- useEffect(() => {
653
- gridData.apiRef.current.scrollToIndexes = ({ rowIndex, colIndex }) => {
654
- const el = viewportRef.current;
655
- if (!el) return;
656
-
657
- if (rowIndex !== undefined && rowIndex >= 0) {
658
- scrollRowIntoView(el, rowIndex, layout.cumulativeHeights, layout.pinnedBottomHeight);
659
- }
660
-
661
- if (colIndex !== undefined && colIndex >= 0) {
662
- // colIndex is an index into all data columns: leftPinned + unpinned + rightPinned
663
- const allDataCols = [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols];
664
- const targetCol = allDataCols[colIndex];
665
- if (!targetCol) return;
666
- const unpinnedIndex = layout.unpinnedColsWithWidth.findIndex(c => c.field === targetCol.field);
667
- if (unpinnedIndex === -1) return; // pinned column — always visible, no scroll needed
668
- const colLocalLeft = unpinnedIndex > 0 ? layout.unpinnedAccWidths[unpinnedIndex - 1] : 0;
669
- const colLocalRight = layout.unpinnedAccWidths[unpinnedIndex] ?? colLocalLeft;
670
- const colRight = layout.leftWidth + colLocalRight;
671
- const { scrollLeft, clientWidth } = el;
672
- if (colLocalLeft < scrollLeft) {
673
- el.scrollLeft = colLocalLeft;
674
- } else if (colRight > scrollLeft + clientWidth) {
675
- el.scrollLeft = colRight - clientWidth;
676
- }
677
- }
678
- };
679
- }, [layout, viewportRef, gridData.apiRef]);
478
+ const spanning = useGridSpanning<R>({
479
+ pinnedTopRows, centerRows, pinnedBottomRows,
480
+ columns: visibleOrderedColumns,
481
+ leftPinnedColumns: layout.leftPinnedCols,
482
+ unpinnedColumns: layout.unpinnedColsWithWidth,
483
+ rightPinnedColumns: layout.rightPinnedCols,
484
+ expandedRowIds: hasDetailPanel ? expandedRowIds : undefined,
485
+ rowMetaMap,
486
+ getRowId: getRowIdOf,
487
+ });
488
+
489
+ // Rendered data columns with their layout-resolved widths, in render order (column group header rows).
490
+ const renderedDataColumns = useMemo(
491
+ () => [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols],
492
+ [layout.leftPinnedCols, layout.unpinnedColsWithWidth, layout.rightPinnedCols]
493
+ );
494
+
495
+ useGridClipboardApi({
496
+ apiRef,
497
+ columns: renderedDataColumns as unknown as GridColDef[],
498
+ getRowId: getRowIdOf,
499
+ containerRef,
500
+ disableKeyboardShortcut: disableClipboardCopy,
501
+ });
502
+ useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
503
+
504
+ const { scrollTop, scrollLeft, overscanRows, handleScroll, setViewportElement } = useGridViewport({
505
+ onRowsScrollEnd, overscanRowCount, rowCount: layout.unpinnedRowsLength, autoHeight, setDimensions, viewportRef,
506
+ });
680
507
 
681
508
  const virtualization = useGridVirtualization({
682
- layout,
683
- scrollTop,
684
- scrollLeft,
509
+ layout, scrollTop, scrollLeft,
685
510
  viewportWidth: state.dimensions.viewportWidth,
686
511
  viewportHeight: state.dimensions.viewportHeight,
687
- autoHeight,
688
- rowReordering,
689
- hasDetailPanel,
690
- checkboxSelection,
691
- pinCheckboxColumn,
692
- pinExpandColumn,
512
+ autoHeight, rowReordering, hasDetailPanel, checkboxSelection, pinCheckboxColumn, pinExpandColumn,
693
513
  overscanRows,
694
514
  });
695
515
 
696
516
 
697
- useEffect(() => {
698
- if (!viewportRef.current) return;
699
-
700
- const resizeObserver = new ResizeObserver((entries) => {
701
- for (const entry of entries) {
702
- const { width, height } = entry.contentRect;
703
- setDimensions(width, height);
704
- }
705
- });
706
-
707
- resizeObserver.observe(viewportRef.current);
708
-
709
- return () => {
710
- resizeObserver.disconnect();
711
- };
712
- }, [setDimensions]);
713
-
714
- const handleSelectionChange = useCallback((rowId: GridRowId, isSelected: boolean) => {
715
- const newSelection = new Set(selectedRowIds);
716
- if (isSelected) {
717
- newSelection.add(rowId);
718
- } else {
719
- newSelection.delete(rowId);
720
- }
721
- const newSelectionArray = Array.from(newSelection);
722
-
723
- if (!isSelectionControlled) {
724
- setInternalRowSelectionModel(newSelectionArray);
725
- }
726
- onRowSelectionModelChange?.(newSelectionArray);
727
- }, [selectedRowIds, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
728
-
729
- const handleSelectAll = useCallback((isSelected: boolean) => {
730
- let newSelection: GridRowId[] = [];
731
- if (isSelected) {
732
- newSelection = effectiveRows.map((row: GridRowModel) => row.id);
733
- }
734
-
735
- if (!isSelectionControlled) {
736
- setInternalRowSelectionModel(newSelection);
737
- }
738
- onRowSelectionModelChange?.(newSelection);
739
- }, [effectiveRows, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
740
-
741
- const handleSort = useCallback((field: string, direction: GridSortDirection) => {
742
- const newSortModel = direction ? [{ field, sort: direction }] : [];
743
-
744
- if (!isSortControlled) {
745
- setInternalSortModel(newSortModel as GridSortItem[]);
746
- }
747
- onSortModelChange?.(newSortModel as GridSortItem[]);
748
- }, [isSortControlled, onSortModelChange, setInternalSortModel]);
749
-
750
- const handleSortAdd = useCallback((field: string, direction: GridSortDirection) => {
751
- const existing = sortModel.filter(item => item.field !== field);
752
- const newSortModel: GridSortItem[] = direction
753
- ? [...existing, { field, sort: direction }]
754
- : existing;
755
-
756
- if (!isSortControlled) {
757
- setInternalSortModel(newSortModel);
758
- }
759
- onSortModelChange?.(newSortModel);
760
- }, [sortModel, isSortControlled, onSortModelChange, setInternalSortModel]);
761
-
762
- const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
763
- allRenderableRows,
764
- navigationColumns,
765
- checkboxSelection,
766
- selectedRowIds,
767
- handleSelectionChange,
768
- handleDetailPanelToggle,
769
- editingHandlers,
770
- setKeyboardMode,
771
- sortModel,
772
- handleSort,
773
- isCellEditable,
774
- pagination,
517
+ const { handleSort, handleSortAdd } = useGridSortHandlers({ sortModel, onSortModelChange: handleSortModelChange });
518
+
519
+ const rowSelectionEnabled = checkboxSelection || !disableRowSelectionOnClick;
520
+ const {
521
+ focusedCell, setFocusedCell, viewportTabIndex, ensureGridFocus, handleFocus, handleBlur, handleKeyDown,
522
+ handleMouseDownCapture,
523
+ } = useGridKeyboardNavigation({
524
+ allRenderableRows, navigationColumns, checkboxSelection, selectedRowIds, handleSelectionChange,
525
+ handleDetailPanelToggle, editingHandlers, setKeyboardMode, sortModel, handleSort, handleSortAdd, multiSort,
526
+ isCellEditable, pagination,
775
527
  pageSize: effectivePaginationModel.pageSize,
776
528
  virtualization,
777
529
  viewportRef,
778
530
  pinnedTopRowCount: pinnedTopRows.length,
531
+ getRowId: getRowIdOf,
532
+ rowMetaMap,
533
+ getSpanOrigin: spanning.getSpanOrigin,
534
+ onRowActivate: handleRowClick,
535
+ toggleRowExpansion: activeHierarchyHandlers?.toggleExpansion,
536
+ handleSelectAll: rowSelection.selectAll,
537
+ rowSelectionEnabled,
538
+ multipleRowSelectionEnabled: !disableMultipleRowSelection,
539
+ pinCheckboxColumn,
540
+ pinExpandColumn,
541
+ rowHeight: effectiveRowHeight,
779
542
  });
780
543
 
781
- const handleCellClick = useCallback((params: GridCellParams<R>) => {
782
- setKeyboardMode(false);
783
- setFocusedCell({ id: params.row.id, field: params.field });
784
-
785
- gridRef.current?.focus({ preventScroll: true });
786
- onCellClick?.(params);
787
- }, [onCellClick, setKeyboardMode, setFocusedCell]);
788
-
789
- const prevEditingCellRef = useRef(editingHandlers.editingCell);
790
- useEffect(() => {
791
- const wasEditing = Boolean(prevEditingCellRef.current);
792
- const isEditing = Boolean(editingHandlers.editingCell);
793
-
794
- if (wasEditing && !isEditing) {
544
+ const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
545
+ ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId: getRowIdOf, onCellClick,
546
+ });
547
+ const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
548
+ columnVisibilityModel,
549
+ onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
550
+ pinnedColumns,
551
+ onPinnedColumnsChange: handlePinnedColumnsChange,
552
+ });
795
553
 
796
- gridRef.current?.focus({ preventScroll: true });
797
- }
554
+ const ariaRows = useGridAriaRows({
555
+ columnGroupingModel,
556
+ pinnedTopCount: pinnedTopRows.length,
557
+ pinnedBottomCount: pinnedBottomRows.length,
558
+ pagination,
559
+ paginationMode,
560
+ paginationModel: effectivePaginationModel,
561
+ paginationRowCount,
562
+ paginatedUnpinnedRowCount: paginatedUnpinnedRows.length,
563
+ sortedUnpinnedRowCount: sortedUnpinnedRows.length,
564
+ });
798
565
 
799
- prevEditingCellRef.current = editingHandlers.editingCell;
800
- }, [editingHandlers.editingCell]);
566
+ const spanRowWindow = useGridSpanRowWindow({
567
+ spanning,
568
+ renderContext: virtualization.renderContext,
569
+ offsetTop: virtualization.offsetTop,
570
+ cumulativeHeights: layout.cumulativeHeights,
571
+ });
801
572
 
802
573
  const visibleRows = useGridVisibleRows<R>({
803
- renderContext: virtualization.renderContext,
804
- pinnedTopRows,
805
- pinnedBottomRows,
806
- paginatedUnpinnedRows,
807
- sortedUnpinnedRows,
808
- pagination,
574
+ renderContext: spanRowWindow.renderContext,
575
+ pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination,
576
+ getRowId: getRowIdOf,
577
+ });
578
+
579
+ const renderColumns = useGridSpanColumnWindow<R>({
580
+ spanning,
581
+ firstColumnIndex: virtualization.renderContext.firstColumnIndex,
582
+ lastColumnIndex: virtualization.renderContext.lastColumnIndex,
583
+ virtualColumns: virtualization.virtualColumns,
584
+ layout,
585
+ rows: visibleRows,
809
586
  });
810
587
 
811
588
  useGridDevWarnings({
@@ -814,82 +591,77 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
814
591
  autoHeight,
815
592
  viewportHeight: state.dimensions.viewportHeight,
816
593
  renderedRowCount: visibleRows.length,
817
- totalRowCount: pinnedTopRows.length + (pagination ? paginatedUnpinnedRows.length : sortedUnpinnedRows.length) + pinnedBottomRows.length,
594
+ totalRowCount: pinnedTopRows.length + centerRows.length + pinnedBottomRows.length,
595
+ pinnedRowsIgnored: isHierarchyEnabled && Boolean(pinnedRows?.top?.length || pinnedRows?.bottom?.length),
596
+ scrollHeight: virtualization.totalHeight,
597
+ listViewWithoutColumn: listViewRequested && !listViewColumn,
598
+ listViewRowCount: listView && listViewColumn ? allRenderableRows.length : 0,
818
599
  });
819
600
 
820
- const allSelected = rows.length > 0 && selectedRowIds.size === rows.length;
821
- const someSelected = selectedRowIds.size > 0 && selectedRowIds.size < rows.length;
601
+ const { allSelected, someSelected } = rowSelection;
602
+
603
+ // Header drag-reorder keeps a column inside its column group (and ungrouped columns outside groups).
604
+ const headerReorderHandlers = useColumnGroupReorderGuard(columnGroupingModel, columnReorderHandlers);
822
605
 
823
606
  const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
824
- const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
825
- const containerRef = React.useRef<HTMLDivElement>(null);
826
- const standalonePanelRef = React.useRef<HTMLDivElement>(null);
607
+ const columnsPanel = useGridColumnsPanel();
608
+ useGridStylesheetWarning(containerRef);
827
609
 
828
610
  const NoRowsOverlaySlot = slots?.noRowsOverlay;
829
611
  const LoadingOverlaySlot = slots?.loadingOverlay;
612
+ const loadingOverlay = LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined;
613
+ const noRowsOverlay = NoRowsOverlaySlot ? <NoRowsOverlaySlot {...slotProps?.noRowsOverlay} /> : undefined;
830
614
  const FooterSlot = slots?.footer;
615
+ // Loading with rows on screen: the rows stay and an indicator runs over them. With no rows the
616
+ // body shows skeletons or the loading overlay instead, and infinite scroll appends skeleton rows.
617
+ const showLoadingOverRows = effectiveLoading && allRenderableRows.length > 0 && paginationMode !== 'infinite';
618
+ const ToolbarSlot = slots?.toolbar;
619
+
620
+ const toolbarProps = useGridToolbarProps<R>({
621
+ toolbar: slots?.toolbar,
622
+ toolbarSlotProps: slotProps?.toolbar,
623
+ apiRef,
624
+ orderedColumns,
625
+ baseColumns: columns,
626
+ aggregationModel,
627
+ onAggregationModelChange: handleAggregationModelChange,
628
+ pivotMode,
629
+ pivotModelProp: propPivotModel,
630
+ onPivotModelChangeProp: onPivotModelChange,
631
+ pivotModel: currentPivotModel,
632
+ onPivotModelChange: handlePivotModelChange,
633
+ filterModel,
634
+ onFilterModelChange: handleFilterModelChange,
635
+ columnVisibilityModel,
636
+ onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
637
+ disableColumnReorder, columnGroupingModel, moveColumn, resetColumnOrder,
638
+ forceColumnsOpen: columnsPanel.toolbarPanelRequested,
639
+ onColumnsPanelClose: columnsPanel.closeToolbarPanel,
640
+ });
831
641
 
832
- const toolbarProps = React.useMemo(() => {
833
- if (!slots?.toolbar) return null;
834
- const reorderHandler = disableColumnReorder
835
- ? undefined
836
- : (fromField: string, toField: string) => {
837
- const currentOrder = [...effectiveColumnOrder];
838
- const fromIdx = currentOrder.indexOf(fromField);
839
- const toIdx = currentOrder.indexOf(toField);
840
- if (fromIdx === -1 || toIdx === -1) return;
841
- const newOrder = [...currentOrder];
842
- newOrder.splice(fromIdx, 1);
843
- newOrder.splice(toIdx, 0, fromField);
844
- if (!columnOrder) setInternalColumnOrder(newOrder);
845
- const col = effectiveColumns.find(c => c.field === fromField);
846
- if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as unknown as GridColDef });
847
- };
848
- return {
849
- apiRef: gridData.apiRef,
850
- columns: orderedColumns as unknown as GridColDef[],
851
- baseColumns: columns as unknown as GridColDef[],
852
- aggregationModel,
853
- onAggregationModelChange: handleAggregationModelChange,
854
- ...(pivotMode || propPivotModel || onPivotModelChange ? {
855
- pivotModel: currentPivotModel,
856
- onPivotModelChange: handlePivotModelChange,
857
- } : {}),
858
- filterModel,
859
- onFilterModelChange,
860
- columnVisibilityModel,
861
- onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
862
- onColumnReorder: reorderHandler,
863
- onColumnOrderReset: disableColumnReorder ? undefined : () => setInternalColumnOrder(columns.map(c => c.field)),
864
- forceColumnsOpen: columnsPanelOpen,
865
- onColumnsPanelClose: () => setColumnsPanelOpen(false),
866
- ...slotProps?.toolbar,
867
- };
868
- }, [
869
- slots?.toolbar, disableColumnReorder, effectiveColumnOrder, orderedColumns, effectiveColumns,
870
- columnOrder, onColumnOrderChange, setInternalColumnOrder, gridData.apiRef,
871
- columns, aggregationModel, handleAggregationModelChange, pivotMode,
872
- propPivotModel, onPivotModelChange, currentPivotModel, handlePivotModelChange,
873
- filterModel, onFilterModelChange, columnVisibilityModel,
874
- handleColumnVisibilityModelChange, columnsPanelOpen, slotProps?.toolbar,
875
- ]);
876
-
877
- // Click-outside handler for standalone column panel
878
- useEffect(() => {
879
- if (!columnsPanelOpen || slots?.toolbar) return;
880
- function handleClickOutside(e: MouseEvent) {
881
- if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
882
- setColumnsPanelOpen(false);
883
- }
884
- }
885
- document.addEventListener('mousedown', handleClickOutside);
886
- return () => document.removeEventListener('mousedown', handleClickOutside);
887
- }, [columnsPanelOpen, slots?.toolbar]);
642
+ // Props the row renderers (top-pinned, centre and bottom-pinned rows) all take.
643
+ const rowRenderProps = {
644
+ rowReordering, columnIndexMap, editingHandlers, isCellEditable, selectedRowIds, checkboxSelection,
645
+ onRowClick: handleRowClick,
646
+ onRowDoubleClick,
647
+ onCellClick: handleCellClick,
648
+ onSelectionChange: handleSelectionChange,
649
+ columnWidths,
650
+ pinnedColumns: effectivePinnedColumns,
651
+ hasDetailPanel, expandedRowIds, getDetailPanelContent, getDetailPanelHeight,
652
+ onDetailPanelToggle: handleDetailPanelToggle,
653
+ onDetailPanelHeightChange: reportDetailPanelHeight,
654
+ pinCheckboxColumn, pinExpandColumn, focusedCell,
655
+ colspanMap: spanning.colspanMap,
656
+ rowSpanningCaches: spanning.rowSpanningCaches,
657
+ rowHeight: effectiveRowHeight,
658
+ rowMetaMap,
659
+ };
888
660
 
889
661
  return (
890
662
  <div
891
663
  ref={containerRef}
892
- className={['ogx', className, autoHeight && 'ogx--auto-height', hasRowSpanning && 'ogx--row-spanning', listView && 'ogx--list-view'].filter(Boolean).join(' ')}
664
+ className={['ogx', className, autoHeight && 'ogx--auto-height', !autoHeight && height === undefined && style?.height === undefined && 'ogx--fill', hasRowSpanning && 'ogx--row-spanning', listView && 'ogx--list-view'].filter(Boolean).join(' ')}
893
665
  style={{
894
666
  ...style,
895
667
  height: height ?? style?.height,
@@ -898,52 +670,65 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
898
670
  } as unknown as React.CSSProperties}
899
671
  aria-busy={effectiveLoading}
900
672
  >
901
- {toolbarProps && <StableToolbar {...toolbarProps} />}
902
-
903
- {!slots?.toolbar && (
904
- <GridStandaloneColumnPanel<R>
905
- isOpen={columnsPanelOpen}
906
- containerRef={containerRef}
907
- panelRef={standalonePanelRef}
908
- effectiveColumns={effectiveColumns}
909
- columnVisibilityModel={columnVisibilityModel}
910
- effectiveColumnOrder={effectiveColumnOrder}
911
- columnOrder={columnOrder}
912
- disableColumnReorder={disableColumnReorder}
913
- onClose={() => setColumnsPanelOpen(false)}
914
- onColumnVisibilityChange={handleColumnVisibilityModelChange}
915
- onColumnOrderChange={onColumnOrderChange}
916
- setInternalColumnOrder={setInternalColumnOrder}
917
- />
673
+ {ToolbarSlot && toolbarProps && (
674
+ <GridToolbarHostContext.Provider value={columnsPanel.toolbarHost}>
675
+ <GridToolbarSlot component={ToolbarSlot} toolbarProps={toolbarProps} />
676
+ </GridToolbarHostContext.Provider>
918
677
  )}
919
678
 
679
+ {/* Used by the column menu's Manage columns when no GridToolbar can show the panel */}
680
+ <GridStandaloneColumnPanel<R>
681
+ isOpen={columnsPanel.standalonePanelOpen}
682
+ containerRef={containerRef}
683
+ panelRef={columnsPanel.standalonePanelRef}
684
+ columns={orderedColumns}
685
+ columnVisibilityModel={columnVisibilityModel}
686
+ disableColumnReorder={disableColumnReorder}
687
+ onClose={columnsPanel.closeStandalonePanel}
688
+ onColumnVisibilityChange={handleColumnVisibilityModelChange}
689
+ onColumnMove={moveColumn}
690
+ onColumnOrderReset={resetColumnOrder}
691
+ columnGroupingModel={columnGroupingModel}
692
+ />
693
+
694
+
920
695
  {listView && listViewColumn && (
921
696
  <GridListView<R>
922
697
  ariaLabel={ariaLabel}
923
698
  allRenderableRows={allRenderableRows}
924
- filteredRows={filteredRows}
699
+ dataRowCount={dataRows.length}
700
+ pinnedTopRowCount={pinnedTopRows.length}
701
+ pinnedBottomRowCount={pinnedBottomRows.length}
702
+ unpinnedRowCount={sortedUnpinnedRows.length}
925
703
  pagination={pagination}
926
704
  effectivePaginationModel={effectivePaginationModel}
705
+ currentPage={rowPipeline.currentPage}
927
706
  pageSizeOptions={pageSizeOptions}
928
707
  selectedRowIds={selectedRowIds}
929
708
  listViewColumn={listViewColumn}
709
+ columnsByField={columnLookup.byField}
710
+ rowMetaMap={rowMetaMap}
930
711
  noRowsLabel={effectiveNoRowsLabel}
931
712
  rowHeight={effectiveRowHeight}
932
713
  checkboxSelection={checkboxSelection}
933
714
  paginationMode={paginationMode}
934
- dataSource={dataSource}
935
- serverRowCount={state.pagination.rowCount || 0}
715
+ serverRowCount={paginationRowCount}
936
716
  paginationSlot={slots?.pagination}
937
717
  paginationSlotProps={slotProps?.pagination}
938
- localeText={localeText ? {
939
- paginationRowsPerPage: localeText.paginationRowsPerPage,
940
- paginationOf: localeText.paginationOf,
941
- paginationPage: localeText.paginationPage,
942
- } : undefined}
718
+ localeText={pickPaginationLocaleText(localeText)}
943
719
  onRowClick={handleRowClick}
944
720
  onRowDoubleClick={onRowDoubleClick}
945
721
  onSelectionChange={handleSelectionChange}
946
722
  onPaginationModelChange={handlePaginationModelChange}
723
+ onRowsScrollEnd={onRowsScrollEnd}
724
+ getRowId={getRowIdOf}
725
+ multiselectable={rowSelectionEnabled && !disableMultipleRowSelection}
726
+ selectable={rowSelectionEnabled}
727
+ onToggleExpansion={activeHierarchyHandlers?.toggleExpansion}
728
+ loading={effectiveLoading}
729
+ loadingOverlay={loadingOverlay}
730
+ noRowsOverlay={noRowsOverlay}
731
+ showPaginationControls={!FooterSlot}
947
732
  />
948
733
  )}
949
734
 
@@ -952,225 +737,153 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
952
737
  ══════════════════════════════════════════════════════════════ */}
953
738
  {!listView && (
954
739
  <div
955
- ref={(el) => {
956
- viewportRef.current = el;
957
- gridRef.current = el;
958
- }}
740
+ ref={setViewportElement}
959
741
  className="ogx__viewport"
960
742
  onScroll={handleScroll}
961
743
  role="grid"
962
744
  aria-label={ariaLabel || 'Data grid'}
963
- aria-rowcount={filteredRows.length + 1}
964
- aria-colcount={
965
- columns.length +
966
- (checkboxSelection ? 1 : 0) +
967
- (hasDetailPanel ? 1 : 0) +
968
- (rowReordering ? 1 : 0)
969
- }
745
+ aria-rowcount={ariaRows.rowCount}
746
+ aria-colcount={navigationColumns.length}
747
+ aria-multiselectable={rowSelectionEnabled && !disableMultipleRowSelection}
970
748
  aria-busy={effectiveLoading}
971
- tabIndex={0}
749
+ tabIndex={viewportTabIndex}
972
750
  onKeyDownCapture={() => { setKeyboardMode(true); }}
973
- onMouseDownCapture={() => { setKeyboardMode(false); }}
751
+ onMouseDownCapture={handleMouseDownCapture}
974
752
  onKeyDown={handleKeyDown}
975
753
  onFocus={handleFocus}
976
754
  onBlur={handleBlur}
977
755
  >
978
756
  <div
979
- ref={contentRef}
980
757
  className="ogx__content"
981
758
  style={{
982
- width: virtualization.totalWidth
759
+ // With right-pinned columns the content spans at least the viewport, so they sit at its right edge.
760
+ width: effectivePinnedColumns?.right?.length
761
+ ? `max(100%, ${virtualization.totalWidth}px)`
762
+ : virtualization.totalWidth
983
763
  }}
984
764
  role="presentation"
985
765
  >
986
- { }
987
- <Header
988
- columns={virtualization.virtualColumns}
989
- allColumns={effectiveColumns}
990
- columnGroupingModel={columnGroupingModel}
991
- checkboxSelection={checkboxSelection}
992
- allSelected={allSelected}
993
- someSelected={someSelected}
994
- onSelectAll={handleSelectAll}
995
- sortModel={sortModel}
996
- onSort={handleSort}
997
- onSortAdd={handleSortAdd}
998
- multiSort={multiSort}
999
-
1000
- onColumnResize={handleColumnResize}
1001
- columnWidths={columnWidths}
1002
- pinnedColumns={effectivePinnedColumns}
1003
-
1004
- focusedCell={focusedCell}
1005
- onHeaderClick={(field) => {
1006
- setFocusedCell({ id: 'HEADER', field });
1007
- setKeyboardMode(false);
1008
- gridRef.current?.focus({ preventScroll: true });
1009
- }}
1010
- onDragStart={columnGroupingModel ? undefined : columnReorderHandlers.onDragStart}
1011
- onDragOver={columnGroupingModel ? undefined : columnReorderHandlers.onDragOver}
1012
- onDragEnd={columnGroupingModel ? undefined : columnReorderHandlers.onDragEnd}
1013
- onDrop={columnGroupingModel ? undefined : columnReorderHandlers.onDrop}
1014
- draggedColumn={columnGroupingModel ? undefined : columnReorderHandlers.draggedColumn}
1015
- dragOverColumn={columnGroupingModel ? undefined : columnReorderHandlers.dragOverColumn}
1016
- rowReordering={rowReordering}
1017
- hasDetailPanel={hasDetailPanel}
1018
- pinCheckboxColumn={pinCheckboxColumn}
1019
- pinExpandColumn={pinExpandColumn}
1020
- aggregationModel={aggregationModel}
1021
- onHideColumn={(field) => {
1022
- handleColumnVisibilityModelChange({
1023
- ...columnVisibilityModel,
1024
- [field]: false,
1025
- });
1026
- }}
1027
- onManageColumns={() => setColumnsPanelOpen(true)}
1028
- onPinColumn={(field, side) => {
1029
- const left = [...(pinnedColumns?.left ?? [])];
1030
- const right = [...(pinnedColumns?.right ?? [])];
1031
-
1032
- const cleanLeft = left.filter(f => f !== field);
1033
- const cleanRight = right.filter(f => f !== field);
1034
- if (side === 'left') {
1035
- handlePinnedColumnsChange({ left: [...cleanLeft, field], right: cleanRight });
1036
- } else if (side === 'right') {
1037
- handlePinnedColumnsChange({ left: cleanLeft, right: [...cleanRight, field] });
1038
- } else {
1039
- handlePinnedColumnsChange({ left: cleanLeft, right: cleanRight });
1040
- }
1041
- }}
1042
- />
766
+ {/* Header and top-pinned rows stick together, so pinned rows sit below every header row. */}
767
+ <div className="ogx__sticky-top" role="presentation">
768
+ <Header
769
+ columns={renderColumns}
770
+ allColumns={renderedDataColumns}
771
+ columnGroupingModel={columnGroupingModel}
772
+ checkboxSelection={checkboxSelection}
773
+ allSelected={allSelected}
774
+ someSelected={someSelected}
775
+ onSelectAll={rowSelection.selectAll}
776
+ sortModel={sortModel}
777
+ onSort={handleSort}
778
+ onSortAdd={handleSortAdd}
779
+ multiSort={multiSort}
780
+
781
+ onColumnResize={handleColumnResize}
782
+ columnWidths={columnWidths}
783
+ pinnedColumns={effectivePinnedColumns}
784
+
785
+ focusedCell={focusedCell}
786
+ columnIndexMap={columnIndexMap}
787
+ onHeaderClick={handleHeaderClick}
788
+ onDragStart={headerReorderHandlers.onDragStart}
789
+ onDragOver={headerReorderHandlers.onDragOver}
790
+ onDragEnd={headerReorderHandlers.onDragEnd}
791
+ onDrop={headerReorderHandlers.onDrop}
792
+ draggedColumn={headerReorderHandlers.draggedColumn}
793
+ dragOverColumn={headerReorderHandlers.dragOverColumn}
794
+ rowReordering={rowReordering}
795
+ hasDetailPanel={hasDetailPanel}
796
+ pinCheckboxColumn={pinCheckboxColumn}
797
+ pinExpandColumn={pinExpandColumn}
798
+ aggregationModel={aggregationModel}
799
+ onHideColumn={handleHideColumn}
800
+ onManageColumns={columnsPanel.openColumnsPanel}
801
+ onPinColumn={handlePinColumn}
802
+ />
803
+
804
+ <GridPinnedRows<R>
805
+ {...rowRenderProps}
806
+ rows={pinnedTopRows}
807
+ position="top"
808
+ ariaRowIndexBase={ariaRows.topBase}
809
+ columns={renderColumns}
810
+ pinnedRows={pinnedRows}
811
+ getRowId={getRowIdOf}
812
+ />
813
+ </div>
1043
814
 
1044
815
  {/* Empty State Overlay (Standard View) — showing after header */}
1045
816
  {!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
1046
817
  <GridEmptyState
1047
818
  noRowsLabel={effectiveNoRowsLabel}
1048
819
  width={virtualization.totalWidth}
1049
- overlay={NoRowsOverlaySlot ? <NoRowsOverlaySlot {...slotProps?.noRowsOverlay} /> : undefined}
820
+ overlay={noRowsOverlay}
1050
821
  />
1051
822
  )}
1052
823
 
1053
- <GridPinnedRows<R>
1054
- rows={pinnedTopRows}
1055
- position="top"
1056
- columns={virtualization.virtualColumns}
1057
- selectedRowIds={selectedRowIds}
1058
- checkboxSelection={checkboxSelection}
1059
- onRowClick={handleRowClick}
1060
- onRowDoubleClick={onRowDoubleClick}
1061
- onCellClick={handleCellClick}
1062
- onSelectionChange={handleSelectionChange}
1063
- columnWidths={columnWidths}
1064
- pinnedColumns={effectivePinnedColumns}
1065
- pinnedRows={pinnedRows}
1066
- hasDetailPanel={hasDetailPanel}
1067
- expandedRowIds={expandedRowIds}
1068
- getDetailPanelContent={getDetailPanelContent}
1069
- getDetailPanelHeight={getDetailPanelHeight}
1070
- onDetailPanelToggle={handleDetailPanelToggle}
1071
- pinCheckboxColumn={pinCheckboxColumn}
1072
- pinExpandColumn={pinExpandColumn}
1073
- focusedCell={focusedCell}
1074
- colspanMap={spanning.colspanMap}
1075
- rowSpanningCaches={spanning.rowSpanningState.caches}
1076
- rowHeight={effectiveRowHeight}
1077
- rowMetaMap={rowMetaMap}
1078
- />
1079
-
1080
824
  <GridVirtualRows<R>
825
+ {...rowRenderProps}
1081
826
  virtualContainerHeight={virtualization.totalHeight - virtualization.pinnedTopHeight - virtualization.pinnedBottomHeight}
1082
- offsetTop={virtualization.offsetTop}
827
+ offsetTop={spanRowWindow.offsetTop}
1083
828
  effectiveLoading={effectiveLoading}
1084
829
  visibleRows={visibleRows}
1085
830
  baseColumns={columns as unknown as GridColDef<R>[]}
1086
- virtualColumns={virtualization.virtualColumns}
831
+ virtualColumns={renderColumns}
1087
832
  viewportWidth={state.dimensions.viewportWidth}
1088
- checkboxSelection={checkboxSelection}
1089
- hasDetailPanel={hasDetailPanel}
1090
- rowReordering={rowReordering}
1091
- rowHeight={effectiveRowHeight}
1092
- pinnedRows={pinnedRows}
1093
- selectedRowIds={selectedRowIds}
1094
- onRowClick={handleRowClick}
1095
- onRowDoubleClick={onRowDoubleClick}
1096
- onCellClick={handleCellClick}
1097
- onSelectionChange={handleSelectionChange}
1098
- columnWidths={columnWidths}
1099
- pinnedColumns={effectivePinnedColumns}
1100
- expandedRowIds={expandedRowIds}
1101
- getDetailPanelContent={getDetailPanelContent}
1102
- getDetailPanelHeight={getDetailPanelHeight}
1103
- onDetailPanelToggle={handleDetailPanelToggle}
1104
- pinCheckboxColumn={pinCheckboxColumn}
1105
- pinExpandColumn={pinExpandColumn}
1106
833
  rowReorderHandlers={rowReorderHandlers}
1107
- editingHandlers={editingHandlers}
1108
- focusedCell={focusedCell}
1109
- colspanMap={spanning.colspanMap}
1110
- rowSpanningCaches={spanning.rowSpanningState.caches}
1111
834
  paginationMode={paginationMode}
1112
835
  dataSourceLoading={state.dataSource.loading}
1113
836
  sortedUnpinnedRowCount={sortedUnpinnedRows.length}
1114
837
  infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
1115
838
  unpinnedRowsLength={layout.unpinnedRowsLength}
1116
- rowMetaMap={rowMetaMap}
1117
- loadingOverlay={LoadingOverlaySlot ? <LoadingOverlaySlot {...slotProps?.loadingOverlay} /> : undefined}
839
+ lastRenderedRowIndex={spanRowWindow.renderContext.lastRowIndex}
840
+ ariaRowIndexOffset={ariaRows.centerOffset}
841
+ loadingOverlay={loadingOverlay}
1118
842
  />
1119
843
 
1120
- <GridPinnedRows<R>
1121
- rows={pinnedBottomRows}
1122
- position="bottom"
1123
- columns={virtualization.virtualColumns}
1124
- selectedRowIds={selectedRowIds}
1125
- checkboxSelection={checkboxSelection}
1126
- onRowClick={handleRowClick}
1127
- onRowDoubleClick={onRowDoubleClick}
1128
- onCellClick={handleCellClick}
1129
- onSelectionChange={handleSelectionChange}
1130
- columnWidths={columnWidths}
1131
- pinnedColumns={effectivePinnedColumns}
1132
- pinnedRows={pinnedRows}
1133
- hasDetailPanel={hasDetailPanel}
1134
- expandedRowIds={expandedRowIds}
1135
- getDetailPanelContent={getDetailPanelContent}
1136
- getDetailPanelHeight={getDetailPanelHeight}
1137
- onDetailPanelToggle={handleDetailPanelToggle}
1138
- pinCheckboxColumn={pinCheckboxColumn}
1139
- pinExpandColumn={pinExpandColumn}
1140
- focusedCell={focusedCell}
1141
- colspanMap={spanning.colspanMap}
1142
- rowSpanningCaches={spanning.rowSpanningState.caches}
1143
- rowHeight={effectiveRowHeight}
1144
- rowMetaMap={rowMetaMap}
1145
- />
1146
-
1147
- {/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
1148
- already contain pre-aggregated values with synthetic field keys
1149
- (e.g. 'Q1\u001frevenue\u001fsum') that don't match aggregationModel keys. */}
1150
- {hasAggregation && !pivotMode && (
1151
- <GridAggregationFooter
1152
- columns={orderedColumns as unknown as GridColDef[]}
1153
- aggregationModel={aggregationModel}
1154
- aggregationResult={aggregationResult}
1155
- columnWidths={resolvedColumnWidths}
1156
- rowHeight={effectiveRowHeight}
1157
- checkboxSelection={checkboxSelection}
1158
- hasDetailPanel={hasDetailPanel}
1159
- rowReordering={rowReordering}
1160
- pinnedColumns={effectivePinnedColumns}
844
+ {/* Bottom-pinned rows and the aggregation footer stick together, footer last. */}
845
+ <div className="ogx__sticky-bottom" role="presentation">
846
+ <GridPinnedRows<R>
847
+ {...rowRenderProps}
848
+ rows={pinnedBottomRows}
849
+ position="bottom"
850
+ rowIndexOffset={ariaRows.bottomRowIndexOffset}
851
+ ariaRowIndexBase={ariaRows.bottomBase}
852
+ columns={renderColumns}
853
+ pinnedRows={pinnedRows}
854
+ getRowId={getRowIdOf}
1161
855
  />
1162
- )}
856
+
857
+ {/* Aggregation Footer Row — laid out from the same virtual columns as the rows, so
858
+ hidden, pinned and scrolled columns line up. Off in pivot mode (hasAggregation). */}
859
+ {hasAggregation && rowGroupingHandlers.rootAggregationPosition !== null && (
860
+ <GridAggregationFooter
861
+ columns={virtualization.virtualColumns as unknown as GridColDef[]}
862
+ aggregationModel={aggregationModel}
863
+ aggregationResult={aggregationResult}
864
+ columnWidths={columnWidths}
865
+ rowHeight={effectiveRowHeight}
866
+ checkboxSelection={checkboxSelection}
867
+ hasDetailPanel={hasDetailPanel}
868
+ rowReordering={rowReordering}
869
+ pinCheckboxColumn={pinCheckboxColumn}
870
+ pinExpandColumn={pinExpandColumn}
871
+ pinnedColumns={effectivePinnedColumns}
872
+ loading={isAggregationLoading}
873
+ />
874
+ )}
875
+ </div>
1163
876
 
1164
877
  </div>
1165
878
  </div>
1166
879
  )}
1167
880
 
1168
- {!listView && FooterSlot && (
881
+ {FooterSlot && (
1169
882
  <FooterSlot
1170
883
  apiRef={gridData.apiRef}
1171
884
  aggregationModel={aggregationModel}
1172
885
  aggregationResult={hasAggregation ? aggregationResult : null}
1173
- rowCount={isRowGrouping ? effectiveRows.length : sortedUnpinnedRows.length}
886
+ rowCount={isHierarchyEnabled && paginationMode !== 'server' ? dataRows.length : paginationRowCount}
1174
887
  pagination={pagination}
1175
888
  paginationModel={effectivePaginationModel}
1176
889
  pageSizeOptions={pageSizeOptions}
@@ -1179,40 +892,33 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1179
892
  />
1180
893
  )}
1181
894
 
1182
- {!listView && !FooterSlot && pagination && (() => {
1183
- const PaginationComponent = slots?.pagination || Pagination;
1184
- return (
1185
- <PaginationComponent
1186
- page={effectivePaginationModel.page}
1187
- pageSize={effectivePaginationModel.pageSize}
1188
- rowCount={state.pagination.rowCount}
1189
- pageSizeOptions={pageSizeOptions}
1190
- onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
1191
- onPageSizeChange={(newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
1192
- localeText={localeText ? {
1193
- paginationRowsPerPage: localeText.paginationRowsPerPage,
1194
- paginationOf: localeText.paginationOf,
1195
- paginationPage: localeText.paginationPage,
1196
- } : undefined}
1197
- {...slotProps?.pagination}
1198
- />
1199
- );
1200
- })()}
1201
-
1202
- {/* Accessibility Live Region */}
1203
- <div className="ogx-aria-live-status" role="status" aria-live="polite">
1204
- {effectiveLoading ? 'Loading data...' : ''}
1205
- {state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
1206
- {!loading && !state.dataSource.error && (
1207
- filteredRows.length === 0
1208
- ? effectiveNoRowsLabel
1209
- : (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
1210
- ? `${filteredRows.length} ${filteredRows.length === 1 ? 'row' : 'rows'} found`
1211
- : ''
1212
- )}
1213
- </div>
1214
-
1215
- <GridErrorOverlay error={state.dataSource.error} />
895
+ {!listView && !FooterSlot && pagination && (
896
+ <GridPaginationArea
897
+ paginationSlot={slots?.pagination}
898
+ paginationSlotProps={slotProps?.pagination}
899
+ page={rowPipeline.currentPage}
900
+ paginationModel={effectivePaginationModel}
901
+ rowCount={paginationRowCount}
902
+ pageSizeOptions={pageSizeOptions}
903
+ onPaginationModelChange={handlePaginationModelChange}
904
+ localeText={localeText}
905
+ />
906
+ )}
907
+
908
+ <GridLiveRegion
909
+ loading={effectiveLoading}
910
+ error={state.dataSource.error}
911
+ noRowsLabel={effectiveNoRowsLabel}
912
+ filteredRowCount={filteredRows.length}
913
+ dataRowCount={dataRows.length}
914
+ filterModel={filterModel}
915
+ />
916
+
917
+ {showLoadingOverRows && (
918
+ <GridLoadingOverlay overlay={loadingOverlay} />
919
+ )}
920
+
921
+ <GridErrorOverlay error={state.dataSource.error} onRetry={dataSource ? dataSourceHandlers.fetchRows : undefined} />
1216
922
  </div>
1217
923
  );
1218
924
  }