@opencorestack/opengridx 0.1.9 → 1.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 +43 -0
- package/dist/index.d.ts +60 -41
- package/dist/opengridx.es.js +5045 -4549
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +11 -11
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/README.md +14 -0
- package/docs/components/aggregation-footer.md +48 -0
- package/docs/components/datagrid.md +3 -0
- package/docs/components/empty-state.md +50 -0
- package/docs/components/error-overlay.md +40 -0
- package/docs/roadmap.md +20 -1
- package/lib/components/Cell/Cell.tsx +9 -10
- package/lib/components/Cell/GridEditInputCell.tsx +4 -4
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
- package/lib/components/DataGrid/DataGrid.tsx +336 -1018
- package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
- package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
- package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
- package/lib/components/DataGrid/GridListView.tsx +116 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
- package/lib/components/Header/ColumnMenu.tsx +2 -2
- package/lib/components/Header/Header.tsx +4 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Row/Row.tsx +10 -10
- package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
- package/lib/components/Toolbar/GridToolbar.tsx +6 -0
- package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
- package/lib/hooks/core/useDataGrid.ts +14 -34
- package/lib/hooks/core/useGridColumns.tsx +228 -0
- package/lib/hooks/core/useGridControlledState.ts +192 -0
- package/lib/hooks/core/useGridRowPipeline.ts +109 -0
- package/lib/hooks/core/useGridScrollSync.ts +52 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
- package/lib/hooks/core/useGridVirtualization.ts +186 -0
- package/lib/hooks/core/useGridVisibleRows.ts +66 -0
- package/lib/hooks/core/useLayout.ts +0 -6
- package/lib/hooks/features/useAggregation.ts +7 -7
- package/lib/hooks/features/useGridDataSource.ts +1 -1
- package/lib/hooks/features/useGridEditing.ts +31 -32
- package/lib/hooks/features/usePivot.test.ts +202 -0
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/useColumnReorder.ts +1 -1
- package/lib/hooks/useRowGrouping.ts +14 -14
- package/lib/hooks/useTreeData.ts +1 -1
- package/lib/state/types.ts +1 -1
- package/lib/state/useGridStateStorage.ts +1 -1
- package/lib/types/index.ts +48 -32
- package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
- package/lib/utils/export/index.ts +22 -17
- package/lib/utils/filtering/index.ts +20 -20
- package/lib/utils/sorting/index.ts +1 -1
- package/package.json +1 -1
package/docs/README.md
CHANGED
|
@@ -22,6 +22,9 @@ Detailed reference for the core structural components.
|
|
|
22
22
|
- **[Column Visibility](components/column-visibility.md)** - Managing visible fields
|
|
23
23
|
- **[Column Grouping](components/column-group-header.md)** - Multi-level nested headers
|
|
24
24
|
- **[Column Resizing](components/column-resize.md)** - Dynamic width controls
|
|
25
|
+
- **[Empty State](components/empty-state.md)** - No-rows overlay and `noRowsOverlay` slot
|
|
26
|
+
- **[Error Overlay](components/error-overlay.md)** - Data source error display
|
|
27
|
+
- **[Aggregation Footer](components/aggregation-footer.md)** - Sticky totals row
|
|
25
28
|
---
|
|
26
29
|
|
|
27
30
|
## 🚀 Features
|
|
@@ -55,6 +58,17 @@ Make the grid your own.
|
|
|
55
58
|
|
|
56
59
|
---
|
|
57
60
|
|
|
61
|
+
## 🏛️ Architecture — Internal Hooks
|
|
62
|
+
Reference for core hooks extracted from `DataGrid.tsx`. Intended for contributors.
|
|
63
|
+
|
|
64
|
+
- **[useGridControlledState](architecture/use-grid-controlled-state.md)** - Controlled/uncontrolled state for all 7 state pairs
|
|
65
|
+
- **[useGridRowPipeline](architecture/use-grid-row-pipeline.md)** - Filter → pin → sort → paginate row pipeline
|
|
66
|
+
- **[useGridVirtualization](architecture/use-grid-virtualization.md)** - Render context and scroll-position math
|
|
67
|
+
- **[useGridColumns](architecture/use-grid-columns.md)** - Column management: effective, ordered, visible, reorder handlers
|
|
68
|
+
- **[useGridVisibleRows](architecture/use-grid-visible-rows.md)** - Render window: pinned + virtual center merge
|
|
69
|
+
- **[useGridScrollSync](architecture/use-grid-scroll-sync.md)** - RAF-batched scroll tick, scrollPosRef, onRowsScrollEnd threshold
|
|
70
|
+
- **[useGridStateSnapshot](architecture/use-grid-state-snapshot.md)** - onStateChange callback delivery on sort/filter/pagination/column changes
|
|
71
|
+
|
|
58
72
|
## 🛠️ Project Meta
|
|
59
73
|
- **[Development Roadmap](roadmap.md)** - What's built and what's next
|
|
60
74
|
- **[Research Notes](research/FEATURE_CATALOG.md)** - Comparative analysis and design decisions
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# `<GridAggregationFooter />`
|
|
2
|
+
|
|
3
|
+
Internal component that renders the sticky aggregation totals row pinned to the bottom of the grid. Displays function name labels (e.g. `sum`, `avg`) alongside their computed values for each configured column.
|
|
4
|
+
|
|
5
|
+
## ⚙️ Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Description |
|
|
8
|
+
| :--- | :--- | :--- |
|
|
9
|
+
| `columns` | `GridColDef[]` | Ordered list of visible column definitions used to lay out cells. |
|
|
10
|
+
| `aggregationModel` | `GridAggregationModel` | Maps field names to function names (`'sum' \| 'avg' \| 'count' \| 'min' \| 'max'`). |
|
|
11
|
+
| `aggregationResult` | `GridAggregationResult` | Computed values keyed by field name, produced by `useAggregation`. |
|
|
12
|
+
| `columnWidths` | `Record<string, number>` | Runtime pixel widths for each field, kept in sync with user resize actions. |
|
|
13
|
+
| `rowHeight` | `number` | Sets `min-height` on the footer row, matching the grid's configured row height. |
|
|
14
|
+
| `checkboxSelection` | `boolean` | When `true`, inserts a 48px spacer at the start to align with the checkbox column. |
|
|
15
|
+
| `hasDetailPanel` | `boolean` | When `true`, inserts a 48px spacer to align with the expand/collapse column. |
|
|
16
|
+
| `rowReordering` | `boolean` | When `true`, inserts a 48px spacer to align with the drag handle column. |
|
|
17
|
+
|
|
18
|
+
## 🔄 When it renders
|
|
19
|
+
|
|
20
|
+
`GridAggregationFooter` renders when `aggregationModel` has at least one entry and `aggregationResult` contains the corresponding computed values. The footer is sticky — it remains visible when the user scrolls vertically.
|
|
21
|
+
|
|
22
|
+
## 🔢 Value formatting
|
|
23
|
+
|
|
24
|
+
Values are formatted by `formatAggregationValue` from `useAggregation`:
|
|
25
|
+
- `count` → integer (e.g. `42`)
|
|
26
|
+
- `sum` / `avg` / `min` / `max` → `toLocaleString()` with up to 2 decimal places
|
|
27
|
+
|
|
28
|
+
## 🎨 Usage via DataGrid prop
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<DataGrid
|
|
32
|
+
aggregationModel={{
|
|
33
|
+
salary: 'sum',
|
|
34
|
+
age: 'avg',
|
|
35
|
+
department: 'count'
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The footer renders automatically. No additional configuration required.
|
|
41
|
+
|
|
42
|
+
## ♿ Accessibility
|
|
43
|
+
|
|
44
|
+
The footer row uses `role="row"` and `aria-label="Aggregation totals"`. Each cell uses `role="gridcell"`. The container uses `aria-live="polite"` so screen readers announce value updates when the data changes.
|
|
45
|
+
|
|
46
|
+
## 🔗 Related
|
|
47
|
+
- [DataGrid](datagrid.md)
|
|
48
|
+
- [Aggregation & Pivot](../features/aggregation-pivot.md)
|
|
@@ -68,3 +68,6 @@ Customize internal components using the `slots` prop.
|
|
|
68
68
|
- [Column Visibility](column-visibility.md)
|
|
69
69
|
- [Column Grouping](column-group-header.md)
|
|
70
70
|
- [Column Resizing](column-resize.md)
|
|
71
|
+
- [Empty State](empty-state.md)
|
|
72
|
+
- [Error Overlay](error-overlay.md)
|
|
73
|
+
- [Aggregation Footer](aggregation-footer.md)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `<GridEmptyState />`
|
|
2
|
+
|
|
3
|
+
Internal component rendered when the grid has no rows to display. Replaces the row viewport with a centered icon and message.
|
|
4
|
+
|
|
5
|
+
## ⚙️ Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Description |
|
|
8
|
+
| :--- | :--- | :--- |
|
|
9
|
+
| `noRowsLabel` | `string` | Text shown beneath the empty-state icon. Controlled by the `noRowsLabel` DataGrid prop. |
|
|
10
|
+
| `width` | `number` | Pixel width of the empty-state container, matched to the grid's total column width. |
|
|
11
|
+
|
|
12
|
+
## 🔄 When it renders
|
|
13
|
+
|
|
14
|
+
`GridEmptyState` renders when `allRenderableRows.length === 0` and the grid is not in a loading state. The loading skeleton takes precedence — if `loading={true}` the skeleton rows are shown instead.
|
|
15
|
+
|
|
16
|
+
## 🎨 Customizing via slot
|
|
17
|
+
|
|
18
|
+
Replace the default empty state with any React component using the `noRowsOverlay` slot:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
function MyEmptyState() {
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ textAlign: 'center', padding: '40px' }}>
|
|
24
|
+
<p>No results match your filter.</p>
|
|
25
|
+
<button onClick={clearFilters}>Clear filters</button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
<DataGrid
|
|
31
|
+
slots={{ noRowsOverlay: MyEmptyState }}
|
|
32
|
+
slotProps={{ noRowsOverlay: { /* custom props if needed */ } }}
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 💬 Custom label (without a full slot replacement)
|
|
37
|
+
|
|
38
|
+
To only change the message text without replacing the whole component, use `noRowsLabel`:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<DataGrid
|
|
42
|
+
noRowsLabel="No employees found for this department."
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Default value: `"No rows"`
|
|
47
|
+
|
|
48
|
+
## 🔗 Related
|
|
49
|
+
- [DataGrid](datagrid.md)
|
|
50
|
+
- [Loading States](../features/loading-states.md)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `<GridErrorOverlay />`
|
|
2
|
+
|
|
3
|
+
Internal component rendered when a `GridDataSource` fetch fails. Displays an error icon, a human-readable message, and a **Retry** button that calls `window.location.reload()`.
|
|
4
|
+
|
|
5
|
+
## ⚙️ Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Description |
|
|
8
|
+
| :--- | :--- | :--- |
|
|
9
|
+
| `error` | `{ message?: string } \| null` | The error object from `state.dataSource.error`. When `null` the component renders nothing (`null` return). |
|
|
10
|
+
|
|
11
|
+
## 🔄 When it renders
|
|
12
|
+
|
|
13
|
+
`GridErrorOverlay` only appears when using a server-side `dataSource` prop. When a fetch throws, DataGrid catches the error and stores it in internal state. The overlay replaces the row viewport until the user retries.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<DataGrid
|
|
17
|
+
dataSource={{
|
|
18
|
+
getRows: async ({ filterModel, sortModel, paginationModel }) => {
|
|
19
|
+
const res = await fetch('/api/employees');
|
|
20
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
21
|
+
return res.json();
|
|
22
|
+
}
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If `getRows` throws, the overlay renders with the thrown error's `message`.
|
|
28
|
+
|
|
29
|
+
## ♿ Accessibility
|
|
30
|
+
|
|
31
|
+
The overlay container uses `role="alert"` and `aria-live="assertive"` so screen readers announce the error immediately when it appears.
|
|
32
|
+
|
|
33
|
+
## 🎨 Customizing
|
|
34
|
+
|
|
35
|
+
`GridErrorOverlay` is not currently exposed as a configurable slot. To implement custom error handling, catch errors in your `dataSource.getRows` and manage the error display in your own component wrapping the grid.
|
|
36
|
+
|
|
37
|
+
## 🔗 Related
|
|
38
|
+
- [DataGrid](datagrid.md)
|
|
39
|
+
- [Data Source](../features/data-source.md)
|
|
40
|
+
- [Loading States](../features/loading-states.md)
|
package/docs/roadmap.md
CHANGED
|
@@ -54,6 +54,20 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
54
54
|
* **Demo**: `AdvancedExcelExportDemo` in demo app (`Data Management` category).
|
|
55
55
|
* **Docs**: `docs/features/export-guide.md` fully updated with feature comparison table and all options.
|
|
56
56
|
|
|
57
|
+
### Code Quality & Architecture *(completed 2026-07-29 → 2026-07-30)*
|
|
58
|
+
* **DataGrid decomposition**: Extracted `useGridControlledState`, `useGridRowPipeline`, `useGridVirtualization`, `useGridScrollSync`, `useGridVisibleRows`, `useGridColumns` hooks, plus `GridEmptyState`, `GridErrorOverlay`, `GridAggregationFooter`, `GridVirtualRows`, `GridPinnedRows`, `GridStandaloneColumnPanel`, `GridListView` components from `DataGrid.tsx`. Each module has a single clear responsibility with typed params and return interfaces.
|
|
59
|
+
* **Full `any` elimination**: Every `any` across `lib/` replaced with proper types or `unknown`. `GridRowModel` index signature, all cell/value/error params, aggregation functions, slot types, and export utilities are now fully typed. Zero `eslint-disable` suppressions remain — every case fixed at the root.
|
|
60
|
+
* **Regression test suite**: 3 test files covering column-visibility-model persistence, pivot aggregation correctness including grand-total avg and null-value exclusion, and pagination rendering/row key stability.
|
|
61
|
+
* **Pivot null-value fix**: `Number(null) === 0` caused null numeric fields to be silently included as `0` in avg/min/max pivot aggregation. Both the per-row bucket loop and the grand-total bucket loop in `usePivot.ts` now guard with `raw != null` before pushing values.
|
|
62
|
+
|
|
63
|
+
### Bug Fixes *(completed 2026-07-30)*
|
|
64
|
+
* **`onRowClick` never fired when `onCellClick` was registered**: `Cell.tsx` called `e.stopPropagation()` on every cell click, silently preventing events from reaching the row's `onClick` handler. Removed — both callbacks now fire in natural bubble order.
|
|
65
|
+
* **Column visibility panel order not updating after reorder**: Panel received `effectiveColumns` (pre-ordering, definition order) instead of `orderedColumns`. Fixed — panel list now reflects the live reordered sequence.
|
|
66
|
+
* **Reset button ignoring column sequence**: Reset only restored visibility; column order was left wherever the user dragged it. Added `onColumnOrderReset` callback that restores `internalColumnOrder` to original definition order.
|
|
67
|
+
* **`FilterPanel` debounce stale-closure**: Debounce effect read `item`/`col.field`/`currentOperator` from stale closure, previously suppressed with `eslint-disable`. Replaced with `useLayoutEffect` refs-sync pattern.
|
|
68
|
+
* **`FilterPanelDemo` inaccessible**: Missing `slots={{ toolbar: GridToolbar }}` meant the filter panel had no UI entry point. Fixed; demo also migrated to `DocsLayout`.
|
|
69
|
+
* **`EventsDemo` dead handlers**: Fixed broken `onRowClick`, dead filter/column-order callbacks, and three `any`-typed handler signatures.
|
|
70
|
+
|
|
57
71
|
---
|
|
58
72
|
|
|
59
73
|
## 📅 Upcoming Features
|
|
@@ -66,6 +80,11 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
66
80
|
### DX & Publishing
|
|
67
81
|
* **npm publish**: Finalize `package.json` metadata, `README.md`, and publish to npm registry.
|
|
68
82
|
|
|
83
|
+
### Demo Site
|
|
84
|
+
* **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
|
+
* **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
|
+
|
|
69
88
|
## ✅ Implemented Features (Recent)
|
|
70
|
-
* **
|
|
89
|
+
* **Demo app (partial)**: 32 interactive examples across 6 categories (Main features, Advanced features, Components, Customization, Tutorials, Resources) with categorized sidebar navigation and per-page Table of Contents. Source code viewer present on 6 of 32 examples. Syntax highlighting and public deployment are in the upcoming Demo Site section above.
|
|
71
90
|
* **Quickstart & Installation Guides**: Comprehensive onboarding documentation for developers.
|
|
@@ -7,9 +7,9 @@ import { GridEditInputCell } from './GridEditInputCell';
|
|
|
7
7
|
|
|
8
8
|
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
9
9
|
onCellClick?: (params: GridCellParams<R>) => void;
|
|
10
|
-
onCellEditStart?: (field: string, value:
|
|
11
|
-
onCellValueChange?: (field: string, newValue:
|
|
12
|
-
value:
|
|
10
|
+
onCellEditStart?: (field: string, value: unknown) => void;
|
|
11
|
+
onCellValueChange?: (field: string, newValue: unknown) => void;
|
|
12
|
+
value: unknown;
|
|
13
13
|
row: R;
|
|
14
14
|
colDef: GridColDef<R>;
|
|
15
15
|
rowIndex: number;
|
|
@@ -27,7 +27,7 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
|
27
27
|
isEditing?: boolean;
|
|
28
28
|
onEditStart?: () => void;
|
|
29
29
|
onEditStop?: (cancel?: boolean) => void;
|
|
30
|
-
onValueChange?: (newValue:
|
|
30
|
+
onValueChange?: (newValue: unknown) => void;
|
|
31
31
|
|
|
32
32
|
colSpanInfo?: CellColSpanInfo;
|
|
33
33
|
rowSpan?: number;
|
|
@@ -63,7 +63,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
63
63
|
} = props;
|
|
64
64
|
|
|
65
65
|
// All hooks must come before any early returns (Rules of Hooks)
|
|
66
|
-
const handleValueChange = React.useCallback((newValue:
|
|
66
|
+
const handleValueChange = React.useCallback((newValue: unknown) => {
|
|
67
67
|
if (onCellValueChange) {
|
|
68
68
|
onCellValueChange(colDef.field, newValue);
|
|
69
69
|
} else {
|
|
@@ -94,7 +94,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
94
94
|
value,
|
|
95
95
|
row,
|
|
96
96
|
field: colDef.field,
|
|
97
|
-
colDef
|
|
97
|
+
colDef,
|
|
98
98
|
rowIndex,
|
|
99
99
|
colIndex
|
|
100
100
|
});
|
|
@@ -104,7 +104,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
104
104
|
value={value}
|
|
105
105
|
row={row}
|
|
106
106
|
field={colDef.field}
|
|
107
|
-
colDef={colDef as
|
|
107
|
+
colDef={colDef as unknown as GridColDef}
|
|
108
108
|
rowIndex={rowIndex}
|
|
109
109
|
colIndex={colIndex}
|
|
110
110
|
onValueChange={handleValueChange}
|
|
@@ -119,7 +119,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
119
119
|
value,
|
|
120
120
|
row,
|
|
121
121
|
field: colDef.field,
|
|
122
|
-
colDef
|
|
122
|
+
colDef,
|
|
123
123
|
rowIndex,
|
|
124
124
|
colIndex
|
|
125
125
|
});
|
|
@@ -131,7 +131,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
131
131
|
const resolvedCellClassName = React.useMemo(() => {
|
|
132
132
|
if (!colDef.cellClassName) return '';
|
|
133
133
|
if (typeof colDef.cellClassName === 'function') {
|
|
134
|
-
return colDef.cellClassName({ value, row, field: colDef.field, colDef
|
|
134
|
+
return colDef.cellClassName({ value, row, field: colDef.field, colDef, rowIndex, colIndex }) || '';
|
|
135
135
|
}
|
|
136
136
|
return colDef.cellClassName;
|
|
137
137
|
}, [colDef, value, row, rowIndex, colIndex]);
|
|
@@ -162,7 +162,6 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
162
162
|
|
|
163
163
|
const handleClick = (e: React.MouseEvent) => {
|
|
164
164
|
if (onCellClick) {
|
|
165
|
-
e.stopPropagation();
|
|
166
165
|
onCellClick({
|
|
167
166
|
row,
|
|
168
167
|
field: colDef.field,
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import type { GridRenderCellParams } from '../../types';
|
|
3
3
|
|
|
4
4
|
export interface GridEditInputCellProps extends GridRenderCellParams {
|
|
5
|
-
onValueChange: (value:
|
|
5
|
+
onValueChange: (value: unknown) => void;
|
|
6
6
|
onCommit: () => void;
|
|
7
7
|
onCancel: () => void;
|
|
8
8
|
}
|
|
@@ -31,7 +31,7 @@ function TextEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputC
|
|
|
31
31
|
|
|
32
32
|
// ─── Number ───────────────────────────────────────────────────────────────────
|
|
33
33
|
function NumberEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
34
|
-
const [local, setLocal] = useState(value
|
|
34
|
+
const [local, setLocal] = useState<string | number>(typeof value === 'number' ? value : '');
|
|
35
35
|
const ref = useRef<HTMLInputElement>(null);
|
|
36
36
|
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
37
37
|
|
|
@@ -94,7 +94,7 @@ function SelectEditor({ value, colDef, onValueChange, onCommit, onCancel }: Grid
|
|
|
94
94
|
<select
|
|
95
95
|
ref={ref}
|
|
96
96
|
className="ogx__edit-select"
|
|
97
|
-
value={value ?? ''}
|
|
97
|
+
value={String(value ?? '')}
|
|
98
98
|
onChange={e => { onValueChange(e.target.value); }}
|
|
99
99
|
onBlur={onCommit}
|
|
100
100
|
onKeyDown={e => {
|
|
@@ -113,7 +113,7 @@ function SelectEditor({ value, colDef, onValueChange, onCommit, onCancel }: Grid
|
|
|
113
113
|
|
|
114
114
|
// ─── Date ─────────────────────────────────────────────────────────────────────
|
|
115
115
|
function DateEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
116
|
-
const [local, setLocal] = useState(value ?? '');
|
|
116
|
+
const [local, setLocal] = useState<string>(typeof value === 'string' ? value : String(value ?? ''));
|
|
117
117
|
const ref = useRef<HTMLInputElement>(null);
|
|
118
118
|
useEffect(() => { ref.current?.focus(); }, []);
|
|
119
119
|
|
|
@@ -10,6 +10,7 @@ export interface ColumnVisibilityPanelProps<R extends GridRowModel = GridRowMode
|
|
|
10
10
|
onShowAll: () => void;
|
|
11
11
|
onHideAll: () => void;
|
|
12
12
|
onColumnReorder?: (fromField: string, toField: string) => void;
|
|
13
|
+
onColumnOrderReset?: () => void;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function SearchIcon() {
|
|
@@ -43,7 +44,7 @@ function DragHandleIcon() {
|
|
|
43
44
|
export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
44
45
|
props: ColumnVisibilityPanelProps<R>
|
|
45
46
|
) {
|
|
46
|
-
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder } = props;
|
|
47
|
+
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder, onColumnOrderReset } = props;
|
|
47
48
|
const [searchQuery, setSearchQuery] = useState('');
|
|
48
49
|
const [dragOverField, setDragOverField] = useState<string | null>(null);
|
|
49
50
|
const dragFieldRef = useRef<string | null>(null);
|
|
@@ -180,7 +181,7 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
|
180
181
|
</label>
|
|
181
182
|
<button
|
|
182
183
|
className="ogx-column-visibility-panel__reset-btn"
|
|
183
|
-
onClick={onShowAll}
|
|
184
|
+
onClick={() => { onShowAll(); onColumnOrderReset?.(); }}
|
|
184
185
|
>
|
|
185
186
|
Reset
|
|
186
187
|
</button>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for bugs fixed in the DataGrid render layer.
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - getVisibleRows() respects pagination (not returning all rows)
|
|
6
|
+
* - Row DOM nodes are not remounted when column widths change
|
|
7
|
+
*
|
|
8
|
+
* Not covered here (require a real browser / ResizeObserver):
|
|
9
|
+
* - translateY offset — depends on computed cumulativeHeights, which requires
|
|
10
|
+
* a real viewport; jsdom reports all dimensions as 0
|
|
11
|
+
* - scroll-ref RAF batching — requires scroll events that jsdom fires but
|
|
12
|
+
* requestAnimationFrame does not execute by default
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { render, act } from '@testing-library/react';
|
|
18
|
+
import { DataGrid } from './DataGrid';
|
|
19
|
+
import { useGridApiRef } from '../../hooks/core/useGridApiRef';
|
|
20
|
+
import type { GridColDef } from '../../types';
|
|
21
|
+
|
|
22
|
+
const COLS: GridColDef[] = [
|
|
23
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
24
|
+
{ field: 'name', headerName: 'Name', width: 150 },
|
|
25
|
+
{ field: 'age', headerName: 'Age', width: 100 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// 10 rows so pagination actually clips the visible set
|
|
29
|
+
const ROWS = Array.from({ length: 10 }, (_, i) => ({
|
|
30
|
+
id: i + 1,
|
|
31
|
+
name: `User ${i + 1}`,
|
|
32
|
+
age: 20 + i,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Bug: getVisibleRows() was returning sortedUnpinnedRows (all rows) regardless
|
|
37
|
+
// of whether pagination was active. Fixed: now returns paginatedUnpinnedRows
|
|
38
|
+
// when a paginationModel is set.
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('getVisibleRows respects pagination', () => {
|
|
42
|
+
it('returns only the current page rows, not all rows', () => {
|
|
43
|
+
const assigningRef = React.createRef<import('../../types').GridApi>();
|
|
44
|
+
|
|
45
|
+
function GridWithRef() {
|
|
46
|
+
const ref = useGridApiRef();
|
|
47
|
+
// expose ref to test scope after render
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
(assigningRef as React.MutableRefObject<import('../../types').GridApi>).current = ref.current;
|
|
50
|
+
});
|
|
51
|
+
return (
|
|
52
|
+
<DataGrid
|
|
53
|
+
rows={ROWS}
|
|
54
|
+
columns={COLS}
|
|
55
|
+
apiRef={ref}
|
|
56
|
+
pagination
|
|
57
|
+
paginationModel={{ page: 0, pageSize: 3 }}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
act(() => {
|
|
63
|
+
render(<GridWithRef />);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// pageSize=3, total=10 → page 0 should expose 3 rows, not 10.
|
|
67
|
+
const visible = assigningRef.current?.getVisibleRows() ?? [];
|
|
68
|
+
expect(visible.length).toBe(3);
|
|
69
|
+
expect(visible.map(r => r.id)).toEqual([1, 2, 3]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns all rows when no pagination is provided', () => {
|
|
73
|
+
const assigningRef = React.createRef<import('../../types').GridApi>();
|
|
74
|
+
|
|
75
|
+
function GridNoPagination() {
|
|
76
|
+
const ref = useGridApiRef();
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
(assigningRef as React.MutableRefObject<import('../../types').GridApi>).current = ref.current;
|
|
79
|
+
});
|
|
80
|
+
return <DataGrid rows={ROWS} columns={COLS} apiRef={ref} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
act(() => {
|
|
84
|
+
render(<GridNoPagination />);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const visible = assigningRef.current?.getVisibleRows() ?? [];
|
|
88
|
+
// Without pagination the full sorted set is returned.
|
|
89
|
+
expect(visible.length).toBe(ROWS.length);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Bug: row keys were `${row.id}-${virtualization.totalWidth}`, so every column
|
|
95
|
+
// resize unmounted and remounted every DOM row node.
|
|
96
|
+
// Fixed: key is now simply `row.id`.
|
|
97
|
+
//
|
|
98
|
+
// Test strategy: capture DOM element identity before and after a column prop
|
|
99
|
+
// change. If the key includes totalWidth the elements will be new nodes; if
|
|
100
|
+
// the key is stable they will be the exact same DOM nodes.
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe('row DOM nodes are stable across column width changes', () => {
|
|
104
|
+
it('does not remount rows when column widths change', () => {
|
|
105
|
+
const { rerender, container } = render(
|
|
106
|
+
<DataGrid rows={ROWS.slice(0, 3)} columns={COLS} />
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Collect the row cell elements before the update.
|
|
110
|
+
const cellsBefore = Array.from(
|
|
111
|
+
container.querySelectorAll('[role="gridcell"]')
|
|
112
|
+
);
|
|
113
|
+
expect(cellsBefore.length).toBeGreaterThan(0);
|
|
114
|
+
|
|
115
|
+
// Update the Name column width — simulates a resize.
|
|
116
|
+
const updatedCols: GridColDef[] = COLS.map(c =>
|
|
117
|
+
c.field === 'name' ? { ...c, width: 250 } : c
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
rerender(<DataGrid rows={ROWS.slice(0, 3)} columns={updatedCols} />);
|
|
121
|
+
|
|
122
|
+
const cellsAfter = Array.from(
|
|
123
|
+
container.querySelectorAll('[role="gridcell"]')
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Same number of cells, and critically the same DOM node instances.
|
|
127
|
+
expect(cellsAfter.length).toBe(cellsBefore.length);
|
|
128
|
+
cellsBefore.forEach((node, i) => {
|
|
129
|
+
expect(cellsAfter[i]).toBe(node);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|