@opencorestack/opengridx 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -0,0 +1,1222 @@
1
+ # Migrating from v2 to v3
2
+
3
+ This guide covers every change in `@opencorestack/opengridx` 3.0.0 that can require action when upgrading from any 2.x release. 3.0.0 is mostly a correctness release: nearly every item below is a bug fix. They are listed here because they change what your users see, what files contain, or what your callbacks receive, and code written around the old behaviour can break.
4
+
5
+ Use the table to find the sections that apply to you. Sections are ordered by how many apps they are likely to affect. The full list of fixes is in [CHANGELOG.md](../../CHANGELOG.md).
6
+
7
+ ## Who needs to do what
8
+
9
+ | If your app… | Go to |
10
+ | :--- | :--- |
11
+ | has columns with `type: 'date'`, `'boolean'`, `'singleSelect'` or `'image'` and no `valueFormatter` / `renderCell` | [§1](#1-cells-are-formatted-by-column-type) |
12
+ | passes `getRowId`, or reads `row.id` on rows that have their own key field | [§2](#2-getrowid-no-longer-writes-an-id-onto-rows) |
13
+ | reads `params.row._hasChildren`, `_treeDepth`, `_isGroupRow` or another underscore field | [§3](#3-underscore-hierarchy-fields-are-no-longer-added-to-rows) |
14
+ | mutates `params.row` inside `renderCell` / `valueGetter` / `valueFormatter` under grouping or tree data, mutates `columns` in place, or has a `valueGetter` that is impure or can throw | [§4](#4-paramsrow-is-now-your-own-row-object) |
15
+ | builds filter items in code, relies on empty filter values, or uses the toolbar search / `renderQuickFilter` | [§5](#5-filtering) |
16
+ | sorts numeric strings, dates stored as strings, accented text, or uses the column menu with multi-sort | [§6](#6-sorting) |
17
+ | relies on Tab moving between cells, on Enter in a non-editable cell, or on `colIndex` / `rowIndex` / `aria-*` values | [§7](#7-keyboard-focus-and-aria) |
18
+ | uses `pagination`, `rowCount`, `pageSizeOptions` or `onPaginationModelChange` | [§8](#8-pagination) |
19
+ | uses checkbox selection, select-all, `disableMultipleRowSelection` or a controlled `rowSelectionModel` | [§9](#9-selection) |
20
+ | calls `apiRef.current.*` methods (setters, `getAllFilteredRows`, `getVisibleRows`, `getVisibleColumns`) | [§10](#10-apiref) |
21
+ | edits cells: `processRowUpdate`, `isCellEditable`, `renderEditCell`, `singleSelect` / `date` / `boolean` editors | [§11](#11-editing) |
22
+ | exports CSV, Excel, JSON, print or PDF files, or parses them downstream | [§12](#12-exports) |
23
+ | shows aggregation totals (footer, group rows, `getAggregationResult()`) | [§13](#13-aggregation-values) |
24
+ | uses row grouping or tree data, parses group row ids, or uses `getAggregationPosition` | [§14](#14-row-grouping-and-tree-data) |
25
+ | passes a `dataSource`, uses infinite scroll or server-side pagination / sorting / filtering | [§15](#15-server-side-data-and-infinite-scroll) |
26
+ | pins columns or rows, sets column widths, uses detail panels, `onRowsScrollEnd`, or has CSS for pinned rows | [§16](#16-layout-and-pinning) |
27
+ | uses `onRowOrderChange`, `onColumnOrderChange`, a controlled `columnOrder`, or tests column resizing | [§17](#17-row-and-column-reordering-and-resizing) |
28
+ | has a `<form>` around the grid, uses the exported `Button` / `Checkbox` / `GridTooltip`, or a custom toolbar | [§18](#18-toolbar-buttons-and-ui-components) |
29
+ | copies rows with Ctrl+C or `apiRef.current.copySelectedRows()` | [§19](#19-clipboard) |
30
+ | uses `DataGridThemeProvider`, a preset theme, or theme heights | [§20](#20-theming) |
31
+ | uses `pivotMode` | [§21](#21-pivot-mode) |
32
+ | uses `colSpan`, `rowSpan` or `columnGroupingModel` | [§22](#22-cell-spanning-and-column-groups) |
33
+ | uses `listView` | [§23](#23-list-view) |
34
+ | has custom CSS targeting `ogx__*` / `ogx-*` classes or grid DOM structure | [§24](#24-dom-and-css-class-changes) |
35
+ | relies on TypeScript types of rows, columns or props | [§25](#25-typing-changes) |
36
+ | persists grid state: `onStateChange`, `initialState`, `useGridStateStorage` (including with SSR) | [§26](#26-state-persistence) |
37
+
38
+ ---
39
+
40
+ ## Before you start
41
+
42
+ ### Clear the bundler cache
43
+
44
+ After upgrading, **restart your dev server and clear the bundler cache** (for Vite: `rm -rf node_modules/.vite`, then start with `--force`). If you skip this, Vite can keep serving the old version even though `node_modules` contains the new one, and the old behaviour looks like a regression.
45
+
46
+ ### Import the stylesheet
47
+
48
+ The stylesheet is **not** loaded by the JavaScript entry. It never was: some v2 docs said it was imported automatically, which was wrong. Import it once, in your app's root file:
49
+
50
+ ```tsx
51
+ import '@opencorestack/opengridx/styles';
52
+ ```
53
+
54
+ Without it the grid is unstyled and its viewport has no bounded height, so **every row renders** (virtualization is effectively off). If your v2 app looked fine, you already have this import. In development, 3.0.0 logs a warning when the stylesheet is not loaded.
55
+
56
+ ### Know the size limit
57
+
58
+ A single scrolling grid tops out at about 645,000 rows at the default 52px row height in Chromium (the browser's maximum element height). Above that, use pagination or a `dataSource`. The grid logs a dev warning when content is taller than browsers can scroll.
59
+
60
+ ---
61
+
62
+ ## 1. Cells are formatted by column type
63
+
64
+ **What changed:** columns with a `type` and no `valueFormatter` are now formatted for display. This applies to cells, `params.formattedValue`, list view, and the CSV, basic Excel, print and PDF exports.
65
+
66
+ | `type` | v2 showed | v3 shows |
67
+ | :--- | :--- | :--- |
68
+ | `'date'` | `String(value)`, e.g. `Mon Mar 04 2024 00:00:00 GMT+0100` | `value.toLocaleDateString()`, e.g. `3/4/2024` |
69
+ | `'boolean'` | `true` / `false` | `Yes` / `No` |
70
+ | `'singleSelect'` | the raw value, e.g. `2` | the matching `valueOptions` label |
71
+ | `'image'` (no `renderCell`) | the URL as text | `<img class="ogx__cell-image">` |
72
+
73
+ **Who is affected:** apps whose tests, snapshots or downstream parsers expect the old text, and apps that post-process `params.formattedValue`.
74
+
75
+ **How to find it:** `grep -rnE "type: *['\"](date|boolean|singleSelect|image)['\"]" src/`
76
+
77
+ **Fix it:** if you want a different format, add a `valueFormatter`; it always wins over the default.
78
+
79
+ ```tsx
80
+ import type { GridColDef } from '@opencorestack/opengridx';
81
+
82
+ const columns: GridColDef[] = [
83
+ {
84
+ field: 'createdAt',
85
+ type: 'date',
86
+ valueFormatter: ({ value }) => (value instanceof Date ? value.toISOString().slice(0, 10) : ''),
87
+ },
88
+ { field: 'active', type: 'boolean', valueFormatter: ({ value }) => (value ? 'true' : 'false') },
89
+ ];
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 2. getRowId no longer writes an id onto rows
95
+
96
+ **What changed:** in v2, `getRowId` rows were copied and `id` was overwritten with `getRowId(row)`. In v3 rows are stored untouched and keyed by `getRowId` internally. Everywhere you receive a row (`params.row`, `onRowClick`, `processRowUpdate`, `apiRef`, `dataSource` rows, tree children), it is your own object.
97
+
98
+ | | v2 | v3 |
99
+ | :--- | :--- | :--- |
100
+ | `row.id` with `getRowId={(r) => r.sku}` | `r.sku` (overwritten) | your row's own `id` (often `undefined`) |
101
+ | `row.id === getRowId(row)` | always true | not guaranteed |
102
+ | Exporting `selectedRows` | matched on `row.id` | matched on `row.id` unless you pass `getRowId` |
103
+ | Duplicate ids | later row won and rendered twice | first row kept, dev warning |
104
+ | Inline `getRowId={(r) => r.sku}` | reset the row store on every render | only a new `rows` array or changed ids reset it |
105
+
106
+ **Who is affected:** apps that pass `getRowId` and then read `row.id`, and apps that export the selection with `getRowId`.
107
+
108
+ **How to find it:** `grep -rn "getRowId" src/`, then check for `.id` reads on rows in the same components.
109
+
110
+ **Fix it:** use your own key, and pass `getRowId` to exports.
111
+
112
+ ```tsx
113
+ import { exportToCsv } from '@opencorestack/opengridx';
114
+ import type { GridApi, GridColDef, GridRowModel } from '@opencorestack/opengridx';
115
+
116
+ interface Product extends GridRowModel { sku: string; name: string }
117
+
118
+ const getRowId = (row: GridRowModel): string => String(row.sku);
119
+
120
+ function exportSelection(api: GridApi, rows: Product[], columns: GridColDef<Product>[]): void {
121
+ exportToCsv(rows, columns, { selectedRows: api.getSelectedRows(), getRowId });
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 3. Underscore hierarchy fields are no longer added to rows
128
+
129
+ **What changed:** until v3, `useTreeData` and `useRowGrouping` copied each row and added these fields to the copy before it reached `renderCell`:
130
+
131
+ `_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`
132
+
133
+ They were deprecated in v1.1 and removed from the TypeScript types then. In v3 they are no longer added at runtime either. The same information has been available as `params.rowMeta` since v1.1.
134
+
135
+ **Who is affected:** code that reads any of these fields. TypeScript does not warn about it, because `GridRowModel` has an index signature. Reading `params.row._hasChildren` type-checks as `unknown` and is simply `undefined` in v3.
136
+
137
+ **How to find it:**
138
+
139
+ ```bash
140
+ grep -rnE "_(hasChildren|treeDepth|isExpanded|groupingField|groupingValue|descendantCount|isGroupRow)\b" src/
141
+ ```
142
+
143
+ **Fix it:** read `params.rowMeta` instead. It is `undefined` for flat rows, so always use optional chaining.
144
+
145
+ | v2 (row field) | v3 (`params.rowMeta`) |
146
+ | :--- | :--- |
147
+ | `params.row._hasChildren` | `params.rowMeta?.hasChildren` |
148
+ | `params.row._treeDepth` | `params.rowMeta?.treeDepth` |
149
+ | `params.row._isExpanded` | `params.rowMeta?.isExpanded` |
150
+ | `params.row._groupingField` | `params.rowMeta?.groupingField` |
151
+ | `params.row._groupingValue` | `params.rowMeta?.groupingValue` |
152
+ | `params.row._descendantCount` | `params.rowMeta?.descendantCount` |
153
+ | `params.row._isGroupRow` | `params.rowMeta?.isGroupRow` |
154
+
155
+ `rowMeta` also has `groupLabel`, the formatted label the grid shows for a group row, and (new) `isGroupFooter` for subtotal rows.
156
+
157
+ ```tsx
158
+ import type { GridColDef } from '@opencorestack/opengridx';
159
+
160
+ // v2: undefined in v3
161
+ const nameV2: GridColDef = {
162
+ field: 'name',
163
+ renderCell: (params) => {
164
+ const row = params.row as Record<string, unknown>;
165
+ return row._hasChildren ? <strong>{params.formattedValue}</strong> : params.formattedValue;
166
+ },
167
+ };
168
+
169
+ // v3
170
+ const name: GridColDef = {
171
+ field: 'name',
172
+ renderCell: (params) =>
173
+ params.rowMeta?.hasChildren ? <strong>{params.formattedValue}</strong> : params.formattedValue,
174
+ };
175
+ ```
176
+
177
+ `rowMeta` (and `formattedValue`) are passed to `renderCell`, `renderEditCell` and the function form of `cellClassName`.
178
+
179
+ **Tree data:** the parent rows the grid creates for missing path segments used to carry `_isGroupRow: true`. Use `params.rowMeta?.isGroupRow` to recognise them (see also [§14](#14-row-grouping-and-tree-data)).
180
+
181
+ ---
182
+
183
+ ## 4. params.row is now your own row object
184
+
185
+ **What changed:** because v2 copied every row to attach the underscore fields, `params.row` under row grouping or tree data was a **copy**. In v3 it is the **same object you passed in `rows`**, exactly as it is without grouping. This also stops rows being re-created on every render, so cells re-render less.
186
+
187
+ **Who is affected:** only code that **mutates** `params.row` (or the `row` passed to `valueGetter` / `valueFormatter`) while grouping or tree data is on. Those mutations used to land on a throwaway copy; now they change your data. Mutating row objects inside render callbacks was never supported, so this should be rare.
188
+
189
+ **How to find it:** search `renderCell`, `valueGetter` and `valueFormatter` bodies for `params.row.x =` or `row.x =`.
190
+
191
+ **Fix it:** don't mutate rows during render. Derive values instead, or update rows through state and `processRowUpdate`.
192
+
193
+ If you compare row objects by identity (`===`), grouped and flat rows now behave the same: the grid passes your objects through unchanged.
194
+
195
+ ### Column definitions and value getters
196
+
197
+ | Behaviour | v2 | v3 |
198
+ | :--- | :--- | :--- |
199
+ | Inline `columns` (a new array each render) | every render re-ran filtering and sorting | reused while the definitions are shallow-equal, so mutating a column object **in place** is not detected |
200
+ | Quick filter text | re-read every cell per keystroke | cached per row object, so a `valueGetter` must be pure (same row object, same result) |
201
+ | A `valueGetter` / `valueFormatter` that throws | crashed the grid once the column was sorted, filtered, aggregated, grouped, pivoted or shown in list view | the value reads as `undefined`, with a dev warning once per column |
202
+
203
+ **Fix it:** pass a new column object (or a new array with new objects) when a definition changes; keep `valueGetter` free of side effects and outside state, and replace a row object when its data changes.
204
+
205
+ ---
206
+
207
+ ## 5. Filtering
208
+
209
+ ### Empty filter values no longer filter
210
+
211
+ **What changed:** a filter item whose value is empty (`undefined`, `null`, `''`, whitespace, `[]`) is now inactive, whatever the operator. Operators that take no value (`isEmpty`, `isNotEmpty`) still apply.
212
+
213
+ | Filter item | v2 | v3 |
214
+ | :--- | :--- | :--- |
215
+ | `{ operator: 'equals', value: null }` | kept only null cells | no filter |
216
+ | `{ operator: '=', value: '' }` | hid every row | no filter |
217
+ | `{ operator: '>', value: '' }` | meant `> 0` | no filter |
218
+ | `{ operator: 'contains', value: '' }` | hid null cells | no filter |
219
+ | `{ operator: 'isAnyOf', value: [] }` | hid every row | no filter |
220
+ | `{ operator: 'isAnyOf', value: 'a' }` (not an array) | matched nothing | treated as `['a']` |
221
+
222
+ **Fix it:** to match empty cells, use `isEmpty`, not an empty value.
223
+
224
+ ### Numeric and date operators
225
+
226
+ | Case | v2 | v3 |
227
+ | :--- | :--- | :--- |
228
+ | blank / whitespace cell with `>`, `<`, `=` … | treated as `0` | empty: never matches |
229
+ | boolean cell with a numeric operator | `1` / `0` | never matches |
230
+ | `!=` and null / blank cells | `!= 0` excluded null | always included |
231
+ | `after` / `onOrAfter` / `before` / `onOrBefore` | ignored (every row passed, warning per row) | filter by date |
232
+ | `is` / `not` on `type: 'date'` | raw string comparison | local calendar day, for `Date`, ISO strings and epoch numbers |
233
+
234
+ `GridFilterOperator` gained `'='`, `'after'`, `'onOrAfter'`, `'before'` and `'onOrBefore'`. **Exhaustive `switch` statements over `GridFilterOperator` need the new cases.**
235
+
236
+ ### Quick filter and toolbar search
237
+
238
+ | | v2 | v3 |
239
+ | :--- | :--- | :--- |
240
+ | What is searched | every value on the row: id, hidden columns, fields that are not columns | visible, filterable columns only |
241
+ | Value searched | raw value (objects as `[object Object]`, dates as `Date.toString()`) | `valueGetter` result formatted by `valueFormatter` (dates as `YYYY-MM-DD`; objects skipped) |
242
+ | Toolbar search `"north 2024"` | one phrase | two words, both must match (in any columns) |
243
+ | `renderQuickFilter` `value` | `quickFilterValues[0]` | all terms joined by spaces |
244
+ | Toolbar search and Filters button without a `filterModel` prop | did nothing unless you wired `onFilterModelChange` | filter the grid themselves |
245
+ | `initialState.filter` | ignored | applied |
246
+
247
+ **Who is affected:** apps that relied on searching hidden columns or ids. **Fix it:** to make a column searchable, it must be visible and `filterable` (the default). To search a value that is not shown, add a column for it.
248
+
249
+ ### Filter panel
250
+
251
+ - Opening the panel no longer emits `onFilterModelChange` or rewrites values (an `isAnyOf` array used to become `"a,b"`).
252
+ - Editing a condition replaces it in place (it moved to the end) and no longer drops filter groups or other conditions on the same column.
253
+ - `singleSelect` columns get a select (multi-select for `isAnyOf`, which emits an array); typed option values are preserved. Boolean "Any" removes the item. Date columns get `<input type="date">`. Clearing a text value keeps the item with value `''`.
254
+ - The panel shows a note when the model has conditions it cannot display (nested groups).
255
+ - The row-grouping `__group__` column is always `filterable: false`; `groupingColDef` cannot override it.
256
+
257
+ ---
258
+
259
+ ## 6. Sorting
260
+
261
+ **What changed:** the comparator is type-aware and locale-aware.
262
+
263
+ | Case | v2 | v3 |
264
+ | :--- | :--- | :--- |
265
+ | numeric strings in `type: 'number'` | compared as text (`"10" < "9"`) | compared as numbers |
266
+ | date strings in `type: 'date'` | compared as text | parsed and compared as dates |
267
+ | text | code-point order | `Intl.Collator` with numeric collation: accents next to their base letter, `item9 < item10` |
268
+ | `NaN` / Invalid Date | broke the sort | sorted with nulls (last ascending, first descending) |
269
+ | mixed numbers and strings | undefined order | numbers first |
270
+ | `valueGetter` columns | not sortable | sort by the computed value |
271
+
272
+ Multi-sort:
273
+
274
+ | Action | v2 | v3 |
275
+ | :--- | :--- | :--- |
276
+ | Shift-click (or `multiSort`) on the primary sort column | moved it to the end | keeps its priority |
277
+ | Column menu **Unsort** | cleared every sort key | removes only that column |
278
+ | Column menu **Asc / Desc** on a sorted column, or with `multiSort` | replaced the model | keeps the other keys |
279
+ | Enter / Space on a header with `multiSort` or Shift | always replaced | appends, like a click |
280
+ | Header click that clears the sort of one column while several are sorted | cleared every sort key | removes only that column |
281
+ | `apiRef.current.sortColumn()` | did not update the grid | replaces the model like a header click (appends under `multiSort`) |
282
+
283
+ **Who is affected:** apps with snapshot tests of sorted output, or server code that expects the old client order.
284
+
285
+ **Fix it:** update the snapshots. There is no per-column comparator; to sort by a different key, return that key from a `valueGetter` and format it for display with `valueFormatter` (or sort on the server with `sortingMode="server"`):
286
+
287
+ ```tsx
288
+ import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
289
+
290
+ interface Ticket extends GridRowModel { id: number; priority: 'low' | 'medium' | 'high' }
291
+
292
+ const RANK: Record<Ticket['priority'], number> = { low: 0, medium: 1, high: 2 };
293
+ const LABEL = ['low', 'medium', 'high'] as const;
294
+
295
+ const priority: GridColDef<Ticket> = {
296
+ field: 'priority',
297
+ type: 'number',
298
+ valueGetter: ({ row }) => RANK[row.priority],
299
+ valueFormatter: ({ value }) => LABEL[Number(value)] ?? '',
300
+ };
301
+ ```
302
+
303
+ ---
304
+
305
+ ## 7. Keyboard, focus and ARIA
306
+
307
+ **What changed:**
308
+
309
+ | Behaviour | v2 | v3 |
310
+ | :--- | :--- | :--- |
311
+ | Tab outside edit mode | moved between editable and system cells | **leaves the grid** in one press (Shift+Tab leaves backwards); focus returns to the last cell when you tab back in |
312
+ | Tab while editing | next editable cell | unchanged |
313
+ | Enter on a non-editable cell | nothing | on a row with children (group rows, tree-data parents): toggles expansion only, no `onRowClick` and no selection; on any other row: behaves like a click (`onRowClick`, click-to-select) |
314
+ | Enter on an editable cell | starts editing | unchanged |
315
+ | Header `focusedCell` (exported `Header` prop) | `{ id: 'HEADER', field }` | `{ id: null, field }` |
316
+ | Ctrl/Cmd+C | copied even when focus was outside the grid | only from a focused grid, never over a text selection |
317
+ | Focus ring on blur | stayed | hidden, but the focused cell is remembered |
318
+ | `Cell` / `Row` / `Header` components | moved DOM focus themselves | the grid owns focus (standalone users must focus cells themselves) |
319
+
320
+ New shortcuts: Shift+Space selects the focused row, Ctrl/Cmd+A selects all, Alt+ArrowRight / Alt+ArrowLeft expand and collapse, Alt+ArrowDown or Ctrl/Cmd+Enter opens a header's column menu, Alt+ArrowLeft / Alt+ArrowRight on a focused header resizes it.
321
+
322
+ Indices:
323
+
324
+ | Value | v2 | v3 |
325
+ | :--- | :--- | :--- |
326
+ | `colIndex` in `GridCellParams`, `renderCell`, `cellClassName`, `renderHeader`, `data-colindex` | relative to the rendered window (changed while scrolling horizontally) | absolute among visible data columns in render order, left-pinned first |
327
+ | `rowIndex` of bottom-pinned rows (`onRowClick`, `getDetailPanelContent`, `data-rowindex`, stripe class) | `0..n`, colliding with page rows | `topPinned + pageRows + i` |
328
+ | `aria-rowindex` | page-local; bottom-pinned rows restarted at 1 | global, 1 = header row, group rows counted |
329
+ | `aria-colindex` | window-relative | absolute, system columns counted |
330
+ | `aria-colcount` | all columns | visible columns, including `__group__` and pivot columns |
331
+ | `aria-sort` | every sorted column | the primary sort column only; others get an `aria-description` with their priority |
332
+
333
+ Also: the detail panel is `role="row"` > `role="gridcell"`; `ExpandIcon` has `tabIndex={-1}`; the `CellErrorBoundary` glyph is `role="img"` (was `status`); hierarchy rows get `aria-level` / `aria-expanded`; the grid gets `aria-multiselectable`.
334
+
335
+ **Who is affected:** apps (and end-user docs) that describe Tab navigation, apps with `onRowClick` handlers that must not fire from the keyboard, code that stores `colIndex` or `rowIndex`, and accessibility tests.
336
+
337
+ **How to find it:** `grep -rnE "colIndex|rowIndex|data-(col|row)index|aria-(row|col)index|['\"]HEADER['\"]" src/`
338
+
339
+ **Fix it:** key your logic on `field` and row `id`, not on indices. If you stored `colIndex` to look up a column, use `params.field`. Enter on an ordinary row now fires `onRowClick` like a click, so make that handler safe to run from the keyboard. On a tree-data parent, Enter only expands or collapses; it does not fire `onRowClick` or select the row (a mouse click on it does both).
340
+
341
+ ---
342
+
343
+ ## 8. Pagination
344
+
345
+ | Behaviour | v2 | v3 |
346
+ | :--- | :--- | :--- |
347
+ | Client-side total | all stored rows, including pinned and filtered-out | filtered, unpinned rows |
348
+ | Current page after data shrinks | stale, empty page | last page, and `onPaginationModelChange({ ...model, page: last })` fires once |
349
+ | `rowCount` prop | read at mount only | read live; a `dataSource` response's `rowCount` wins |
350
+ | Default `pageSize` when 100 is not in `pageSizeOptions` | 100 (not in the list) | the first option |
351
+ | `pageSize` missing from `pageSizeOptions` | select showed a blank value | shown as an extra option |
352
+ | Rows-per-page select accessible name | hard-coded "Rows per page" | `localeText.paginationRowsPerPage` |
353
+ | `paginationMode="server"` (and server sort / filter) without a `dataSource` | the grid re-sliced / re-sorted / re-filtered your server page | rows are shown as given |
354
+ | `slots.footer` `rowCount` | page length | server total |
355
+ | `slots.footer` `rowCount` in a flat grid | included pinned rows | excludes pinned rows (under tree data or grouping it counts data rows) |
356
+ | `pagination` with `paginationMode="infinite"` | sliced rows | pager and slicing ignored |
357
+
358
+ **Who is affected:** controlled `paginationModel` users (you now receive a page correction and must adopt it), and server-paginated grids that relied on the grid's extra slicing.
359
+
360
+ **Fix it:** in controlled mode, always store the model passed to `onPaginationModelChange`:
361
+
362
+ ```tsx
363
+ import { useState } from 'react';
364
+ import { DataGrid } from '@opencorestack/opengridx';
365
+ import type { GridColDef, GridPaginationModel, GridRowModel } from '@opencorestack/opengridx';
366
+
367
+ export function Orders({ rows, columns }: { rows: GridRowModel[]; columns: GridColDef[] }) {
368
+ const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({ page: 0, pageSize: 25 });
369
+ return (
370
+ <DataGrid
371
+ rows={rows}
372
+ columns={columns}
373
+ pagination
374
+ pageSizeOptions={[25, 50, 100]}
375
+ paginationModel={paginationModel}
376
+ onPaginationModelChange={setPaginationModel}
377
+ />
378
+ );
379
+ }
380
+ ```
381
+
382
+ ---
383
+
384
+ ## 9. Selection
385
+
386
+ | Behaviour | v2 | v3 |
387
+ | :--- | :--- | :--- |
388
+ | Select-all checkbox | replaced the selection with every stored row, including filtered-out rows | adds the filtered rows; deselect removes only those |
389
+ | Header checkbox state | counted stale ids and group ids | reflects the filtered rows only |
390
+ | `disableMultipleRowSelection` | honoured by row click only | also caps checkboxes, Space and `apiRef`; removes the select-all checkbox |
391
+ | Rows removed from `rows` | their ids stayed selected | pruned; `onRowSelectionModelChange` fires once with the pruned model |
392
+ | An action that leaves the selection as it was (for example Ctrl/Cmd+A when every row is already selected, or `apiRef.current.selectRow(id, true)` on a selected row) | UI handlers fired `onRowSelectionModelChange` with an identical model | does not fire |
393
+ | `apiRef.current.selectRow(s)` with a synthetic id (group, subtotal, auto-parent, pivot Grand Total) or with no effect | selected it / fired | ignored; fires nothing |
394
+ | Clicking an already-selected row | deselected it and fired | unchanged: deselects it and fires |
395
+ | `apiRef.current.getSelectedRows()` inside `onRowSelectionModelChange` | previous selection | the new selection |
396
+ | Group rows | had a checkbox | no checkbox, never selected |
397
+ | Exported `Header` | always rendered select-all | renders it only when `onSelectAll` is passed |
398
+
399
+ Pruning applies when the grid owns the rows: no `dataSource`, client pagination and filtering, not pivot mode.
400
+
401
+ **Who is affected:** controlled `rowSelectionModel` users, and apps that expected select-all to include hidden rows.
402
+
403
+ **Fix it:** with a controlled model, adopt whatever `onRowSelectionModelChange` passes you (including the pruned model). If you need "select every row including filtered-out ones", set the model yourself from your data.
404
+
405
+ ---
406
+
407
+ ## 10. apiRef
408
+
409
+ **What changed:** in v2 several `apiRef` methods wrote to an internal store the grid did not render from. In v3 they drive the grid and fire the matching callbacks.
410
+
411
+ | Method | v2 | v3 |
412
+ | :--- | :--- | :--- |
413
+ | `sortColumn`, `setFilterModel`, `setPage`, `setPageSize`, `selectRow`, `selectRows` | no visible effect, no callbacks | change the grid and fire `onSortModelChange` / `onFilterModelChange` / `onPaginationModelChange` / `onRowSelectionModelChange` |
414
+ | `getSortModel`, `getFilterModel`, `getSelectedRows` | stale | live |
415
+ | `getVisibleRows`, `getAllFilteredRows` | excluded pinned rows | include pinned rows |
416
+ | `getAllFilteredRows` under grouping / tree data | the visible hierarchy, including group rows | every filtered data row, in fully expanded order |
417
+ | `getAllFilteredRows` under tree data with a filter | — | only the rows that match (not their unmatched ancestors); follows screen order when sorting by the hierarchy column |
418
+ | `getVisibleColumns` | included hidden columns, definition order | visible columns in render order: left-pinned, unpinned, right-pinned |
419
+ | `getGroupedExportRows` | dropped collapsed groups' rows, ignored sort and filter | includes collapsed groups, follows sort and filter, subtotals over exported rows |
420
+ | `getAggregationModel` | the internal object | a content-equal copy |
421
+ | `copySelectedRows` | resolved and logged on failure | rejects (see [§19](#19-clipboard)) |
422
+ | `useGridApiRef().current` before mount | `null` | a no-op API; the live API is installed in a layout effect |
423
+
424
+ **Who is affected:** apps that call a setter and then also update their own state "because the grid ignored it". With a controlled prop, the setter now fires your change callback, so both paths run.
425
+
426
+ **How to find it:** `grep -rnE "apiRef\.current[?!]?\.(sortColumn|setFilterModel|setPage|setPageSize|selectRows?|getAllFilteredRows|getVisibleRows|getVisibleColumns)" src/`
427
+
428
+ **Fix it:** call the setter and let your change callback update controlled state; remove duplicate `setState` calls.
429
+
430
+ ---
431
+
432
+ ## 11. Editing
433
+
434
+ | Behaviour | v2 | v3 |
435
+ | :--- | :--- | :--- |
436
+ | `isCellEditable` | only decided Tab stops | also governs double-click, Enter and `aria-readonly`; returning `true` for a column without `editable` no longer makes it a Tab stop |
437
+ | Row-grouping group rows, auto-created tree parents | Enter opened an editor whose value was discarded | not editable |
438
+ | Tree-data parent rows | Enter only | Enter and double-click |
439
+ | `processRowUpdate` per commit | sometimes twice (Tab; Enter then blur) | exactly once |
440
+ | Edit when the cell unmounts (scroll, filter, page) or another edit starts | dropped | committed: `processRowUpdate` is called |
441
+ | Synchronous `processRowUpdate` result | applied after a microtask | applied in the same event |
442
+ | `processRowUpdate` returns a non-object | `TypeError` about `.id` | `onProcessRowUpdateError` receives an `Error` whose message starts with `[OpenGridX] processRowUpdate must return the updated row object (or a Promise of it)`; the editor stays open |
443
+ | `singleSelect` editor commit | option value as a string (`"2"`) | the original option value (`2`, or the object) |
444
+ | `date` editor on `Date` cells | opened blank, committed `'YYYY-MM-DD'` | shows the date, commits a `Date`; ISO datetimes keep their time; clearing commits `null` (was `''`) |
445
+ | `boolean` editor | committed on `setTimeout(0)`, stayed open on blur | commits synchronously and on blur |
446
+ | Enter during IME composition | committed | ignored |
447
+ | `onRowDoubleClick` on editable cells | suppressed | fires for the double-click that opens the editor (not inside an open editor) |
448
+ | `onCellClick` / `onRowClick` for clicks inside an editor | fired | do not fire |
449
+ | Focus after commit | always refocused the grid viewport | refocuses the grid only if focus was lost to `<body>` |
450
+ | `valueGetter` + `editable` without `valueSetter` | wrote `row[field]` silently | still writes `row[field]`, with a dev warning once per column |
451
+
452
+ **Who is affected:** anyone with `processRowUpdate` that has side effects (API calls): it now also runs for edits that used to be lost, so make sure it is safe to call when the user scrolls away. Apps with string-parsing workarounds for `singleSelect` or date values.
453
+
454
+ **How to find it:** `grep -rnE "processRowUpdate|isCellEditable|renderEditCell|Number\(newRow|parseInt\(newRow" src/`
455
+
456
+ **Fix it:** remove value-type workarounds, and add a `valueSetter` for editable computed columns:
457
+
458
+ ```tsx
459
+ import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
460
+
461
+ interface Person extends GridRowModel { id: number; first: string; last: string }
462
+
463
+ const fullName: GridColDef<Person> = {
464
+ field: 'fullName',
465
+ editable: true,
466
+ valueGetter: ({ row }) => `${row.first} ${row.last}`,
467
+ valueSetter: ({ value, row }) => {
468
+ const [first = '', ...rest] = String(value ?? '').split(' ');
469
+ return { ...row, first, last: rest.join(' ') };
470
+ },
471
+ };
472
+ ```
473
+
474
+ Custom editors now get callbacks, so they no longer need to reach into grid internals:
475
+
476
+ ```tsx
477
+ import type { GridColDef } from '@opencorestack/opengridx';
478
+
479
+ const notes: GridColDef = {
480
+ field: 'notes',
481
+ editable: true,
482
+ renderEditCell: ({ value, onValueChange, onCommit, onCancel }) => (
483
+ <textarea
484
+ aria-label="Notes"
485
+ value={String(value ?? '')}
486
+ onChange={(e) => onValueChange(e.target.value)}
487
+ onKeyDown={(e) => {
488
+ if (e.key === 'Enter' && !e.shiftKey) onCommit();
489
+ if (e.key === 'Escape') onCancel();
490
+ }}
491
+ />
492
+ ),
493
+ };
494
+ ```
495
+
496
+ The type of `renderEditCell`'s parameter is now `GridRenderEditCellParams`, a superset of `GridRenderCellParams`, so existing editors still type-check.
497
+
498
+ For the exported `Cell` / `Row` components: `Cell.onEditStop` gains an optional second argument `field`; `Cell` no longer stops propagation of the double-click that starts an edit (so it reaches the row and `onRowDoubleClick`); `Row.onEditStop` params gain optional `id` / `field`; new optional `Row.isCellEditable`; pinned rows report `aria-readonly`. See [§25](#25-typing-changes) for the type-level details.
499
+
500
+ ---
501
+
502
+ ## 12. Exports
503
+
504
+ ### Grouped export labels match the grid
505
+
506
+ In v2, group-header labels differed between export formats: `exportToExcelAdvanced` used the column's `groupingValueFormatter`, falling back to `"Header: value"` (the column's `headerName`), while CSV, basic Excel, JSON, print and PDF ignored `groupingValueFormatter` and always wrote `"field: value"`.
507
+
508
+ In v3 every format writes the label the grid shows. They use, in order:
509
+
510
+ 1. the entry's `groupLabel`, which `apiRef.current.getGroupedExportRows()` now sets from the grid's own label;
511
+ 2. otherwise the grouping column's `groupingValueFormatter`;
512
+ 3. otherwise `"field: value"`, the grid's documented default.
513
+
514
+ **Who is affected:** `exportToExcelAdvanced` users grouping by a column **without** a `groupingValueFormatter` (headers change from `Department: Engineering` to `dept: Engineering`), and CSV / basic Excel / JSON / print / PDF users grouping by a column **with** one (headers now use the formatter).
515
+
516
+ **Fix it:** set `groupingValueFormatter` on the grouping column. It then applies in the grid and in every export format:
517
+
518
+ ```tsx
519
+ import type { GridColDef } from '@opencorestack/opengridx';
520
+
521
+ const dept: GridColDef = {
522
+ field: 'dept',
523
+ headerName: 'Department',
524
+ groupingValueFormatter: ({ value }) => `Department: ${String(value)}`,
525
+ };
526
+ ```
527
+
528
+ ### File contents
529
+
530
+ | Change | v2 | v3 | Opt out / fix |
531
+ | :--- | :--- | :--- | :--- |
532
+ | CSV byte-order mark | none | starts with `EF BB BF` | `bom: false` |
533
+ | Text starting with `=` `+` `-` `@` tab or CR (CSV, basic Excel) | written raw | prefixed with `'` (`-abc` → `'-abc`; negative numbers unchanged) | `escapeFormulas: false` |
534
+ | Non-empty `selectedRows` together with `groupedRows` | exported every grouped row | exports only the selection, flat | pass `selectedRows: []` to export grouped |
535
+ | `selectedRows` with `aggregationResult` / `aggregationModel` | totals over all rows | totals recomputed over the selection | pre-filter rows yourself to keep your own totals |
536
+ | `valueFormatter` for `null` / `undefined` | skipped (empty cell) | called; its placeholder is exported | return `''` for empty values |
537
+ | `count` / `unique` totals | ran the column formatter (`$2.00`) | plain numbers (`2`) | — |
538
+ | Aggregate formatter `row` argument | `{}` | the record of aggregated values | — |
539
+ | Date `min` / `max` into the formatter | epoch ms | `Date` | — |
540
+ | Throwing formatter on a total | export threw | falls back to the default format | — |
541
+ | First column with an aggregate | label replaced the value | `Subtotal: 300`, `Grand Total: …`, PDF `TOTAL: 30` | — |
542
+ | `isSpacer` columns | exported | excluded everywhere | — |
543
+ | Grid system columns passed in `columns` (`__checkbox_col__`, `__expand_col__`, `__reorder_col__`, `__group__`) | exported unless `exportable: false` | always excluded (grouped exports write labels from `groupedRows`) | — |
544
+ | Type-based default formatting | none | see [§1](#1-cells-are-formatted-by-column-type) | add a `valueFormatter` |
545
+
546
+ ### Format-specific changes
547
+
548
+ | Function | Change |
549
+ | :--- | :--- |
550
+ | `exportToExcel` | A `.xlsx` file name downloads as `.xls` with a warning (the file is HTML; Excel refuses it as `.xlsx`). Sheet names are sanitised (`\ / ? * : [ ]` → `-`, 31 characters). Text cells are marked as text, so `00501` keeps its leading zeros. |
551
+ | `exportToJson` | Flat `aggregation.values` are raw numbers (were locale strings like `"8,320,000"`). Grouped subtotals and grand total contain only exported columns. |
552
+ | `printGrid` | Image URLs other than `http(s)`, `data:image`, `blob:` and relative are printed as text; missing alt text falls back to the field name (was `"undefined"`). |
553
+ | `exportToExcelAdvanced` | Dates carry the local wall-clock date (east of UTC they showed the previous day). ISO strings and epoch numbers in date columns become date cells; numeric strings in number columns become numbers; `'true'` / `'false'` in boolean columns become booleans; NaN / Infinity / Invalid Date become empty cells; objects become JSON text (formula / hyperlink objects are no longer live). |
554
+ | `exportToExcelAdvanced` | Default `numFmt` is `#,##0` for integers and `#,##0.##` otherwise; number columns get no column-level `numFmt`; `count` / `unique` use `#,##0`; `includeSummary` and the summary sheet are numeric. |
555
+ | `exportToExcelAdvanced` | `rows: 'selected'` with no selection writes a header-only sheet (was all rows), and its `includeSummary` is recomputed over the selection. Invalid or duplicate sheet names are sanitised (`X-Y`, `Data (2)`) instead of throwing. `embedImage` reads the URL after `valueGetter`; SVG / WebP / AVIF write the URL as text (were broken `.png`). |
556
+ | `exportToPdf` | Characters outside WinAnsi print as `?` with a console warning; pass `font` (a Unicode `.ttf`, base64) to print them. U+202F / U+2212 become a space / `-`. The filter line joins with ` AND ` / ` OR ` (was ` • `), shows groups and the search, skips value-less rules. An invalid `headerTextColor` falls back to white (was indigo). Title and filter line wrap. A custom `JsPDF` must provide `splitTextToSize`, `addFileToVFS` and `addFont`. |
557
+
558
+ **Who is affected:** anything that parses exported files: importers, diff-based tests, scripts that read CSV without handling a BOM, and workflows that open basic Excel exports under a `.xlsx` name.
559
+
560
+ **How to find it:** `grep -rnE "exportTo(Csv|Excel|ExcelAdvanced|Json|Pdf)|printGrid" src/`
561
+
562
+ **Fix it:** strip the BOM in CSV parsers (or pass `bom: false`), pass `getRowId` with `selectedRows` ([§2](#2-getrowid-no-longer-writes-an-id-onto-rows)), rename `.xlsx` to `.xls` for `exportToExcel` (or use `exportToExcelAdvanced` for real `.xlsx`):
563
+
564
+ ```tsx
565
+ import { exportToCsv, exportToExcel } from '@opencorestack/opengridx';
566
+ import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
567
+
568
+ function exportAll(rows: GridRowModel[], columns: GridColDef[]): void {
569
+ exportToCsv(rows, columns, { fileName: 'orders.csv', bom: false, escapeFormulas: true });
570
+ exportToExcel(rows, columns, { fileName: 'orders.xls' });
571
+ }
572
+ ```
573
+
574
+ ---
575
+
576
+ ## 13. Aggregation values
577
+
578
+ | Case | v2 | v3 |
579
+ | :--- | :--- | :--- |
580
+ | Blank strings, booleans, arrays in `sum` / `avg` / `min` / `max` | counted as 0 (`avg([10, '', 20])` = 10; `sum` over a boolean column = count of `true`) | ignored (`avg` = 15; boolean `sum` = 0) |
581
+ | `count` / `unique` | counted `''` and whitespace-only strings | skip them |
582
+ | `min` / `max` over dates | epoch ms | `Date`, in `getAggregationResult()`, group-row `aggregatedValues`, `getGroupedExportRows()`, pivot cells and `usePivot` rows |
583
+ | `valueGetter` columns | aggregated `row[field]` (usually 0) | aggregate the computed value |
584
+ | `aggregable: false` with a model entry | summed anyway | ignored (footer, group rows, pivot, exports) |
585
+ | Footer `sum` / `avg` / `min` / `max` text | `formatAggregationValue` | the column's `valueFormatter` (with `row` = the aggregation result); falls back if it throws |
586
+ | Group rows: `count` / `unique` | ran the column formatter (`$2.00`) | plain (`2`) |
587
+ | Group rows: aggregated column without formatter | `3000` | locale number `3,000` |
588
+ | Under grouping: footer totals | aggregated the visible rows (double-counted expanded groups) | the filtered data rows, independent of expansion |
589
+ | Server-driven footer (infinite mode, `filterMode="server"` only) | summed the loaded rows | server totals, or `—` |
590
+ | Server totals while a new `getAggregations` is pending or after it failed | previous result | `{}` (shown as `—`); `isLoading` / `error` follow the current request |
591
+
592
+ **Who is affected:** apps that display or test totals, and code that treats `min` / `max` results as numbers.
593
+
594
+ **How to find it:** `grep -rnE "getAggregationResult|aggregationModel|aggregatedValues" src/`
595
+
596
+ **Fix it:** handle `Date` results:
597
+
598
+ ```ts
599
+ function toTimestamp(value: unknown): number | null {
600
+ if (value instanceof Date) return value.getTime();
601
+ return typeof value === 'number' ? value : null;
602
+ }
603
+ ```
604
+
605
+ If you want blanks counted as zero, store `0` in the data or compute the value in a `valueGetter`.
606
+
607
+ ---
608
+
609
+ ## 14. Row grouping and tree data
610
+
611
+ | Behaviour | v2 | v3 |
612
+ | :--- | :--- | :--- |
613
+ | `renderCell` for synthetic rows (group rows, subtotal rows, auto-created tree parents) | not called | called; return `undefined` to keep the default rendering |
614
+ | Click on a real tree-data parent row | toggled expansion | selects the row and fires `onRowClick` (the chevron, Enter and Alt+Arrow keys expand) |
615
+ | Click on a row-grouping group row | toggled | unchanged; does not fire `onRowClick` |
616
+ | Auto-created tree parent row object | carried the segment name | `{ id }` only; use `rowMeta.groupLabel` for the label |
617
+ | Subtotals, "(n)" count, `descendantCount` | counted filtered-out rows | only rows that pass the filter; groups left empty are hidden; tree `descendantCount` is recursive |
618
+ | `getAggregationPosition` | ignored | honoured; called for each group and once with `null` for the grand total (return `null` for it to hide the footer) |
619
+ | Group row ids for non-string values | `auto-group-<field>-<String(value)>-<parent>` | the value part is a typed key (`\u001fnumber:1`, `\u001fnull`, `\u001fdate:<ms>`), so `1` and `'1'` are different groups; string values unchanged |
620
+ | Tree auto-parent ids | `auto-group-a/b` | `auto-group-["a","b"]` (JSON path, so segments may contain `/`) |
621
+ | Group rows | had a checkbox, could get a detail panel | no checkbox, no detail panel, not editable |
622
+ | `treeData` without `getTreeDataPath` | undefined behaviour | rows shown flat |
623
+ | `pinnedRows` under tree data or grouping | vanished, leaving a gap | stay in the hierarchy, with a dev warning |
624
+ | Server `sortingMode` / `filterMode` | the hierarchy still filtered and sorted client-side | not re-filtered or re-sorted |
625
+ | `groupingColDef` under `treeData` | ignored | adds the pinned `__group__` column, which takes the hierarchy toggle |
626
+ | Row grouping with a `paginationMode="server"` `dataSource` | grouped only the first page | one request for `[0, Number.MAX_SAFE_INTEGER)`, with a dev warning; make sure your server can return every row |
627
+
628
+ **Who is affected:** apps with custom `renderCell` that assume it only runs for data rows, apps that parse group ids, and apps whose tree parents were expected to expand on click.
629
+
630
+ **How to find it:** `grep -rnE "auto-group-|getAggregationPosition|isGroupRow" src/`
631
+
632
+ **Fix it:** guard `renderCell`, and never parse ids; use `rowMeta`:
633
+
634
+ ```tsx
635
+ import type { GridColDef } from '@opencorestack/opengridx';
636
+
637
+ const amount: GridColDef = {
638
+ field: 'amount',
639
+ renderCell: (params) => {
640
+ if (params.rowMeta?.isGroupRow) return undefined; // keep the grid's group rendering
641
+ return <span className="amount">{params.formattedValue}</span>;
642
+ },
643
+ };
644
+
645
+ const groupKey = (meta: { groupingField?: string; groupingValue?: unknown } | undefined) =>
646
+ meta ? `${meta.groupingField ?? ''}=${String(meta.groupingValue)}` : null;
647
+ ```
648
+
649
+ Make `getAggregationPosition` handle `null`:
650
+
651
+ ```tsx
652
+ import type { GridTreeNode } from '@opencorestack/opengridx';
653
+
654
+ const getAggregationPosition = (node: GridTreeNode | null): 'inline' | 'footer' | null =>
655
+ node === null ? 'footer' : node.isExpanded ? 'footer' : 'inline';
656
+ ```
657
+
658
+ Keep `getTreeDataPath` stable (module scope or `useCallback`); a new function rebuilds the tree.
659
+
660
+ ---
661
+
662
+ ## 15. Server-side data and infinite scroll
663
+
664
+ | Behaviour | v2 | v3 |
665
+ | :--- | :--- | :--- |
666
+ | `dataSource` with every mode `'client'` | `getRows` never called | called once with `{ startRow: 0, endRow: Number.MAX_SAFE_INTEGER }` |
667
+ | Server sort / filter with client pagination | refetched per page, showed one page | all rows requested once; page changes do not refetch |
668
+ | Infinite scroll request range | `[page * pageSize, page * pageSize + pageSize)` (could skip rows) | `[rows loaded so far, (page + 1) * pageSize)` |
669
+ | Infinite scroll after sort / filter / `pageSize` / `dataSource` change | continued from the current page | restarts at row 0 and fires `onPaginationModelChange({ ...model, page: 0 })` |
670
+ | Tree children request | `endRow: -1` | `endRow: Number.MAX_SAFE_INTEGER`, plus `aggregationModel` |
671
+ | Server tree with `defaultGroupingExpansionDepth` | lazy nodes stayed collapsed | nodes to that depth auto-expand and load, one request per node (`-1` expands everything, which can mean many requests) |
672
+ | Tree children request fails | error overlay covered the grid | node collapses, error logged |
673
+ | Retry button | `window.location.reload()` | re-runs `getRows`; hidden without a `dataSource` |
674
+ | When a refetch happens | any new object identity (`filterModel`, `sortModel`, `aggregationModel`, `dataSource`) | a new `getRows` function or changed content |
675
+ | `loading` / `aria-busy` | after the 300 ms debounce | from the moment a request is scheduled |
676
+ | Live-region error fallback | `Error: Unknown error` | `Error: An unexpected error occurred while loading the data.` |
677
+ | Inline `rows={[]}` next to a `dataSource` | wiped fetched rows | ignored |
678
+
679
+ **Who is affected:** every `getRows` implementation that validates `startRow` / `endRow`, treats `endRow: -1` as "all", or assumes page-aligned requests.
680
+
681
+ **How to find it:** `grep -rnE "getRows|endRow|startRow|paginationMode[=:{ ]*['\"]infinite" src/`
682
+
683
+ **Fix it:** slice by the requested range, whatever it is:
684
+
685
+ ```ts
686
+ import type { GridGetRowsParams, GridGetRowsResponse, GridRowModel } from '@opencorestack/opengridx';
687
+
688
+ async function getRows(params: GridGetRowsParams): Promise<GridGetRowsResponse> {
689
+ const all: GridRowModel[] = await fetchSortedFiltered(params); // your server call
690
+ const end = Math.min(params.endRow, all.length);
691
+ return { rows: all.slice(params.startRow, end), rowCount: all.length };
692
+ }
693
+
694
+ declare function fetchSortedFiltered(params: GridGetRowsParams): Promise<GridRowModel[]>;
695
+ ```
696
+
697
+ Keep `getRows` stable (`useCallback` or module scope) if you do not want a refetch.
698
+
699
+ ---
700
+
701
+ ## 16. Layout and pinning
702
+
703
+ | Behaviour | v2 | v3 |
704
+ | :--- | :--- | :--- |
705
+ | Pinned column order | cells in column order, offsets in pinned order (overlaps) | `pinnedColumns.left` / `.right` order; the column menu appends, so the most recently pinned column sits next to the scrolling area |
706
+ | Pinned `'30%'` / `'auto'` / `flex` / no width | 100px | sized like unpinned columns |
707
+ | Fixed or resized widths outside `minWidth` / `maxWidth` | layout used the raw width, CSS clamped only the cell | layout clamps |
708
+ | Several `%` columns | cascaded | share the width left after fixed-width columns |
709
+ | `getDetailPanelHeight` returns `0` | 200px | 0px |
710
+ | `getDetailPanelHeight` returns `'auto'` | 200px | measured height |
711
+ | Detail height without `getDetailPanelHeight` | 200px (docs said `'auto'`) | 200px (docs corrected) |
712
+ | Detail callbacks | called for every rendered row, including group rows | only for expanded data rows; group-row ids in `detailPanelExpandedRowIds` are ignored |
713
+ | `onRowsScrollEnd` | fired on every scroll event near the bottom (including horizontal), never for a short list | once per arrival; re-armed when you leave the zone or `rowCount` changes; also fires after mount or a rows change when the end is already in view |
714
+ | Sticky header and pinned rows | `.ogx__pinned-rows--top` / `--bottom` were sticky | wrapped in `div.ogx__sticky-top` / `div.ogx__sticky-bottom` (see [§24](#24-dom-and-css-class-changes)) |
715
+ | z-index of sticky body system cells | 4 / 5 / 11 | 12; header drag / expand cells have no inline z-index |
716
+ | Returning from list view | scroll reset | scroll position restored |
717
+
718
+ **Who is affected:** apps with `onRowsScrollEnd` loaders that relied on repeated firing, apps with custom z-index or sticky CSS, and apps whose pinned columns were pinned out of column order.
719
+
720
+ **How to find it:** `grep -rnE "pinnedColumns|onRowsScrollEnd|getDetailPanelHeight|ogx__pinned-rows" src/`
721
+
722
+ **Fix it:** list pinned fields in the order you want them on screen; guard `onRowsScrollEnd` loaders against concurrent calls rather than relying on repeat events.
723
+
724
+ ```tsx
725
+ import { DataGrid } from '@opencorestack/opengridx';
726
+ import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
727
+
728
+ export function Pinned({ rows, columns }: { rows: GridRowModel[]; columns: GridColDef[] }) {
729
+ // Left to right on screen: id, then name.
730
+ return <DataGrid rows={rows} columns={columns} pinnedColumns={{ left: ['id', 'name'], right: ['actions'] }} />;
731
+ }
732
+ ```
733
+
734
+ ---
735
+
736
+ ## 17. Row and column reordering and resizing
737
+
738
+ | Behaviour | v2 | v3 |
739
+ | :--- | :--- | :--- |
740
+ | `onRowOrderChange` `oldIndex` / `targetIndex` | page-local, in sorted / filtered order, pinned rows excluded | positions in your `rows` prop |
741
+ | Group rows, auto-created tree parents | draggable | not draggable or droppable |
742
+ | Pinned rows with `rowReordering` | no handle cell (misaligned) | empty `.ogx__cell--drag-handle` (`draggable=false`) |
743
+ | `rowReordering` in pivot mode | handles shown | no effect |
744
+ | `onColumnOrderChange` indices | varied by path | positions in the full current column order (hidden columns and `__group__` included) |
745
+ | Keeping a controlled `columnOrder` in sync | `onColumnOrderChange` only | use the new `onColumnOrderModelChange(columnOrder)` |
746
+ | Columns panel **Reset** | fired `onColumnOrderChange` (or nothing) | fires `onColumnOrderModelChange` |
747
+ | Pinned headers | draggable, drop targets | neither |
748
+ | `pinnable: false` | also blocked drag-reorder | only blocks pinning |
749
+ | `disableColumnReorder` + `columnOrder` | order ignored | order applied |
750
+ | Resize limits | 50–1000px | `minWidth ?? 50` (never above the current width), no maximum unless `maxWidth` |
751
+ | Click on the resize handle without moving | resized / sorted | nothing |
752
+ | Resize events | mouse events on `document` | pointer events with capture on the handle (touch and pen work) |
753
+ | Right-pinned column resize | right edge | left edge (`ogx-column-resize-handle--start`); drag left to widen |
754
+ | `dragstart` data types | `text/plain` | `application/x-ogx-row` / `application/x-ogx-column` plus `text/plain` |
755
+ | Toolbar, pivot and standalone panels | portal mounted in render | mounted one layout effect later (invisible to users; matters for tests) |
756
+
757
+ **Who is affected:** apps that added page offsets to `onRowOrderChange` indices, apps with a controlled `columnOrder`, and tests that simulate resizing with mouse events.
758
+
759
+ **How to find it:** `grep -rnE "onRowOrderChange|onColumnOrderChange|columnOrder=|mousedown|fireEvent\.mouse(Down|Move|Up)" src/`
760
+
761
+ **Fix it:** remove page-offset workarounds, and switch controlled column order to the model callback:
762
+
763
+ ```tsx
764
+ import { useState } from 'react';
765
+ import { DataGrid } from '@opencorestack/opengridx';
766
+ import type { GridColDef, GridColumnOrder, GridRowModel, GridRowOrderChangeParams } from '@opencorestack/opengridx';
767
+
768
+ export function Reorderable({ initialRows, columns }: { initialRows: GridRowModel[]; columns: GridColDef[] }) {
769
+ const [rows, setRows] = useState(initialRows);
770
+ const [columnOrder, setColumnOrder] = useState<GridColumnOrder>(columns.map((c) => c.field));
771
+
772
+ const onRowOrderChange = ({ oldIndex, targetIndex }: GridRowOrderChangeParams) => {
773
+ setRows((prev) => {
774
+ const next = [...prev];
775
+ const [moved] = next.splice(oldIndex, 1);
776
+ next.splice(targetIndex, 0, moved);
777
+ return next;
778
+ });
779
+ };
780
+
781
+ return (
782
+ <DataGrid
783
+ rows={rows}
784
+ columns={columns}
785
+ rowReordering
786
+ onRowOrderChange={onRowOrderChange}
787
+ columnOrder={columnOrder}
788
+ onColumnOrderModelChange={setColumnOrder}
789
+ />
790
+ );
791
+ }
792
+ ```
793
+
794
+ In tests, resize with `fireEvent.pointerDown` / `pointerMove` / `pointerUp` on the handle instead of mouse events.
795
+
796
+ ---
797
+
798
+ ## 18. Toolbar, buttons and UI components
799
+
800
+ | Behaviour | v2 | v3 |
801
+ | :--- | :--- | :--- |
802
+ | Grid buttons and the exported `Button` | default `type="submit"`: clicking a toolbar or pager button inside a `<form>` submitted it | `type="button"`; pass `type="submit"` explicitly |
803
+ | Exported `Checkbox` | added `aria-label` "Select" / "Deselect" / "Select some" | no default label; pass `label` or `aria-label` |
804
+ | `GridTooltip` | rendered into `document.body`, `position: absolute` | renders into `.ogx-theme-provider`, `position: fixed`, opens on focus, child gets `aria-describedby` |
805
+ | Column menu | offered Hide on `hideable: false`, pinning on `pinnable: false` | omits them |
806
+ | Manage columns with a custom `slots.toolbar` that does not render `GridToolbar` | did nothing | opens a standalone panel |
807
+ | `onColumnsPanelClose` | fired only on click outside | fires on every close (Escape, button) |
808
+ | Standalone `GridToolbar` without `onAggregationModelChange` | showed Summaries | hides it; pills include `unique` and respect `availableAggregationFunctions` |
809
+ | Toolbar triggers | no popup ARIA | `aria-haspopup="dialog"`, `aria-expanded` |
810
+ | `loading` with rows present | nothing shown | progress bar (or `slots.loadingOverlay`) over the rows |
811
+
812
+ **Who is affected:** apps with a form around the grid that relied on a grid button submitting it (unlikely; the usual symptom was accidental submits), apps using the exported `Checkbox` without a label, and CSS or tests that look for tooltips under `body`.
813
+
814
+ **How to find it:** `grep -rnE "<(Button|Checkbox|GridTooltip)\b" src/`
815
+
816
+ **Fix it:**
817
+
818
+ ```tsx
819
+ import { Button, Checkbox } from '@opencorestack/opengridx';
820
+
821
+ export function Actions({ checked, onToggle }: { checked: boolean; onToggle: () => void }) {
822
+ return (
823
+ <>
824
+ <Checkbox checked={checked} onChange={onToggle} aria-label="Select order" />
825
+ <Button type="submit">Save</Button>
826
+ </>
827
+ );
828
+ }
829
+ ```
830
+
831
+ ---
832
+
833
+ ## 19. Clipboard
834
+
835
+ | Behaviour | v2 | v3 |
836
+ | :--- | :--- | :--- |
837
+ | `apiRef.current.copySelectedRows()` on failure | resolved, logged | **rejects**; Ctrl+C still only logs |
838
+ | Copied columns | every column definition, in definition order, hidden included | visible columns in screen order (left-pinned first); `exportable: false` dropped |
839
+ | Copied rows | selected rows on the current page, in expanded groups | every selected row that passes the filter (other pages, pinned rows, collapsed groups) |
840
+ | Values with tab, newline, CR or `"` | raw | wrapped in quotes, inner quotes doubled |
841
+ | Fields starting with `__` | dropped | copied, unless system or `exportable: false` |
842
+ | `valueGetter` columns | copied `row[field]` | copied the getter value; `valueFormatter` is called for null / undefined (a throw gives an empty cell) |
843
+ | `execCommand` fallback | ran on every copy | only when `navigator.clipboard` is missing or rejects |
844
+ | Ctrl+C already `preventDefault`ed by your handler | copied anyway | left alone |
845
+ | Formulas | not neutralised | still not neutralised (copying is not a file export) |
846
+
847
+ New: `disableClipboardCopy` turns off the Ctrl/Cmd+C handler (the `apiRef` method still works).
848
+
849
+ **Fix it:**
850
+
851
+ ```ts
852
+ import type { GridApi } from '@opencorestack/opengridx';
853
+
854
+ async function copy(api: GridApi): Promise<boolean> {
855
+ try {
856
+ await api.copySelectedRows();
857
+ return true;
858
+ } catch {
859
+ return false; // clipboard permission denied or unavailable
860
+ }
861
+ }
862
+ ```
863
+
864
+ ---
865
+
866
+ ## 20. Theming
867
+
868
+ | Behaviour | v2 | v3 |
869
+ | :--- | :--- | :--- |
870
+ | `DataGridThemeProvider` palette | partly followed the OS colour scheme (`darkTheme` in a light browser was unreadable) | pins a complete palette chosen by `GridTheme.mode` (`'light'` default), whatever the OS |
871
+ | Brand presets / `colors.primary` | did not recolour toolbar, filter panel, column menu, selection, focus | recolour them |
872
+ | `grid.rowHeightStandard` / `rowHeightCompact` / `rowHeightComfortable` / `headerHeight` | no effect | size rows and header (props `rowHeight` / `headerHeight` still win); `compactTheme` gives 36px rows, 40px header, 12px text |
873
+ | `toolbar.*`, `scrollbar.*`, `overlays.itemDanger*`, `grid.cellFocusBorder` | no effect | applied |
874
+ | Provider toolbar colours | fixed | follow `grid.headerBackground` / `headerText` / `borderColor` unless `toolbar.*` is set |
875
+ | `darkTheme` toolbar | transparent | `#1e293b`, hover `#334155` |
876
+ | Expanded, unfocused quick search | no border | input border (primary only while focused) |
877
+ | Accent colours | fixed colours | CSS `color-mix()`: Chrome 111+, Safari 16.2+, Firefox 113+ |
878
+ | `skeleton.darkBaseColor` / `darkHighlightColor` | typed, no effect | removed |
879
+
880
+ **Who is affected:** apps that wrap the grid in `DataGridThemeProvider` and expected it to switch with the OS, apps using `compactTheme` (rows get shorter), and apps that must support browsers older than the versions above.
881
+
882
+ **How to find it:** `grep -rnE "DataGridThemeProvider|compactTheme|darkBaseColor|darkHighlightColor" src/`
883
+
884
+ **Fix it:** pick the theme yourself from the colour scheme, and delete the removed skeleton keys:
885
+
886
+ ```tsx
887
+ import { useSyncExternalStore, type ReactNode } from 'react';
888
+ import { DataGridThemeProvider, darkTheme } from '@opencorestack/opengridx';
889
+ import type { GridTheme } from '@opencorestack/opengridx';
890
+
891
+ const query = '(prefers-color-scheme: dark)';
892
+ const subscribe = (cb: () => void) => {
893
+ const mql = window.matchMedia(query);
894
+ mql.addEventListener('change', cb);
895
+ return () => mql.removeEventListener('change', cb);
896
+ };
897
+ const lightTheme: GridTheme = { mode: 'light' };
898
+
899
+ export function ThemedGrid({ children }: { children: ReactNode }) {
900
+ const dark = useSyncExternalStore(subscribe, () => window.matchMedia(query).matches, () => false);
901
+ return <DataGridThemeProvider theme={dark ? darkTheme : lightTheme}>{children}</DataGridThemeProvider>;
902
+ }
903
+ ```
904
+
905
+ ---
906
+
907
+ ## 21. Pivot mode
908
+
909
+ | Behaviour | v2 | v3 |
910
+ | :--- | :--- | :--- |
911
+ | Pivot row ids | `0`, `1`, `2` … (collided with source row ids) | `__pivot_row__:["North"]` (JSON of the row-field values) |
912
+ | Filters and quick filter | applied to pivot rows | source-column filters and the quick filter apply to source rows (quick filter searches all filterable source columns); filters on generated value columns apply to pivot rows; OR across both kinds acts like AND |
913
+ | Grand Total row | could move when sorting, was selected by select-all | always last, never selectable (click, select-all, `apiRef`); absent when there are no rows |
914
+ | `getRowId` | applied to pivot rows (collapsed them into one) | ignored for pivot rows |
915
+ | `treeData` / `rowGroupingModel` | crashed or regrouped | turned off while pivoting, with a dev warning |
916
+ | `dataSource` | broken output | pivot ignored, with a dev warning |
917
+ | `getAggregationResult()` / `slots.footer` | totals over pivot rows | `null` |
918
+ | Empty result | columns disappeared | value columns stay; no-rows overlay shows |
919
+ | Row-label columns | lost formatter / renderer / type / alignment; hidden with their source | keep them; `hideable: false`; ignore `columnVisibilityModel` |
920
+ | Column keys | text order | natural order (numbers numerically); header labels formatted; a blank group label is `null` |
921
+ | Column order | controlled `columnOrder` could put value columns first | pivot columns have their own order; a controlled `columnOrder` is ignored for them; leaving pivot restores your order |
922
+ | `groupable: false` / `aggregable: false` fields | used | skipped as row / column fields and as values; PivotPanel does not offer them |
923
+ | Default `avg` format | raw | locale-grouped (`56,666.67`) |
924
+
925
+ `usePivot`: `UsePivotReturn` is now a type alias with the same shape; `pivotRows` has no Grand Total when there are no rows.
926
+
927
+ **Who is affected:** apps that store pivot row ids or rely on selection in pivot mode. **Fix it:** do not persist pivot row ids across pivot model changes; read the row-field values from the row instead.
928
+
929
+ ---
930
+
931
+ ## 22. Cell spanning and column groups
932
+
933
+ | Behaviour | v2 | v3 |
934
+ | :--- | :--- | :--- |
935
+ | Hidden columns inside a `colSpan` | counted (width, `aria-colspan`, next visible column shifted) | skipped: the span covers the next visible columns |
936
+ | `colSpan` next to a pinned column | could cover a column in another section | clamped to its own pinned section |
937
+ | `rowSpan` across pinned and scrolling rows | crossed | clamped to its row section and to the first row with an expanded detail panel |
938
+ | `rowSpan` at group boundaries | could start on or cross group, subtotal, auto-parent and pivot Grand Total rows | stops at them |
939
+ | `colSpan` `params.value` / `params.colIndex` | raw `row[field]`; index counted system columns in unpinned order | `valueGetter` result; rendered data-column index, left-pinned first (same as `renderCell`) |
940
+ | Span values `1.5` / `Infinity` / `NaN` | fractional ARIA / hang | `1` / to the end / no span |
941
+ | Throwing span callback | unmounted the grid | span 1 for that cell, dev warning |
942
+ | `colSpan` + `rowSpan` | only the origin column hidden in following rows | the whole rectangle is covered |
943
+ | Merged cell width | capped by the origin column's `min/maxWidth` | not capped |
944
+ | Render window | spans crossing the window edge were cut | extra rows / columns rendered so spans stay intact |
945
+ | Column group header rows | did not follow widths, hiding, reordering, pinning | follow the rendered columns; a split group shows one labelled cell per run |
946
+ | Header drag-reorder with `columnGroupingModel` | disabled for every column | allowed within the same innermost group; toolbar and Columns panel get the same restriction |
947
+ | Keyboard | focus lost on covered cells | ArrowRight / ArrowDown from a span origin skip covered cells; moving into a covered cell focuses the origin |
948
+ | `GridColumnGroup.headerClassName` | ignored (documented as a background colour) | CSS class added to the group's header cells |
949
+
950
+ CSS: `.ogx-col-group-row` no longer has `overflow: hidden`; new `ogx-col-group-cell--pinned`, `--pinned-left`, `--pinned-right`; filler cells lost `role="columnheader"`; `aria-rowcount` includes the group depth. The unexported `ColumnGroupHeader` component was deleted.
951
+
952
+ **Who is affected:** apps whose span callbacks count on hidden columns or read `params.colIndex`. **Fix it:** compute spans from `params.field` and `params.value`.
953
+
954
+ ---
955
+
956
+ ## 23. List view
957
+
958
+ | Behaviour | v2 | v3 |
959
+ | :--- | :--- | :--- |
960
+ | `renderCell` params | `value` undefined; no `formattedValue` / `rowMeta`; `colDef` was `{ field }` | real values, `formattedValue`, `rowMeta` and the column definition |
961
+ | `slots.footer` | not rendered | rendered, replaces the list pagination |
962
+ | `loading`, `slots.loadingOverlay`, `slots.noRowsOverlay` | ignored; "No Data" while loading | honoured |
963
+ | `listView` without `listViewColumn` | empty | falls back to the grid, with a dev warning |
964
+ | Row checkboxes | Tab stops | not Tab stops; rows are focusable, arrow keys move between rows, Shift+Space selects |
965
+ | Empty state | `role="status"` | no `role="status"` |
966
+ | `aria-rowindex` / `aria-rowcount` | restarted at 2 every page / page size | absolute / total |
967
+ | Tree data and grouping | no expand control | expand chevron and depth indent (`.ogx-list-view__expand`) |
968
+ | Summary and paging with a `dataSource` | loaded rows | server total |
969
+ | `onRowsScrollEnd` | never fired | fires once per arrival at the bottom, like the grid view |
970
+
971
+ ---
972
+
973
+ ## 24. DOM and CSS class changes
974
+
975
+ `ogx__*` / `ogx-*` class names and the grid's DOM structure are public API: consumers style and test against them. These changed in 3.0.0.
976
+
977
+ ### Structure
978
+
979
+ | Change | v2 | v3 | Action |
980
+ | :--- | :--- | :--- | :--- |
981
+ | Sticky header and top-pinned rows | `.ogx__pinned-rows--top` was sticky | header + top-pinned rows inside `div.ogx__sticky-top` | move `position` / `top` / `z-index` rules to `.ogx__sticky-top` |
982
+ | Bottom-pinned rows and aggregation footer | `.ogx__pinned-rows--bottom` was sticky | inside `div.ogx__sticky-bottom` | move rules to `.ogx__sticky-bottom` |
983
+ | Empty-state overlay | before top-pinned rows | after them | adjust sibling selectors |
984
+ | Aggregation footer | cells for hidden columns; spacers not sticky | rendered from visible columns; spacers `.ogx__aggregation-spacer` (sticky: `--pinned`) | update selectors that count footer cells |
985
+ | Tooltip | child of `body` | child of `.ogx-theme-provider`, `position: fixed` | update tooltip selectors |
986
+ | Detail panel | `role` mismatch | `role="row"` > `role="gridcell"` | update a11y test queries |
987
+ | Column group filler cells | `role="columnheader"` | no role, hidden from AT | update a11y test queries |
988
+ | Sticky body system cells z-index | 4 / 5 / 11 | 12 | re-check custom overlays above the grid body |
989
+ | Columns panel item | `<label class="ogx-column-visibility-panel__item-label">` wrapping the checkbox and a `<span class="ogx-column-visibility-panel__label">` | `<div class="ogx-column-visibility-panel__item-label">` with the checkbox and a `<label for> class="ogx-column-visibility-panel__label"` | update selectors and test queries that expect a `label` / `span` |
990
+ | Double-click on an editable cell (exported `Cell`) | propagation stopped when it started an edit | bubbles to the row | remove workarounds for the missing `onRowDoubleClick` / row `dblclick` |
991
+
992
+ ### Added classes
993
+
994
+ | Class | On |
995
+ | :--- | :--- |
996
+ | `ogx__sticky-top`, `ogx__sticky-bottom` | sticky wrappers |
997
+ | `ogx__header-cell--pinned-left-last`, `ogx__header-cell--pinned-right-first` | pinned-section edge header cells |
998
+ | `ogx__cell--pinned-left-last`, `ogx__cell--pinned-right-first` | pinned-section edge body cells |
999
+ | `ogx__aggregation-spacer`, `ogx__aggregation-spacer--pinned` | footer spacers |
1000
+ | `ogx__aggregation-footer--loading` | footer while server totals load |
1001
+ | `ogx__loading-bar`, `ogx__loading-overlay--over-rows`, `ogx__loading-overlay--custom` | loading over existing rows |
1002
+ | `ogx__row--group-footer` | subtotal rows in `'footer'` position |
1003
+ | `ogx__cell-image` | default `<img>` for `type: 'image'` |
1004
+ | `ogx-column-resize-handle--start` | resize handle on the left edge (right-pinned columns) |
1005
+ | `ogx-col-group-cell--pinned`, `ogx-col-group-cell--pinned-left`, `ogx-col-group-cell--pinned-right` | pinned column-group cells |
1006
+ | `ogx-filter__hidden-note`, `ogx-filter__value-multiselect` | filter panel |
1007
+ | `ogx-list-view__expand`, `ogx-list-view__loading` | list view |
1008
+ | `ogx-tooltip--left`, `ogx-tooltip--right` | tooltip placements |
1009
+ | `ogx-column-visibility-panel__item-label` | Columns panel item row; the class existed in v2 but had no stylesheet rule |
1010
+
1011
+ ### Changed or removed rules
1012
+
1013
+ | Class | Change |
1014
+ | :--- | :--- |
1015
+ | `.ogx__pinned-rows--top`, `.ogx__pinned-rows--bottom` | no longer `position: sticky` |
1016
+ | `.ogx-col-group-row` | no longer `overflow: hidden` |
1017
+ | `.ogx__cell--drag-handle` | now also rendered (empty, `draggable=false`) on pinned rows |
1018
+ | `.ogx-expand-icon`, `.ogx__detail-panel` | their `prefers-color-scheme: dark` rules were removed; the theme palette colours them |
1019
+ | `.ogx-global-search--expanded` | shows the input border while expanded and unfocused; the primary border and focus shadow moved to `:focus-within` |
1020
+ | `.ogx__cell--focused`, `.ogx__header-cell--focused`, `.ogx__header-cell--focus-visible` | outline colour reads `--ogx-grid-cell-focus-border` (falls back to `--ogx-color-primary`) |
1021
+ | `.ogx__header-cell--drag-over` | background `--ogx-color-primary-light` (was the undefined `--ogx-color-blue-50`) |
1022
+ | `.ogx-column-resize-handle` | `touch-action: none`; a `:focus-visible` line colour |
1023
+ | `.ogx-list-view__row` | `:focus-visible` outline (rows are focusable) |
1024
+ | `.ogx-toolbar` and its buttons / chips, `.ogx-global-search`, scrollbars, filter-panel delete button | read the `--ogx-toolbar-*`, `--ogx-scrollbar-*` and `--ogx-overlay-item-danger-*` variables the theme provider sets (the filter-panel delete button used `--ogx-toolbar-btn-danger-*`); fallbacks match the v2 colours except the danger-button hover |
1025
+
1026
+ **How to find it:** `grep -rnE "ogx(__|-)[a-z-]+" src/ --include='*.css' --include='*.scss' --include='*.ts' --include='*.tsx'`
1027
+
1028
+ ---
1029
+
1030
+ ## 25. Typing changes
1031
+
1032
+ Most of these make code compile that did not compile before. A few make code that used to compile (unchecked) into a type error, because the types now describe what the grid actually passes.
1033
+
1034
+ ### Your own row types now work
1035
+
1036
+ **What changed:** every public generic is constrained by `GridValidRowModel` (`object`) instead of `GridRowModel`. Interfaces and type aliases can be used as the row type without `extends GridRowModel` or an index signature, and an untyped `GridColDef[]` can be passed next to typed rows. The default row type is still `GridRowModel`, so untyped code is unchanged.
1037
+
1038
+ | | v2 | v3 |
1039
+ | :--- | :--- | :--- |
1040
+ | `interface Employee { id: number; name: string }` as the row type | error: does not satisfy `GridRowModel` | compiles |
1041
+ | `const columns: GridColDef[] = …` with `rows: Employee[]` | TS2322 (the README example failed under `strict`) | compiles |
1042
+ | `renderCell` / `valueGetter` / `valueSetter` params with `GridColDef<Employee>` | only via index-signature workarounds | typed as `Employee` |
1043
+ | `getRowId` option of the export functions | `(row: GridRowModel)` | `(row: R)` |
1044
+
1045
+ **Fix it:** remove workarounds such as `extends GridRowModel`, `[key: string]: unknown` added only for the grid, or `as unknown as GridColDef[]` casts.
1046
+
1047
+ ```tsx
1048
+ import { DataGrid } from '@opencorestack/opengridx';
1049
+ import type { GridColDef } from '@opencorestack/opengridx';
1050
+
1051
+ interface Employee { id: number; name: string; salary: number }
1052
+
1053
+ const columns: GridColDef<Employee>[] = [
1054
+ { field: 'name', headerName: 'Name' },
1055
+ { field: 'salary', valueFormatter: ({ row }) => `$${row.salary.toLocaleString()}` },
1056
+ ];
1057
+
1058
+ export function Staff({ rows }: { rows: Employee[] }) {
1059
+ return <DataGrid rows={rows} columns={columns} />;
1060
+ }
1061
+ ```
1062
+
1063
+ ### `DataGrid` has two overloads
1064
+
1065
+ `DataGrid` is declared with a typed overload (`DataGridProps<R>`) and an untyped-columns overload (`DataGridUntypedColumnsProps<R>`). As a result, `React.ComponentProps<typeof DataGrid>` now resolves to the untyped overload. **Use `DataGridProps<R>`** when you need the props type (for a wrapper component, for example).
1066
+
1067
+ ```tsx
1068
+ import { DataGrid } from '@opencorestack/opengridx';
1069
+ import type { DataGridProps, GridValidRowModel } from '@opencorestack/opengridx';
1070
+
1071
+ export function MyGrid<R extends GridValidRowModel>(props: DataGridProps<R>) {
1072
+ return <DataGrid {...props} density="compact" />;
1073
+ }
1074
+ ```
1075
+
1076
+ ### Slots are typed with the props the grid passes
1077
+
1078
+ Each slot has an exported props type: `GridToolbarSlotProps`, `GridPaginationSlotProps`, `GridOverlaySlotProps` and `GridFooterSlotProps`. Slot components are checked against them.
1079
+
1080
+ | | v2 | v3 |
1081
+ | :--- | :--- | :--- |
1082
+ | `slots.footer: ({ rowCount }: { rowCount: number }) => …` | error (slots were `ComponentType<Record<string, unknown>>`) | compiles, `rowCount` is checked |
1083
+ | Inline `slots.toolbar: (props) => …` | `props` untyped | `props` is `GridToolbarSlotProps` |
1084
+ | A class / `memo` / `forwardRef` slot typed `Record<string, unknown>` | compiled | type it with the slot's props type |
1085
+ | Known keys in `slotProps.toolbar` / `pagination` / `footer` | unchecked | checked (extra keys still allowed) |
1086
+
1087
+ ### Other type-level changes
1088
+
1089
+ - `groupingColDef` is `Partial<GridColDef<R>>`: `field` is no longer required (a dummy `field` still type-checks and is ignored).
1090
+ - `GridAggregationPosition` is `'inline' | 'footer' | null` (it was an unexported, unused `'footer' | 'inline' | 'both'`).
1091
+ - `useGridApiRef()` returns `MutableRefObject<GridApi>`, which type-checks against the `apiRef` prop under `@types/react` 18.
1092
+ - `GridInitialState` is an interface that accepts partial `columns`.
1093
+ - `GridFilterOperator` has five new members (see [§5](#5-filtering)); exhaustive `switch` statements need the new cases.
1094
+ - `renderEditCell` takes `GridRenderEditCellParams` (a superset of the old params). Existing editors still type-check, but **calling** `col.renderEditCell(params)` yourself with a `GridRenderCellParams` is now a type error: pass a `GridRenderEditCellParams` (add `onValueChange`, `onCommit`, `onCancel`).
1095
+ - `GridColDef<Row>` is assignable to `GridColDef` when `Row` is a type alias or extends `GridRowModel`. It is **not** when `Row` is an interface without an index signature: type the array as `GridColDef<Row>[]` instead of `GridColDef[]`, or keep the columns untyped and use the untyped-columns overload.
1096
+ - `usePivot` is declared to return `PivotResult`; `UsePivotReturn` is now a type alias of it (same fields). An alias cannot be augmented with declaration merging.
1097
+ - Exported `Header`: `focusedCell.id` is `GridRowId | null` (a header cell has `id: null`, was the string `'HEADER'`). Code that assigns it to `string | number` needs a `null` check.
1098
+ - Exported `Cell`: `onEditStop` is `(cancel?: boolean, field?: string) => void`. Exported `Row`: `onEditStop` params are `{ cancel?, id?, field? }`, and `rowSpanningCaches.hiddenCellOriginMap` is `Record<GridRowId, Record<string, GridRowId>>` (was `Record<number, Record<string, number>>`).
1099
+ - New optional props on the exported components: `Cell.isPinnedEdge`, `Cell.valueError`, `Row.onDetailPanelHeightChange`, `Row.isCellEditable`, `Row.rowId`, and `ariaRowIndex` / `ariaColIndex` / `columnIndexMap` on `Row`, `Cell` and `Header`.
1100
+ - The export option types are generic (`CsvExportOptions<R>`, `PdfExportOptions<R>`, …) and `UseAggregationParams<R>.columns` is `GridColDef<R>[]` (an overload still accepts `GridColDef[]`).
1101
+ - `GridApi` is not generic: its methods still return `GridRowModel`. `params.value` is still `unknown`; read typed values from `params.row`.
1102
+ - Removed unused, never-exported types from `lib/types`: `GridEditCellProps`, `GridRowModes`, `GridRowModesModel`, `GridDetailPanelContent`, `GridDetailPanelState`, `GridVirtualizationState`, `GridRenderContext`, `GridAggregationFunction`.
1103
+
1104
+ ---
1105
+
1106
+ ## 26. State persistence
1107
+
1108
+ **What changed:**
1109
+
1110
+ | Behaviour | v2 | v3 |
1111
+ | :--- | :--- | :--- |
1112
+ | When `onStateChange` fires | on every new prop identity, so inline models (`sortModel={[…]}`) or storing the state in parent state could loop | once on mount, then only when the state's value changes |
1113
+ | `onStateChange` payload | no `density` | includes `density: { density }` |
1114
+ | `columns.pinnedColumns` / `columns.columnOrder` in the payload | could contain the synthetic `__group__` column | only your own columns |
1115
+ | `initialState.density` | ignored | applied (the `density` prop still wins) |
1116
+ | `useGridStateStorage` when storage is blocked (cookie blocking, sandboxed iframes) | threw | no persistence, no error |
1117
+ | `useGridStateStorage` `clearState()` | undone by the next debounced write or the flush on unmount | final: removes the saved state and cancels any pending write |
1118
+ | `useGridStateStorage` writes | on every re-render | only after a state change (debounced), plus a pending write on unmount |
1119
+
1120
+ **Who is affected:** apps that save grid state, and in particular:
1121
+
1122
+ - **The storage key can change** (per user, per view) while the grid stays mounted. The grid reads `initialState` only when it mounts, so remount it with the key: `<DataGrid key={storageKey} … />`. Without the remount the grid keeps its current state and its next change is saved under the new key.
1123
+ - **You call `clearState()`** and expect the grid to reset. It does not reset the mounted grid, and the grid's next state change is saved again. Remount the grid to start from defaults.
1124
+ - **Server-side rendering.** `useGridStateStorage` reads storage during the first render. On the server there is no storage, so the server renders the default state while the client's first render uses the saved state: a hydration mismatch. When users can have saved state, render the grid on the client only.
1125
+
1126
+ **How to find it:** `grep -rnE "onStateChange|useGridStateStorage|initialState" src/`
1127
+
1128
+ **Fix it:** remount with the key, and render a persisted grid on the client only in an SSR app:
1129
+
1130
+ ```tsx
1131
+ import { useSyncExternalStore } from 'react';
1132
+ import { DataGrid, useGridStateStorage } from '@opencorestack/opengridx';
1133
+ import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
1134
+
1135
+ interface OrdersGridProps { userId: string; rows: GridRowModel[]; columns: GridColDef[] }
1136
+
1137
+ const subscribe = () => () => {};
1138
+ // false on the server and during hydration, true after it.
1139
+ const useIsClient = () => useSyncExternalStore(subscribe, () => true, () => false);
1140
+
1141
+ function SavedOrdersGrid({ userId, rows, columns }: OrdersGridProps) {
1142
+ const storageKey = `orders-grid-${userId}`;
1143
+ const { initialState, onStateChange } = useGridStateStorage(storageKey);
1144
+ return (
1145
+ <DataGrid
1146
+ key={storageKey}
1147
+ rows={rows}
1148
+ columns={columns}
1149
+ initialState={initialState}
1150
+ onStateChange={onStateChange}
1151
+ />
1152
+ );
1153
+ }
1154
+
1155
+ export function OrdersGrid(props: OrdersGridProps) {
1156
+ return useIsClient() ? <SavedOrdersGrid {...props} /> : null;
1157
+ }
1158
+ ```
1159
+
1160
+ In Next.js you can instead load the component with `dynamic(() => import('./OrdersGrid'), { ssr: false })`.
1161
+
1162
+ ---
1163
+
1164
+ ## Removed or renamed API
1165
+
1166
+ | Removed / renamed | Replacement |
1167
+ | :--- | :--- |
1168
+ | Runtime `row._hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow` | `params.rowMeta.*` ([§3](#3-underscore-hierarchy-fields-are-no-longer-added-to-rows)) |
1169
+ | `row.id` written by `getRowId` | your own key; `getRowId` option on exports ([§2](#2-getrowid-no-longer-writes-an-id-onto-rows)) |
1170
+ | `GridThemeSkeleton.darkBaseColor`, `darkHighlightColor` | pick a dark theme via `GridTheme.mode` / `darkTheme` |
1171
+ | Header `focusedCell.id === 'HEADER'` | `focusedCell.id === null` |
1172
+ | Default `aria-label` on `Checkbox` | pass `label` / `aria-label` |
1173
+ | Default `type="submit"` on `Button` | pass `type="submit"` |
1174
+ | OS-following palette inside `DataGridThemeProvider` | choose the theme yourself ([§20](#20-theming)) |
1175
+ | `exportToExcel` writing `.xlsx` names | `.xls`, or `exportToExcelAdvanced` for real `.xlsx` |
1176
+ | `endRow: -1` in tree-children requests | `endRow: Number.MAX_SAFE_INTEGER` |
1177
+
1178
+ ## New API you may want
1179
+
1180
+ | API | What it does |
1181
+ | :--- | :--- |
1182
+ | `GridColDef.valueSetter` | write edits of computed columns back onto the row |
1183
+ | `renderEditCell` `onValueChange` / `onCommit` / `onCancel` | build custom editors without internals |
1184
+ | `onColumnOrderModelChange` | receive the whole column order after every change |
1185
+ | `disableClipboardCopy` | let your own Ctrl/Cmd+C handler own the shortcut |
1186
+ | `CsvExportOptions.bom`, `.escapeFormulas`; `ExcelExportOptions.escapeFormulas` | control BOM and formula escaping |
1187
+ | `getRowId` on every export's options | match `selectedRows` when rows are keyed by `getRowId` |
1188
+ | `PdfExportOptions.font` | print non-Latin-1 text in PDFs |
1189
+ | `GridFilterOperator` `'='`, `'after'`, `'onOrAfter'`, `'before'`, `'onOrBefore'` | numeric equality and date ranges |
1190
+ | `GridTheme.mode`, `colors.white` / `black` / `gray`, `grid.cellFontSize` / `headerFontSize` | complete palettes and font sizes |
1191
+ | `GridRowMeta.isGroupFooter` | recognise subtotal rows in `'footer'` position |
1192
+ | `GridGroupedExportRow.groupLabel` | the grid's group label on export rows |
1193
+ | Keyboard: Shift+Space, Ctrl/Cmd+A, Alt+Arrow expand / collapse, Alt+ArrowDown column menu, Alt+Arrow header resize | keyboard parity with the mouse |
1194
+ | `Pagination` component and props types (`CellProps`, `RowProps`, `HeaderProps`, `SkeletonProps`, `FilterPanelProps`, `PaginationProps`, `GridTooltipProps`, `ButtonProps`, `InputProps`, `CheckboxProps`) | typed custom slots and wrappers |
1195
+ | Type exports `GridRenderEditCellParams`, `GridValueSetterParams`, `GridGroupedExportRow`, `GridRowScrollEndParams`, `GridColumnOrder`, `GridDataSourceState`, `GridDetailPanelHeight`, `GridAggregationPosition`, `GridSlots`, `GridSlotProps`, `GridThemeToolbar`, `GridThemeOverlays`, `GridThemeScrollbar`, `GridThemeSkeleton`, `GridThemeGrayScale` | import instead of redeclaring |
1196
+
1197
+ ---
1198
+
1199
+ ## Checklist
1200
+
1201
+ - [ ] Restart the dev server and clear the bundler cache (`rm -rf node_modules/.vite`).
1202
+ - [ ] Confirm `import '@opencorestack/opengridx/styles'` is in your app root.
1203
+ - [ ] Check columns with `type: 'date' | 'boolean' | 'singleSelect' | 'image'`; add a `valueFormatter` where you want the old text (§1).
1204
+ - [ ] With `getRowId`: stop reading `row.id`; pass `getRowId` to exports of `selectedRows` (§2).
1205
+ - [ ] Replace underscore hierarchy fields with `params.rowMeta` (§3) and remove writes to `params.row` in render callbacks; replace column objects instead of mutating them and keep `valueGetter` pure (§4).
1206
+ - [ ] Review programmatic filter items with empty values, exhaustive `GridFilterOperator` switches, and search expectations (§5).
1207
+ - [ ] Update sorted-output snapshots; where you need a custom order, use a `valueGetter` that returns the sort key (§6).
1208
+ - [ ] Update keyboard docs/tests for Tab and Enter; stop storing `colIndex` / `rowIndex` (§7).
1209
+ - [ ] In controlled pagination and selection, adopt the models the callbacks pass you (§8, §9).
1210
+ - [ ] Remove duplicate state updates around `apiRef` setters (§10).
1211
+ - [ ] Make `processRowUpdate` safe to run for edits committed on scroll-away; drop value-type workarounds; add `valueSetter` for computed editable columns (§11).
1212
+ - [ ] Re-check exported files: BOM, `'` prefixes, selection vs grouped, `.xls` name, xlsx cell types, PDF fonts (§12).
1213
+ - [ ] Handle `Date` results from `min` / `max`; re-check totals with blank cells (§13).
1214
+ - [ ] Guard `renderCell` for synthetic rows; stop parsing group ids; handle `null` in `getAggregationPosition` (§14).
1215
+ - [ ] Make `getRows` honour any `startRow` / `endRow`, including `Number.MAX_SAFE_INTEGER` (§15).
1216
+ - [ ] Order `pinnedColumns` as you want them on screen; move sticky CSS to `.ogx__sticky-top` / `.ogx__sticky-bottom` (§16, §24).
1217
+ - [ ] Remove page offsets from `onRowOrderChange`; use `onColumnOrderModelChange`; switch resize tests to pointer events (§17).
1218
+ - [ ] Label exported `Checkbox`es; pass `type="submit"` where a `Button` should submit (§18).
1219
+ - [ ] Wrap `copySelectedRows()` in `try` / `catch` (§19).
1220
+ - [ ] Choose light or dark theme yourself; remove `darkBaseColor` / `darkHighlightColor`; check `compactTheme` heights (§20).
1221
+ - [ ] Run `tsc --noEmit` and your test suite (§25).
1222
+ - [ ] With persisted state: remount the grid when the storage key changes, and render it client-only under SSR (§26).