@opencorestack/opengridx 2.0.3 → 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.
- package/CHANGELOG.md +54 -3
- package/dist/index.d.ts +50 -2
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +3805 -3692
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +14 -4
- package/docs/components/column-visibility.md +40 -16
- package/docs/components/datagrid.md +1 -0
- package/docs/customization/slots-api.md +18 -6
- package/docs/features/export-guide.md +22 -3
- package/docs/features/sorting-pagination.md +2 -0
- package/docs/features/tree-data-grouping.md +8 -1
- package/docs/features/virtualization.md +66 -17
- package/docs/roadmap.md +13 -7
- package/lib/components/Cell/Cell.tsx +1 -0
- package/lib/components/DataGrid/DataGrid.css +7 -0
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +103 -0
- package/lib/components/DataGrid/DataGrid.tsx +45 -11
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +183 -0
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridListView.tsx +3 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +3 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +8 -1
- package/lib/components/ListView/ListViewRow.tsx +3 -0
- package/lib/components/Row/Row.tsx +10 -0
- package/lib/hooks/core/useGridColumns.tsx +8 -3
- package/lib/hooks/core/useGridDevWarnings.ts +42 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +12 -14
- package/lib/hooks/core/useGridScrollSync.ts +62 -15
- package/lib/hooks/core/useGridVirtualization.ts +4 -1
- package/lib/hooks/core/useGridVisibleRows.ts +3 -2
- package/lib/hooks/features/useAggregation.ts +3 -38
- package/lib/hooks/useRowGrouping.test.ts +162 -1
- package/lib/hooks/useRowGrouping.ts +42 -59
- package/lib/hooks/useTreeData.test.ts +83 -0
- package/lib/hooks/useTreeData.ts +44 -43
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +22 -2
- package/lib/utils/aggregation/aggregation.test.ts +16 -0
- package/lib/utils/aggregation/index.ts +62 -0
- package/lib/utils/export/exportToExcelAdvanced.test.ts +109 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +89 -5
- package/lib/utils/scroll/index.ts +28 -0
- package/llms.txt +22 -13
- package/package.json +7 -3
package/docs/API_REFERENCE.md
CHANGED
|
@@ -18,9 +18,10 @@ The main component for displaying and interacting with data.
|
|
|
18
18
|
| `rowHeight` | `number` | `52` | Height of each row in pixels. |
|
|
19
19
|
| `headerHeight` | `number` | `56` | Height of the header row. |
|
|
20
20
|
| `autoHeight` | `boolean` | `false` | Adjust grid height to match row total. |
|
|
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. |
|
|
21
22
|
| `loading` | `boolean` | `false` | Shows a loading skeleton overlay. |
|
|
22
23
|
| `checkboxSelection` | `boolean` | `false` | Enable row selection via checkboxes. |
|
|
23
|
-
| `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). |
|
|
24
25
|
| `paginationMode` | `'client' \| 'server' \| 'infinite'` | `'client'` | How to handle paging. |
|
|
25
26
|
| `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). |
|
|
26
27
|
| `onPaginationModelChange` | `(model: GridPaginationModel) => void` | — | Fired when page or page size changes. |
|
|
@@ -29,11 +30,12 @@ The main component for displaying and interacting with data.
|
|
|
29
30
|
| `height` | `number \| string` | `undefined` | Total height of the grid container. |
|
|
30
31
|
| `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Visual row density. |
|
|
31
32
|
| `initialState` | `GridInitialState` | `undefined` | Starting state for sorting, filters, etc. |
|
|
32
|
-
| `slots` | `GridSlots` | `{}` | Custom component overrides. |
|
|
33
|
+
| `slots` | `GridSlots` | `{}` | Custom component overrides: `toolbar`, `pagination`, `noRowsOverlay`, `loadingOverlay`, `footer`. See [Slots API](customization/slots-api.md). |
|
|
33
34
|
| `slotProps` | `Record<string, unknown>` | `{}` | Props passed to custom slots. |
|
|
34
35
|
| `filterModel` | `GridFilterModel` | `undefined` | Active filters. |
|
|
35
36
|
| `sortModel` | `GridSortItem[]` | `undefined` | Active sorting. |
|
|
36
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. |
|
|
37
39
|
| `onCellClick` | `(params: GridCellParams) => void` | — | Fired when a cell is clicked. |
|
|
38
40
|
| `onStateChange` | `(state: GridState) => void` | — | Fired on any internal state update. |
|
|
39
41
|
| `processRowUpdate` | `(new, old) => R \| Promise<R>` | — | Fired after a cell edit is committed. |
|
|
@@ -301,7 +303,7 @@ Defines the behavior and appearance of a single column.
|
|
|
301
303
|
|
|
302
304
|
| Property | Type | Description |
|
|
303
305
|
| :--- | :--- | :--- |
|
|
304
|
-
| `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`. |
|
|
305
307
|
| `renderHeader` | `(params: GridRenderHeaderParams) => ReactNode` | Custom header cell renderer. Use for icons, sort indicators, or rich headers. |
|
|
306
308
|
| `renderEditCell` | `(params: GridRenderCellParams) => ReactNode` | Custom editor rendered when the cell enters edit mode. Requires `editable: true`. |
|
|
307
309
|
|
|
@@ -350,11 +352,19 @@ Defines the behavior and appearance of a single column.
|
|
|
350
352
|
| `exportToCsv(rows, cols, options?)` | `void` | Triggers download of CSV file. |
|
|
351
353
|
| `exportToJson(rows, cols, options?)` | `void` | Triggers download of JSON file. |
|
|
352
354
|
| `exportToExcel(rows, cols, options?)` | `void` | Basic `.xls` export (zero-dep). |
|
|
353
|
-
| `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+). |
|
|
354
356
|
| `printGrid(rows, cols, title \| options)` | `void` | Opens browser print dialog. |
|
|
355
357
|
|
|
356
358
|
---
|
|
357
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
|
+
|
|
358
368
|
## 🎨 Theming & Styling
|
|
359
369
|
|
|
360
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
|
-
|
|
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 {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
###
|
|
28
|
-
You can also render it persistently beside the grid or within a custom modal.
|
|
48
|
+
### Props (`ColumnVisibilityPanelProps`)
|
|
29
49
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
@@ -35,6 +35,7 @@ function MyGrid() {
|
|
|
35
35
|
| `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). Omit for uncontrolled; use `initialState` to set the initial page/pageSize. |
|
|
36
36
|
| `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Available page size options. |
|
|
37
37
|
| `initialState` | `GridInitialState` | `undefined` | Initial configuration (sorting, columns, pagination, etc). |
|
|
38
|
+
| `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. |
|
|
38
39
|
|
|
39
40
|
## 🔃 Sorting
|
|
40
41
|
|
|
@@ -113,30 +113,42 @@ function CustomLoader() {
|
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
### 5. `footer`
|
|
116
|
-
Replace the entire footer section
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
139
|
+
{String(rowCount)} rows · Total: {totals?.amount ?? '—'}
|
|
131
140
|
</div>
|
|
132
141
|
);
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
<DataGrid
|
|
136
|
-
|
|
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
|
|
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` | — |
|
|
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` | — |
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -84,16 +112,37 @@ Disable virtualization for small datasets:
|
|
|
84
112
|
|
|
85
113
|
### Overscan Buffer
|
|
86
114
|
|
|
87
|
-
The overscan buffer determines how many extra rows are rendered outside the viewport.
|
|
115
|
+
The overscan buffer determines how many extra rows are rendered outside the viewport. The grid adapts this buffer automatically based on scroll velocity so fast scrolls never produce blank flashes.
|
|
116
|
+
|
|
117
|
+
**Adaptive algorithm (`useGridScrollSync`):**
|
|
118
|
+
- Scroll velocity (px/ms) is measured on every scroll event
|
|
119
|
+
- The overscan tier is chosen from the table below and applied immediately
|
|
120
|
+
- 200 ms after scrolling stops the buffer decays back to `overscanRowCount`
|
|
121
|
+
|
|
122
|
+
| Velocity (px/ms) | Overscan rows |
|
|
123
|
+
|---|---|
|
|
124
|
+
| < 0.5 | 3 |
|
|
125
|
+
| 0.5 – 3 | 5 |
|
|
126
|
+
| 3 – 15 | 12 |
|
|
127
|
+
| 15 – 40 | 20 |
|
|
128
|
+
| > 40 | 30 |
|
|
129
|
+
|
|
130
|
+
**Configuring the floor:**
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// Never render fewer than 5 rows outside the viewport
|
|
134
|
+
<DataGrid
|
|
135
|
+
rows={rows}
|
|
136
|
+
columns={columns}
|
|
137
|
+
overscanRowCount={5}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
88
140
|
|
|
89
|
-
|
|
90
|
-
- Default overscan: **5 rows** (above and below viewport)
|
|
91
|
-
- Total rendered rows: ~(viewport rows + 10)
|
|
141
|
+
`overscanRowCount` (default `3`) sets the minimum — the adaptive algorithm always produces a value ≥ this floor.
|
|
92
142
|
|
|
93
143
|
**Trade-offs:**
|
|
94
|
-
- **Smaller
|
|
95
|
-
- **Larger
|
|
96
|
-
- **Very large overscan (10+)**: Diminishing returns, wasted rendering
|
|
144
|
+
- **Smaller floor (1-3)**: Less idle memory, relies on adaptive headroom for fast scrolls
|
|
145
|
+
- **Larger floor (5-8)**: Pre-renders more rows at rest, trades memory for smoother slow scrolls
|
|
97
146
|
|
|
98
147
|
### Scroll Performance
|
|
99
148
|
|
|
@@ -136,7 +185,7 @@ When using detail panels, row heights become variable. The grid automatically:
|
|
|
136
185
|
|
|
137
186
|
## Virtualization with Grouping/Tree Data
|
|
138
187
|
|
|
139
|
-
|
|
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`).
|
|
140
189
|
|
|
141
190
|
```tsx
|
|
142
191
|
// Tree data with virtualization
|
|
@@ -149,7 +198,7 @@ Row grouping and tree data work seamlessly with virtualization:
|
|
|
149
198
|
/>
|
|
150
199
|
```
|
|
151
200
|
|
|
152
|
-
**Note:**
|
|
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`.
|
|
153
202
|
|
|
154
203
|
## Debugging Virtualization
|
|
155
204
|
|
|
@@ -171,14 +220,14 @@ You should see console logs only for visible rows + overscan buffer.
|
|
|
171
220
|
|
|
172
221
|
## Known Limitations
|
|
173
222
|
|
|
174
|
-
1. **
|
|
223
|
+
1. **Unbounded containers** - Virtualization needs a bounded viewport height (see above)
|
|
175
224
|
2. **Dynamic row heights** - Requires full dataset scan for accurate scroll calculations
|
|
176
225
|
3. **Horizontal scrolling** - May show brief flicker with very wide grids
|
|
177
226
|
|
|
178
227
|
## Future Improvements
|
|
179
228
|
|
|
180
|
-
- [
|
|
181
|
-
- [
|
|
229
|
+
- [x] Column virtualization (unpinned columns, 6-column overscan)
|
|
230
|
+
- [x] Adaptive overscan based on scroll velocity (`overscanRowCount` prop, v2.0.4)
|
|
182
231
|
- [ ] Virtual scrollbar for extremely large datasets (millions of rows)
|
|
183
232
|
- [ ] Intersection Observer API for better scroll detection
|
|
184
233
|
|
|
@@ -187,9 +236,9 @@ You should see console logs only for visible rows + overscan buffer.
|
|
|
187
236
|
| Feature | OpenGridX | MUI X DataGrid |
|
|
188
237
|
|---------|--------------|----------------|
|
|
189
238
|
| Row virtualization | ✅ Yes | ✅ Yes |
|
|
190
|
-
| Column virtualization |
|
|
239
|
+
| Column virtualization | ✅ Yes (unpinned columns) | ✅ Yes (Pro) |
|
|
191
240
|
| Variable row heights | ✅ Yes | ✅ Yes |
|
|
192
|
-
| Overscan buffer |
|
|
241
|
+
| Overscan buffer | 3–30 rows (adaptive, velocity-based) | 3-8 rows (adaptive) |
|
|
193
242
|
| Max recommended rows | 100,000+ | 100,000+ |
|
|
194
243
|
|
|
195
244
|
## Related Documentation
|
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**:
|
|
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;
|
|
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).
|
|
@@ -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
|
+
});
|