@opencorestack/opengridx 2.0.4 → 3.0.0

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