@opencorestack/opengridx 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,6 +1,6 @@
1
1
  # 🔢 Sorting & Pagination
2
2
 
3
- Efficiently naviagte and organize through large datasets using OpenGridX's sorting and pagination engines.
3
+ Efficiently navigate and organize through large datasets using OpenGridX's sorting and pagination engines.
4
4
 
5
5
  ---
6
6
 
@@ -34,18 +34,32 @@ OpenGridX supports two modes of multi-column sorting:
34
34
 
35
35
  Each active sort column shows a numbered priority badge (`1`, `2`, `3`…) next to its arrow.
36
36
 
37
- - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc → desc → removed`.
37
+ - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc → desc → removed`. The column keeps its priority while its direction changes; it is not moved to the end of the sort model.
38
38
  - A plain click *without* `multiSort` and without Shift always resets to a single-key sort on that column.
39
+ - The column menu acts on that column only: **Unsort** removes just that key, and **Sort Ascending / Descending** on a column that is already sorted changes its direction in place. On a column that is not sorted yet it appends with `multiSort` and replaces the model without it, like a plain click.
39
40
  - Set `sortable: false` on a column to exclude it entirely.
40
41
 
42
+ ### How values are compared (client-side)
43
+
44
+ - Cells are read through the column's `valueGetter`, so computed columns sort by the value they display.
45
+ - `type: 'number'` columns compare numerically even when the data holds numeric strings (`'9'` sorts before `'10'`). `type: 'date'` columns parse date strings (`'YYYY-MM-DD'` is read as a local date) and compare chronologically.
46
+ - Strings use language-aware ordering (`Intl.Collator`): case is ignored, accented letters sort next to their base letter (`'Émile'` between `'Adam'` and `'Zoe'`), and digit runs compare by value (`'item9'` before `'item10'`).
47
+ - `null`, `undefined`, `NaN`, an Invalid Date and, in number / date columns, values that cannot be parsed are "empty": they sort last in ascending order and first in descending order.
48
+ - A column that mixes kinds of value (numbers and strings, say) is ordered by kind first — numbers, dates, booleans, strings — so the order does not depend on the input order.
49
+ - Ties keep the rows' original order.
50
+
41
51
  ```tsx
42
52
  // Controlled multi-sort — set programmatically or drive from UI with multiSort:
53
+ const [sortModel, setSortModel] = useState<GridSortItem[]>([
54
+ { field: 'department', sort: 'asc' },
55
+ { field: 'salary', sort: 'desc' },
56
+ ]);
57
+
43
58
  <DataGrid
59
+ rows={rows}
60
+ columns={columns}
44
61
  multiSort
45
- sortModel={[
46
- { field: 'department', sort: 'asc' },
47
- { field: 'salary', sort: 'desc' },
48
- ]}
62
+ sortModel={sortModel}
49
63
  onSortModelChange={setSortModel}
50
64
  />
51
65
  ```
@@ -62,27 +76,64 @@ Disable sorting for specific columns in `GridColDef`:
62
76
 
63
77
  OpenGridX supports standard page-based pagination and infinite scrolling.
64
78
 
79
+ > **Pagination is ignored while row grouping is active.** When `rowGroupingModel` is non-empty the grid renders all groups in one scrollable, virtualized view and does not show the pager, even if `pagination` is set. A development-mode `console.warn` flags this combination. Give a grouped grid a bounded height so virtualization limits what renders; see [Virtualization](./virtualization.md#the-grid-needs-a-bounded-height).
80
+
65
81
  ### Usage
66
82
  ```tsx
83
+ // Uncontrolled: the grid keeps the page; initialState sets where it starts
67
84
  <DataGrid
85
+ rows={rows}
86
+ columns={columns}
68
87
  pagination
69
- paginationModel={{ page: 0, pageSize: 10 }}
70
88
  pageSizeOptions={[5, 10, 20, 50]}
89
+ initialState={{ pagination: { paginationModel: { page: 0, pageSize: 10 } } }}
90
+ />
91
+
92
+ // Controlled: pass onPaginationModelChange too, or the pager cannot change page
93
+ const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 });
94
+ <DataGrid
95
+ rows={rows}
96
+ columns={columns}
97
+ pagination
98
+ paginationModel={paginationModel}
99
+ onPaginationModelChange={setPaginationModel}
71
100
  />
72
101
  ```
73
102
 
103
+ The pager counts the rows being paged: the rows that pass the filter, excluding pinned rows (pinned rows show on every page).
104
+
105
+ Without `paginationModel` or `initialState.pagination`, the page size is 100 when `pageSizeOptions` offers it and otherwise the first option, so the rows-per-page select always matches the rows shown.
106
+
107
+ ### When the rows shrink
108
+ If the current page ends up past the last page (new `rows`, a filter, collapsed tree nodes), the grid shows the last page and reports the corrected model through `onPaginationModelChange`, so Previous and Next continue from the page on screen. The correction waits until rows are present and nothing is loading, so a restored page (for example from `initialState.pagination`) survives an initially empty `rows` array. Under server pagination the server's `rowCount` decides; the grid does not clamp.
109
+
110
+ ### Page sizes outside `pageSizeOptions`
111
+ When the page size in use is not one of `pageSizeOptions` (for example a controlled `paginationModel={{ page: 0, pageSize: 20 }}` with `pageSizeOptions={[10, 25, 50]}`), the rows-per-page select adds it as an extra option, so it shows the size actually used. A page size below 1 is treated as 1.
112
+
74
113
  ### Server-Side Pagination
75
- When using `paginationMode="server"`, you must provide the `rowCount` and handle page changes in your `dataSource`.
114
+ With `paginationMode="server"` the grid does not slice rows: `rows` is the current page. There are two ways to supply it.
115
+
116
+ **Fetch the page yourself** — control `paginationModel`, pass the page's rows and the server total as `rowCount`. `rowCount` is read on every render, so update it when the total changes.
76
117
 
77
118
  ```tsx
119
+ const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 });
120
+ const { rows, total } = usePageQuery(paginationModel); // your data fetching
121
+
78
122
  <DataGrid
123
+ rows={rows} // only the current page
124
+ columns={columns}
79
125
  pagination
80
126
  paginationMode="server"
81
- rowCount={1000} // Total rows on server
82
- onPaginationModelChange={(model) => fetchNextPage(model)}
127
+ rowCount={total} // total rows on the server
128
+ paginationModel={paginationModel}
129
+ onPaginationModelChange={setPaginationModel}
83
130
  />
84
131
  ```
85
132
 
133
+ **Or use a `dataSource`** — the grid calls `getRows` with `startRow` / `endRow` and uses the response's `rowCount` as the total (the `rowCount` prop is only a fallback until the first response). See [Server-Side Data](./data-source.md).
134
+
135
+ `sortingMode="server"` and `filterMode="server"` work the same way: the grid fires `onSortModelChange` / `onFilterModelChange` and shows `rows` as given, without re-sorting or re-filtering them, with or without a `dataSource`.
136
+
86
137
  ### Props
87
138
  | Prop | Type | Default | Description |
88
139
  | :--- | :--- | :--- | :--- |
@@ -90,14 +141,15 @@ When using `paginationMode="server"`, you must provide the `rowCount` and handle
90
141
  | `paginationMode` | `'client' \| 'server' \| 'infinite'` | `'client'` | Location of the pagination logic. |
91
142
  | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). |
92
143
  | `onPaginationModelChange` | `(model: GridPaginationModel) => void` | — | Fired when page or page size changes. |
93
- | `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Options for the rows-per-page selector. |
144
+ | `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Options for the rows-per-page selector. The uncontrolled default page size is 100 when offered, else the first option. |
145
+ | `rowCount` | `number` | — | Server total for `paginationMode="server"` without a `dataSource` (with one, the response's `rowCount` is used). |
94
146
 
95
147
  ---
96
148
 
97
149
  ## ⚡ Infinite Scrolling
98
150
  For a more modern experience, use infinite scroll combined with virtualization.
99
151
 
100
- 1. Set `paginationMode="infinite"`.
101
- 2. Implement `onRowsScrollEnd` or use the `dataSource.getRows` method to fetch data as the user scrolls.
152
+ 1. Set `paginationMode="infinite"` and pass a `dataSource`.
153
+ 2. Advance `paginationModel.page` from `onRowsScrollEnd`; the grid requests the next rows from `dataSource.getRows` and appends them. The pager is not shown and rows are not sliced.
102
154
 
103
- 📖 **[Full Infinite Scroll Guide](docs/features/infinite-scroll.md)**
155
+ 📖 **[Full Infinite Scroll Guide](./infinite-scroll.md)**
@@ -6,18 +6,29 @@ Save and restore the grid's configuration (sorting, filtering, column order, etc
6
6
 
7
7
  ## 🏗️ The `initialState` Prop
8
8
 
9
- You can pre-configure the grid status on mount using the `initialState` prop.
9
+ You can pre-configure the grid status on mount using the `initialState` prop. Every part is optional, including each field of `columns`.
10
10
 
11
11
  ```tsx
12
12
  <DataGrid
13
+ rows={rows}
14
+ columns={columns}
13
15
  initialState={{
14
16
  sorting: {
15
17
  sortModel: [{ field: 'name', sort: 'asc' }]
16
- }
18
+ },
19
+ filter: {
20
+ filterModel: { items: [{ field: 'age', operator: '>', value: 30 }] }
21
+ },
22
+ columns: {
23
+ columnVisibilityModel: { age: false }
24
+ },
25
+ density: { density: 'compact' }
17
26
  }}
18
27
  />
19
28
  ```
20
29
 
30
+ `initialState` is read once, when the grid mounts. It seeds the grid's own (uncontrolled) state, so it has no effect on a model you also pass as a prop (`sortModel`, `filterModel`, `paginationModel`, …), and the `density` prop wins over `initialState.density`.
31
+
21
32
  ---
22
33
 
23
34
  ## ⚓ The `useGridStateStorage` Hook
@@ -28,26 +39,81 @@ OpenGridX provides a built-in hook to simplify `localStorage` persistence.
28
39
  import { DataGrid, useGridStateStorage } from '@opencorestack/opengridx';
29
40
 
30
41
  export default function MyGrid() {
31
- const { initialState, onStateChange } = useGridStateStorage('my-app-storage-key');
42
+ const { initialState, onStateChange, clearState } = useGridStateStorage('my-app-storage-key');
32
43
 
33
44
  return (
34
- <DataGrid
35
- initialState={initialState}
36
- onStateChange={onStateChange}
37
- // ...
38
- />
45
+ <>
46
+ <button type="button" onClick={clearState}>Forget saved layout</button>
47
+ <DataGrid
48
+ rows={rows}
49
+ columns={columns}
50
+ initialState={initialState}
51
+ onStateChange={onStateChange}
52
+ />
53
+ </>
39
54
  );
40
55
  }
41
56
  ```
42
57
 
58
+ Writes are debounced (`debounceMs`, default 300 ms), and a pending write is flushed when the component unmounts. When the browser blocks storage (cookie blocking, sandboxed iframes), the hook falls back to no persistence instead of throwing.
59
+
60
+ Options (pass an object instead of the key string):
61
+
62
+ | Option | Type | Default | Description |
63
+ | :--- | :--- | :--- | :--- |
64
+ | `key` | `string` | — | Storage key. |
65
+ | `debounceMs` | `number` | `300` | Delay before a state change is written. |
66
+ | `include` | `(keyof GridState)[]` | all | Only persist these parts, e.g. `['sorting', 'columns']`. |
67
+ | `storage` | `{ getItem, setItem, removeItem }` | `window.localStorage` | Any Storage-like object (e.g. `sessionStorage`). |
68
+
69
+ `clearState()` removes the saved state and cancels any pending write, so the old state is not written back afterwards. It does not reset the mounted grid, and the grid's next state change is saved again; remount the grid (for example with a new `key`) to start from defaults.
70
+
71
+ ### Server-side rendering (Next.js, Remix)
72
+
73
+ `useGridStateStorage` is client-only. It reads storage in its initial render, so on the server `initialState` is empty while the browser's first render already has the saved state: when saved state exists, React reports a hydration mismatch (the sort arrows, column order or page differ). Render the persisted grid only after mount, for example:
74
+
75
+ ```tsx
76
+ function PersistedGrid() {
77
+ const { initialState, onStateChange } = useGridStateStorage('my-app-storage-key');
78
+ return <DataGrid rows={rows} columns={columns} initialState={initialState} onStateChange={onStateChange} />;
79
+ }
80
+
81
+ export default function Page() {
82
+ const [mounted, setMounted] = useState(false);
83
+ useEffect(() => setMounted(true), []);
84
+ return mounted ? <PersistedGrid /> : <GridPlaceholder />;
85
+ }
86
+ ```
87
+
88
+ In Next.js, `dynamic(() => import('./PersistedGrid'), { ssr: false })` does the same.
89
+
90
+ ### Changing the key
91
+
92
+ The grid reads `initialState` only when it mounts. If the key can change while the grid stays on screen (a per-user or per-view key), remount the grid with the key so it starts from the new key's saved state:
93
+
94
+ ```tsx
95
+ const storageKey = `grid-${userId}`;
96
+ const { initialState, onStateChange } = useGridStateStorage(storageKey);
97
+
98
+ <DataGrid key={storageKey} rows={rows} columns={columns} initialState={initialState} onStateChange={onStateChange} />
99
+ ```
100
+
101
+ Without the remount the grid keeps its current state, and its next change is saved under the new key.
102
+
103
+ ### Server-side rendering
104
+
105
+ The hook reads storage during the first render. On the server there is no storage, so the server HTML uses the default state while the client's first render uses the saved one, and React reports a hydration mismatch. If users can have saved state, render the persisted grid on the client only (for example after mount, or with Next.js `dynamic(..., { ssr: false })`). The [migration guide, §26](../migration/v2-to-v3.md#26-state-persistence) has a complete example.
106
+
43
107
  ---
44
108
 
45
109
  ## 📡 Tracking Changes
46
110
 
47
- Use the `onStateChange` callback to listen for any modifications to the grid state and save them to `localStorage` or a database.
111
+ Use the `onStateChange` callback to listen for modifications to the grid state and save them to `localStorage` or a database. It fires once on mount and then whenever the state's *value* changes — re-rendering the grid with equal (even inline) props does not fire it, so storing the state in parent React state is safe.
48
112
 
49
113
  ```tsx
50
114
  <DataGrid
115
+ rows={rows}
116
+ columns={columns}
51
117
  onStateChange={(state) => {
52
118
  localStorage.setItem('grid-state', JSON.stringify(state));
53
119
  }}
@@ -58,9 +124,11 @@ Use the `onStateChange` callback to listen for any modifications to the grid sta
58
124
 
59
125
  ## 🧩 Supported State Objects
60
126
 
61
- The following features support state persistence:
127
+ The following features support state persistence (they appear in the `onStateChange` payload and are restored from `initialState`):
62
128
  - **Sorting**: `sortModel`
63
129
  - **Filtering**: `filterModel`
64
130
  - **Pagination**: `paginationModel`
65
131
  - **Columns**: `columnVisibilityModel`, `columnOrder`, `pinnedColumns`, `columnWidths`
66
132
  - **Density**: `density`
133
+
134
+ Only your own columns appear in the column state: the grid's synthetic grouping column (added by `groupingColDef`) is never included.
@@ -19,10 +19,6 @@ import {
19
19
  <DataGrid
20
20
  rows={rows}
21
21
  columns={columns}
22
- filterModel={filterModel}
23
- onFilterModelChange={setFilterModel}
24
- columnVisibilityModel={columnVisibilityModel}
25
- onColumnVisibilityModelChange={setColumnVisibilityModel}
26
22
  slots={{ toolbar: GridToolbar }}
27
23
  slotProps={{
28
24
  toolbar: {
@@ -39,6 +35,19 @@ import {
39
35
  />
40
36
  ```
41
37
 
38
+ When the toolbar is mounted through `slots.toolbar`, the grid passes it the filter, column-visibility, aggregation and column-order state and handlers itself, so search, Filters, Columns and Summaries work without any controlled props (v3.0+: the search box and Filters button no longer need `filterModel` / `onFilterModelChange`). Pass `filterModel` + `onFilterModelChange` (and the other controlled pairs) only when you want to own that state.
39
+
40
+ ### Which buttons appear
41
+
42
+ `GridToolbar` renders a control only when it has the handler it needs. Under `slots.toolbar` the grid supplies these, except for pivot:
43
+
44
+ | Control | Shown when |
45
+ | :--- | :--- |
46
+ | Search + Filters | `onFilterModelChange` is present (always under `slots.toolbar`) |
47
+ | Columns | `onColumnVisibilityModelChange` is present (always under `slots.toolbar`) |
48
+ | Pivot | `onPivotModelChange` is present (under `slots.toolbar`: when `pivotMode`, `pivotModel` or `onPivotModelChange` is set on the grid) |
49
+ | Summaries | `onAggregationModelChange` is present (always under `slots.toolbar`; a standalone `<GridToolbar>` without it shows no Summaries button, v3.0+) |
50
+
42
51
  ---
43
52
 
44
53
  ## New props on `GridToolbarProps`
@@ -70,12 +79,12 @@ interface ToolbarButtonRenderProps {
70
79
 
71
80
  ```ts
72
81
  interface ToolbarQuickFilterRenderProps {
73
- value: string;
74
- onChange: (value: string) => void;
82
+ value: string; // quickFilterValues joined with spaces
83
+ onChange: (value: string) => void; // split on whitespace into quickFilterValues terms
75
84
  }
76
85
  ```
77
86
 
78
- Both types are exported from the package:
87
+ Both types are exported from the package. `slotProps.toolbar` is typed as `Record<string, unknown>`, so annotate render-prop parameters with these types (an unannotated `(props) => …` is an implicit `any` under `strict`):
79
88
 
80
89
  ```ts
81
90
  import type { ToolbarButtonRenderProps, ToolbarQuickFilterRenderProps } from '@opencorestack/opengridx';
@@ -104,7 +113,7 @@ function MySearchBar({ value, onChange }: ToolbarQuickFilterRenderProps) {
104
113
 
105
114
  slotProps={{
106
115
  toolbar: {
107
- renderQuickFilter: (props) => <MySearchBar {...props} />,
116
+ renderQuickFilter: (props: ToolbarQuickFilterRenderProps) => <MySearchBar {...props} />,
108
117
  },
109
118
  }}
110
119
  ```
@@ -126,7 +135,7 @@ function MyFilterBtn({ onClick, isOpen, activeCount }: ToolbarButtonRenderProps)
126
135
 
127
136
  slotProps={{
128
137
  toolbar: {
129
- renderFilterButton: (props) => <MyFilterBtn {...props} />,
138
+ renderFilterButton: (props: ToolbarButtonRenderProps) => <MyFilterBtn {...props} />,
130
139
  },
131
140
  }}
132
141
  ```
@@ -144,7 +153,7 @@ const apiRef = useGridApiRef();
144
153
  slotProps={{
145
154
  toolbar: {
146
155
  renderExportButton: () => (
147
- <button onClick={() => exportToCsv(apiRef.current.getAllRows(), columns, { filename: 'export' })}>
156
+ <button onClick={() => exportToCsv(apiRef.current.getAllFilteredRows(), apiRef.current.getVisibleColumns(), { fileName: 'export.csv' })}>
148
157
  Export CSV
149
158
  </button>
150
159
  ),
@@ -158,11 +167,11 @@ const apiRef = useGridApiRef();
158
167
  ```tsx
159
168
  slotProps={{
160
169
  toolbar: {
161
- renderQuickFilter: (props) => <MySearch {...props} />,
162
- renderColumnsButton: (props) => <MyColumnsBtn {...props} />,
163
- renderFilterButton: (props) => <MyFilterBtn {...props} />,
164
- renderAggregationButton: (props) => <MyAggBtn {...props} />,
165
- renderExportButton: () => <MyExportBtn />,
170
+ renderQuickFilter: (props: ToolbarQuickFilterRenderProps) => <MySearch {...props} />,
171
+ renderColumnsButton: (props: ToolbarButtonRenderProps) => <MyColumnsBtn {...props} />,
172
+ renderFilterButton: (props: ToolbarButtonRenderProps) => <MyFilterBtn {...props} />,
173
+ renderAggregationButton: (props: ToolbarButtonRenderProps) => <MyAggBtn {...props} />,
174
+ renderExportButton: () => <MyExportBtn />,
166
175
  },
167
176
  }}
168
177
  ```
@@ -190,7 +199,7 @@ slotProps={{
190
199
 
191
200
  When you provide a custom button, the toolbar renders your element as the trigger but keeps its own open/close state. The panel (Columns, Filters, or Aggregation) is positioned relative to the wrapper `<div>` surrounding your custom button, so placement is automatic — you don't need to pass any refs.
192
201
 
193
- Panel close behaviour is unchanged: Escape key closes, click-outside closes (except the Filter panel, which requires an explicit Close button to let users type without it dismissing).
202
+ Panel close behaviour is unchanged: Escape closes every panel, and click-outside closes every panel except the Filter panel, which has an explicit Close button so users can type without it dismissing. However the Columns panel closes, `onColumnsPanelClose` is called.
194
203
 
195
204
  ---
196
205
 
@@ -201,5 +210,5 @@ These built-in features are unaffected regardless of which render props you prov
201
210
  - Column panel content (drag-to-reorder, show/hide toggles)
202
211
  - Filter panel content (operators, values, logic operator)
203
212
  - Aggregation panel content (per-column function pills)
204
- - Pivot panel (if `onPivotModelChange` is wired)
213
+ - Pivot panel (when the Pivot button is shown, see above)
205
214
  - `children` / `rightContent` slot behaviour
@@ -12,14 +12,32 @@ Row grouping allows you to categorize rows based on common column values.
12
12
  ```tsx
13
13
  <DataGrid
14
14
  rows={rows}
15
+ columns={columns}
15
16
  rowGroupingModel={['department', 'role']}
16
17
  />
17
18
  ```
18
19
 
19
20
  ### Features
20
- - **Multi-Level Groups**: Nest data as deeply as needed.
21
+ - **Multi-Level Groups**: Nest data as deeply as needed — one level per field in `rowGroupingModel`, in order.
21
22
  - **Aggregation Integration**: Summarize values automatically for each group level.
22
- - **Expansion Control**: Control which groups are expanded by default.
23
+ - **Expansion Control**: `defaultGroupingExpansionDepth` sets how many levels start expanded (default `0`: all collapsed; `-1`: everything expanded).
24
+ - **Aggregate placement**: `getAggregationPosition(node)` returns `'inline'` (on the group row, the default), `'footer'` (a subtotal row after the group's children while it is expanded) or `null` (hidden). It is also called once with `null` for the grand-total footer row; returning `null` there hides the footer.
25
+
26
+ ### Behaviour to know about
27
+
28
+ - **Pagination is ignored** while `rowGroupingModel` is active. All groups render in one scrollable, virtualized view, so the grid needs a bounded height ([Virtualization](./virtualization.md#the-grid-needs-a-bounded-height)). A development-mode warning is logged if you pass `pagination` too.
29
+ - **Expansion state survives data updates** (row grouping and tree data). Groups and nodes the user expanded or collapsed stay that way when `rows` changes: inline edits, live refreshes, new array identities, and lazily loaded server-side children. The state resets to `defaultGroupingExpansionDepth` only when `rowGroupingModel` or `defaultGroupingExpansionDepth` changes value.
30
+ - **Group aggregation uses the same functions as the footer.** `sum`, `avg`, `count`, `min`, `max` and `unique` ignore `null`/`undefined`, so a group of `[10, null, 20]` gives `min` 10, `avg` 15 and `count` 2. `availableAggregationFunctions` on a column is honoured for group rows too.
31
+ - **The expand toggle, indentation and group label sit in the leftmost column on screen**, after column order, visibility and pinning are applied — not in whichever column is first in `columns`. Hiding or moving that column no longer leaves group rows without a label or toggle (fixed in v3.0).
32
+ - **Keyboard and screen readers** (v3.0): **Enter** on a non-editable cell of a group row or a tree-data parent expands or collapses it, and **Alt+ArrowRight** / **Alt+ArrowLeft** expand / collapse it. Rows expose `aria-level` (depth + 1), and rows with children expose `aria-expanded`. The expand buttons inside cells are not separate Tab stops. See [Keyboard & Accessibility](keyboard-navigation.md).
33
+ - **`valueFormatter` applies to grouped rows** the same as flat rows (fixed in v2.1; earlier versions rendered raw values for every column once grouping was on).
34
+ - **Group subtotals and the `(n)` count follow the filter.** They cover only the leaf rows that pass the current filter, and groups with no matching row are left out (v3.0; before, they counted hidden rows too). Groups keep their order of first appearance while filtering.
35
+ - **Grouping values are compared by value, not by text**: `null` and `'null'`, `1` and `'1'`, `true` and `'true'` are separate groups; equal dates share one. A column with a `valueGetter` is grouped by the value it computes.
36
+ - **`groupable: false`** fields are dropped from the model before grouping, so the other levels keep their depth, indentation, `defaultGroupingExpansionDepth` and export depth.
37
+ - **Sorting the column that shows the group labels orders the groups by their grouping value**; sorting an aggregated column orders them by the aggregate.
38
+ - **Group rows are synthetic.** They have no selection checkbox and no detail panel, their ids never appear in the selection model, clicking them toggles them (without `onRowClick`), and a column's `valueGetter` is not called for them. `renderCell` *is* called for them: see [GridRowMeta](../architecture/grid-row-meta.md#accessing-metadata-in-rendercell).
39
+ - **Server modes**: with `filterMode="server"` / `sortingMode="server"` the grid does not filter or sort the rows again on the client, in row grouping and tree data as in a flat grid.
40
+ - **Pass stable callbacks.** `rowGroupingModel` and `aggregationModel` are compared by value, so inline arrays and objects are fine, but a new `getTreeDataPath` or `columns` identity on every render rebuilds the whole hierarchy. Define them at module scope or memoize them.
23
41
 
24
42
  ---
25
43
 
@@ -29,15 +47,29 @@ Tree data is used for data that has a natural parent-child relationship (e.g., a
29
47
 
30
48
  ### Implementation
31
49
  1. Enable `treeData={true}`.
32
- 2. Provide a `getTreeDataPath` function to define the hierarchy.
50
+ 2. Provide a `getTreeDataPath` function to define the hierarchy. It is required: without it the rows are shown flat. Define it at module scope or with `useCallback`.
33
51
 
34
52
  ```tsx
35
53
  <DataGrid
54
+ rows={rows}
55
+ columns={columns}
36
56
  treeData
37
- getTreeDataPath={(row) => row.hierarchyPath} // e.g. ['CEO', 'VP Engineering', 'Manager']
57
+ getTreeDataPath={getPath} // e.g. (row) => row.hierarchyPath: ['CEO', 'VP Engineering', 'Manager']
38
58
  />
39
59
  ```
40
60
 
61
+ ### Behaviour to know about
62
+
63
+ - **Path segments can contain any character**, including `/`: `['a/b']` and `['a', 'b']` are different paths.
64
+ - **Missing parents are created for you.** A path segment with no row of its own gets a synthetic parent row (`rowMeta.isGroupRow`, label in `rowMeta.groupLabel`, row object `{ id }`) shown in the toggle column as `label (n)`, whatever that column is called. It is shown only when a row under it passes the filter, and sorting the toggle column orders it by its label.
65
+ - **Parents that are your own rows behave like any row**: clicking one fires `onRowClick` and selects it; its chevron expands it. (Before v3.0 a click toggled it and never fired `onRowClick`.) From the keyboard it expands with Enter or Alt+ArrowRight (see below) and **Shift+Space** selects it.
66
+ - **`descendantCount`** (the `(n)`) counts the rows below a node at any depth that pass the filter. A parent whose children are all filtered out is shown without a toggle.
67
+ - **Invalid paths are reported in development**: a row whose path is empty is shown as a top-level row, rows sharing a path attach their children to the first of them, and `treeData` without `getTreeDataPath` shows the rows flat. Each logs a `console.warn`.
68
+ - **Pagination** pages the flattened visible tree; keyboard navigation stays on the current page.
69
+ - **Lazy (server) trees**: the children of an expanded node with `serverChildrenCount` are fetched when it is expanded, and fetched again after a server re-fetch (a sort or filter change) replaces the rows. `defaultGroupingExpansionDepth` counts such a node as expandable (v3.0+), so `-1` expands every lazy node and loads its children.
70
+ - **`apiRef.getAllFilteredRows()` with a filter** returns the rows that match the filter, the same set select-all and the aggregation footer use. Ancestors shown only to give a match its context are not in it (v3.0+; before, they were).
71
+ - **Row grouping with `paginationMode="server"`**: grouping turns pagination off, so a `dataSource` is asked for every row (see [Data Source](data-source.md#2-server-side-pagination)).
72
+
41
73
  ---
42
74
 
43
75
  ## ⚙️ API Reference
@@ -47,15 +79,18 @@ Tree data is used for data that has a natural parent-child relationship (e.g., a
47
79
  | :--- | :--- | :--- | :--- |
48
80
  | `rowGroupingModel` | `string[]` | `[]` | Fields to group by (in order). |
49
81
  | `treeData` | `boolean` | `false` | Enable tree data mode. |
50
- | `getTreeDataPath` | `(row) => string[]` | `undefined` | Function to get the path for a row. |
51
- | `defaultGroupingExpansionDepth` | `number` | `0` | How many levels to expand on load. |
52
- | `groupingColDef` | `GridColDef` | `undefined` | Custom configuration for the generated group column. |
82
+ | `getTreeDataPath` | `(row) => string[]` | `undefined` | Path of a row. Required with `treeData`. |
83
+ | `defaultGroupingExpansionDepth` | `number` | `0` | How many levels start expanded (`0` = collapsed, `-1` = all). |
84
+ | `groupingColDef` | `Partial<GridColDef<R>>` | `undefined` | Adds a dedicated group column (row grouping or tree data). |
85
+ | `getAggregationPosition` | `(node: GridTreeNode \| null) => 'inline' \| 'footer' \| null` | `undefined` | Where group aggregates appear; called with `null` for the grand total. |
86
+
87
+ Hierarchy information is never written onto your row objects: read it from `params.rowMeta` (`isGroupRow`, `hasChildren`, `treeDepth`, `groupLabel`, …) in `renderCell`. See [GridRowMeta](../architecture/grid-row-meta.md).
53
88
 
54
89
  ---
55
90
 
56
91
  ## 🎨 Customizing the Group Column
57
92
 
58
- When `rowGroupingModel` is active, pass `groupingColDef` to configure a dedicated `__group__` column that is prepended at position 0 and auto-pinned left, separate from your data columns. It accepts any `GridColDef` fields except `field` (always `'__group__'`).
93
+ When `rowGroupingModel` or `treeData` is active, pass `groupingColDef` to configure a dedicated `__group__` column that is prepended at position 0 and auto-pinned left, separate from your data columns. Its type is `Partial<GridColDef<R>>`: every key is optional, and a `field` you pass is ignored (it is always `'__group__'`). It also forces `hideable`, `sortable`, `filterable`, `pinnable` and `exportable` to `false`. Defaults: `headerName: 'Group'`, `width: 220`. With tree data (v3.0) the column shows the last segment of each row's path, and a `valueGetter` in `groupingColDef` can show something else (for example the whole path).
59
94
 
60
95
  ```tsx
61
96
  <DataGrid
@@ -69,7 +104,7 @@ When `rowGroupingModel` is active, pass `groupingColDef` to configure a dedicate
69
104
  />
70
105
  ```
71
106
 
72
- Without `groupingColDef`, the group toggle is overlaid on the first data column. With it, the group indicator always appears in its own fixed column regardless of your `columns` order.
107
+ Without `groupingColDef`, the group toggle is overlaid on the leftmost column on screen. With it, the group indicator always appears in its own fixed column regardless of your `columns` order. A `renderCell` in `groupingColDef` renders the group rows' label cell (return `undefined` to keep the default label).
73
108
 
74
109
  ---
75
110
 
@@ -17,8 +17,8 @@ Virtualization is a technique that dramatically improves performance when workin
17
17
  The grid calculates which rows are visible based on:
18
18
  - Current scroll position
19
19
  - Viewport height
20
- - Row height (fixed or variable)
21
- - Overscan buffer (default: 5 rows)
20
+ - Row height (the fixed `rowHeight`, plus the height of any expanded detail panels)
21
+ - Overscan buffer (adaptive, minimum `overscanRowCount`, default 3 rows)
22
22
 
23
23
  ```tsx
24
24
  // Example: 1000 rows, but only ~15 rendered at a time
@@ -29,21 +29,69 @@ The grid calculates which rows are visible based on:
29
29
  />
30
30
  ```
31
31
 
32
- **Performance characteristics:**
32
+ **Performance characteristics** (when the grid has a bounded height — see below):
33
33
  - ✅ Renders ~15-25 rows regardless of total dataset size
34
34
  - ✅ Constant memory usage
35
35
  - ✅ Smooth 60fps scrolling
36
- - ✅ Supports millions of rows
36
+ - ⚠️ Not millions of rows. Browsers cap an element's height (33,554,432px in Chromium, about 17.9M px in Firefox) and the grid does no scroll scaling: it lays every row out at its real height, so rows past the cap cannot be scrolled to. The limit is roughly `33,554,432 / rowHeight` rows in Chromium (about 645,000 at the default 52px), and about half that in Firefox (about 340,000). Beyond that, use pagination, or server-side paging / infinite scroll. In development the grid warns when the content passes 17.8M px.
37
37
 
38
38
  ### Column Virtualization
39
39
 
40
- Currently, the DataGrid renders all columns. Column virtualization (rendering only visible columns) is planned for future releases.
40
+ Unpinned columns are virtualized horizontally: only the columns in view plus 6 on each side are rendered, with spacer cells standing in for the rest. Pinned columns are always rendered.
41
+
42
+ ### Import the stylesheet
43
+
44
+ Virtualization depends on the grid's CSS. Import it once in your app:
45
+
46
+ ```tsx
47
+ import '@opencorestack/opengridx/styles';
48
+ ```
49
+
50
+ Without the stylesheet the viewport does not scroll, so the grid renders every row. In development the grid logs a one-time `[OpenGridX] The grid stylesheet is not loaded` warning.
51
+
52
+ ### The grid needs a bounded height
53
+
54
+ Virtualization renders as many rows as fit in the grid's viewport. If the grid's container has no bounded height, the viewport grows to fit **every** row, so every row is rendered. Nothing errors; the grid just stops virtualizing.
55
+
56
+ The most common cause is a flex child that is missing `min-height: 0`. A flex item defaults to `min-height: auto`, which lets it grow to its content:
57
+
58
+ ```tsx
59
+ // ❌ Grows to fit all rows — virtualization is effectively off
60
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
61
+ <div style={{ flex: 1 }}>
62
+ <DataGrid rows={rows} columns={columns} height="100%" />
63
+ </div>
64
+ </div>
65
+
66
+ // ✅ Bounded — only visible rows render
67
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
68
+ <div style={{ flex: 1, minHeight: 0 }}>
69
+ <DataGrid rows={rows} columns={columns} height="100%" />
70
+ </div>
71
+ </div>
72
+ ```
73
+
74
+ `height="100%"` only helps if every ancestor up to a fixed-height element also has a definite height.
75
+
76
+ **`DataGridThemeProvider` counts as an ancestor.** It renders a plain `<div class="ogx-theme-provider">` around the grid, and that wrapper is auto-height like any other `div`. When you use `height="100%"` inside it, give the wrapper a height as well:
77
+
78
+ ```tsx
79
+ <div style={{ flex: 1, minHeight: 0 }}>
80
+ <DataGridThemeProvider theme={theme} style={{ height: '100%' }}>
81
+ <DataGrid rows={rows} columns={columns} height="100%" />
82
+ </DataGridThemeProvider>
83
+ </div>
84
+ ```
85
+
86
+ In development builds the grid logs a `console.warn` when it detects that it is rendering every row of a dataset larger than 200 rows because its viewport is unbounded.
87
+
88
+ Pagination can hide this problem, because a paginated grid only has one page of rows to render. Row grouping switches pagination off (see below), so an unbounded container that looked fine with pagination can freeze the tab once grouping is enabled.
41
89
 
42
90
  ## Configuration
43
91
 
44
92
  ### Row Height
45
93
 
46
- The grid supports both fixed and variable row heights:
94
+ Every data row has the same height. There is no per-row height callback; the only rows of a different height are expanded detail panels (see below).
47
95
 
48
96
  ```tsx
49
97
  // Fixed row height (default: 52px)
@@ -157,7 +205,7 @@ When using detail panels, row heights become variable. The grid automatically:
157
205
 
158
206
  ## Virtualization with Grouping/Tree Data
159
207
 
160
- Row grouping and tree data work seamlessly with virtualization:
208
+ Grouped and tree rows go through the same virtualizer as flat rows. Expanding a group of any size adds its rows to the virtual row list, not to the DOM: in a bounded container, expanding a 25,000-row group renders ~20 DOM rows (verified in Chromium by `DataGrid.virtualization.browser.test.tsx`).
161
209
 
162
210
  ```tsx
163
211
  // Tree data with virtualization
@@ -170,7 +218,7 @@ Row grouping and tree data work seamlessly with virtualization:
170
218
  />
171
219
  ```
172
220
 
173
- **Note:** When groups are collapsed, only visible rows are rendered. Expanding a group dynamically adds rows to the render list.
221
+ **Note:** `pagination` is ignored while `rowGroupingModel` is active, so a grouped grid relies entirely on virtualization to bound what it renders. Make sure its container has a bounded height. A development-mode warning is logged if `pagination` is passed together with `rowGroupingModel`.
174
222
 
175
223
  ## Debugging Virtualization
176
224
 
@@ -192,13 +240,14 @@ You should see console logs only for visible rows + overscan buffer.
192
240
 
193
241
  ## Known Limitations
194
242
 
195
- 1. **Column virtualization** - Not yet implemented (all columns are rendered)
196
- 2. **Dynamic row heights** - Requires full dataset scan for accurate scroll calculations
197
- 3. **Horizontal scrolling** - May show brief flicker with very wide grids
243
+ 1. **Unbounded containers** - Virtualization needs a bounded viewport height (see above)
244
+ 2. **Browser height cap** - Rows past roughly `33,554,432 / rowHeight` (Chromium; less in Firefox) cannot be scrolled to (see above)
245
+ 3. **Uniform row heights** - Only detail panels vary the height; cumulative heights are recomputed over all rows when panels change
246
+ 4. **Horizontal scrolling** - May show brief flicker with very wide grids
198
247
 
199
248
  ## Future Improvements
200
249
 
201
- - [ ] Column virtualization for grids with 50+ columns
250
+ - [x] Column virtualization (unpinned columns, 6-column overscan)
202
251
  - [x] Adaptive overscan based on scroll velocity (`overscanRowCount` prop, v2.0.4)
203
252
  - [ ] Virtual scrollbar for extremely large datasets (millions of rows)
204
253
  - [ ] Intersection Observer API for better scroll detection
@@ -208,10 +257,10 @@ You should see console logs only for visible rows + overscan buffer.
208
257
  | Feature | OpenGridX | MUI X DataGrid |
209
258
  |---------|--------------|----------------|
210
259
  | Row virtualization | ✅ Yes | ✅ Yes |
211
- | Column virtualization | ❌ Planned | ✅ Yes (Pro) |
212
- | Variable row heights | ✅ Yes | ✅ Yes |
260
+ | Column virtualization | ✅ Yes (unpinned columns) | ✅ Yes (Pro) |
261
+ | Variable row heights | Detail panels only | ✅ Yes |
213
262
  | Overscan buffer | 3–30 rows (adaptive, velocity-based) | 3-8 rows (adaptive) |
214
- | Max recommended rows | 100,000+ | 100,000+ |
263
+ | Max scrollable rows | ~645,000 at 52px in Chromium (browser height cap) | — |
215
264
 
216
265
  ## Related Documentation
217
266