@opencorestack/opengridx 2.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Performance Best Practices
|
|
2
|
+
|
|
3
|
+
Tips for getting the most out of OpenGridX with large datasets.
|
|
4
|
+
|
|
5
|
+
## Stabilize column definitions
|
|
6
|
+
|
|
7
|
+
The most common cause of unnecessary re-renders is a new column array being created on every render.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
// ✅ Good: stable reference
|
|
11
|
+
const columns = useMemo<GridColDef[]>(() => [
|
|
12
|
+
{ field: 'id', headerName: 'ID', width: 70 },
|
|
13
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
14
|
+
], []);
|
|
15
|
+
|
|
16
|
+
// ❌ Bad: new array on every render → column layout is recomputed every render
|
|
17
|
+
const columns = [
|
|
18
|
+
{ field: 'id', headerName: 'ID', width: 70 },
|
|
19
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
20
|
+
];
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The same applies to the `rows` prop — if you derive rows from server data, wrap the derivation in `useMemo`.
|
|
24
|
+
|
|
25
|
+
## Keep cell renderers lightweight
|
|
26
|
+
|
|
27
|
+
`renderCell` is called for every visible cell on every render. Expensive work inside it multiplies with overscan.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// ✅ Good: memoized sub-component
|
|
31
|
+
const StatusBadge = React.memo(({ value }: { value: string }) => (
|
|
32
|
+
<span className={`badge badge--${value}`}>{value}</span>
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
{ field: 'status', renderCell: (p) => <StatusBadge value={String(p.value)} /> }
|
|
36
|
+
|
|
37
|
+
// ❌ Bad: inline object / new function reference every render
|
|
38
|
+
{ field: 'status', renderCell: (p) => <span style={{ color: 'red' }}>{String(p.value)}</span> }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use fixed row heights when possible
|
|
42
|
+
|
|
43
|
+
Every row has the same height (`rowHeight` or the `density` preset); there is no per-row height callback. Expanded detail panels add their own height, which the grid folds into a cumulative-height table, so many expanded panels (and `'auto'` panels, which are measured after they render) cost more than plain rows.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Fixed height — fastest
|
|
47
|
+
<DataGrid rows={rows} columns={columns} rowHeight={48} />
|
|
48
|
+
|
|
49
|
+
// Variable height — necessary for detail panels, but adds cost
|
|
50
|
+
<DataGrid rows={rows} columns={columns} getDetailPanelHeight={() => 200} />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Choose the right density
|
|
54
|
+
|
|
55
|
+
`density="compact"` and `density="comfortable"` replace `rowHeight` with a preset (32 px / 72 px, or the theme's `grid.rowHeightCompact` / `grid.rowHeightComfortable`); `density="standard"` uses `rowHeight`. Compact rows fit more rows in the viewport.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<DataGrid density="compact" /> // 32 px rows — more rows in viewport
|
|
59
|
+
<DataGrid density="standard" /> // rowHeight (default 52 px)
|
|
60
|
+
<DataGrid density="comfortable" /> // 72 px rows
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Tune the overscan buffer
|
|
64
|
+
|
|
65
|
+
`overscanRowCount` (default `3`) is the minimum rows rendered outside the viewport. The grid raises it automatically based on scroll velocity. Raise the floor only if you still see blank flashes at the default.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// Raise the floor for very fast scrolling environments
|
|
69
|
+
<DataGrid overscanRowCount={8} rows={rows} columns={columns} />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See [Virtualization](./features/virtualization.md) for the full adaptive-overscan details.
|
|
73
|
+
|
|
74
|
+
## Avoid `autoHeight` on large datasets
|
|
75
|
+
|
|
76
|
+
`autoHeight` sizes the grid to all of its rows, so every row is rendered. Only use it when the dataset is small (< 100 rows), or together with `pagination`.
|
|
77
|
+
|
|
78
|
+
## Give the grid a bounded height, and import the stylesheet
|
|
79
|
+
|
|
80
|
+
Virtualization only works when the viewport has a fixed height: pass `height`, or put the grid in a container with a definite height (a flex child needs `min-height: 0`). In an unbounded container the viewport grows to fit every row and every row is rendered; a dev-mode warning reports this.
|
|
81
|
+
|
|
82
|
+
The same happens when the stylesheet is missing, because the viewport's `overflow: auto` comes from it (a dev-mode warning reports a missing stylesheet too). Import it once in your app:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import '@opencorestack/opengridx/styles';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Know the browser's height limit
|
|
89
|
+
|
|
90
|
+
The grid sizes its scroll content to `rowCount × rowHeight` pixels and does not scale scroll positions. Browsers cap an element's height (Chromium at 33,554,432 px, Firefox lower), so at the default 52 px row height rows past roughly 645,000 cannot be scrolled to. A dev-mode warning reports it. For more rows, use pagination, `paginationMode="server"` or infinite scroll with a `dataSource`.
|
|
91
|
+
|
|
92
|
+
## Use server-side operations for very large datasets
|
|
93
|
+
|
|
94
|
+
Client-side filtering, sorting, and pagination load the full dataset into JS. For very large datasets, prefer server-side modes. The simplest way is a `dataSource`: the grid calls `getRows` with the page range, sort model and filter model, and shows the response (see [Server-Side Data](./features/data-source.md)).
|
|
95
|
+
|
|
96
|
+
Without a `dataSource`, fetch the rows yourself from the change callbacks and control the models:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<DataGrid
|
|
100
|
+
rows={pageRows} // the current page, already filtered and sorted by the server
|
|
101
|
+
columns={columns}
|
|
102
|
+
filterMode="server"
|
|
103
|
+
sortingMode="server"
|
|
104
|
+
pagination
|
|
105
|
+
paginationMode="server"
|
|
106
|
+
rowCount={totalRows} // server total
|
|
107
|
+
paginationModel={paginationModel}
|
|
108
|
+
onPaginationModelChange={setPaginationModel} // refetch in an effect on these models
|
|
109
|
+
sortModel={sortModel}
|
|
110
|
+
onSortModelChange={setSortModel}
|
|
111
|
+
filterModel={filterModel}
|
|
112
|
+
onFilterModelChange={setFilterModel}
|
|
113
|
+
/>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In server modes the grid shows `rows` as given: it does not re-filter, re-sort or slice them (v3.0+; earlier versions did unless a `dataSource` was set).
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
## Related
|
|
120
|
+
|
|
121
|
+
- [Virtualization](./features/virtualization.md)
|
|
122
|
+
- [Server-Side Data](./features/data-source.md)
|
|
123
|
+
- [Sorting & Pagination](./features/sorting-pagination.md)
|
|
124
|
+
- [Infinite Scroll](./features/infinite-scroll.md)
|
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.
|
|
@@ -42,9 +42,9 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
42
42
|
|
|
43
43
|
### Accessibility & Clipboard *(completed 2026-03-03)*
|
|
44
44
|
* **ARIA roles**: `role="grid"`, `role="row"`, `role="gridcell"`, `role="columnheader"` on correct elements.
|
|
45
|
-
* **ARIA attributes**: `aria-sort`, `aria-selected`, `aria-label`, `aria-expanded`, `aria-readonly`, `aria-haspopup` throughout.
|
|
46
|
-
* **Keyboard navigation**:
|
|
47
|
-
* **`aria-rowcount` and `aria-colcount`**:
|
|
45
|
+
* **ARIA attributes**: `aria-sort`, `aria-selected`, `aria-label`, `aria-expanded`, `aria-readonly`, `aria-haspopup` throughout. v3.0 adds `aria-level` / `aria-expanded` on hierarchy rows, `aria-multiselectable`, and `aria-sort` on the primary sort column only.
|
|
46
|
+
* **Keyboard navigation**: Enter / Escape / Arrow keys / Home / End / PageUp / PageDown, Shift+Space and Ctrl+A selection, Alt+ArrowDown for the column menu; the grid is a single Tab stop (v3.0). See [Keyboard & Accessibility](features/keyboard-navigation.md).
|
|
47
|
+
* **`aria-rowcount` / `aria-rowindex` and `aria-colcount` / `aria-colindex`**: Count header rows and system columns, number rows across pages and pinned rows, and match between header and body cells (corrected in v3.0).
|
|
48
48
|
* **Clipboard**: Full TSV-formatted copy support (Ctrl+C / Cmd+C) for seamless integration with Excel/Google Sheets.
|
|
49
49
|
|
|
50
50
|
### Advanced Excel Export *(completed 2026-03-03)*
|
|
@@ -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`.
|
|
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`. The runtime shim (underscore-prefixed fields on copied row objects) was kept through v2.x and removed in v3.0 — see `docs/migration/v2-to-v3.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).
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
/* Show ring when keyboard navigation is active */
|
|
24
24
|
.ogx--kb .ogx__cell--focused {
|
|
25
|
-
outline: 2px solid var(--ogx-color-primary, #2563eb);
|
|
25
|
+
outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary, #2563eb));
|
|
26
26
|
outline-offset: -2px;
|
|
27
27
|
}
|
|
28
28
|
.ogx__cell--editable {
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
.ogx:not(.ogx--row-spanning) .ogx__row--selected:hover .ogx__cell--pinned-right {
|
|
71
71
|
background-color: var(--ogx-grid-row-selected-hover-background);
|
|
72
72
|
}
|
|
73
|
+
/* Section edge cells show a shadow outside their box; the content stays clipped by .ogx__cell-content. */
|
|
74
|
+
.ogx__cell--pinned-left-last,
|
|
75
|
+
.ogx__cell--pinned-right-first {
|
|
76
|
+
overflow: visible;
|
|
77
|
+
}
|
|
73
78
|
.ogx__cell--pinned-left-last::after {
|
|
74
79
|
content: '';
|
|
75
80
|
position: absolute;
|
|
@@ -101,6 +106,13 @@
|
|
|
101
106
|
white-space: nowrap;
|
|
102
107
|
width: 100%;
|
|
103
108
|
}
|
|
109
|
+
/* Default rendering of a type: 'image' column: the image fits the row height. */
|
|
110
|
+
.ogx__cell-image {
|
|
111
|
+
display: block;
|
|
112
|
+
max-width: 100%;
|
|
113
|
+
max-height: calc(var(--ogx-row-height) - 8px);
|
|
114
|
+
object-fit: contain;
|
|
115
|
+
}
|
|
104
116
|
.ogx__cell--spanned {
|
|
105
117
|
background-color: var(--ogx-grid-background);
|
|
106
118
|
z-index: 10;
|
|
@@ -5,6 +5,7 @@ import type { CellColSpanInfo } from '../../hooks/features/useGridSpanning';
|
|
|
5
5
|
|
|
6
6
|
import { GridEditInputCell } from './GridEditInputCell';
|
|
7
7
|
import { CellErrorBoundary } from './CellErrorBoundary';
|
|
8
|
+
import { formatValueByType } from '../../utils/values';
|
|
8
9
|
|
|
9
10
|
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
10
11
|
onCellClick?: (params: GridCellParams<R>) => void;
|
|
@@ -15,25 +16,41 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
|
15
16
|
colDef: GridColDef<R>;
|
|
16
17
|
rowIndex: number;
|
|
17
18
|
colIndex: number;
|
|
19
|
+
/** 1-based `aria-colindex` (system columns included). Defaults to `colIndex + 1`. */
|
|
20
|
+
ariaColIndex?: number;
|
|
18
21
|
isSelected?: boolean;
|
|
19
22
|
onClick?: (event: React.MouseEvent) => void;
|
|
20
23
|
width?: number;
|
|
21
24
|
pinnedPosition?: GridPinnedPosition | null;
|
|
22
25
|
pinnedOffset?: number;
|
|
26
|
+
/**
|
|
27
|
+
* The cell is the last left-pinned or first right-pinned cell of its row: it gets
|
|
28
|
+
* `ogx__cell--pinned-left-last` / `ogx__cell--pinned-right-first` (the section edge shadow).
|
|
29
|
+
*/
|
|
30
|
+
isPinnedEdge?: boolean;
|
|
23
31
|
|
|
24
32
|
isFocused?: boolean;
|
|
25
33
|
isFocusVisible?: boolean;
|
|
26
34
|
|
|
35
|
+
/** Whether this cell may enter edit mode (column, row and `isCellEditable` already resolved). */
|
|
27
36
|
isEditable?: boolean;
|
|
28
37
|
isEditing?: boolean;
|
|
29
38
|
onEditStart?: () => void;
|
|
30
|
-
|
|
39
|
+
/** Ends this cell's edit. `field` identifies the cell, so a late call cannot end another cell's edit. */
|
|
40
|
+
onEditStop?: (cancel?: boolean, field?: string) => void;
|
|
31
41
|
onValueChange?: (newValue: unknown) => void;
|
|
32
42
|
|
|
33
43
|
colSpanInfo?: CellColSpanInfo;
|
|
34
44
|
rowSpan?: number;
|
|
35
45
|
isHiddenByRowSpan?: boolean;
|
|
36
46
|
rowMeta?: GridRowMeta;
|
|
47
|
+
/** The error the column's valueGetter threw for this cell (computed by Row); shown as a cell error. */
|
|
48
|
+
valueError?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface FormattedValue {
|
|
52
|
+
formattedValue: string;
|
|
53
|
+
error?: unknown;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
@@ -46,11 +63,13 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
46
63
|
colDef,
|
|
47
64
|
rowIndex,
|
|
48
65
|
colIndex,
|
|
66
|
+
ariaColIndex,
|
|
49
67
|
isSelected,
|
|
50
68
|
onClick,
|
|
51
69
|
width,
|
|
52
70
|
pinnedPosition,
|
|
53
71
|
pinnedOffset,
|
|
72
|
+
isPinnedEdge,
|
|
54
73
|
isFocused,
|
|
55
74
|
isFocusVisible,
|
|
56
75
|
isEditable,
|
|
@@ -62,7 +81,8 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
62
81
|
colSpanInfo,
|
|
63
82
|
rowSpan: rowSpanProp,
|
|
64
83
|
isHiddenByRowSpan,
|
|
65
|
-
rowMeta
|
|
84
|
+
rowMeta,
|
|
85
|
+
valueError
|
|
66
86
|
} = props;
|
|
67
87
|
|
|
68
88
|
// All hooks must come before any early returns (Rules of Hooks)
|
|
@@ -74,47 +94,85 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
74
94
|
}
|
|
75
95
|
}, [onCellValueChange, onValueChange, colDef.field]);
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
// A valueFormatter that throws is contained to its cell (shown as a cell error), like renderCell.
|
|
98
|
+
// Synthetic rows (group, subtotal, auto-created tree parent) have no value in most columns: the
|
|
99
|
+
// formatter, written for data rows, is only called for the values they do hold.
|
|
100
|
+
const isSyntheticRow = rowMeta?.isGroupRow === true;
|
|
101
|
+
const { formattedValue, error: formatError } = React.useMemo<FormattedValue>(() => {
|
|
102
|
+
if (colDef.valueFormatter && !(isSyntheticRow && value == null)) {
|
|
103
|
+
try {
|
|
104
|
+
return { formattedValue: colDef.valueFormatter({ value, row, field: colDef.field }) };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { formattedValue: '', error: error ?? new Error('valueFormatter failed') };
|
|
107
|
+
}
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
// Without a formatter the column type decides: locale date, Yes / No, singleSelect label.
|
|
111
|
+
return { formattedValue: formatValueByType(value, colDef) };
|
|
112
|
+
}, [value, row, colDef, isSyntheticRow]);
|
|
92
113
|
|
|
93
114
|
const resolvedCellClassName = React.useMemo(() => {
|
|
94
115
|
if (!colDef.cellClassName) return '';
|
|
95
116
|
if (typeof colDef.cellClassName === 'function') {
|
|
96
|
-
|
|
117
|
+
try {
|
|
118
|
+
return colDef.cellClassName({ value, formattedValue, row, field: colDef.field, colDef, rowIndex, colIndex, rowMeta }) || '';
|
|
119
|
+
} catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
return colDef.cellClassName;
|
|
99
|
-
}, [colDef, value, row, rowIndex, colIndex]);
|
|
124
|
+
}, [colDef, value, formattedValue, row, rowIndex, colIndex, rowMeta]);
|
|
100
125
|
|
|
101
|
-
const
|
|
126
|
+
const cellError = valueError ?? formatError;
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
const field = colDef.field;
|
|
129
|
+
const handleCommit = React.useCallback(() => { onEditStop?.(false, field); }, [onEditStop, field]);
|
|
130
|
+
const handleCancel = React.useCallback(() => { onEditStop?.(true, field); }, [onEditStop, field]);
|
|
131
|
+
|
|
132
|
+
// A failed renderCell is retried when the row, the renderer or the value changes.
|
|
133
|
+
const errorResetKey = React.useMemo(() => [row, colDef.renderCell, value], [row, colDef.renderCell, value]);
|
|
134
|
+
|
|
135
|
+
// A cell that unmounts while editing (scrolled out of the render window, filtered or paged away)
|
|
136
|
+
// gets no blur, so its edit would be left open with no editor. Commit it instead, as blur would.
|
|
137
|
+
// Deferred to a microtask so React StrictMode's simulated unmount + remount does not commit.
|
|
138
|
+
const editorMountedRef = React.useRef(false);
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
if (!isEditing || !onEditStop) return;
|
|
141
|
+
editorMountedRef.current = true;
|
|
142
|
+
return () => {
|
|
143
|
+
editorMountedRef.current = false;
|
|
144
|
+
queueMicrotask(() => {
|
|
145
|
+
if (!editorMountedRef.current) onEditStop(false, field);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
}, [isEditing, onEditStop, field]);
|
|
108
149
|
|
|
109
150
|
if (colSpanInfo?.spannedByColSpan) {
|
|
110
151
|
return null;
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
const colSpan = colSpanInfo?.cellProps.colSpan ?? 1;
|
|
155
|
+
const isColSpanOrigin = colSpan > 1;
|
|
156
|
+
// A merged cell takes the summed widths of the columns it covers (computed by Row from the
|
|
157
|
+
// resolved layout widths); the origin column's own min/max width does not apply to it.
|
|
158
|
+
const cellWidth = (isColSpanOrigin ? colSpanInfo?.cellProps.width : undefined) ?? width ?? colDef.width ?? 100;
|
|
159
|
+
const sizeStyle: React.CSSProperties = {
|
|
160
|
+
width: 'var(--width)',
|
|
161
|
+
'--width': typeof cellWidth === 'number' ? `${cellWidth}px` : cellWidth,
|
|
162
|
+
flexGrow: (isColSpanOrigin ? colSpanInfo?.cellProps.flexGrow : undefined) ?? colDef.flex ?? 0,
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
flexBasis: 'auto',
|
|
165
|
+
boxSizing: 'border-box',
|
|
166
|
+
minWidth: isColSpanOrigin ? undefined : colDef.minWidth,
|
|
167
|
+
maxWidth: isColSpanOrigin ? undefined : colDef.maxWidth,
|
|
168
|
+
} as React.CSSProperties;
|
|
169
|
+
|
|
113
170
|
if (isHiddenByRowSpan) {
|
|
171
|
+
// Keeps the covered column's slot so the rest of the row stays under its headers.
|
|
114
172
|
return (
|
|
115
173
|
<div
|
|
116
174
|
className="ogx__cell ogx__cell--hidden"
|
|
117
|
-
style={
|
|
175
|
+
style={sizeStyle}
|
|
118
176
|
role="presentation"
|
|
119
177
|
data-field={colDef.field}
|
|
120
178
|
data-colindex={colIndex}
|
|
@@ -123,6 +181,9 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
123
181
|
}
|
|
124
182
|
|
|
125
183
|
const handleClick = (e: React.MouseEvent) => {
|
|
184
|
+
// Clicks inside an open editor (placing the caret, opening a select) belong to the editor:
|
|
185
|
+
// treating them as cell clicks would move focus to the grid and commit the edit.
|
|
186
|
+
if (isEditing) return;
|
|
126
187
|
if (onCellClick) {
|
|
127
188
|
onCellClick({
|
|
128
189
|
row,
|
|
@@ -136,17 +197,19 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
136
197
|
onClick?.(e);
|
|
137
198
|
};
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
// The double-click is left to bubble so the row's onRowDoubleClick still fires.
|
|
201
|
+
const handleDoubleClick = () => {
|
|
202
|
+
// Double-clicking inside an open editor (to select a word) must not restart the edit.
|
|
203
|
+
if (isEditing || !isEditable) return;
|
|
204
|
+
if (onCellEditStart) {
|
|
205
|
+
onCellEditStart(colDef.field, value);
|
|
206
|
+
} else {
|
|
207
|
+
onEditStart?.();
|
|
147
208
|
}
|
|
148
209
|
};
|
|
149
210
|
|
|
211
|
+
const renderEditCell = colDef.renderEditCell;
|
|
212
|
+
|
|
150
213
|
const isPinned = pinnedPosition !== null && pinnedPosition !== undefined;
|
|
151
214
|
|
|
152
215
|
const classNames = [
|
|
@@ -158,24 +221,16 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
158
221
|
colDef.type && `ogx__cell--type-${colDef.type}`,
|
|
159
222
|
isPinned && 'ogx__cell--pinned',
|
|
160
223
|
isPinned && `ogx__cell--pinned-${pinnedPosition}`,
|
|
224
|
+
isPinned && isPinnedEdge && `ogx__cell--pinned-${pinnedPosition}-${pinnedPosition === 'left' ? 'last' : 'first'}`,
|
|
161
225
|
isEditing && 'ogx__cell--editing',
|
|
162
226
|
(rowSpanProp && rowSpanProp > 1) && 'ogx__cell--spanned',
|
|
163
227
|
resolvedCellClassName
|
|
164
228
|
].filter(Boolean).join(' ');
|
|
165
229
|
|
|
166
|
-
const colSpan = colSpanInfo?.cellProps?.colSpan || 1;
|
|
167
230
|
const rowSpan = rowSpanProp || 1;
|
|
168
|
-
const cellWidth = colSpanInfo?.cellProps?.width ?? width ?? colDef.width ?? 100;
|
|
169
231
|
|
|
170
232
|
const style: React.CSSProperties = {
|
|
171
|
-
|
|
172
|
-
'--width': typeof cellWidth === 'number' ? `${cellWidth}px` : cellWidth,
|
|
173
|
-
flexGrow: colDef.flex ?? 0,
|
|
174
|
-
flexShrink: 0,
|
|
175
|
-
flexBasis: colDef.flex ? 'auto' : 'auto',
|
|
176
|
-
boxSizing: 'border-box',
|
|
177
|
-
minWidth: colDef.minWidth,
|
|
178
|
-
maxWidth: colDef.maxWidth,
|
|
233
|
+
...sizeStyle,
|
|
179
234
|
position: isPinned ? 'sticky' : undefined,
|
|
180
235
|
zIndex: colDef.zIndex ?? (isPinned ? 3 : undefined),
|
|
181
236
|
padding: isEditing ? 0 : undefined
|
|
@@ -199,13 +254,12 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
199
254
|
|
|
200
255
|
return (
|
|
201
256
|
<div
|
|
202
|
-
ref={cellRef}
|
|
203
257
|
className={classNames}
|
|
204
258
|
style={style}
|
|
205
259
|
onClick={handleClick}
|
|
206
260
|
onDoubleClick={handleDoubleClick}
|
|
207
261
|
role="gridcell"
|
|
208
|
-
aria-colindex={colIndex + 1}
|
|
262
|
+
aria-colindex={ariaColIndex ?? colIndex + 1}
|
|
209
263
|
aria-readonly={!isEditable || undefined}
|
|
210
264
|
tabIndex={-1}
|
|
211
265
|
data-field={colDef.field}
|
|
@@ -215,35 +269,52 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
215
269
|
>
|
|
216
270
|
<div className="ogx__cell-content">
|
|
217
271
|
{isEditing && (onCellValueChange || onValueChange) && onEditStop ? (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
row
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
272
|
+
renderEditCell ? (
|
|
273
|
+
<CellErrorBoundary
|
|
274
|
+
field={colDef.field}
|
|
275
|
+
resetKey={row}
|
|
276
|
+
renderFn={() => renderEditCell({
|
|
277
|
+
value,
|
|
278
|
+
formattedValue,
|
|
279
|
+
row,
|
|
280
|
+
field: colDef.field,
|
|
281
|
+
colDef,
|
|
282
|
+
rowIndex,
|
|
283
|
+
colIndex,
|
|
284
|
+
rowMeta,
|
|
285
|
+
onValueChange: handleValueChange,
|
|
286
|
+
onCommit: handleCommit,
|
|
287
|
+
onCancel: handleCancel,
|
|
288
|
+
})}
|
|
289
|
+
/>
|
|
228
290
|
) : (
|
|
229
291
|
<GridEditInputCell
|
|
230
292
|
value={value}
|
|
293
|
+
formattedValue={formattedValue}
|
|
231
294
|
row={row}
|
|
232
295
|
field={colDef.field}
|
|
233
296
|
colDef={colDef as unknown as GridColDef}
|
|
234
297
|
rowIndex={rowIndex}
|
|
235
298
|
colIndex={colIndex}
|
|
299
|
+
rowMeta={rowMeta}
|
|
236
300
|
onValueChange={handleValueChange}
|
|
237
|
-
onCommit={
|
|
238
|
-
onCancel={
|
|
301
|
+
onCommit={handleCommit}
|
|
302
|
+
onCancel={handleCancel}
|
|
239
303
|
/>
|
|
240
304
|
)
|
|
241
|
-
) :
|
|
305
|
+
) : cellError !== undefined ? (
|
|
242
306
|
<CellErrorBoundary
|
|
243
307
|
field={colDef.field}
|
|
244
308
|
resetKey={row}
|
|
309
|
+
renderFn={() => { throw cellError; }}
|
|
310
|
+
/>
|
|
311
|
+
) : colDef.renderCell ? (
|
|
312
|
+
<CellErrorBoundary
|
|
313
|
+
field={colDef.field}
|
|
314
|
+
resetKey={errorResetKey}
|
|
245
315
|
renderFn={() => colDef.renderCell!({
|
|
246
316
|
value,
|
|
317
|
+
formattedValue,
|
|
247
318
|
row,
|
|
248
319
|
field: colDef.field,
|
|
249
320
|
colDef,
|
|
@@ -252,6 +323,8 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
252
323
|
rowMeta,
|
|
253
324
|
})}
|
|
254
325
|
/>
|
|
326
|
+
) : colDef.type === 'image' && typeof value === 'string' && value !== '' ? (
|
|
327
|
+
<img className="ogx__cell-image" src={value} alt={colDef.headerName ?? colDef.field} loading="lazy" />
|
|
255
328
|
) : (
|
|
256
329
|
formattedValue
|
|
257
330
|
)}
|
|
@@ -10,9 +10,17 @@ function CellRenderTarget({ renderFn }: { renderFn: () => React.ReactNode }) {
|
|
|
10
10
|
interface CellErrorBoundaryProps {
|
|
11
11
|
renderFn: () => React.ReactNode;
|
|
12
12
|
field: string;
|
|
13
|
+
/** The error is cleared when this changes. An array is compared element by element. */
|
|
13
14
|
resetKey?: unknown;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function sameResetKey(a: unknown, b: unknown): boolean {
|
|
18
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
19
|
+
return a.length === b.length && a.every((item, i) => Object.is(item, b[i]));
|
|
20
|
+
}
|
|
21
|
+
return Object.is(a, b);
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
interface CellErrorBoundaryState {
|
|
17
25
|
hasError: boolean;
|
|
18
26
|
error: Error | null;
|
|
@@ -29,13 +37,11 @@ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, C
|
|
|
29
37
|
props: CellErrorBoundaryProps,
|
|
30
38
|
state: CellErrorBoundaryState
|
|
31
39
|
): Partial<CellErrorBoundaryState> | null {
|
|
32
|
-
if (
|
|
40
|
+
if (sameResetKey(props.resetKey, state.resetKey)) return null;
|
|
41
|
+
if (state.hasError) {
|
|
33
42
|
return { hasError: false, error: null, resetKey: props.resetKey };
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
return { resetKey: props.resetKey };
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
44
|
+
return { resetKey: props.resetKey };
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
static getDerivedStateFromError(error: Error): Partial<CellErrorBoundaryState> {
|
|
@@ -53,7 +59,7 @@ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, C
|
|
|
53
59
|
return (
|
|
54
60
|
<div
|
|
55
61
|
className="ogx__cell-error"
|
|
56
|
-
role="
|
|
62
|
+
role="img"
|
|
57
63
|
aria-label={`Error in cell: ${this.props.field}`}
|
|
58
64
|
title={this.state.error?.message ?? 'Render error'}
|
|
59
65
|
>
|