@opencorestack/opengridx 0.1.9 → 1.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 (64) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +162 -19
  3. package/dist/index.d.ts +60 -41
  4. package/dist/opengridx.es.js +5045 -4549
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +11 -11
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +17 -7
  9. package/docs/README.md +14 -0
  10. package/docs/components/aggregation-footer.md +48 -0
  11. package/docs/components/datagrid.md +8 -3
  12. package/docs/components/empty-state.md +50 -0
  13. package/docs/components/error-overlay.md +40 -0
  14. package/docs/features/editing-reordering.md +11 -7
  15. package/docs/features/filtering.md +10 -8
  16. package/docs/features/selection.md +2 -1
  17. package/docs/features/sorting-pagination.md +3 -1
  18. package/docs/features/state-persistence.md +1 -2
  19. package/docs/roadmap.md +20 -1
  20. package/lib/components/Cell/Cell.tsx +9 -10
  21. package/lib/components/Cell/GridEditInputCell.tsx +4 -4
  22. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  23. package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
  24. package/lib/components/DataGrid/DataGrid.tsx +336 -1018
  25. package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
  26. package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
  27. package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
  28. package/lib/components/DataGrid/GridListView.tsx +116 -0
  29. package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
  30. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
  31. package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
  32. package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
  33. package/lib/components/Header/ColumnMenu.tsx +2 -2
  34. package/lib/components/Header/Header.tsx +4 -4
  35. package/lib/components/ListView/ListViewRow.tsx +2 -2
  36. package/lib/components/Row/Row.tsx +10 -10
  37. package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
  38. package/lib/components/Toolbar/GridToolbar.tsx +6 -0
  39. package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
  40. package/lib/hooks/core/useDataGrid.ts +14 -34
  41. package/lib/hooks/core/useGridColumns.tsx +228 -0
  42. package/lib/hooks/core/useGridControlledState.ts +192 -0
  43. package/lib/hooks/core/useGridRowPipeline.ts +109 -0
  44. package/lib/hooks/core/useGridScrollSync.ts +52 -0
  45. package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
  46. package/lib/hooks/core/useGridVirtualization.ts +186 -0
  47. package/lib/hooks/core/useGridVisibleRows.ts +66 -0
  48. package/lib/hooks/core/useLayout.ts +0 -6
  49. package/lib/hooks/features/useAggregation.ts +7 -7
  50. package/lib/hooks/features/useGridDataSource.ts +1 -1
  51. package/lib/hooks/features/useGridEditing.ts +31 -32
  52. package/lib/hooks/features/usePivot.test.ts +202 -0
  53. package/lib/hooks/features/usePivot.ts +0 -0
  54. package/lib/hooks/useColumnReorder.ts +1 -1
  55. package/lib/hooks/useRowGrouping.ts +14 -14
  56. package/lib/hooks/useTreeData.ts +1 -1
  57. package/lib/state/types.ts +1 -1
  58. package/lib/state/useGridStateStorage.ts +1 -1
  59. package/lib/types/index.ts +48 -32
  60. package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
  61. package/lib/utils/export/index.ts +22 -17
  62. package/lib/utils/filtering/index.ts +20 -20
  63. package/lib/utils/sorting/index.ts +1 -1
  64. package/package.json +1 -1
@@ -1,30 +1,33 @@
1
1
  import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
2
2
  import { useLayout } from '../../hooks/core/useLayout';
3
3
  import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavigation';
4
- import ReactDOM from 'react-dom';
5
- import { ListViewRow } from '../ListView/ListViewRow';
6
- import type { GridState } from '../../state/types';
4
+ import { useGridControlledState } from '../../hooks/core/useGridControlledState';
5
+ import { useGridRowPipeline } from '../../hooks/core/useGridRowPipeline';
6
+ import { useGridVirtualization } from '../../hooks/core/useGridVirtualization';
7
+ import { useGridColumns } from '../../hooks/core/useGridColumns';
8
+ import { useGridVisibleRows } from '../../hooks/core/useGridVisibleRows';
9
+ import { useGridScrollSync } from '../../hooks/core/useGridScrollSync';
10
+ import { useGridStateSnapshot } from '../../hooks/core/useGridStateSnapshot';
11
+ import { GridAggregationFooter } from './GridAggregationFooter';
12
+ import { GridEmptyState } from './GridEmptyState';
13
+ import { GridErrorOverlay } from './GridErrorOverlay';
7
14
  import { Header } from '../Header/Header';
8
- import { Row } from '../Row/Row';
9
- import { SkeletonRow } from '../SkeletonRow';
10
15
  import { Pagination } from '../Pagination/Pagination';
11
16
  import { useDataGrid } from '../../hooks/core/useDataGrid';
12
- import { useColumnReorder } from '../../hooks/useColumnReorder';
13
17
  import { useRowReorder } from '../../hooks/useRowReorder';
14
18
  import { useTreeData } from '../../hooks/useTreeData';
15
19
  import { useRowGrouping } from '../../hooks/useRowGrouping';
16
20
  import { useGridEditing } from '../../hooks/features/useGridEditing';
17
21
  import { useGridSpanning } from '../../hooks/features/useGridSpanning';
18
22
  import { useGridDataSource } from '../../hooks/features/useGridDataSource';
19
- import { useAggregation, formatAggregationValue } from '../../hooks/features/useAggregation';
23
+ import { useAggregation } from '../../hooks/features/useAggregation';
20
24
  import { usePivot } from '../../hooks/features/usePivot';
21
25
  import { useGridClipboard } from '../../hooks/features/useGridClipboard';
22
- import { ExpandIcon } from '../ui/ExpandIcon';
23
- import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
24
- import { getPinnedRowGroups, isRowPinned } from '../../utils/pinning';
25
- import { sortRows } from '../../utils/sorting';
26
- import { filterRows } from '../../utils/filtering';
27
- import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColumnOrderChangeParams, GridColDef, GridRenderCellParams, GridRowParams, GridCellParams, GridPaginationModel, GridDataSource, GridAggregationResult, GridAggregationModel, GridPivotModel, GridColumnPinning, GridFilterModel } from '../../types';
26
+ import { GridListView } from './GridListView';
27
+ import { GridPinnedRows } from './GridPinnedRows';
28
+ import { GridVirtualRows } from './GridVirtualRows';
29
+ import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
30
+ import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem } from '../../types';
28
31
 
29
32
  export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
30
33
  const {
@@ -113,15 +116,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
113
116
  const defaultFilterModel: GridFilterModel = useMemo(() => ({ items: [] }), []);
114
117
  const filterModel = (propFilterModel || defaultFilterModel) as GridFilterModel;
115
118
 
116
- const [internalSortModel, setInternalSortModel] = useState<{ field: string; sort: 'asc' | 'desc' }[]>(
117
- () => initialState?.sorting?.sortModel ?? []
118
- );
119
- const isSortControlled = propSortModel !== undefined;
120
- const sortModel = isSortControlled ? propSortModel : internalSortModel;
121
-
122
119
  const defaultRowGroupingModel = useMemo(() => [], []);
123
120
  const rowGroupingModel = propRowGroupingModel || defaultRowGroupingModel;
124
- const isAggregationControlled = propAggregationModel !== undefined;
125
121
 
126
122
  // ─── Stabilize toolbar component identity ────────────────────────────────
127
123
  // Problem: demos/users often define their toolbar as an inline function
@@ -148,51 +144,46 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
148
144
  const Toolbar = latestToolbarRef.current;
149
145
  return Toolbar ? (Toolbar as (p: typeof props) => React.ReactElement | null)(props) : null;
150
146
  }).current;
151
- const [internalAggregationModel, setInternalAggregationModel] = useState<GridAggregationModel>(
152
- () => propAggregationModel ?? {}
153
- );
154
- const aggregationModel = isAggregationControlled ? propAggregationModel : internalAggregationModel;
155
-
156
- const handleAggregationModelChange = useCallback((model: GridAggregationModel) => {
157
- if (!isAggregationControlled) {
158
- setInternalAggregationModel(model);
159
- }
160
- onAggregationModelChange?.(model);
161
- }, [isAggregationControlled, onAggregationModelChange]);
162
147
 
163
- const [internalColumnVisibilityModel, setInternalColumnVisibilityModel] = useState<Record<string, boolean>>(
164
- () => initialState?.columns?.columnVisibilityModel ?? {}
165
- );
166
- const isColumnVisibilityControlled = propColumnVisibilityModel !== undefined;
167
- const columnVisibilityModel = isColumnVisibilityControlled ? propColumnVisibilityModel : internalColumnVisibilityModel;
148
+ const controlledState = useGridControlledState({
149
+ initialState,
150
+ sortModel: propSortModel,
151
+ onSortModelChange,
152
+ aggregationModel: propAggregationModel,
153
+ onAggregationModelChange,
154
+ columnVisibilityModel: propColumnVisibilityModel,
155
+ onColumnVisibilityModelChange,
156
+ pinnedColumns: propPinnedColumns,
157
+ onPinnedColumnsChange,
158
+ pivotModel: propPivotModel,
159
+ onPivotModelChange,
160
+ paginationModel,
161
+ onPaginationModelChange,
162
+ rowSelectionModel: propRowSelectionModel,
163
+ onRowSelectionModelChange: propOnRowSelectionModelChange,
164
+ });
168
165
 
169
- const handleColumnVisibilityModelChange = useCallback((model: Record<string, boolean>) => {
170
- if (!isColumnVisibilityControlled) {
171
- setInternalColumnVisibilityModel(model);
172
- }
173
- onColumnVisibilityModelChange?.(model);
174
- }, [isColumnVisibilityControlled, onColumnVisibilityModelChange]);
166
+ const {
167
+ sortModel,
168
+ isSortControlled,
169
+ setInternalSortModel,
170
+ aggregationModel,
171
+ handleAggregationModelChange,
172
+ columnVisibilityModel,
173
+ handleColumnVisibilityModelChange,
174
+ pinnedColumns,
175
+ handlePinnedColumnsChange,
176
+ currentPivotModel,
177
+ handlePivotModelChange,
178
+ effectivePaginationModel,
179
+ handlePaginationModelChange,
180
+ selectedRowIds,
181
+ isSelectionControlled,
182
+ setInternalRowSelectionModel,
183
+ } = controlledState;
175
184
 
176
- const [internalPinnedColumns, setInternalPinnedColumns] = useState<GridColumnPinning>(
177
- () => initialState?.columns?.pinnedColumns ?? {}
178
- );
179
- const isPinnedColumnsControlled = propPinnedColumns !== undefined;
180
- const pinnedColumns = isPinnedColumnsControlled ? propPinnedColumns : internalPinnedColumns;
185
+ const onRowSelectionModelChange = propOnRowSelectionModelChange;
181
186
 
182
- const handlePinnedColumnsChange = useCallback((model: GridColumnPinning) => {
183
- if (!isPinnedColumnsControlled) {
184
- setInternalPinnedColumns(model);
185
- }
186
- onPinnedColumnsChange?.(model);
187
- }, [isPinnedColumnsControlled, onPinnedColumnsChange]);
188
-
189
- const EMPTY_PIVOT_MODEL: GridPivotModel = useMemo(() => ({ rowFields: [], columnFields: [], valueFields: [] }), []);
190
- const [internalPivotModel, setInternalPivotModel] = useState<GridPivotModel>(propPivotModel ?? EMPTY_PIVOT_MODEL);
191
- const currentPivotModel = propPivotModel ?? internalPivotModel;
192
- const handlePivotModelChange = useCallback((model: GridPivotModel) => {
193
- setInternalPivotModel(model);
194
- onPivotModelChange?.(model);
195
- }, [onPivotModelChange]);
196
187
  const pivot = usePivot(rows as GridRowModel[], columns as unknown as GridColDef[], currentPivotModel, pivotMode);
197
188
 
198
189
  const activeRows = (pivotMode && pivot.isValid ? pivot.pivotRows : rows) as unknown as R[];
@@ -204,25 +195,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
204
195
  setServerAggregationResults(null);
205
196
  }, [aggregationModel]);
206
197
 
207
- const [internalPaginationModel, setInternalPaginationModel] = useState(
208
- () => initialState?.pagination?.paginationModel ?? paginationModel
209
- );
210
- const isPaginationControlled = props.paginationModel !== undefined;
211
- const effectivePaginationModel = isPaginationControlled ? paginationModel : internalPaginationModel;
212
-
213
- const handlePaginationModelChange = useCallback((newModel: GridPaginationModel) => {
214
- if (!isPaginationControlled) {
215
- setInternalPaginationModel(newModel);
216
- }
217
- onPaginationModelChange?.(newModel);
218
- }, [isPaginationControlled, onPaginationModelChange]);
219
-
220
- const [internalRowSelectionModel, setInternalRowSelectionModel] = useState<GridRowId[]>([]);
221
- const isSelectionControlled = propRowSelectionModel !== undefined;
222
- const rowSelectionModel = isSelectionControlled ? propRowSelectionModel : internalRowSelectionModel;
223
- const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
224
- const onRowSelectionModelChange = propOnRowSelectionModelChange;
225
-
226
198
  const viewportRef = useRef<HTMLDivElement>(null);
227
199
  const contentRef = useRef<HTMLDivElement>(null);
228
200
  const gridRef = useRef<HTMLDivElement>(null);
@@ -240,12 +212,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
240
212
  dataSourceRef.current = dataSource;
241
213
 
242
214
  const fetchChildrenRef = useRef<((parentId: GridRowId, groupKeys: string[]) => Promise<void>) | null>(null);
243
- const treeDataRef = useRef<any>(null);
215
+ const treeDataRef = useRef<ReturnType<typeof useTreeData> | null>(null);
244
216
 
245
- const handleNodeExpansion = useCallback((node: any) => {
217
+ const handleNodeExpansion = useCallback((node: GridTreeNode) => {
246
218
  if (dataSourceRef.current && treeData) {
247
219
 
248
- if (node.serverChildrenCount > 0 && node.children.length === 0) {
220
+ if ((node.serverChildrenCount ?? 0) > 0 && (node.children ?? []).length === 0) {
249
221
 
250
222
  const groupKeys = treeDataRef.current?.getNodePath(node.id) || [node.groupingKey];
251
223
  fetchChildrenRef.current?.(node.id, groupKeys);
@@ -267,13 +239,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
267
239
  apiRef,
268
240
  setRows,
269
241
  setColumns,
270
- setScroll,
271
242
  setDimensions,
272
243
  setDataSourceLoading,
273
244
  setDataSourceError,
274
245
  setRowCount
275
246
  } = gridData;
276
247
 
248
+ const { scrollTop, scrollLeft, handleScroll } = useGridScrollSync({ onRowsScrollEnd });
249
+
277
250
  useEffect(() => {
278
251
  if (propApiRef) {
279
252
  propApiRef.current = apiRef.current;
@@ -337,7 +310,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
337
310
  const { dispatch } = gridData;
338
311
  const currentRows = Array.from(gridData.state.rows.idRowsLookup.values());
339
312
  const nextRows = currentRows.map(r =>
340
- (r as any).id === (updatedRow as any).id ? updatedRow : r
313
+ r.id === updatedRow.id ? updatedRow : r
341
314
  );
342
315
  dispatch({ type: 'SET_ROWS', payload: nextRows });
343
316
  },
@@ -352,9 +325,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
352
325
 
353
326
  const handleRowClick = useCallback((params: GridRowParams<R>) => {
354
327
  const { row, id } = params;
355
- const rowData = row as any;
356
328
 
357
- if (isHierarchyEnabled && rowData._hasChildren) {
329
+ if (isHierarchyEnabled && row._hasChildren) {
358
330
  activeHierarchyHandlers?.toggleExpansion(id);
359
331
  return;
360
332
  }
@@ -362,120 +334,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
362
334
  onRowClick?.(params);
363
335
  }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick]);
364
336
 
365
- const effectiveColumns = useMemo(() => {
366
- if (!isHierarchyEnabled) return activeColumns;
367
-
368
- return activeColumns.map((col, index) => {
369
-
370
- if (index === 0) {
371
- return {
372
- ...col,
373
- renderCell: (params: GridRenderCellParams<R>) => {
374
- const { _treeDepth, _hasChildren, _isExpanded, _groupingField, _groupingValue, _descendantCount } = params.row as any;
375
- const depth = _treeDepth || 0;
376
- const paddingLeft = depth * 24;
377
-
378
- let content = col.renderCell ? col.renderCell(params) : params.value;
379
-
380
- if (isTreeData && _hasChildren && params.row._isGroupRow) {
381
- content = (
382
- <div className="ogx__group-cell-content">
383
- {params.value}
384
- {_descendantCount !== undefined && _descendantCount > 0 ? ` (${_descendantCount})` : ''}
385
- </div>
386
- );
387
- }
388
-
389
- // For row grouping, override the first column content to show the group label
390
- if (isRowGrouping && _hasChildren && _groupingField) {
391
- content = (
392
- <div className="ogx__group-cell-content">
393
- {col.field === _groupingField ? '' : `${_groupingField}: `}
394
- {String(_groupingValue)}
395
- {_descendantCount !== undefined ? ` (${_descendantCount})` : ''}
396
- </div>
397
- );
398
- }
399
-
400
- return (
401
- <div style={{ display: 'flex', alignItems: 'center', paddingLeft, width: '100%', height: '100%' }}>
402
- <div
403
- style={{
404
- marginRight: 4,
405
- width: 24,
406
- height: 24,
407
- display: 'flex',
408
- alignItems: 'center',
409
- justifyContent: 'center',
410
- cursor: _hasChildren ? 'pointer' : 'default',
411
- flexShrink: 0
412
- }}
413
- >
414
-
415
- {_hasChildren ? (
416
- <div
417
- onClick={(e) => {
418
- e.stopPropagation();
419
- e.preventDefault();
420
- activeHierarchyHandlers?.toggleExpansion(params.row.id);
421
- }}
422
- onMouseDown={(e) => {
423
- e.stopPropagation();
424
- }}
425
- style={{
426
- cursor: 'pointer',
427
- display: 'flex',
428
- alignItems: 'center',
429
- justifyContent: 'center',
430
- pointerEvents: 'auto',
431
- zIndex: 10,
432
- position: 'relative'
433
- }}
434
- >
435
- <ExpandIcon
436
- isExpanded={_isExpanded}
437
- onClick={(e) => {
438
- e.stopPropagation();
439
- e.preventDefault();
440
- activeHierarchyHandlers?.toggleExpansion(params.row.id);
441
- }}
442
- />
443
- </div>
444
- ) : null}
445
- </div>
446
- <div style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
447
- {content}
448
- </div>
449
- </div>
450
- );
451
- }
452
- };
453
- }
454
-
455
- return {
456
- ...col,
457
- renderCell: (params: GridRenderCellParams<R>) => {
458
- const { _hasChildren, _groupingField } = params.row as any;
459
-
460
- if (isRowGrouping && _hasChildren) {
461
-
462
- if (col.field === _groupingField) {
463
- return null;
464
- }
465
-
466
- if (params.value !== undefined && params.value !== null) {
467
- return col.renderCell ? col.renderCell(params) : params.value;
468
- }
469
-
470
- return null;
471
- }
472
-
473
- return col.renderCell ? col.renderCell(params) : params.value;
474
- }
475
- };
476
- }) as GridColDef<R>[];
477
- }, [activeColumns, isHierarchyEnabled, isRowGrouping, activeHierarchyHandlers]);
478
-
479
337
  const dataSourceHandlers = useGridDataSource({
480
338
  dataSource,
481
339
  sortModel,
@@ -498,68 +356,16 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
498
356
  }
499
357
  }, [dataSourceHandlers.fetchChildren]);
500
358
 
501
- const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
502
- () => initialState?.columns?.columnWidths ?? {}
503
- );
504
-
505
- const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(() =>
506
- initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
507
- );
508
-
509
- const effectiveColumnOrder = columnOrder || internalColumnOrder;
510
-
511
359
  useEffect(() => {
512
360
  if (!dataSource) {
513
361
  setRows(activeRows);
514
362
  }
515
363
  }, [activeRows, setRows, dataSource]);
516
364
 
517
- useEffect(() => {
518
- setColumns(activeColumns as any);
519
- }, [activeColumns, setColumns]);
520
-
521
- useEffect(() => {
522
- if (pivotMode) {
523
- setInternalColumnOrder(activeColumns.map(col => col.field));
524
- }
525
- }, [pivotMode, activeColumns]);
526
-
527
- const orderedColumns = useMemo(() => {
528
- if (disableColumnReorder) return effectiveColumns as GridColDef<R>[];
529
-
530
- const orderMap = new Map(effectiveColumnOrder.map((field, index) => [field, index]));
531
- return [...(effectiveColumns as GridColDef<R>[])].sort((a, b) => {
532
- const aIndex = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
533
- const bIndex = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
534
- return aIndex - bIndex;
535
- });
536
- }, [effectiveColumns, effectiveColumnOrder, disableColumnReorder]);
537
-
538
- const visibleOrderedColumns = useMemo(() => {
539
- return orderedColumns.filter(col => columnVisibilityModel[col.field] !== false);
540
- }, [orderedColumns, columnVisibilityModel]);
541
-
542
- const columnReorderHandlers = useColumnReorder({
543
- columns: orderedColumns,
544
- onColumnOrderChange: useCallback((params: GridColumnOrderChangeParams) => {
545
- const { oldIndex, targetIndex } = params;
546
-
547
- const newOrder = [...effectiveColumnOrder];
548
- const [movedField] = newOrder.splice(oldIndex, 1);
549
- newOrder.splice(targetIndex, 0, movedField);
550
-
551
- if (!columnOrder) {
552
- setInternalColumnOrder(newOrder);
553
- }
554
-
555
- onColumnOrderChange?.(params);
556
- }, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
557
- disableColumnReorder,
558
- });
559
-
365
+ // ── Detail panel (hoisted — hasDetailPanel feeds into useGridColumns) ──────
366
+ const hasDetailPanel = Boolean(getDetailPanelContent);
560
367
  const [internalExpandedRowIds, setInternalExpandedRowIds] = useState<Set<GridRowId>>(new Set());
561
368
  const expandedRowIds = controlledExpandedRowIds ?? internalExpandedRowIds;
562
- const hasDetailPanel = Boolean(getDetailPanelContent);
563
369
 
564
370
  const handleDetailPanelToggle = useCallback((rowId: GridRowId) => {
565
371
  const newExpandedRowIds = new Set(expandedRowIds);
@@ -568,45 +374,43 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
568
374
  } else {
569
375
  newExpandedRowIds.add(rowId);
570
376
  }
571
-
572
377
  if (controlledExpandedRowIds === undefined) {
573
378
  setInternalExpandedRowIds(newExpandedRowIds);
574
379
  }
575
380
  onDetailPanelExpandedRowIdsChange?.(newExpandedRowIds);
576
381
  }, [expandedRowIds, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange]);
577
382
 
578
- const navigationColumns = useMemo(() => {
579
- const cols: { field: string; editable?: boolean }[] = [];
580
- if (rowReordering) cols.push({ field: '__reorder_col__' });
581
- if (hasDetailPanel) cols.push({ field: '__expand_col__' });
582
- if (checkboxSelection) cols.push({ field: '__checkbox_col__' });
583
- return [...cols, ...orderedColumns];
584
- }, [orderedColumns, checkboxSelection, hasDetailPanel, rowReordering]);
585
-
586
- const handleColumnResize = useCallback((field: string, newWidth: number) => {
587
- setColumnWidths(prev => ({ ...prev, [field]: newWidth }));
588
- }, []);
589
-
590
- const onStateChangeRef = useRef(onStateChange);
591
- onStateChangeRef.current = onStateChange;
592
-
593
- useEffect(() => {
594
- if (!onStateChangeRef.current) return;
595
-
596
- const snapshot: GridState = {
597
- sorting: { sortModel: sortModel as { field: string; sort: 'asc' | 'desc' }[] },
598
- filter: { filterModel },
599
- pagination: { paginationModel: effectivePaginationModel },
600
- columns: {
601
- columnWidths,
602
- columnOrder: effectiveColumnOrder,
603
- columnVisibilityModel,
604
- pinnedColumns: pinnedColumns,
605
- },
606
- };
383
+ // ── Column management ─────────────────────────────────────────────────────
384
+ const {
385
+ effectiveColumns,
386
+ orderedColumns,
387
+ visibleOrderedColumns,
388
+ navigationColumns,
389
+ columnWidths,
390
+ effectiveColumnOrder,
391
+ setInternalColumnOrder,
392
+ columnReorderHandlers,
393
+ handleColumnResize,
394
+ } = useGridColumns<R>({
395
+ activeColumns,
396
+ isHierarchyEnabled,
397
+ isRowGrouping,
398
+ isTreeData,
399
+ activeHierarchyHandlers,
400
+ columnVisibilityModel,
401
+ columnOrder,
402
+ onColumnOrderChange,
403
+ disableColumnReorder,
404
+ pivotMode,
405
+ checkboxSelection,
406
+ hasDetailPanel,
407
+ rowReordering,
408
+ initialState,
409
+ setColumns,
410
+ });
607
411
 
608
- onStateChangeRef.current(snapshot);
609
- }, [
412
+ useGridStateSnapshot({
413
+ onStateChange,
610
414
  sortModel,
611
415
  filterModel,
612
416
  effectivePaginationModel,
@@ -614,61 +418,34 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
614
418
  effectiveColumnOrder,
615
419
  columnVisibilityModel,
616
420
  pinnedColumns,
617
- ]);
618
-
619
- const baseRows = useMemo(() => {
620
- return effectiveRows;
621
- }, [effectiveRows]);
622
-
623
- const filteredRows = useMemo(() => {
624
- if (activeHierarchyHandlers) {
625
-
626
- return (activeHierarchyHandlers.getVisibleRows() || []) as R[];
627
- }
628
-
629
- if (filterMode === 'server' && dataSource) {
630
- return baseRows;
631
- }
632
-
633
- return filterRows(baseRows, filterModel);
634
- }, [baseRows, filterModel, activeHierarchyHandlers, filterMode, dataSource]);
635
-
636
- const { top: pinnedTopRows, center: unpinnedRows, bottom: pinnedBottomRows } = useMemo(() => {
637
- if (activeHierarchyHandlers) {
638
-
639
- return { top: [], center: filteredRows, bottom: [] };
640
- }
641
- return getPinnedRowGroups(filteredRows, pinnedRows);
642
- }, [filteredRows, pinnedRows, activeHierarchyHandlers]);
643
-
644
- const sortedUnpinnedRows = useMemo(() => {
645
- if (activeHierarchyHandlers) {
646
- return unpinnedRows;
647
- }
648
-
649
- if (sortingMode === 'server' && dataSource) {
650
- return unpinnedRows;
651
- }
652
-
653
- return sortRows(unpinnedRows, sortModel);
654
- }, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, dataSource]);
655
-
656
- const paginatedUnpinnedRows = useMemo(() => {
657
- if (!pagination) return sortedUnpinnedRows;
658
-
659
- if (paginationMode === 'server' && dataSource) {
660
- return sortedUnpinnedRows;
661
- }
421
+ });
662
422
 
663
- const start = effectivePaginationModel.page * effectivePaginationModel.pageSize;
664
- const end = start + effectivePaginationModel.pageSize;
665
- return sortedUnpinnedRows.slice(start, end);
666
- }, [sortedUnpinnedRows, pagination, effectivePaginationModel.page, effectivePaginationModel.pageSize, paginationMode, dataSource]);
423
+ const rowPipeline = useGridRowPipeline<GridRowModel>({
424
+ effectiveRows: effectiveRows as GridRowModel[],
425
+ activeHierarchyHandlers: activeHierarchyHandlers as { getVisibleRows: () => GridRowModel[] } | null,
426
+ filterMode,
427
+ filterModel,
428
+ dataSource: dataSource as GridDataSource<GridRowModel> | undefined,
429
+ sortModel,
430
+ sortingMode,
431
+ pagination,
432
+ paginationMode,
433
+ effectivePaginationModel,
434
+ pinnedRows,
435
+ isLoading: state.dataSource.loading,
436
+ pageSize: effectivePaginationModel.pageSize,
437
+ });
438
+ const filteredRows = rowPipeline.filteredRows as R[];
439
+ const pinnedTopRows = rowPipeline.pinnedTopRows as R[];
440
+ const pinnedBottomRows = rowPipeline.pinnedBottomRows as R[];
441
+ const sortedUnpinnedRows = rowPipeline.sortedUnpinnedRows as R[];
442
+ const paginatedUnpinnedRows = rowPipeline.paginatedUnpinnedRows as R[];
443
+ const allRenderableRows = rowPipeline.allRenderableRows as R[];
667
444
 
668
445
  useEffect(() => {
669
- gridData.apiRef.current.getVisibleRows = () => sortedUnpinnedRows;
670
- gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as any[];
671
- }, [sortedUnpinnedRows, effectiveColumns, gridData.apiRef]);
446
+ gridData.apiRef.current.getVisibleRows = () => pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
447
+ gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as unknown as GridColDef[];
448
+ }, [sortedUnpinnedRows, paginatedUnpinnedRows, pagination, effectiveColumns, gridData.apiRef]);
672
449
 
673
450
  const rowReorderHandlers = useRowReorder({
674
451
  rows: pagination ? paginatedUnpinnedRows : sortedUnpinnedRows,
@@ -677,29 +454,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
677
454
  rowReordering
678
455
  });
679
456
 
680
- const allRenderableRows = useMemo(() => {
681
-
682
- if (activeHierarchyHandlers) {
683
- return unpinnedRows;
684
- }
685
- const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
686
- const base = [...pinnedTopRows, ...centerRows, ...pinnedBottomRows];
687
-
688
- // Append skeleton rows while an infinite-scroll fetch is in flight
689
- if (paginationMode === 'infinite' && state.dataSource.loading && base.length > 0) {
690
- const skeletonCount = Math.min(effectivePaginationModel.pageSize, 20);
691
- const skeletons = Array.from({ length: skeletonCount }, (_, i) => ({
692
- id: `__skeleton_${i}__`,
693
- _isSkeleton: true,
694
- }));
695
- return [...base, ...skeletons] as typeof base;
696
- }
697
-
698
- return base;
699
- }, [pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows, pagination,
700
- activeHierarchyHandlers, unpinnedRows, paginationMode, state.dataSource.loading,
701
- effectivePaginationModel.pageSize]);
702
-
703
457
  const spanning = useGridSpanning(
704
458
  allRenderableRows,
705
459
  navigationColumns,
@@ -710,7 +464,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
710
464
  rows: filteredRows,
711
465
  aggregationModel,
712
466
  isServerSide: !!(dataSource && (paginationMode === 'server' || sortingMode === 'server')),
713
- dataSource: dataSource as any,
467
+ dataSource,
714
468
  filterModel,
715
469
  sortModel,
716
470
  serverAggregationResults,
@@ -783,148 +537,20 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
783
537
  };
784
538
  }, [layout, viewportRef, gridData.apiRef]);
785
539
 
786
- const virtualization = useMemo(() => {
787
- const {
788
- unpinnedAccWidths,
789
- unpinnedColsWithWidth,
790
- leftPinnedCols,
791
- rightPinnedCols,
792
- totalWidth,
793
- leftWidth,
794
- rightWidth,
795
- unpinnedTotalWidth,
796
- systemColumnsWidth,
797
- cumulativeHeights,
798
- pinnedTopHeight,
799
- pinnedBottomHeight,
800
- unpinnedRowsHeight,
801
- rowHeights,
802
- unpinnedRowsLength
803
- } = layout;
804
-
805
- const scrollLeft = state.virtualization.scrollLeft;
806
- const scrollTop = state.virtualization.scrollTop;
807
- const viewportWidth = state.dimensions.viewportWidth || 1000;
808
- const viewportHeight = autoHeight ? (pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + 50) : (state.dimensions.viewportHeight || 600);
809
-
810
- const overscanCols = 6;
811
-
812
- const visibleLocalStart = Math.max(0, scrollLeft - leftWidth);
813
- const visibleLocalEnd = scrollLeft + viewportWidth - leftWidth;
814
-
815
- let firstUnpinnedIndex = 0;
816
- let lastUnpinnedIndex = unpinnedColsWithWidth.length - 1;
817
-
818
- let low = 0, high = unpinnedAccWidths.length - 1;
819
- while (low <= high) {
820
- const mid = (low + high) >>> 1;
821
- if (unpinnedAccWidths[mid] < visibleLocalStart) {
822
- low = mid + 1;
823
- } else {
824
- high = mid - 1;
825
- }
826
- }
827
- firstUnpinnedIndex = Math.max(0, low - overscanCols);
828
-
829
- for (let i = firstUnpinnedIndex; i < unpinnedAccWidths.length; i++) {
830
- if ((i === 0 ? 0 : unpinnedAccWidths[i - 1]) > visibleLocalEnd) {
831
- lastUnpinnedIndex = Math.min(unpinnedColsWithWidth.length - 1, i + overscanCols);
832
- break;
833
- }
834
- lastUnpinnedIndex = Math.min(unpinnedColsWithWidth.length - 1, i + overscanCols);
835
- }
836
-
837
- const leftSpacerWidth = firstUnpinnedIndex > 0 ? unpinnedAccWidths[firstUnpinnedIndex - 1] : 0;
838
- const rightSpacerWidth = unpinnedTotalWidth - unpinnedAccWidths[lastUnpinnedIndex];
839
-
840
- const virtualColumns: any[] = [
841
- ...leftPinnedCols,
842
- ...(leftSpacerWidth > 0 ? [{ field: '__spacer_left__', width: leftSpacerWidth, isSpacer: true }] : []),
843
- ...unpinnedColsWithWidth.slice(firstUnpinnedIndex, lastUnpinnedIndex + 1),
844
- ...(rightSpacerWidth > 0 ? [{ field: '__spacer_right__', width: rightSpacerWidth, isSpacer: true }] : []),
845
- ...rightPinnedCols
846
- ];
847
-
848
- const hasHorizontalScroll = viewportWidth > 0 && totalWidth > viewportWidth;
849
- const scrollbarBuffer = hasHorizontalScroll ? 16 : 0;
850
- const totalHeight = pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + scrollbarBuffer + 2;
851
-
852
- const overscanRows = 5;
853
- let firstRowIndex = 0;
854
- let lastRowIndex = unpinnedRowsLength - 1;
855
- let offsetTop = 0;
856
-
857
- let rLow = 0, rHigh = cumulativeHeights.length - 1;
858
- while (rLow <= rHigh) {
859
- const mid = (rLow + rHigh) >>> 1;
860
- if (cumulativeHeights[mid] < scrollTop) {
861
- rLow = mid + 1;
862
- } else {
863
- rHigh = mid - 1;
864
- }
865
- }
866
- firstRowIndex = Math.max(0, rLow - overscanRows);
867
- offsetTop = firstRowIndex > 0 ? cumulativeHeights[firstRowIndex - 1] : 0;
868
-
869
- for (let i = firstRowIndex; i < cumulativeHeights.length; i++) {
870
- if (cumulativeHeights[i] >= scrollTop + viewportHeight) {
871
- lastRowIndex = Math.min(unpinnedRowsLength - 1, i + overscanRows);
872
- break;
873
- }
874
- }
875
-
876
- return {
877
- renderContext: {
878
- firstRowIndex,
879
- lastRowIndex,
880
- firstColumnIndex: firstUnpinnedIndex,
881
- lastColumnIndex: lastUnpinnedIndex
882
- },
883
- offsetTop,
884
- offsetLeft: 0,
885
- totalHeight,
886
- pinnedTopHeight,
887
- pinnedBottomHeight,
888
- totalWidth,
889
- rowHeights,
890
- cumulativeHeights,
891
- virtualColumns,
892
- columnMetrics: {
893
- leftPinnedWidth: leftWidth,
894
- rightPinnedWidth: rightWidth,
895
- unpinnedAccWidths,
896
- unpinnedCols: unpinnedColsWithWidth,
897
- totalSpecialsWidth: systemColumnsWidth,
898
- pinnedSpecialsWidth: (rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0) + (checkboxSelection && pinCheckboxColumn ? 48 : 0)
899
- }
900
- };
901
- }, [
540
+ const virtualization = useGridVirtualization({
902
541
  layout,
903
- state.virtualization.scrollTop,
904
- state.virtualization.scrollLeft,
905
- state.dimensions.viewportHeight,
906
- state.dimensions.viewportWidth,
907
- autoHeight
908
- ]);
909
-
910
- const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
911
- const target = event.currentTarget;
912
- setScroll(target.scrollTop, target.scrollLeft);
913
-
914
- if (onRowsScrollEnd) {
915
- const { scrollTop, scrollHeight, clientHeight } = target;
916
- const scrollThreshold = 100;
917
-
918
- if (scrollHeight - scrollTop - clientHeight < scrollThreshold) {
542
+ scrollTop,
543
+ scrollLeft,
544
+ viewportWidth: state.dimensions.viewportWidth,
545
+ viewportHeight: state.dimensions.viewportHeight,
546
+ autoHeight,
547
+ rowReordering,
548
+ hasDetailPanel,
549
+ checkboxSelection,
550
+ pinCheckboxColumn,
551
+ pinExpandColumn,
552
+ });
919
553
 
920
- onRowsScrollEnd({
921
- visibleTop: scrollTop,
922
- visibleBottom: scrollTop + clientHeight,
923
- viewportHeight: clientHeight
924
- });
925
- }
926
- }
927
- }, [setScroll, onRowsScrollEnd]);
928
554
 
929
555
  useEffect(() => {
930
556
  if (!viewportRef.current) return;
@@ -956,28 +582,28 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
956
582
  setInternalRowSelectionModel(newSelectionArray);
957
583
  }
958
584
  onRowSelectionModelChange?.(newSelectionArray);
959
- }, [selectedRowIds, isSelectionControlled, onRowSelectionModelChange]);
585
+ }, [selectedRowIds, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
960
586
 
961
587
  const handleSelectAll = useCallback((isSelected: boolean) => {
962
588
  let newSelection: GridRowId[] = [];
963
589
  if (isSelected) {
964
- newSelection = baseRows.map(row => row.id);
590
+ newSelection = effectiveRows.map((row: GridRowModel) => row.id);
965
591
  }
966
592
 
967
593
  if (!isSelectionControlled) {
968
594
  setInternalRowSelectionModel(newSelection);
969
595
  }
970
596
  onRowSelectionModelChange?.(newSelection);
971
- }, [rows, isSelectionControlled, onRowSelectionModelChange]);
597
+ }, [effectiveRows, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
972
598
 
973
599
  const handleSort = useCallback((field: string, direction: GridSortDirection) => {
974
600
  const newSortModel = direction ? [{ field, sort: direction }] : [];
975
601
 
976
602
  if (!isSortControlled) {
977
- setInternalSortModel(newSortModel as any);
603
+ setInternalSortModel(newSortModel as GridSortItem[]);
978
604
  }
979
- onSortModelChange?.(newSortModel as any);
980
- }, [isSortControlled, onSortModelChange]);
605
+ onSortModelChange?.(newSortModel as GridSortItem[]);
606
+ }, [isSortControlled, onSortModelChange, setInternalSortModel]);
981
607
 
982
608
  const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
983
609
  allRenderableRows,
@@ -1018,44 +644,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1018
644
  prevEditingCellRef.current = editingHandlers.editingCell;
1019
645
  }, [editingHandlers.editingCell]);
1020
646
 
1021
- const visibleRows = useMemo(() => {
1022
- const { firstRowIndex, lastRowIndex } = virtualization.renderContext;
1023
-
1024
- const topPinnedCount = pinnedTopRows.length;
1025
-
1026
- const topPinned = pinnedTopRows.map((row, index) => ({ row, rowIndex: index }));
1027
- const bottomPinned = pinnedBottomRows.map((row, index) => ({
1028
- row,
1029
- rowIndex: topPinnedCount + (pagination ? paginatedUnpinnedRows.length : sortedUnpinnedRows.length) + index
1030
- }));
1031
-
1032
- const centerStartIndex = Math.max(0, firstRowIndex - topPinnedCount);
1033
- const centerEndIndex = Math.min(
1034
- pagination ? paginatedUnpinnedRows.length : sortedUnpinnedRows.length,
1035
- lastRowIndex - topPinnedCount + 1
1036
- );
1037
-
1038
- const centerRows = (pagination ? paginatedUnpinnedRows : sortedUnpinnedRows)
1039
- .slice(centerStartIndex, centerEndIndex)
1040
- .map((row, index) => ({
1041
- row,
1042
- rowIndex: topPinnedCount + centerStartIndex + index
1043
- }));
1044
-
1045
- const combined = [...topPinned, ...centerRows, ...bottomPinned];
1046
-
1047
- const seenIds = new Set<GridRowId>();
1048
- const deduplicated = combined.filter(item => {
1049
- if (seenIds.has(item.row.id)) {
1050
- return false;
1051
- }
1052
- seenIds.add(item.row.id);
1053
- return true;
1054
- });
1055
-
1056
- return deduplicated;
1057
- }, [virtualization.renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination,
1058
- paginationMode, state.dataSource.loading, effectivePaginationModel.pageSize, rowHeight]);
647
+ const visibleRows = useGridVisibleRows<R>({
648
+ renderContext: virtualization.renderContext,
649
+ pinnedTopRows,
650
+ pinnedBottomRows,
651
+ paginatedUnpinnedRows,
652
+ sortedUnpinnedRows,
653
+ pagination,
654
+ });
1059
655
 
1060
656
 
1061
657
  const allSelected = rows.length > 0 && selectedRowIds.size === rows.length;
@@ -1065,6 +661,51 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1065
661
  const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
1066
662
  const containerRef = React.useRef<HTMLDivElement>(null);
1067
663
 
664
+ const toolbarProps = React.useMemo(() => {
665
+ if (!slots?.toolbar) return null;
666
+ const reorderHandler = disableColumnReorder
667
+ ? undefined
668
+ : (fromField: string, toField: string) => {
669
+ const currentOrder = [...effectiveColumnOrder];
670
+ const fromIdx = currentOrder.indexOf(fromField);
671
+ const toIdx = currentOrder.indexOf(toField);
672
+ if (fromIdx === -1 || toIdx === -1) return;
673
+ const newOrder = [...currentOrder];
674
+ newOrder.splice(fromIdx, 1);
675
+ newOrder.splice(toIdx, 0, fromField);
676
+ if (!columnOrder) setInternalColumnOrder(newOrder);
677
+ const col = effectiveColumns.find(c => c.field === fromField);
678
+ if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as unknown as GridColDef });
679
+ };
680
+ return {
681
+ apiRef: gridData.apiRef,
682
+ columns: orderedColumns as unknown as GridColDef[],
683
+ baseColumns: columns as unknown as GridColDef[],
684
+ aggregationModel,
685
+ onAggregationModelChange: handleAggregationModelChange,
686
+ ...(pivotMode || propPivotModel || onPivotModelChange ? {
687
+ pivotModel: currentPivotModel,
688
+ onPivotModelChange: handlePivotModelChange,
689
+ } : {}),
690
+ filterModel,
691
+ onFilterModelChange,
692
+ columnVisibilityModel,
693
+ onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
694
+ onColumnReorder: reorderHandler,
695
+ onColumnOrderReset: disableColumnReorder ? undefined : () => setInternalColumnOrder(columns.map(c => c.field)),
696
+ forceColumnsOpen: columnsPanelOpen,
697
+ onColumnsPanelClose: () => setColumnsPanelOpen(false),
698
+ ...slotProps?.toolbar,
699
+ };
700
+ }, [
701
+ slots?.toolbar, disableColumnReorder, effectiveColumnOrder, orderedColumns, effectiveColumns,
702
+ columnOrder, onColumnOrderChange, setInternalColumnOrder, gridData.apiRef,
703
+ columns, aggregationModel, handleAggregationModelChange, pivotMode,
704
+ propPivotModel, onPivotModelChange, currentPivotModel, handlePivotModelChange,
705
+ filterModel, onFilterModelChange, columnVisibilityModel,
706
+ handleColumnVisibilityModelChange, columnsPanelOpen, slotProps?.toolbar,
707
+ ]);
708
+
1068
709
  // Click-outside handler for standalone column panel
1069
710
  useEffect(() => {
1070
711
  if (!columnsPanelOpen || slots?.toolbar) return;
@@ -1087,188 +728,49 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1087
728
  height: height ?? style?.height,
1088
729
  '--ogx-row-height': `${rowHeight}px`,
1089
730
  '--ogx-header-height': `${headerHeight}px`
1090
- } as any}
731
+ } as unknown as React.CSSProperties}
1091
732
  aria-busy={effectiveLoading}
1092
733
  >
1093
- { }
1094
- {slots?.toolbar && (() => {
1095
- const toolbarProps = {
1096
- apiRef: gridData.apiRef,
1097
- columns: effectiveColumns as unknown as GridColDef[],
1098
- baseColumns: columns as unknown as GridColDef[],
1099
- aggregationModel,
1100
- onAggregationModelChange: handleAggregationModelChange,
1101
-
1102
- ...(pivotMode || propPivotModel || onPivotModelChange ? {
1103
- pivotModel: currentPivotModel,
1104
- onPivotModelChange: handlePivotModelChange,
1105
- } : {}),
1106
-
1107
- filterModel,
1108
- onFilterModelChange,
1109
- columnVisibilityModel,
1110
- onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
1111
-
1112
- onColumnReorder: disableColumnReorder ? undefined : (fromField: string, toField: string) => {
1113
- const currentOrder = [...effectiveColumnOrder];
1114
- const fromIdx = currentOrder.indexOf(fromField);
1115
- const toIdx = currentOrder.indexOf(toField);
1116
- if (fromIdx === -1 || toIdx === -1) return;
1117
- const newOrder = [...currentOrder];
1118
- newOrder.splice(fromIdx, 1);
1119
- newOrder.splice(toIdx, 0, fromField);
1120
- if (!columnOrder) setInternalColumnOrder(newOrder);
1121
- const col = effectiveColumns.find(c => c.field === fromField); if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as any });
1122
- },
1123
-
1124
- forceColumnsOpen: columnsPanelOpen,
1125
- onColumnsPanelClose: () => setColumnsPanelOpen(false),
1126
-
1127
- ...slotProps?.toolbar,
1128
- };
1129
- return <StableToolbar {...toolbarProps} />;
1130
- })()}
1131
-
1132
- {/* Fallback standalone column panel when no toolbar is present */}
1133
- {!slots?.toolbar && columnsPanelOpen && ReactDOM.createPortal(
1134
- <div
1135
- id="ogx-standalone-col-panel"
1136
- style={(() => {
1137
- const rect = containerRef.current?.getBoundingClientRect();
1138
- return {
1139
- position: 'fixed',
1140
- top: rect ? rect.top + 8 : 16,
1141
- right: rect ? window.innerWidth - rect.right + 8 : 16,
1142
- zIndex: 9999,
1143
- display: 'inline-block',
1144
- };
1145
- })()}
1146
- >
1147
- <button
1148
- onClick={() => setColumnsPanelOpen(false)}
1149
- style={{
1150
- position: 'absolute',
1151
- top: -12,
1152
- right: -12,
1153
- width: 24,
1154
- height: 24,
1155
- display: 'flex',
1156
- alignItems: 'center',
1157
- justifyContent: 'center',
1158
- background: '#334155',
1159
- color: '#fff',
1160
- border: 'none',
1161
- borderRadius: '50%',
1162
- cursor: 'pointer',
1163
- fontSize: 14,
1164
- lineHeight: 1,
1165
- zIndex: 1,
1166
- boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
1167
- }}
1168
- aria-label="Close"
1169
- >×</button>
1170
- <ColumnVisibilityPanel
1171
- columns={effectiveColumns}
1172
- visibleColumns={new Set(
1173
- effectiveColumns
1174
- .filter(col => columnVisibilityModel[col.field] !== false)
1175
- .map(col => col.field)
1176
- )}
1177
- onVisibilityChange={(field, isVisible) => {
1178
- handleColumnVisibilityModelChange({ ...columnVisibilityModel, [field]: isVisible });
1179
- }}
1180
- onShowAll={() => {
1181
- const next = { ...columnVisibilityModel };
1182
- effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = true; });
1183
- handleColumnVisibilityModelChange(next);
1184
- }}
1185
- onHideAll={() => {
1186
- const next = { ...columnVisibilityModel };
1187
- effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
1188
- handleColumnVisibilityModelChange(next);
1189
- }}
1190
- onColumnReorder={disableColumnReorder ? undefined : (fromField, toField) => {
1191
- const currentOrder = [...effectiveColumnOrder];
1192
- const fromIdx = currentOrder.indexOf(fromField);
1193
- const toIdx = currentOrder.indexOf(toField);
1194
- if (fromIdx === -1 || toIdx === -1) return;
1195
- const newOrder = [...currentOrder];
1196
- newOrder.splice(fromIdx, 1);
1197
- newOrder.splice(toIdx, 0, fromField);
1198
- if (!columnOrder) {
1199
- setInternalColumnOrder(newOrder);
1200
- }
1201
- onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: effectiveColumns.find(c => c.field === fromField) as any });
1202
- }}
1203
- />
1204
- </div>,
1205
- containerRef.current?.closest('.ogx-theme-provider') || document.body
734
+ {toolbarProps && <StableToolbar {...toolbarProps} />}
735
+
736
+ {!slots?.toolbar && (
737
+ <GridStandaloneColumnPanel<R>
738
+ isOpen={columnsPanelOpen}
739
+ containerRef={containerRef}
740
+ effectiveColumns={effectiveColumns}
741
+ columnVisibilityModel={columnVisibilityModel}
742
+ effectiveColumnOrder={effectiveColumnOrder}
743
+ columnOrder={columnOrder}
744
+ disableColumnReorder={disableColumnReorder}
745
+ onClose={() => setColumnsPanelOpen(false)}
746
+ onColumnVisibilityChange={handleColumnVisibilityModelChange}
747
+ onColumnOrderChange={onColumnOrderChange}
748
+ setInternalColumnOrder={setInternalColumnOrder}
749
+ />
1206
750
  )}
1207
751
 
1208
- {/* ═══════════════════════════════════════════════════════════════
1209
- LIST VIEW — single-column card layout
1210
- ═══════════════════════════════════════════════════════════════ */}
1211
752
  {listView && listViewColumn && (
1212
- <div
1213
- className="ogx-list-view"
1214
- role="grid"
1215
- aria-label={ariaLabel || 'Data grid list view'}
1216
- aria-rowcount={allRenderableRows.length + 1}
1217
- >
1218
- {/* Info bar */}
1219
- <div className="ogx-list-view__toolbar">
1220
- <span>
1221
- {filteredRows.length} {filteredRows.length === 1 ? 'item' : 'items'}
1222
- {pagination ? ` · page ${effectivePaginationModel.page + 1} of ${Math.ceil(filteredRows.length / effectivePaginationModel.pageSize) || 1}` : ''}
1223
- {selectedRowIds.size > 0 ? ` · ${selectedRowIds.size} selected` : ''}
1224
- </span>
1225
- </div>
1226
-
1227
- {/* Scrollable rows — pagination stays OUTSIDE this div */}
1228
- <div className="ogx-list-view__rows">
1229
- {allRenderableRows.length === 0 ? (
1230
- <div className="ogx-list-view__empty" aria-live="polite" role="status">
1231
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
1232
- <rect x="3" y="3" width="18" height="18" rx="2" />
1233
- <path d="M3 9h18M9 21V9" />
1234
- </svg>
1235
- {noRowsLabel}
1236
- </div>
1237
- ) : (
1238
- allRenderableRows.map((row, idx) => (
1239
- <ListViewRow<R>
1240
- key={row.id}
1241
- row={row as R}
1242
- rowIndex={idx}
1243
- listViewColumn={listViewColumn as any}
1244
- isSelected={selectedRowIds.has(row.id)}
1245
- checkboxSelection={checkboxSelection}
1246
- rowHeight={rowHeight}
1247
- onRowClick={(r) => handleRowClick({ row: r, id: r.id, rowIndex: idx })}
1248
- onSelectionChange={handleSelectionChange}
1249
- />
1250
- ))
1251
- )}
1252
- </div>
1253
-
1254
- {/* Pagination in list view — use filteredRows.length as total since state.pagination.rowCount is only set for server-side */}
1255
- {pagination && (() => {
1256
- const PaginationComponent = slots?.pagination || Pagination;
1257
- const totalRowCount = (paginationMode === 'server' && dataSource)
1258
- ? (state.pagination.rowCount || 0)
1259
- : filteredRows.length;
1260
- const paginationProps = {
1261
- page: effectivePaginationModel.page,
1262
- pageSize: effectivePaginationModel.pageSize,
1263
- rowCount: totalRowCount,
1264
- pageSizeOptions: pageSizeOptions,
1265
- onPageChange: (newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage }),
1266
- onPageSizeChange: (newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 }),
1267
- ...slotProps?.pagination
1268
- };
1269
- return <PaginationComponent {...paginationProps} />;
1270
- })()}
1271
- </div>
753
+ <GridListView<R>
754
+ ariaLabel={ariaLabel}
755
+ allRenderableRows={allRenderableRows}
756
+ filteredRows={filteredRows}
757
+ pagination={pagination}
758
+ effectivePaginationModel={effectivePaginationModel}
759
+ pageSizeOptions={pageSizeOptions}
760
+ selectedRowIds={selectedRowIds}
761
+ listViewColumn={listViewColumn}
762
+ noRowsLabel={noRowsLabel}
763
+ rowHeight={rowHeight}
764
+ checkboxSelection={checkboxSelection}
765
+ paginationMode={paginationMode}
766
+ dataSource={dataSource}
767
+ serverRowCount={state.pagination.rowCount || 0}
768
+ paginationSlot={slots?.pagination}
769
+ paginationSlotProps={slotProps?.pagination}
770
+ onRowClick={handleRowClick}
771
+ onSelectionChange={handleSelectionChange}
772
+ onPaginationModelChange={handlePaginationModelChange}
773
+ />
1272
774
  )}
1273
775
 
1274
776
  {/* ══════════════════════════════════════════════════════════════
@@ -1310,7 +812,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1310
812
  { }
1311
813
  <Header
1312
814
  columns={virtualization.virtualColumns}
1313
- allColumns={effectiveColumns as any}
815
+ allColumns={effectiveColumns}
1314
816
  columnGroupingModel={columnGroupingModel}
1315
817
  checkboxSelection={checkboxSelection}
1316
818
  allSelected={allSelected}
@@ -1365,299 +867,135 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1365
867
 
1366
868
  {/* Empty State Overlay (Standard View) — showing after header */}
1367
869
  {!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
1368
- <div className="ogx__empty" style={{ width: virtualization.totalWidth }}>
1369
- <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
1370
- <rect x="3" y="3" width="18" height="18" rx="2" />
1371
- <path d="M3 9h18M9 21V9" />
1372
- </svg>
1373
- <span>{noRowsLabel}</span>
1374
- </div>
870
+ <GridEmptyState noRowsLabel={noRowsLabel} width={virtualization.totalWidth} />
1375
871
  )}
1376
872
 
1377
- { }
1378
- {pinnedTopRows.length > 0 && (
1379
- <div className="ogx__pinned-rows ogx__pinned-rows--top" role="rowgroup">
1380
- {pinnedTopRows.map((row, index) => (
1381
- <Row
1382
- key={row.id}
1383
- row={row}
1384
- columns={virtualization.virtualColumns}
1385
- rowIndex={index}
1386
- isSelected={selectedRowIds.has(row.id)}
1387
- checkboxSelection={checkboxSelection}
1388
- onRowClick={handleRowClick}
1389
- onCellClick={handleCellClick}
1390
- onSelectionChange={handleSelectionChange}
1391
- columnWidths={columnWidths}
1392
- pinnedColumns={pinnedColumns}
1393
- pinnedRows={pinnedRows}
1394
-
1395
- hasDetailPanel={hasDetailPanel}
1396
- isDetailPanelExpanded={expandedRowIds.has(row.id)}
1397
- detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: index }) : null}
1398
- detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: index }) || 200}
1399
- onDetailPanelToggle={handleDetailPanelToggle}
1400
- pinCheckboxColumn={pinCheckboxColumn}
1401
- pinExpandColumn={pinExpandColumn}
1402
-
1403
- focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
1404
-
1405
- colspanMap={spanning.colspanMap}
1406
- rowSpanningCaches={spanning.rowSpanningState.caches}
1407
- rowHeight={rowHeight}
1408
- />
1409
- ))}
1410
- </div>
1411
- )}
873
+ <GridPinnedRows<R>
874
+ rows={pinnedTopRows}
875
+ position="top"
876
+ columns={virtualization.virtualColumns}
877
+ selectedRowIds={selectedRowIds}
878
+ checkboxSelection={checkboxSelection}
879
+ onRowClick={handleRowClick}
880
+ onCellClick={handleCellClick}
881
+ onSelectionChange={handleSelectionChange}
882
+ columnWidths={columnWidths}
883
+ pinnedColumns={pinnedColumns}
884
+ pinnedRows={pinnedRows}
885
+ hasDetailPanel={hasDetailPanel}
886
+ expandedRowIds={expandedRowIds}
887
+ getDetailPanelContent={getDetailPanelContent}
888
+ getDetailPanelHeight={getDetailPanelHeight}
889
+ onDetailPanelToggle={handleDetailPanelToggle}
890
+ pinCheckboxColumn={pinCheckboxColumn}
891
+ pinExpandColumn={pinExpandColumn}
892
+ focusedCell={focusedCell}
893
+ colspanMap={spanning.colspanMap}
894
+ rowSpanningCaches={spanning.rowSpanningState.caches}
895
+ rowHeight={rowHeight}
896
+ />
1412
897
 
1413
- { }
1414
- <div
1415
- className="ogx__virtual-container"
1416
- style={{
1417
- height: `${virtualization.totalHeight - virtualization.pinnedTopHeight - virtualization.pinnedBottomHeight}px`
1418
- }}
1419
- role="presentation"
1420
- >
1421
- { }
1422
- <div
1423
- className="ogx__rows"
1424
- style={{
1425
- transform: `translateY(${virtualization.renderContext.firstRowIndex * rowHeight}px)`
1426
- }}
1427
- role="rowgroup"
1428
- >
1429
- {effectiveLoading && visibleRows.length === 0 ? (
1430
-
1431
- (() => {
1432
-
1433
- const skeletonColumns = columns.length > 0
1434
- ? columns
1435
- : (() => {
1436
-
1437
- const availableWidth = (state.dimensions.viewportWidth || 1000) -
1438
- (checkboxSelection ? 48 : 0) -
1439
- (getDetailPanelContent ? 48 : 0) -
1440
- (rowReordering ? 48 : 0);
1441
-
1442
- const columnWidth = 150;
1443
- const columnCount = Math.max(1, Math.ceil(availableWidth / columnWidth));
1444
-
1445
- return Array.from({ length: columnCount }, (_, i) => ({
1446
- field: `placeholder_${i}`,
1447
- headerName: '',
1448
- width: columnWidth
1449
- }));
1450
- })();
1451
-
1452
- return Array.from({ length: 10 }).map((_, index) => (
1453
- <SkeletonRow
1454
- key={`skeleton-${index}`}
1455
- columns={skeletonColumns}
1456
- rowHeight={rowHeight}
1457
- checkboxSelection={checkboxSelection}
1458
- hasDetailPanel={!!getDetailPanelContent}
1459
- rowReordering={rowReordering}
1460
- />
1461
- ));
1462
- })()
1463
- ) : (
1464
- visibleRows
1465
- .filter(({ row }) => !isRowPinned(row.id, pinnedRows))
1466
- // Defensive deduplication to prevent React key collision
1467
- .filter((item, index, self) =>
1468
- index === self.findIndex(t => t.row.id === item.row.id)
1469
- )
1470
- .map(({ row, rowIndex: actualIndex }) => {
1471
- return (
1472
- <Row
1473
- key={`${row.id}-${virtualization.totalWidth}`}
1474
- row={row}
1475
- columns={virtualization.virtualColumns}
1476
- rowIndex={actualIndex}
1477
- isSelected={selectedRowIds.has(row.id)}
1478
- checkboxSelection={checkboxSelection}
1479
- onRowClick={handleRowClick}
1480
- onCellClick={handleCellClick}
1481
- onSelectionChange={handleSelectionChange}
1482
- columnWidths={columnWidths}
1483
- pinnedColumns={pinnedColumns}
1484
- pinnedRows={pinnedRows}
1485
- // Detail Panel
1486
- hasDetailPanel={hasDetailPanel}
1487
- isDetailPanelExpanded={expandedRowIds.has(row.id)}
1488
- detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: actualIndex }) : null}
1489
- detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: actualIndex }) || 200}
1490
- onDetailPanelToggle={handleDetailPanelToggle}
1491
- pinCheckboxColumn={pinCheckboxColumn}
1492
- pinExpandColumn={pinExpandColumn}
1493
- // Row Reordering
1494
- rowReordering={rowReordering}
1495
- onDragStart={rowReorderHandlers.onDragStart}
1496
- onDragOver={rowReorderHandlers.onDragOver}
1497
- onDragEnd={rowReorderHandlers.onDragEnd}
1498
- onDrop={rowReorderHandlers.onDrop}
1499
- isDragging={rowReorderHandlers.draggedRowId === row.id}
1500
- isDragOver={rowReorderHandlers.dragOverRowId === row.id}
1501
- // Editing
1502
- editingCell={editingHandlers.editingCell}
1503
- onEditStart={editingHandlers.startCellEdit}
1504
- onEditStop={editingHandlers.stopCellEdit}
1505
- onEditCellValueChange={editingHandlers.setEditCellValue}
1506
- // Focus
1507
- focusedCellField={focusedCell != null && focusedCell.id === row.id ? focusedCell.field : null}
1508
- // Spanning
1509
- colspanMap={spanning.colspanMap}
1510
- rowSpanningCaches={spanning.rowSpanningState.caches}
1511
- rowHeight={rowHeight}
1512
- />
1513
- );
1514
- })
1515
- )}
1516
-
1517
- {/* Infinite Scroll Skeletons (Inline with flow, inside translateY container) */}
1518
- {paginationMode === 'infinite' && state.dataSource.loading && sortedUnpinnedRows.length > 0 && (() => {
1519
- const skeletonCount = Math.min(effectivePaginationModel.pageSize, 20);
1520
- // The virtual container translates its children down by `firstRowIndex * rowHeight`.
1521
- // Since these skeletons are at the VERY BOTTOM of all rows, their actual array index
1522
- // is precisely after the last real row.
1523
- const startIdx = layout.unpinnedRowsLength;
1524
-
1525
- // However, they are rendered INSIDE the translateY block. The real rows handled this nicely
1526
- // because they just layout one after another. Skeletons can do exactly the same, EXCEPT we might
1527
- // need to offset them so they sit at the bottom.
1528
- return (
1529
- <div className="ogx__skeleton-group">
1530
- {Array.from({ length: skeletonCount }).map((_, i) => (
1531
- <Row
1532
- key={`__skeleton_${i}__`}
1533
- row={{ id: `__skeleton_${i}__`, _isSkeleton: true } as any}
1534
- columns={virtualization.virtualColumns}
1535
- rowIndex={startIdx + i}
1536
- rowHeight={rowHeight}
1537
- />
1538
- ))}
1539
- </div>
1540
- );
1541
- })()}
1542
- </div>
1543
-
1544
- </div>
1545
-
1546
- {/* Pinned Bottom Rows */}
1547
- {pinnedBottomRows.length > 0 && (
1548
- <div className="ogx__pinned-rows ogx__pinned-rows--bottom" role="rowgroup">
1549
- {pinnedBottomRows.map((row, index) => (
1550
- <Row
1551
- key={row.id}
1552
- row={row}
1553
- columns={virtualization.virtualColumns}
1554
- rowIndex={index}
1555
- isSelected={selectedRowIds.has(row.id)}
1556
- checkboxSelection={checkboxSelection}
1557
- onRowClick={handleRowClick}
1558
- onCellClick={handleCellClick}
1559
- onSelectionChange={handleSelectionChange}
1560
- columnWidths={columnWidths}
1561
- pinnedColumns={pinnedColumns}
1562
- pinnedRows={pinnedRows}
1563
- // Detail Panel
1564
- hasDetailPanel={hasDetailPanel}
1565
- isDetailPanelExpanded={expandedRowIds.has(row.id)}
1566
- detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: index }) : null}
1567
- detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: index }) || 200}
1568
- onDetailPanelToggle={handleDetailPanelToggle}
1569
- pinCheckboxColumn={pinCheckboxColumn}
1570
- pinExpandColumn={pinExpandColumn}
1571
- // Focus
1572
- focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
1573
- // Spanning
1574
- colspanMap={spanning.colspanMap}
1575
- rowSpanningCaches={spanning.rowSpanningState.caches}
1576
- rowHeight={rowHeight}
1577
- />
1578
- ))}
1579
- </div>
1580
- )}
898
+ <GridVirtualRows<R>
899
+ virtualContainerHeight={virtualization.totalHeight - virtualization.pinnedTopHeight - virtualization.pinnedBottomHeight}
900
+ offsetTop={virtualization.offsetTop}
901
+ effectiveLoading={effectiveLoading}
902
+ visibleRows={visibleRows}
903
+ baseColumns={columns as unknown as GridColDef<R>[]}
904
+ virtualColumns={virtualization.virtualColumns}
905
+ viewportWidth={state.dimensions.viewportWidth}
906
+ checkboxSelection={checkboxSelection}
907
+ hasDetailPanel={hasDetailPanel}
908
+ rowReordering={rowReordering}
909
+ rowHeight={rowHeight}
910
+ pinnedRows={pinnedRows}
911
+ selectedRowIds={selectedRowIds}
912
+ onRowClick={handleRowClick}
913
+ onCellClick={handleCellClick}
914
+ onSelectionChange={handleSelectionChange}
915
+ columnWidths={columnWidths}
916
+ pinnedColumns={pinnedColumns}
917
+ expandedRowIds={expandedRowIds}
918
+ getDetailPanelContent={getDetailPanelContent}
919
+ getDetailPanelHeight={getDetailPanelHeight}
920
+ onDetailPanelToggle={handleDetailPanelToggle}
921
+ pinCheckboxColumn={pinCheckboxColumn}
922
+ pinExpandColumn={pinExpandColumn}
923
+ rowReorderHandlers={rowReorderHandlers}
924
+ editingHandlers={editingHandlers}
925
+ focusedCell={focusedCell}
926
+ colspanMap={spanning.colspanMap}
927
+ rowSpanningCaches={spanning.rowSpanningState.caches}
928
+ paginationMode={paginationMode}
929
+ dataSourceLoading={state.dataSource.loading}
930
+ sortedUnpinnedRowCount={sortedUnpinnedRows.length}
931
+ infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
932
+ unpinnedRowsLength={layout.unpinnedRowsLength}
933
+ />
934
+
935
+ <GridPinnedRows<R>
936
+ rows={pinnedBottomRows}
937
+ position="bottom"
938
+ columns={virtualization.virtualColumns}
939
+ selectedRowIds={selectedRowIds}
940
+ checkboxSelection={checkboxSelection}
941
+ onRowClick={handleRowClick}
942
+ onCellClick={handleCellClick}
943
+ onSelectionChange={handleSelectionChange}
944
+ columnWidths={columnWidths}
945
+ pinnedColumns={pinnedColumns}
946
+ pinnedRows={pinnedRows}
947
+ hasDetailPanel={hasDetailPanel}
948
+ expandedRowIds={expandedRowIds}
949
+ getDetailPanelContent={getDetailPanelContent}
950
+ getDetailPanelHeight={getDetailPanelHeight}
951
+ onDetailPanelToggle={handleDetailPanelToggle}
952
+ pinCheckboxColumn={pinCheckboxColumn}
953
+ pinExpandColumn={pinExpandColumn}
954
+ focusedCell={focusedCell}
955
+ colspanMap={spanning.colspanMap}
956
+ rowSpanningCaches={spanning.rowSpanningState.caches}
957
+ rowHeight={rowHeight}
958
+ />
1581
959
 
1582
960
  {/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
1583
961
  already contain pre-aggregated values with synthetic field keys
1584
962
  (e.g. 'Q1\u001frevenue\u001fsum') that don't match aggregationModel keys. */}
1585
963
  {hasAggregation && !pivotMode && (
1586
- <div
1587
- className="ogx__aggregation-footer"
1588
- role="row"
1589
- aria-label="Aggregation totals"
1590
- aria-live="polite"
1591
- style={{
1592
- minHeight: `${rowHeight}px`
1593
- }}
1594
- >
1595
- {checkboxSelection && (
1596
- <div style={{ width: 48, flexShrink: 0 }} />
1597
- )}
1598
- {hasDetailPanel && (
1599
- <div style={{ width: 48, flexShrink: 0 }} />
1600
- )}
1601
- {rowReordering && (
1602
- <div style={{ width: 48, flexShrink: 0 }} />
1603
- )}
1604
- {orderedColumns.map((col) => {
1605
- const fnName = (aggregationModel as Record<string, string>)[col.field];
1606
- const rawValue = aggregationResult[col.field];
1607
- const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
1608
-
1609
- return (
1610
- <div
1611
- key={col.field}
1612
- className="ogx__aggregation-cell"
1613
- role="gridcell"
1614
- style={{
1615
- width: colWidth,
1616
- minWidth: colWidth,
1617
- maxWidth: colWidth,
1618
- textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
1619
- }}
1620
- >
1621
- {fnName ? (
1622
- <>
1623
- <span className="ogx__aggregation-label">
1624
- {fnName}
1625
- </span>
1626
- <span className="ogx__aggregation-value">
1627
- {formatAggregationValue(rawValue, fnName)}
1628
- </span>
1629
- </>
1630
- ) : null}
1631
- </div>
1632
- );
1633
- })}
1634
- </div>
964
+ <GridAggregationFooter
965
+ columns={orderedColumns as unknown as GridColDef[]}
966
+ aggregationModel={aggregationModel}
967
+ aggregationResult={aggregationResult}
968
+ columnWidths={columnWidths}
969
+ rowHeight={rowHeight}
970
+ checkboxSelection={checkboxSelection}
971
+ hasDetailPanel={hasDetailPanel}
972
+ rowReordering={rowReordering}
973
+ />
1635
974
  )}
1636
975
 
1637
976
  </div>
1638
977
  </div>
1639
978
  )}
1640
979
 
1641
- {/* Pagination — shared between grid and list view when not inside list view block */}
1642
980
  {!listView && pagination && (() => {
1643
981
  const PaginationComponent = slots?.pagination || Pagination;
1644
- const paginationProps = {
1645
- page: effectivePaginationModel.page,
1646
- pageSize: effectivePaginationModel.pageSize,
1647
- rowCount: state.pagination.rowCount,
1648
- pageSizeOptions: pageSizeOptions,
1649
- onPageChange: (newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage }),
1650
- onPageSizeChange: (newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 }),
1651
- ...slotProps?.pagination
1652
- };
1653
-
1654
- return <PaginationComponent {...paginationProps} />;
982
+ return (
983
+ <PaginationComponent
984
+ page={effectivePaginationModel.page}
985
+ pageSize={effectivePaginationModel.pageSize}
986
+ rowCount={state.pagination.rowCount}
987
+ pageSizeOptions={pageSizeOptions}
988
+ onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
989
+ onPageSizeChange={(newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
990
+ {...slotProps?.pagination}
991
+ />
992
+ );
1655
993
  })()}
1656
994
 
1657
995
  {/* Accessibility Live Region */}
1658
996
  <div className="ogx-aria-live-status" role="status" aria-live="polite">
1659
997
  {effectiveLoading ? 'Loading data...' : ''}
1660
- {state.dataSource.error ? `Error: ${state.dataSource.error.message || 'Unknown error'}` : ''}
998
+ {state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
1661
999
  {!loading && !state.dataSource.error && (
1662
1000
  filteredRows.length === 0
1663
1001
  ? noRowsLabel
@@ -1667,27 +1005,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1667
1005
  )}
1668
1006
  </div>
1669
1007
 
1670
- {state.dataSource.error && (
1671
- <div className="ogx-error-overlay" aria-live="assertive" role="alert">
1672
- <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--ogx-color-error)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
1673
- <circle cx="12" cy="12" r="10" />
1674
- <line x1="12" y1="8" x2="12" y2="12" />
1675
- <line x1="12" y1="16" x2="12.01" y2="16" />
1676
- </svg>
1677
- <div className="ogx-error-overlay__title">
1678
- Oops! Something went wrong
1679
- </div>
1680
- <div className="ogx-error-overlay__message">
1681
- {state.dataSource.error.message || 'An unexpected error occurred while loading the data.'}
1682
- </div>
1683
- <button
1684
- onClick={() => window.location.reload()}
1685
- className="ogx-button ogx-button--primary"
1686
- >
1687
- Retry
1688
- </button>
1689
- </div>
1690
- )}
1008
+ <GridErrorOverlay error={state.dataSource.error} />
1691
1009
  </div>
1692
1010
  );
1693
1011
  }