@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.
- package/CHANGELOG.md +45 -3
- package/dist/index.d.ts +43 -2
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +3927 -3839
- 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 +13 -4
- package/docs/components/column-visibility.md +40 -16
- 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 +36 -8
- 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 +42 -10
- 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/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 +15 -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
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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:**
|
|
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. **
|
|
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
|
-
- [
|
|
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 |
|
|
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**:
|
|
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
|
+
});
|