@opencorestack/opengridx 1.3.0 → 2.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.
@@ -45,6 +45,7 @@ The main component for displaying and interacting with data.
45
45
  | :--- | :--- | :--- | :--- |
46
46
  | `sortingMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles sorting. The grid calls `onSortModelChange` but does not re-sort rows locally. |
47
47
  | `filterMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles filtering. The grid calls `onFilterModelChange` but does not re-filter rows locally. |
48
+ | `multiSort` | `boolean` | `false` | When `true`, clicking any sortable column header appends/cycles it in the sort model instead of replacing it — no Shift key required. Shift+click always appends regardless of this prop. |
48
49
  | `onSortModelChange` | `(model: GridSortItem[]) => void` | — | Fired when the active sort model changes. |
49
50
  | `onFilterModelChange` | `(model: GridFilterModel) => void` | — | Fired when the active filter model changes. |
50
51
 
@@ -81,7 +82,6 @@ The main component for displaying and interacting with data.
81
82
  | `pinnedColumns` | `GridColumnPinning` | — | Columns pinned to the left or right viewport edges. See [Column & Row Pinning](#-column--row-pinning). |
82
83
  | `onPinnedColumnsChange` | `(model: GridColumnPinning) => void` | — | Fired when column pinning changes. |
83
84
  | `pinnedRows` | `GridRowPinning` | — | Row IDs pinned to the top or bottom of the viewport. |
84
- | `onPinnedRowsChange` | `(model: GridRowPinning) => void` | — | Fired when row pinning changes. |
85
85
  | `pinCheckboxColumn` | `boolean` | `false` | Keeps the checkbox column visible during horizontal scrolling. |
86
86
  | `pinExpandColumn` | `boolean` | `false` | Keeps the Master-Detail expansion column visible during horizontal scrolling. |
87
87
 
@@ -107,7 +107,7 @@ The main component for displaying and interacting with data.
107
107
  | :--- | :--- | :--- | :--- |
108
108
  | `treeData` | `boolean` | `false` | Enables hierarchical tree data display. |
109
109
  | `getTreeDataPath` | `(row: R) => string[]` | — | Returns the hierarchy path for a row (e.g. `['Engineering', 'Frontend']`). |
110
- | `groupingColDef` | `GridColDef` | — | Overrides the auto-generated tree expand/collapse column definition. |
110
+ | `groupingColDef` | `Partial<GridColDef>` | — | Configures the dedicated `__group__` column prepended at position 0 (auto-pinned left) when row grouping is active. Accepts `headerName`, `width`, `renderCell`, and any non-system `GridColDef` field. The column's `field` is always `'__group__'`; it is never sorted, exported, hidden, or re-ordered. |
111
111
  | `defaultGroupingExpansionDepth` | `number` | `0` | Number of tree levels expanded on initial render (`-1` = all). |
112
112
 
113
113
  #### Row Grouping & Aggregation
@@ -115,7 +115,6 @@ The main component for displaying and interacting with data.
115
115
  | Prop | Type | Default | Description |
116
116
  | :--- | :--- | :--- | :--- |
117
117
  | `rowGroupingModel` | `GridRowGroupingModel` | `[]` | Array of field names to group rows by (e.g. `['department', 'team']`). See [Row Grouping](#️-row-grouping). |
118
- | `onRowGroupingModelChange` | `(model: GridRowGroupingModel) => void` | — | Fired when the grouping model changes. |
119
118
  | `aggregationModel` | `GridAggregationModel` | — | Map of `field → aggFn` (e.g. `{ salary: 'sum', age: 'avg' }`). See [Aggregation Reference](#-aggregation-reference). |
120
119
  | `onAggregationModelChange` | `(model: GridAggregationModel) => void` | — | Fired when the aggregation model changes. |
121
120
  | `getAggregationPosition` | `(groupNode: GridTreeNode \| null) => 'inline' \| 'footer' \| null` | — | Controls where aggregation results appear. `'inline'` = inside the group row, `'footer'` = a dedicated row below the group, `null` = hidden. Pass `null` groupNode = grand-total (root) position. |
@@ -279,7 +278,7 @@ Defines the behavior and appearance of a single column.
279
278
  | :--- | :--- | :--- | :--- |
280
279
  | `field` | `string` | — | **Required.** Unique identifier matching the row object key. |
281
280
  | `headerName` | `string` | — | Text shown in the column header cell. |
282
- | `description` | `string` | — | Tooltip shown on header hover (accessibility label). |
281
+ | `description` | `string` | — | Tooltip shown on column header hover (rendered as the HTML `title` attribute — improves accessibility). |
283
282
  | `width` | `number \| string` | `100` | Fixed width in pixels or a percentage string. |
284
283
  | `minWidth` | `number` | — | Minimum width in pixels (enforced during resize). |
285
284
  | `maxWidth` | `number` | — | Maximum width in pixels (enforced during resize). |
@@ -335,9 +334,10 @@ Defines the behavior and appearance of a single column.
335
334
  | `pinnable` | `boolean` | `true` | Allow this column to be pinned via the UI. |
336
335
  | `disableColumnMenu` | `boolean` | `false` | Hide the column header kebab/context menu. |
337
336
  | `exportable` | `boolean` | `true` | Set to `false` to exclude from CSV, Excel, JSON, and Print exports. |
338
- | `groupable` | `boolean` | `true` | Allow this column to be used as a row grouping dimension. |
337
+ | `groupable` | `boolean` | `true` | Allow this column to be used as a row grouping dimension. Set to `false` to prevent this field from being grouped, even when it appears in `rowGroupingModel`. |
338
+ | `groupingValueFormatter` | `(params: { field: string; value: unknown }) => string` | — | Custom formatter for group-header labels when this column is the active grouping field. Falls back to `"field: value"` when omitted. |
339
339
  | `aggregable` | `boolean` | `true` | Allow this column to be aggregated. |
340
- | `availableAggregationFunctions` | `string[]` | all built-ins | Restrict which aggregation functions are available for this column (e.g. `['sum', 'avg']`). |
340
+ | `availableAggregationFunctions` | `string[]` | all built-ins | Restrict which aggregation functions are computed and available for this column (e.g. `['sum', 'avg']`). Functions outside this list are skipped even if set in `aggregationModel`. |
341
341
 
342
342
  ---
343
343
 
@@ -1087,7 +1087,6 @@ import { DataGrid, GridRowGroupingModel, GridAggregationModel } from '@opencores
1087
1087
  import { useState } from 'react';
1088
1088
 
1089
1089
  export default function GroupingExample() {
1090
- const [rowGroupingModel, setRowGroupingModel] = useState<GridRowGroupingModel>(['department']);
1091
1090
  const [aggregationModel, setAggregationModel] = useState<GridAggregationModel>({
1092
1091
  salary: 'sum',
1093
1092
  age: 'avg',
@@ -1097,8 +1096,7 @@ export default function GroupingExample() {
1097
1096
  <DataGrid
1098
1097
  rows={rows}
1099
1098
  columns={columns}
1100
- rowGroupingModel={rowGroupingModel}
1101
- onRowGroupingModelChange={setRowGroupingModel}
1099
+ rowGroupingModel={['department']}
1102
1100
  aggregationModel={aggregationModel}
1103
1101
  onAggregationModelChange={setAggregationModel}
1104
1102
  />
@@ -27,11 +27,29 @@ function MyGrid() {
27
27
  | `getRowId` | `(row: R) => GridRowId` | `row.id` | Unique ID for each row. |
28
28
  | `height` | `number \| string` | `undefined` | Height of the grid container. |
29
29
  | `loading` | `boolean` | `false` | Displays a loading skeleton/shimmer. |
30
+ | `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Row height preset: compact = 32 px, standard = `rowHeight`, comfortable = 72 px. |
30
31
  | `checkboxSelection` | `boolean` | `false` | Enable row checkboxes. |
32
+ | `disableRowSelectionOnClick` | `boolean` | `false` | When `true`, clicking a row does not toggle its selection. |
33
+ | `disableMultipleRowSelection` | `boolean` | `false` | When `true`, at most one row can be selected at a time. |
31
34
  | `pagination` | `boolean` | `false` | Enable/Disable bottom pagination bar. |
32
- | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). |
35
+ | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). Omit for uncontrolled; use `initialState` to set the initial page/pageSize. |
33
36
  | `pageSizeOptions` | `number[]` | `[10, 25, 50]` | Available page size options. |
34
- | `initialState` | `GridInitialState` | `undefined` | Initial configuration (sorting, columns, etc). |
37
+ | `initialState` | `GridInitialState` | `undefined` | Initial configuration (sorting, columns, pagination, etc). |
38
+
39
+ ## 🔃 Sorting
40
+
41
+ | Prop | Type | Default | Description |
42
+ | :--- | :--- | :--- | :--- |
43
+ | `sortModel` | `GridSortItem[]` | — | Controlled sort state. |
44
+ | `onSortModelChange` | `(model: GridSortItem[]) => void` | — | Fired when the sort model changes. |
45
+ | `multiSort` | `boolean` | `false` | When `true`, every click appends/cycles the column in the sort model instead of replacing it. Shift+click always appends regardless of this prop. |
46
+
47
+ ## 🌳 Row Grouping
48
+
49
+ | Prop | Type | Default | Description |
50
+ | :--- | :--- | :--- | :--- |
51
+ | `rowGroupingModel` | `string[]` | `[]` | Fields to group by (in order). |
52
+ | `groupingColDef` | `GridColDef` | — | Config for the dedicated `__group__` column created at position 0, auto-pinned left, when grouping is active. |
35
53
 
36
54
  ## 🖱️ Interaction & Events
37
55
 
@@ -9,7 +9,9 @@ Manages the column headers, sorting triggers, column resizing, and column groupi
9
9
  | `columns` | `GridColDef[]` | Column definitions for the visible viewport. |
10
10
  | `columnGroupingModel` | `GridColumnGroupingModel` | Hierarchy for multi-level header spanning. |
11
11
  | `sortModel` | `GridSortModel` | Current sorting state for highlight and icons. |
12
- | `onSort` | `(field, direction) => void` | Callback triggered when a header is clicked. |
12
+ | `onSort` | `(field, direction) => void` | Callback triggered on a plain click — replaces the sort model with a single key. |
13
+ | `onSortAdd` | `(field, direction) => void` | Callback triggered on Shift+click or when `multiSort` is active — appends/cycles the key without replacing others. |
14
+ | `multiSort` | `boolean` | When `true`, every click routes to `onSortAdd` instead of `onSort`. Wired from the `multiSort` prop on `<DataGrid>`. |
13
15
  | `onColumnResize` | `Function` | Callback for manual column width changes. |
14
16
  | `pinnedColumns` | `GridColumnPinning` | Coordinates sticky positioning for headers. |
15
17
  | `checkboxSelection` | `boolean` | Renders the "Select All" checkbox. |
@@ -30,23 +30,18 @@ The grid supports "Zebra" striping via CSS:
30
30
 
31
31
  When `getDetailPanelContent` is provided, the `<Row />` renders an expandable container below itself to show supplemental data.
32
32
 
33
- ## ⚠️ v1.1 — Hierarchy Field Deprecation
33
+ ## ⚠️ v2.0 — Runtime `_*` hierarchy fields removed
34
34
 
35
- The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer typed on `GridRowModel`. They will be removed from the runtime row object in v2.0.
36
-
37
- **Before (still works at runtime in v1.1 — `params.row._hasChildren` is `unknown`, not `boolean`; assignment to typed variables and arithmetic will error, truthiness checks will not):**
35
+ The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer injected onto the runtime row object as of v2.0. Use `params.rowMeta` instead:
38
36
 
39
37
  ```tsx
38
+ // ❌ v1 shim — no longer works in v2:
40
39
  renderCell: (params) => {
41
- // v1.1 accepts this at runtime; _hasChildren resolves to unknown via [key: string]: unknown
42
40
  const hasChildren = (params.row as Record<string, unknown>)._hasChildren;
43
41
  return hasChildren ? <GroupIcon /> : params.value;
44
42
  }
45
- ```
46
43
 
47
- **After (v1.1+):**
48
-
49
- ```tsx
44
+ // ✅ v2:
50
45
  renderCell: (params) => {
51
46
  const hasChildren = params.rowMeta?.hasChildren;
52
47
  return hasChildren ? <GroupIcon /> : params.value;
@@ -55,7 +55,6 @@ Rows can be pinned to the top or bottom of the grid. Pinned rows remain visible
55
55
  | `pinnedColumns` | `GridColumnPinning` | `{ left: string[], right: string[] }` |
56
56
  | `pinnedRows` | `GridRowPinning` | `{ top: GridRowId[], bottom: GridRowId[] }` |
57
57
  | `onPinnedColumnsChange` | `(model) => void` | Callback triggered when pinning changes. |
58
- | `onPinnedRowsChange` | `(model) => void` | Callback triggered when row pinning changes. |
59
58
 
60
59
  ---
61
60
 
@@ -36,6 +36,28 @@ const [selection, setSelection] = useState<GridRowId[]>([]);
36
36
  />
37
37
  ```
38
38
 
39
+ ### Disable Click-to-Select
40
+
41
+ Use `disableRowSelectionOnClick` to prevent clicking a row from changing the selection (useful when rows have their own click actions like navigation). Selection via checkboxes still works.
42
+
43
+ ```tsx
44
+ <DataGrid
45
+ checkboxSelection
46
+ disableRowSelectionOnClick
47
+ />
48
+ ```
49
+
50
+ ### Single-Row Selection Only
51
+
52
+ Use `disableMultipleRowSelection` to cap selection to one row at a time. Clicking a second row deselects the first; clicking an already-selected row deselects it.
53
+
54
+ ```tsx
55
+ <DataGrid
56
+ disableMultipleRowSelection
57
+ onRowSelectionModelChange={(model) => console.log('selected:', model)}
58
+ />
59
+ ```
60
+
39
61
  ---
40
62
 
41
63
  ## ⚙️ API Reference
@@ -15,6 +15,41 @@ Sorting can be toggled by clicking column headers. It supports single-column and
15
15
  | `sortModel` | `GridSortItem[]` | Controlled state of active sorts. |
16
16
  | `onSortModelChange` | `(model) => void` | Callback triggered when sorting changes. |
17
17
 
18
+ ### Multi-Column Sort
19
+
20
+ OpenGridX supports two modes of multi-column sorting:
21
+
22
+ **`multiSort` prop (single-click, recommended):** Pass `multiSort` to make every click append or cycle a column in the sort model rather than replacing it. No modifier key required.
23
+
24
+ ```tsx
25
+ <DataGrid
26
+ multiSort
27
+ sortModel={sortModel}
28
+ onSortModelChange={setSortModel}
29
+ ...
30
+ />
31
+ ```
32
+
33
+ **Shift+Click (always available):** Even without `multiSort`, holding **Shift** while clicking a column header appends it to the active sort. This works in all grids regardless of the prop.
34
+
35
+ Each active sort column shows a numbered priority badge (`1`, `2`, `3`…) next to its arrow.
36
+
37
+ - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc → desc → removed`.
38
+ - A plain click *without* `multiSort` and without Shift always resets to a single-key sort on that column.
39
+ - Set `sortable: false` on a column to exclude it entirely.
40
+
41
+ ```tsx
42
+ // Controlled multi-sort — set programmatically or drive from UI with multiSort:
43
+ <DataGrid
44
+ multiSort
45
+ sortModel={[
46
+ { field: 'department', sort: 'asc' },
47
+ { field: 'salary', sort: 'desc' },
48
+ ]}
49
+ onSortModelChange={setSortModel}
50
+ />
51
+ ```
52
+
18
53
  ### Column-Specific Config
19
54
  Disable sorting for specific columns in `GridColDef`:
20
55
  ```typescript
@@ -54,14 +54,57 @@ Tree data is used for data that has a natural parent-child relationship (e.g., a
54
54
  ---
55
55
 
56
56
  ## 🎨 Customizing the Group Column
57
- By default, OpenGridX creates a `__group__` column. You can customize its appearance:
57
+
58
+ When `rowGroupingModel` is active, pass `groupingColDef` to configure a dedicated `__group__` column that is prepended at position 0 and auto-pinned left, separate from your data columns. It accepts any `GridColDef` fields except `field` (always `'__group__'`).
58
59
 
59
60
  ```tsx
60
61
  <DataGrid
62
+ rows={rows}
63
+ columns={columns}
64
+ rowGroupingModel={['department']}
61
65
  groupingColDef={{
62
- headerName: 'Hierarchy',
63
- width: 300,
64
- renderCell: (params) => { /* custom renderer */ }
66
+ headerName: 'Department Group',
67
+ width: 240,
65
68
  }}
66
69
  />
67
70
  ```
71
+
72
+ Without `groupingColDef`, the group toggle is overlaid on the first data column. With it, the group indicator always appears in its own fixed column regardless of your `columns` order.
73
+
74
+ ---
75
+
76
+ ## 🏷️ Customizing Group Labels (`groupingValueFormatter`)
77
+
78
+ By default each group-header row displays `"field: value"`. Use `groupingValueFormatter` on a `GridColDef` to override the label for that grouping level:
79
+
80
+ ```tsx
81
+ const columns: GridColDef[] = [
82
+ {
83
+ field: 'department',
84
+ headerName: 'Department',
85
+ groupingValueFormatter: ({ value }) => `📁 ${String(value)}`,
86
+ },
87
+ { field: 'name', headerName: 'Name' },
88
+ { field: 'salary', headerName: 'Salary' },
89
+ ];
90
+
91
+ <DataGrid rows={rows} columns={columns} rowGroupingModel={['department']} />
92
+ // Group headers now show: "📁 Engineering", "📁 HR", etc.
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🚫 Preventing a Column from Being Grouped (`groupable: false`)
98
+
99
+ Set `groupable: false` on a `GridColDef` to prevent that field from being used as a grouping dimension. The column still renders normally, but the grid skips it when building the group tree:
100
+
101
+ ```tsx
102
+ const columns: GridColDef[] = [
103
+ { field: 'id', headerName: 'ID', groupable: false },
104
+ { field: 'department', headerName: 'Department' },
105
+ ];
106
+
107
+ // Even if rowGroupingModel includes 'id', it will be silently skipped.
108
+ <DataGrid rows={rows} columns={columns} rowGroupingModel={['id', 'department']} />
109
+ // Result: grouped by 'department' only — 'id' is skipped.
110
+ ```
@@ -39,7 +39,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
39
39
  rowHeight = 52,
40
40
  headerHeight = 56,
41
41
  autoHeight = false,
42
+ density,
42
43
  checkboxSelection = false,
44
+ disableRowSelectionOnClick = false,
45
+ disableMultipleRowSelection = false,
43
46
  rowSelectionModel: propRowSelectionModel,
44
47
  onRowSelectionModelChange: propOnRowSelectionModelChange,
45
48
  onRowClick,
@@ -50,7 +53,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
50
53
  sortModel: propSortModel,
51
54
  onSortModelChange,
52
55
  pagination: propPagination = false,
53
- paginationModel = { page: 0, pageSize: 100 },
56
+ paginationModel,
54
57
  onPaginationModelChange,
55
58
  pageSizeOptions = [10, 25, 50, 100],
56
59
  pinnedColumns: propPinnedColumns,
@@ -74,6 +77,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
74
77
  treeData = false,
75
78
  getTreeDataPath,
76
79
  defaultGroupingExpansionDepth,
80
+ groupingColDef,
77
81
 
78
82
  rowGroupingModel: propRowGroupingModel,
79
83
 
@@ -86,6 +90,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
86
90
  onProcessRowUpdateError,
87
91
 
88
92
  dataSource,
93
+ multiSort = false,
89
94
  sortingMode = 'client',
90
95
  filterMode = 'client',
91
96
  paginationMode = 'client',
@@ -191,8 +196,35 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
191
196
 
192
197
  const pivot = usePivot(rows as GridRowModel[], columns as unknown as GridColDef[], currentPivotModel, pivotMode);
193
198
 
199
+ const effectiveRowHeight = density === 'compact' ? 32 : density === 'comfortable' ? 72 : rowHeight;
200
+
194
201
  const activeRows = (pivotMode && pivot.isValid ? pivot.pivotRows : rows) as unknown as R[];
195
- const activeColumns = (pivotMode && pivot.isValid ? pivot.pivotColumns : columns) as unknown as GridColDef<R>[];
202
+ const baseColumns = (pivotMode && pivot.isValid ? pivot.pivotColumns : columns) as unknown as GridColDef<R>[];
203
+
204
+ // When groupingColDef is provided and row grouping is active, prepend a
205
+ // dedicated synthetic __group__ column at position 0 (pinned-left).
206
+ const isRowGroupingActive = Boolean(propRowGroupingModel && propRowGroupingModel.length > 0);
207
+
208
+ // Auto-pin __group__ column to the left when groupingColDef is active.
209
+ const effectivePinnedColumns = (groupingColDef && isRowGroupingActive && !pivotMode)
210
+ ? { ...pinnedColumns, left: ['__group__', ...((pinnedColumns?.left ?? []).filter(f => f !== '__group__'))] }
211
+ : pinnedColumns;
212
+
213
+ const activeColumns = (groupingColDef && isRowGroupingActive && !pivotMode)
214
+ ? [
215
+ {
216
+ headerName: 'Group',
217
+ width: 220,
218
+ ...groupingColDef,
219
+ field: '__group__',
220
+ hideable: false,
221
+ sortable: false,
222
+ pinnable: false,
223
+ exportable: false,
224
+ } as unknown as GridColDef<R>,
225
+ ...baseColumns,
226
+ ]
227
+ : baseColumns;
196
228
 
197
229
  const [serverAggregationResults, setServerAggregationResults] = useState<GridAggregationResult | null>(null);
198
230
 
@@ -245,7 +277,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
245
277
  const gridData = useDataGrid({
246
278
  rows: normalizedRows,
247
279
  columns: activeColumns,
248
- rowHeight,
280
+ rowHeight: effectiveRowHeight,
249
281
  headerHeight,
250
282
  rowCount: propRowCount,
251
283
  columnVisibilityModel,
@@ -309,6 +341,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
309
341
  const rowGroupingHandlers = useRowGrouping({
310
342
  rows: effectiveRows,
311
343
  getRowId: effectiveGetRowId,
344
+ columns: activeColumns,
312
345
  rowGroupingModel,
313
346
  aggregationModel,
314
347
  defaultGroupingExpansionDepth,
@@ -355,7 +388,27 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
355
388
  }
356
389
 
357
390
  onRowClick?.(params);
358
- }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap]);
391
+
392
+ if (!disableRowSelectionOnClick) {
393
+ let next: GridRowId[];
394
+ if (disableMultipleRowSelection) {
395
+ const alreadySelected = selectedRowIds.has(id as GridRowId);
396
+ next = alreadySelected ? [] : [id as GridRowId];
397
+ } else {
398
+ const newSelection = new Set(selectedRowIds);
399
+ if (newSelection.has(id as GridRowId)) {
400
+ newSelection.delete(id as GridRowId);
401
+ } else {
402
+ newSelection.add(id as GridRowId);
403
+ }
404
+ next = Array.from(newSelection);
405
+ }
406
+ if (!isSelectionControlled) {
407
+ setInternalRowSelectionModel(next);
408
+ }
409
+ onRowSelectionModelChange?.(next);
410
+ }
411
+ }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap, disableRowSelectionOnClick, disableMultipleRowSelection, selectedRowIds, isSelectionControlled, setInternalRowSelectionModel, onRowSelectionModelChange]);
359
412
 
360
413
  const dataSourceHandlers = useGridDataSource({
361
414
  dataSource,
@@ -440,7 +493,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
440
493
  columnWidths,
441
494
  effectiveColumnOrder,
442
495
  columnVisibilityModel,
443
- pinnedColumns,
496
+ pinnedColumns: effectivePinnedColumns,
444
497
  });
445
498
 
446
499
  const rowPipeline = useGridRowPipeline<GridRowModel>({
@@ -486,6 +539,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
486
539
 
487
540
  const { aggregationResult } = useAggregation({
488
541
  rows: filteredRows,
542
+ columns: activeColumns as unknown as GridColDef[],
489
543
  aggregationModel,
490
544
  isServerSide: !!(dataSource && (paginationMode === 'server' || sortingMode === 'server')),
491
545
  dataSource,
@@ -548,7 +602,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
548
602
  }, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef, isRowGrouping, rowGroupingHandlers, sortedUnpinnedRows]);
549
603
 
550
604
  const layout = useLayout({
551
- rowHeight,
605
+ rowHeight: effectiveRowHeight,
552
606
  pagination,
553
607
  paginatedUnpinnedRows,
554
608
  sortedUnpinnedRows,
@@ -557,7 +611,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
557
611
  pinnedTopRowsLength: pinnedTopRows.length,
558
612
  pinnedBottomRowsLength: pinnedBottomRows.length,
559
613
  visibleOrderedColumns,
560
- pinnedColumns,
614
+ pinnedColumns: effectivePinnedColumns,
561
615
  columnWidths,
562
616
  viewportWidth: state.dimensions.viewportWidth || 1000,
563
617
  checkboxSelection,
@@ -688,6 +742,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
688
742
  onSortModelChange?.(newSortModel as GridSortItem[]);
689
743
  }, [isSortControlled, onSortModelChange, setInternalSortModel]);
690
744
 
745
+ const handleSortAdd = useCallback((field: string, direction: GridSortDirection) => {
746
+ const existing = sortModel.filter(item => item.field !== field);
747
+ const newSortModel: GridSortItem[] = direction
748
+ ? [...existing, { field, sort: direction }]
749
+ : existing;
750
+
751
+ if (!isSortControlled) {
752
+ setInternalSortModel(newSortModel);
753
+ }
754
+ onSortModelChange?.(newSortModel);
755
+ }, [sortModel, isSortControlled, onSortModelChange, setInternalSortModel]);
756
+
691
757
  const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
692
758
  allRenderableRows,
693
759
  navigationColumns,
@@ -809,7 +875,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
809
875
  style={{
810
876
  ...style,
811
877
  height: height ?? style?.height,
812
- '--ogx-row-height': `${rowHeight}px`,
878
+ '--ogx-row-height': `${effectiveRowHeight}px`,
813
879
  '--ogx-header-height': `${headerHeight}px`
814
880
  } as unknown as React.CSSProperties}
815
881
  aria-busy={effectiveLoading}
@@ -844,7 +910,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
844
910
  selectedRowIds={selectedRowIds}
845
911
  listViewColumn={listViewColumn}
846
912
  noRowsLabel={effectiveNoRowsLabel}
847
- rowHeight={rowHeight}
913
+ rowHeight={effectiveRowHeight}
848
914
  checkboxSelection={checkboxSelection}
849
915
  paginationMode={paginationMode}
850
916
  dataSource={dataSource}
@@ -909,10 +975,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
909
975
  onSelectAll={handleSelectAll}
910
976
  sortModel={sortModel}
911
977
  onSort={handleSort}
978
+ onSortAdd={handleSortAdd}
979
+ multiSort={multiSort}
912
980
 
913
981
  onColumnResize={handleColumnResize}
914
982
  columnWidths={columnWidths}
915
- pinnedColumns={pinnedColumns}
983
+ pinnedColumns={effectivePinnedColumns}
916
984
 
917
985
  focusedCell={focusedCell}
918
986
  onHeaderClick={(field) => {
@@ -969,7 +1037,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
969
1037
  onCellClick={handleCellClick}
970
1038
  onSelectionChange={handleSelectionChange}
971
1039
  columnWidths={columnWidths}
972
- pinnedColumns={pinnedColumns}
1040
+ pinnedColumns={effectivePinnedColumns}
973
1041
  pinnedRows={pinnedRows}
974
1042
  hasDetailPanel={hasDetailPanel}
975
1043
  expandedRowIds={expandedRowIds}
@@ -981,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
981
1049
  focusedCell={focusedCell}
982
1050
  colspanMap={spanning.colspanMap}
983
1051
  rowSpanningCaches={spanning.rowSpanningState.caches}
984
- rowHeight={rowHeight}
1052
+ rowHeight={effectiveRowHeight}
985
1053
  rowMetaMap={rowMetaMap}
986
1054
  />
987
1055
 
@@ -996,14 +1064,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
996
1064
  checkboxSelection={checkboxSelection}
997
1065
  hasDetailPanel={hasDetailPanel}
998
1066
  rowReordering={rowReordering}
999
- rowHeight={rowHeight}
1067
+ rowHeight={effectiveRowHeight}
1000
1068
  pinnedRows={pinnedRows}
1001
1069
  selectedRowIds={selectedRowIds}
1002
1070
  onRowClick={handleRowClick}
1003
1071
  onCellClick={handleCellClick}
1004
1072
  onSelectionChange={handleSelectionChange}
1005
1073
  columnWidths={columnWidths}
1006
- pinnedColumns={pinnedColumns}
1074
+ pinnedColumns={effectivePinnedColumns}
1007
1075
  expandedRowIds={expandedRowIds}
1008
1076
  getDetailPanelContent={getDetailPanelContent}
1009
1077
  getDetailPanelHeight={getDetailPanelHeight}
@@ -1033,7 +1101,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1033
1101
  onCellClick={handleCellClick}
1034
1102
  onSelectionChange={handleSelectionChange}
1035
1103
  columnWidths={columnWidths}
1036
- pinnedColumns={pinnedColumns}
1104
+ pinnedColumns={effectivePinnedColumns}
1037
1105
  pinnedRows={pinnedRows}
1038
1106
  hasDetailPanel={hasDetailPanel}
1039
1107
  expandedRowIds={expandedRowIds}
@@ -1045,7 +1113,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1045
1113
  focusedCell={focusedCell}
1046
1114
  colspanMap={spanning.colspanMap}
1047
1115
  rowSpanningCaches={spanning.rowSpanningState.caches}
1048
- rowHeight={rowHeight}
1116
+ rowHeight={effectiveRowHeight}
1049
1117
  rowMetaMap={rowMetaMap}
1050
1118
  />
1051
1119
 
@@ -1058,11 +1126,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1058
1126
  aggregationModel={aggregationModel}
1059
1127
  aggregationResult={aggregationResult}
1060
1128
  columnWidths={resolvedColumnWidths}
1061
- rowHeight={rowHeight}
1129
+ rowHeight={effectiveRowHeight}
1062
1130
  checkboxSelection={checkboxSelection}
1063
1131
  hasDetailPanel={hasDetailPanel}
1064
1132
  rowReordering={rowReordering}
1065
- pinnedColumns={pinnedColumns}
1133
+ pinnedColumns={effectivePinnedColumns}
1066
1134
  />
1067
1135
  )}
1068
1136