@opencorestack/opengridx 2.0.4 → 2.1.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 (44) hide show
  1. package/CHANGELOG.md +45 -3
  2. package/dist/index.d.ts +43 -2
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +3927 -3839
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +13 -4
  9. package/docs/components/column-visibility.md +40 -16
  10. package/docs/customization/slots-api.md +18 -6
  11. package/docs/features/export-guide.md +22 -3
  12. package/docs/features/sorting-pagination.md +2 -0
  13. package/docs/features/tree-data-grouping.md +8 -1
  14. package/docs/features/virtualization.md +36 -8
  15. package/docs/roadmap.md +13 -7
  16. package/lib/components/Cell/Cell.tsx +1 -0
  17. package/lib/components/DataGrid/DataGrid.css +7 -0
  18. package/lib/components/DataGrid/DataGrid.defects.test.tsx +103 -0
  19. package/lib/components/DataGrid/DataGrid.tsx +42 -10
  20. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +183 -0
  21. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  22. package/lib/components/DataGrid/GridListView.tsx +3 -0
  23. package/lib/components/DataGrid/GridPinnedRows.tsx +3 -0
  24. package/lib/components/DataGrid/GridVirtualRows.tsx +8 -1
  25. package/lib/components/ListView/ListViewRow.tsx +3 -0
  26. package/lib/components/Row/Row.tsx +10 -0
  27. package/lib/hooks/core/useGridColumns.tsx +8 -3
  28. package/lib/hooks/core/useGridDevWarnings.ts +42 -0
  29. package/lib/hooks/core/useGridKeyboardNavigation.ts +12 -14
  30. package/lib/hooks/core/useGridVisibleRows.ts +3 -2
  31. package/lib/hooks/features/useAggregation.ts +3 -38
  32. package/lib/hooks/useRowGrouping.test.ts +162 -1
  33. package/lib/hooks/useRowGrouping.ts +42 -59
  34. package/lib/hooks/useTreeData.test.ts +83 -0
  35. package/lib/hooks/useTreeData.ts +44 -43
  36. package/lib/index.ts +2 -0
  37. package/lib/types/index.ts +15 -2
  38. package/lib/utils/aggregation/aggregation.test.ts +16 -0
  39. package/lib/utils/aggregation/index.ts +62 -0
  40. package/lib/utils/export/exportToExcelAdvanced.test.ts +109 -0
  41. package/lib/utils/export/exportToExcelAdvanced.ts +89 -5
  42. package/lib/utils/scroll/index.ts +28 -0
  43. package/llms.txt +22 -13
  44. package/package.json +7 -3
@@ -21,7 +21,7 @@ The main component for displaying and interacting with data.
21
21
  | `overscanRowCount` | `number` | `3` | Minimum rows rendered outside the visible viewport. The grid adapts this upward automatically based on scroll velocity — this prop sets the floor. |
22
22
  | `loading` | `boolean` | `false` | Shows a loading skeleton overlay. |
23
23
  | `checkboxSelection` | `boolean` | `false` | Enable row selection via checkboxes. |
24
- | `pagination` | `boolean` | `false` | Enable the bottom pagination bar. |
24
+ | `pagination` | `boolean` | `false` | Enable the bottom pagination bar. Ignored while `rowGroupingModel` is active (dev-mode warning). |
25
25
  | `paginationMode` | `'client' \| 'server' \| 'infinite'` | `'client'` | How to handle paging. |
26
26
  | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). |
27
27
  | `onPaginationModelChange` | `(model: GridPaginationModel) => void` | — | Fired when page or page size changes. |
@@ -30,11 +30,12 @@ The main component for displaying and interacting with data.
30
30
  | `height` | `number \| string` | `undefined` | Total height of the grid container. |
31
31
  | `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Visual row density. |
32
32
  | `initialState` | `GridInitialState` | `undefined` | Starting state for sorting, filters, etc. |
33
- | `slots` | `GridSlots` | `{}` | Custom component overrides. |
33
+ | `slots` | `GridSlots` | `{}` | Custom component overrides: `toolbar`, `pagination`, `noRowsOverlay`, `loadingOverlay`, `footer`. See [Slots API](customization/slots-api.md). |
34
34
  | `slotProps` | `Record<string, unknown>` | `{}` | Props passed to custom slots. |
35
35
  | `filterModel` | `GridFilterModel` | `undefined` | Active filters. |
36
36
  | `sortModel` | `GridSortItem[]` | `undefined` | Active sorting. |
37
37
  | `onRowClick` | `(params: GridRowParams) => void` | — | Fired when a row is clicked. |
38
+ | `onRowDoubleClick` | `(params: GridRowParams) => void` | — | Fired when a row is double-clicked (v2.1+). Also fires for group rows; not fired on the checkbox, expand icon, drag handle or an open editor. |
38
39
  | `onCellClick` | `(params: GridCellParams) => void` | — | Fired when a cell is clicked. |
39
40
  | `onStateChange` | `(state: GridState) => void` | — | Fired on any internal state update. |
40
41
  | `processRowUpdate` | `(new, old) => R \| Promise<R>` | — | Fired after a cell edit is committed. |
@@ -302,7 +303,7 @@ Defines the behavior and appearance of a single column.
302
303
 
303
304
  | Property | Type | Description |
304
305
  | :--- | :--- | :--- |
305
- | `renderCell` | `(params: GridRenderCellParams) => ReactNode` | Fully custom cell renderer. Receives `value`, `row`, `field`, `rowIndex`, `colIndex`. |
306
+ | `renderCell` | `(params: GridRenderCellParams) => ReactNode` | Fully custom cell renderer. Receives `value`, `formattedValue` (v2.1+, the `valueFormatter` output), `row`, `field`, `colDef`, `rowIndex`, `colIndex`, `rowMeta`. |
306
307
  | `renderHeader` | `(params: GridRenderHeaderParams) => ReactNode` | Custom header cell renderer. Use for icons, sort indicators, or rich headers. |
307
308
  | `renderEditCell` | `(params: GridRenderCellParams) => ReactNode` | Custom editor rendered when the cell enters edit mode. Requires `editable: true`. |
308
309
 
@@ -351,11 +352,19 @@ Defines the behavior and appearance of a single column.
351
352
  | `exportToCsv(rows, cols, options?)` | `void` | Triggers download of CSV file. |
352
353
  | `exportToJson(rows, cols, options?)` | `void` | Triggers download of JSON file. |
353
354
  | `exportToExcel(rows, cols, options?)` | `void` | Basic `.xls` export (zero-dep). |
354
- | `exportToExcelAdvanced(rows, cols, options?)` | `Promise<void>` | Real `.xlsx` export (styled, multi-sheet, lazy-loaded). |
355
+ | `exportToExcelAdvanced(rows, cols, options?)` | `Promise<void>` | Real `.xlsx` export (styled, multi-sheet, lazy-loaded). Accepts `groupedRows` for outlined grouped reports (v2.1+). |
355
356
  | `printGrid(rows, cols, title \| options)` | `void` | Opens browser print dialog. |
356
357
 
357
358
  ---
358
359
 
360
+ ## 🧩 Standalone Components
361
+
362
+ | Component | Description |
363
+ | :--- | :--- |
364
+ | `ColumnVisibilityPanel` | Controlled column show/hide (and optional reorder) list, for use outside the grid (v2.1+). Props: `ColumnVisibilityPanelProps`. See [Column Visibility](components/column-visibility.md). |
365
+
366
+ ---
367
+
359
368
  ## 🎨 Theming & Styling
360
369
 
361
370
  ### `DataGridThemeProvider`
@@ -12,26 +12,50 @@ The `ColumnVisibilityPanel` provides an interactive list for users to toggle the
12
12
 
13
13
  ## 🛠️ Usage
14
14
 
15
- ### Toolbar Integration
16
- The most common way to use the visibility panel is within a dropdown menu in the grid's toolbar.
15
+ The grid's toolbar and header menu already include this panel. Import it yourself (exported since v2.1) only when you want the list somewhere else, such as a sidebar or modal. It is a controlled component: you own the visibility model and pass it in.
17
16
 
18
17
  ```tsx
19
- import { ColumnVisibilityPanel } from '@opencorestack/opengridx';
20
-
21
- // In your Toolbar component
22
- <Dropdown content={<ColumnVisibilityPanel />}>
23
- <button>Columns</button>
24
- </Dropdown>
18
+ import { useMemo, useState } from 'react';
19
+ import { DataGrid, ColumnVisibilityPanel } from '@opencorestack/opengridx';
20
+ import type { GridColDef } from '@opencorestack/opengridx';
21
+
22
+ function Report({ rows, columns }: { rows: Row[]; columns: GridColDef<Row>[] }) {
23
+ const [visibility, setVisibility] = useState<Record<string, boolean>>({});
24
+ const visibleColumns = useMemo(
25
+ () => new Set(columns.filter(c => visibility[c.field] !== false).map(c => c.field)),
26
+ [columns, visibility],
27
+ );
28
+ const setAll = (visible: boolean) =>
29
+ setVisibility(Object.fromEntries(columns.filter(c => c.hideable !== false).map(c => [c.field, visible])));
30
+
31
+ return (
32
+ <div style={{ display: 'flex' }}>
33
+ <aside className="my-column-sidebar">
34
+ <ColumnVisibilityPanel
35
+ columns={columns}
36
+ visibleColumns={visibleColumns}
37
+ onVisibilityChange={(field, isVisible) => setVisibility(v => ({ ...v, [field]: isVisible }))}
38
+ onShowAll={() => setAll(true)}
39
+ onHideAll={() => setAll(false)}
40
+ />
41
+ </aside>
42
+ <DataGrid rows={rows} columns={columns} columnVisibilityModel={visibility} onColumnVisibilityModelChange={setVisibility} />
43
+ </div>
44
+ );
45
+ }
25
46
  ```
26
47
 
27
- ### Direct Integration
28
- You can also render it persistently beside the grid or within a custom modal.
48
+ ### Props (`ColumnVisibilityPanelProps`)
29
49
 
30
- ```tsx
31
- <div className="my-column-sidebar">
32
- <ColumnVisibilityPanel />
33
- </div>
34
- ```
50
+ | Prop | Type | Description |
51
+ | :--- | :--- | :--- |
52
+ | `columns` | `GridColDef[]` | Columns to list |
53
+ | `visibleColumns` | `Set<string>` | Fields currently visible |
54
+ | `onVisibilityChange` | `(field, isVisible) => void` | Toggle one column |
55
+ | `onShowAll` / `onHideAll` | `() => void` | Bulk actions (only `hideable` columns are affected) |
56
+ | `onColumnReorder` | `(fromField, toField) => void` | Optional — enables drag-to-reorder in the list |
57
+ | `onColumnOrderReset` | `() => void` | Optional — shows a reset-order action |
58
+ | `showNonHideableColumns` | `boolean` | Show `hideable: false` columns as disabled rows. Default `false` |
35
59
 
36
60
  ---
37
61
 
@@ -47,7 +71,7 @@ You can also render it persistently beside the grid or within a custom modal.
47
71
 
48
72
  ## 🎨 Controlling Visibility Programmatically
49
73
 
50
- The column visibility panel is an internal component; it cannot be replaced via the slots API. Use the controlled props to drive visibility from outside:
74
+ The built-in panel cannot be replaced through the slots API. To drive visibility from outside, use the controlled props, optionally together with a standalone `ColumnVisibilityPanel` as shown above:
51
75
 
52
76
  ```tsx
53
77
  <DataGrid
@@ -113,30 +113,42 @@ function CustomLoader() {
113
113
  ```
114
114
 
115
115
  ### 5. `footer`
116
- Replace the entire footer section (including pagination).
116
+ Replace the entire footer section below the grid, including the default pagination. Rendered whenever the slot is set, including while row grouping is active.
117
117
 
118
118
  **Props received:**
119
119
  ```typescript
120
120
  {
121
- // Any props from slotProps.footer
121
+ apiRef: React.MutableRefObject<GridApi>;
122
+ aggregationModel: GridAggregationModel;
123
+ aggregationResult: GridAggregationResult | null; // null when no aggregationModel
124
+ rowCount: number; // filtered rows (leaf rows when grouping)
125
+ pagination: boolean; // effective: false while row grouping is active
126
+ paginationModel: GridPaginationModel;
127
+ pageSizeOptions: number[];
128
+ onPaginationModelChange: (model: GridPaginationModel) => void;
129
+ // ...plus any props from slotProps.footer
122
130
  }
123
131
  ```
124
132
 
125
- **Example:**
133
+ **Example — a persistent grand-total bar that stays in sync with filtering:**
126
134
  ```tsx
127
- function CustomFooter() {
135
+ function TotalsFooter({ aggregationResult, rowCount }: Record<string, unknown>) {
136
+ const totals = aggregationResult as Record<string, number> | null;
128
137
  return (
129
138
  <div style={{ padding: '12px', borderTop: '1px solid #e0e0e0' }}>
130
- <span>Custom footer content</span>
139
+ {String(rowCount)} rows · Total: {totals?.amount ?? '—'}
131
140
  </div>
132
141
  );
133
142
  }
134
143
 
135
144
  <DataGrid
136
- slots={{ footer: CustomFooter }}
145
+ aggregationModel={{ amount: 'sum' }}
146
+ slots={{ footer: TotalsFooter }}
137
147
  />
138
148
  ```
139
149
 
150
+ > `noRowsOverlay`, `loadingOverlay` and `footer` were declared in the types before v2.1 but never rendered. They are wired from v2.1.
151
+
140
152
  ## Complete Example
141
153
 
142
154
  ```tsx
@@ -47,7 +47,7 @@ exportToCsv(rows, columns, {
47
47
  includeHeaders: true,
48
48
  delimiter: ',',
49
49
  selectedRows: [1, 2, 3], // export only these IDs
50
- aggregationResult: aggResult, // adds SUM/AVG totals row
50
+ aggregationResult: aggResult, // adds two rows: function labels (SUM/AVG), then values
51
51
  aggregationModel: { salary: 'sum' },
52
52
  });
53
53
  ```
@@ -60,7 +60,7 @@ exportToCsv(rows, columns, {
60
60
  | `includeHeaders` | `boolean` | `true` | Include column header row |
61
61
  | `delimiter` | `string` | `','` | Field delimiter |
62
62
  | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
63
- | `aggregationResult` | `object \| null` | — | Aggregation values row |
63
+ | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
64
64
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
65
65
 
66
66
  ---
@@ -89,7 +89,7 @@ exportToExcel(rows, columns, {
89
89
  | `sheetName` | `string` | `'Sheet1'` | Sheet tab name |
90
90
  | `includeHeaders` | `boolean` | `true` | Include column header row |
91
91
  | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
92
- | `aggregationResult` | `object \| null` | — | Aggregation values row |
92
+ | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
93
93
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
94
94
 
95
95
  ---
@@ -163,6 +163,9 @@ await exportToExcelAdvanced(rows, columns, {
163
163
  | `aggregationResult` | `object \| null` | — | Aggregation totals |
164
164
  | `aggregationModel` | `object \| null` | — | Aggregation function labels |
165
165
  | `selectedRows` | `(string\|number)[]` | — | IDs for `rows: 'selected'` sheets |
166
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` (v2.1+). See [Grouped export](#grouped-export-to-advanced-excel) |
167
+ | `groupHeaderFillColor` | `string` | `'#e8eaf6'` | Group-header row fill (grouped export) |
168
+ | `groupSubtotalFillColor` | `string` | `'#f0f4ff'` | Group-subtotal row fill (grouped export) |
166
169
 
167
170
  ### `ExcelSheetDefinition`
168
171
 
@@ -187,6 +190,22 @@ await exportToExcelAdvanced(rows, columns, {
187
190
  | `imageWidth` | `number` | Width of embedded image (px, default: 40) |
188
191
  | `imageHeight`| `number` | Height of embedded image (px, default: 40) |
189
192
 
193
+ ### Grouped export to advanced Excel
194
+
195
+ Since v2.1, pass the grid's grouped structure to keep number formats, styling and summary sheets on grouped reports:
196
+
197
+ ```tsx
198
+ await exportToExcelAdvanced(rows, columns, {
199
+ fileName: 'sales-by-region.xlsx',
200
+ groupedRows: apiRef.current.getGroupedExportRows() ?? undefined,
201
+ aggregationModel,
202
+ aggregationResult: apiRef.current.getAggregationResult(),
203
+ columnStyles: { amount: { numFmt: '$#,##0.00' } },
204
+ });
205
+ ```
206
+
207
+ Sheets with `rows: 'all'` are written in grouped order: a bold group-header row (label from the column's `groupingValueFormatter`, else `"Header: value"`), the leaf rows, a `Subtotal` row per group, and a final `Grand Total`. Rows use Excel's native outlining, so users can collapse groups in Excel. Subtotal and total values are written as numbers so `columnStyles[field].numFmt` applies. A `grand-total` entry replaces the sheet's `includeSummary` rows, so totals are not duplicated. `rows: 'selected'` sheets ignore `groupedRows` and export flat.
208
+
190
209
  ### Summary Sheet
191
210
 
192
211
  Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone aggregation sheet:
@@ -62,6 +62,8 @@ Disable sorting for specific columns in `GridColDef`:
62
62
 
63
63
  OpenGridX supports standard page-based pagination and infinite scrolling.
64
64
 
65
+ > **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).
66
+
65
67
  ### Usage
66
68
  ```tsx
67
69
  <DataGrid
@@ -17,10 +17,17 @@ Row grouping allows you to categorize rows based on common column values.
17
17
  ```
18
18
 
19
19
  ### Features
20
- - **Multi-Level Groups**: Nest data as deeply as needed.
20
+ - **Multi-Level Groups**: Nest data as deeply as needed — one level per field in `rowGroupingModel`, in order.
21
21
  - **Aggregation Integration**: Summarize values automatically for each group level.
22
22
  - **Expansion Control**: Control which groups are expanded by default.
23
23
 
24
+ ### Behaviour to know about
25
+
26
+ - **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.
27
+ - **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.
28
+ - **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.
29
+ - **`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).
30
+
24
31
  ---
25
32
 
26
33
  ## 🌿 Tree Data
@@ -18,7 +18,7 @@ The grid calculates which rows are visible based on:
18
18
  - Current scroll position
19
19
  - Viewport height
20
20
  - Row height (fixed or variable)
21
- - Overscan buffer (default: 5 rows)
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,7 +29,7 @@ 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
@@ -37,7 +37,35 @@ The grid calculates which rows are visible based on:
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
+ ### The grid needs a bounded height
43
+
44
+ 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.
45
+
46
+ 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:
47
+
48
+ ```tsx
49
+ // ❌ Grows to fit all rows — virtualization is effectively off
50
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
51
+ <div style={{ flex: 1 }}>
52
+ <DataGrid rows={rows} columns={columns} height="100%" />
53
+ </div>
54
+ </div>
55
+
56
+ // ✅ Bounded — only visible rows render
57
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
58
+ <div style={{ flex: 1, minHeight: 0 }}>
59
+ <DataGrid rows={rows} columns={columns} height="100%" />
60
+ </div>
61
+ </div>
62
+ ```
63
+
64
+ `height="100%"` only helps if every ancestor up to a fixed-height element also has a definite height.
65
+
66
+ 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.
67
+
68
+ 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
69
 
42
70
  ## Configuration
43
71
 
@@ -157,7 +185,7 @@ When using detail panels, row heights become variable. The grid automatically:
157
185
 
158
186
  ## Virtualization with Grouping/Tree Data
159
187
 
160
- Row grouping and tree data work seamlessly with virtualization:
188
+ 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
189
 
162
190
  ```tsx
163
191
  // Tree data with virtualization
@@ -170,7 +198,7 @@ Row grouping and tree data work seamlessly with virtualization:
170
198
  />
171
199
  ```
172
200
 
173
- **Note:** When groups are collapsed, only visible rows are rendered. Expanding a group dynamically adds rows to the render list.
201
+ **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
202
 
175
203
  ## Debugging Virtualization
176
204
 
@@ -192,13 +220,13 @@ You should see console logs only for visible rows + overscan buffer.
192
220
 
193
221
  ## Known Limitations
194
222
 
195
- 1. **Column virtualization** - Not yet implemented (all columns are rendered)
223
+ 1. **Unbounded containers** - Virtualization needs a bounded viewport height (see above)
196
224
  2. **Dynamic row heights** - Requires full dataset scan for accurate scroll calculations
197
225
  3. **Horizontal scrolling** - May show brief flicker with very wide grids
198
226
 
199
227
  ## Future Improvements
200
228
 
201
- - [ ] Column virtualization for grids with 50+ columns
229
+ - [x] Column virtualization (unpinned columns, 6-column overscan)
202
230
  - [x] Adaptive overscan based on scroll velocity (`overscanRowCount` prop, v2.0.4)
203
231
  - [ ] Virtual scrollbar for extremely large datasets (millions of rows)
204
232
  - [ ] Intersection Observer API for better scroll detection
@@ -208,7 +236,7 @@ You should see console logs only for visible rows + overscan buffer.
208
236
  | Feature | OpenGridX | MUI X DataGrid |
209
237
  |---------|--------------|----------------|
210
238
  | Row virtualization | ✅ Yes | ✅ Yes |
211
- | Column virtualization | ❌ Planned | ✅ Yes (Pro) |
239
+ | Column virtualization | ✅ Yes (unpinned columns) | ✅ Yes (Pro) |
212
240
  | Variable row heights | ✅ Yes | ✅ Yes |
213
241
  | Overscan buffer | 3–30 rows (adaptive, velocity-based) | 3-8 rows (adaptive) |
214
242
  | Max recommended rows | 100,000+ | 100,000+ |
package/docs/roadmap.md CHANGED
@@ -5,7 +5,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
5
5
  ## ✅ Implemented Features
6
6
 
7
7
  ### Core
8
- * **Virtualization**: High-performance row virtualization for handling large datasets. Column virtualization is planned (currently all columns are rendered).
8
+ * **Virtualization**: Row virtualization plus horizontal virtualization of unpinned columns. Requires a bounded grid height (see `docs/features/virtualization.md`).
9
9
  * **Sorting**: Client-side and server-side multi-column sorting.
10
10
  * **Filtering**: Client-side filtering with support for various operators.
11
11
  * **Pagination**: Client-side and server-side pagination with customizable page sizes.
@@ -74,21 +74,27 @@ This document tracks the current status of features in `OpenGridX` and outlines
74
74
 
75
75
  ### Advanced Data Features
76
76
  * **Interactive Pivot Builder**: Drag-and-drop UI for users to dynamically create pivot tables.
77
- * **Native PDF Reporting**: Automated PDF generation with customizable layouts.
78
77
  * **Advanced Charts Integration**: Inline sparklines and trend visualization.
79
78
 
80
- ### DX & Publishing
81
- * **npm publish**: Finalize `package.json` metadata, `README.md`, and publish to npm registry.
82
-
83
79
  ### Demo Site
84
80
  * **Full source viewer coverage**: Migrate all 32 examples to `DocsLayout` using Vite `?raw` auto-loading (eliminates string-literal source maintenance). See `docs/superpowers/plans/2026-07-29-demo-app-refactor.md`.
85
81
  * **Syntax highlighting**: Prism.js token-based highlighting in the source viewer.
86
- * **GitHub Pages deployment**: CI/CD via GitHub Actions (`base: '/OpenGridX/'` already configured in `vite.config.js`).
87
82
 
88
83
  ## ✅ Implemented Features (Recent)
89
84
 
85
+ ### Consumer defect fixes *(v2.1.0, 2026-09-23)*
86
+ - Row-grouping subtotals now use the shared aggregation functions (nulls ignored, `unique` supported, `availableAggregationFunctions` honoured); `min`/`max` no longer overflow the call stack on very large datasets.
87
+ - Group and tree-data expansion state survives row updates, including server-side lazily loaded children.
88
+ - Rows are positioned correctly when rows are pinned to the top; keyboard navigation and `scrollToIndexes` keep the target row fully visible below the sticky header.
89
+ - `valueFormatter` applies under row grouping; `formattedValue` is passed to `renderCell`.
90
+ - `slots.footer`, `slots.noRowsOverlay` and `slots.loadingOverlay` are rendered (they were typed but ignored).
91
+ - New `onRowDoubleClick`; `ColumnVisibilityPanel` exported; `exportToExcelAdvanced` accepts `groupedRows`.
92
+ - Dev-mode warnings for an unbounded grid container and for `pagination` with row grouping.
93
+ - Real-browser test project (Vitest browser mode + Playwright/Chromium), `npm run test:browser`; the npm publish workflow now runs unit and browser tests before publishing.
94
+ - Already shipped, previously listed as upcoming: npm publishing (since 0.1.0), native PDF export (v1.2.1), GitHub Pages deployment.
95
+
90
96
  ### Library Hardening *(completed 2026-09-02)*
91
- - **`GridRowMeta`**: Hierarchy metadata (`hasChildren`, `treeDepth`, `groupingField`, `groupingValue`, `descendantCount`, `isExpanded`, `isGroupRow`) moved from `GridRowModel` into a separate `Map<GridRowId, GridRowMeta>`. Exposed as `params.rowMeta` in `renderCell`. Runtime shim maintains backward compatibility; removed in v2.0.
97
+ - **`GridRowMeta`**: Hierarchy metadata (`hasChildren`, `treeDepth`, `groupingField`, `groupingValue`, `descendantCount`, `isExpanded`, `isGroupRow`) moved from `GridRowModel` into a separate `Map<GridRowId, GridRowMeta>`. Exposed as `params.rowMeta` in `renderCell`. Runtime shim (underscore-prefixed fields on the row object) maintains backward compatibility; still present as of v2.1.0, deprecated for removal in a future major version — see `docs/architecture/grid-row-meta.md`.
92
98
  - **`CellErrorBoundary`**: Custom `renderCell` errors are now caught per-cell. Failing cells show a `⚠` indicator with the error as a tooltip; the rest of the grid renders normally.
93
99
  - **`GridLocaleText` / `localeText` prop**: All user-visible pagination strings (`paginationRowsPerPage`, `paginationOf`, `paginationPage`, `noRowsLabel`) are now overrideable for internationalisation.
94
100
  - **Core hook tests**: `useGridRowPipeline`, `useGridControlledState`, `useGridKeyboardNavigation` now have full test coverage (25 new tests).
@@ -244,6 +244,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
244
244
  resetKey={row}
245
245
  renderFn={() => colDef.renderCell!({
246
246
  value,
247
+ formattedValue,
247
248
  row,
248
249
  field: colDef.field,
249
250
  colDef,
@@ -122,6 +122,13 @@
122
122
  color: var(--ogx-color-gray-300);
123
123
  }
124
124
 
125
+ .ogx__loading-overlay {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ min-height: 200px;
130
+ }
131
+
125
132
  .ogx-error-overlay {
126
133
  position: absolute;
127
134
  top: 0;
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { DataGrid } from './DataGrid';
4
+ import type { GridColDef } from '../../types';
5
+
6
+ // Regression tests for the OpenGridX 2.0.4 consumer defect report.
7
+
8
+ type Row = { id: number; region: string; amount: number };
9
+
10
+ const ROWS: Row[] = [
11
+ { id: 1, region: 'North', amount: 1200 },
12
+ { id: 2, region: 'North', amount: 800 },
13
+ { id: 3, region: 'South', amount: 450 },
14
+ ];
15
+
16
+ const COLUMNS: GridColDef<Row>[] = [
17
+ { field: 'region', headerName: 'Region', width: 150 },
18
+ { field: 'amount', headerName: 'Amount', width: 150, valueFormatter: ({ value }) => `$${String(value)}` },
19
+ ];
20
+
21
+ describe('D3 — valueFormatter survives row grouping', () => {
22
+ it('renders formatted values without grouping', () => {
23
+ render(<DataGrid rows={ROWS} columns={COLUMNS} />);
24
+ expect(screen.getByText('$1200')).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders formatted values for leaf rows when rowGroupingModel is active', () => {
28
+ render(<DataGrid rows={ROWS} columns={COLUMNS} rowGroupingModel={['region']} defaultGroupingExpansionDepth={-1} />);
29
+ expect(screen.getByText('$1200')).toBeInTheDocument();
30
+ expect(screen.queryByText('1200')).not.toBeInTheDocument();
31
+ });
32
+
33
+ it('passes formattedValue to a consumer renderCell', () => {
34
+ const renderCell = vi.fn(() => null);
35
+ const cols: GridColDef<Row>[] = [COLUMNS[0], { ...COLUMNS[1], renderCell }];
36
+ render(<DataGrid rows={ROWS} columns={cols} />);
37
+ expect(renderCell).toHaveBeenCalledWith(expect.objectContaining({ value: 1200, formattedValue: '$1200' }));
38
+ });
39
+ });
40
+
41
+ describe('D2 — slots that were typed but never rendered', () => {
42
+ it('renders slots.footer with grid state, replacing the default pagination', () => {
43
+ const Footer = vi.fn((props: Record<string, unknown>) => <div data-testid="footer">rows:{String(props.rowCount)} note:{String(props.note)}</div>);
44
+ render(
45
+ <DataGrid
46
+ rows={ROWS}
47
+ columns={COLUMNS}
48
+ pagination
49
+ aggregationModel={{ amount: 'sum' }}
50
+ slots={{ footer: Footer }}
51
+ slotProps={{ footer: { note: 'hi' } }}
52
+ />
53
+ );
54
+ expect(screen.getByTestId('footer')).toHaveTextContent('rows:3 note:hi');
55
+ expect(Footer).toHaveBeenCalledWith(expect.objectContaining({ aggregationResult: { amount: 2450 } }), undefined);
56
+ expect(document.querySelector('.ogx-pagination, .ogx__pagination')).toBeNull();
57
+ });
58
+
59
+ it('renders slots.noRowsOverlay when there are no rows', () => {
60
+ render(<DataGrid rows={[]} columns={COLUMNS} slots={{ noRowsOverlay: () => <div>Nothing here</div> }} />);
61
+ expect(screen.getByText('Nothing here')).toBeInTheDocument();
62
+ });
63
+
64
+ it('renders slots.loadingOverlay while loading with no rows', () => {
65
+ render(<DataGrid rows={[]} columns={COLUMNS} loading slots={{ loadingOverlay: () => <div>Fetching…</div> }} />);
66
+ expect(screen.getByText('Fetching…')).toBeInTheDocument();
67
+ });
68
+ });
69
+
70
+ describe('D6 — onRowDoubleClick', () => {
71
+ it('fires with the row params on double-click', () => {
72
+ const onRowDoubleClick = vi.fn();
73
+ render(<DataGrid rows={ROWS} columns={COLUMNS} onRowDoubleClick={onRowDoubleClick} />);
74
+ fireEvent.doubleClick(screen.getByText('$800'));
75
+ expect(onRowDoubleClick).toHaveBeenCalledWith(expect.objectContaining({ id: 2, row: ROWS[1] }));
76
+ });
77
+
78
+ it('fires in list view as well', () => {
79
+ const onRowDoubleClick = vi.fn();
80
+ render(
81
+ <DataGrid rows={ROWS} columns={COLUMNS} listView
82
+ listViewColumn={{ field: 'card', renderCell: ({ row }) => <span>card-{row.id}</span> }}
83
+ onRowDoubleClick={onRowDoubleClick} />
84
+ );
85
+ fireEvent.doubleClick(screen.getByText('card-3'));
86
+ expect(onRowDoubleClick).toHaveBeenCalledWith(expect.objectContaining({ id: 3, row: ROWS[2] }));
87
+ });
88
+ });
89
+
90
+ describe('Row grouping — expansion survives row edits', () => {
91
+ it('keeps a user-expanded group open when the rows prop changes', () => {
92
+ const { rerender, container } = render(<DataGrid rows={ROWS} columns={COLUMNS} rowGroupingModel={['region']} />);
93
+ const rowCount = () => container.querySelectorAll('.ogx__rows [role="row"]').length;
94
+ expect(rowCount()).toBe(2);
95
+
96
+ fireEvent.click(container.querySelector('.ogx__rows [role="row"]')!);
97
+ expect(rowCount()).toBe(4);
98
+
99
+ rerender(<DataGrid rows={ROWS.map(r => (r.id === 1 ? { ...r, amount: 1300 } : r))} columns={COLUMNS} rowGroupingModel={['region']} />);
100
+ expect(rowCount()).toBe(4);
101
+ expect(screen.getByText('$1300')).toBeInTheDocument();
102
+ });
103
+ });