@opencorestack/opengridx 3.0.1 → 3.2.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 +24 -0
- package/README.md +4 -2
- package/dist/index.d.ts +61 -0
- package/dist/opengridx.es.js +4279 -4132
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +14 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +18 -1
- package/docs/architecture/datagrid-orchestration.md +1 -0
- package/docs/components/column-resize.md +13 -1
- package/docs/features/pdf-export.md +38 -2
- package/docs/features/sorting-pagination.md +26 -0
- package/docs/migration/v2-to-v3.md +1 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +12 -3
- package/lib/components/DataGrid/DataGrid.tsx +3 -0
- package/lib/components/Header/Header.tsx +4 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +2 -0
- package/lib/hooks/core/useGridColumnAutosize.ts +52 -0
- package/lib/hooks/core/useGridRowPipeline.ts +2 -2
- package/lib/hooks/useRowGrouping.ts +20 -3
- package/lib/hooks/useTreeData.ts +8 -1
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +63 -0
- package/lib/utils/columnAutosize.ts +56 -0
- package/lib/utils/export/exportToPdf.ts +167 -21
- package/lib/utils/export/index.ts +1 -1
- package/lib/utils/pivot/index.ts +11 -2
- package/lib/utils/sorting/index.ts +77 -24
- package/llms.txt +10 -2
- package/package.json +1 -1
package/docs/API_REFERENCE.md
CHANGED
|
@@ -294,6 +294,8 @@ Access these methods via the `apiRef` prop.
|
|
|
294
294
|
| `setPage(page)` | `void` | Change current page (0-indexed). Fires `onPaginationModelChange`. |
|
|
295
295
|
| `setPageSize(pageSize)` | `void` | Change the page size and go to page 0. Fires `onPaginationModelChange`. |
|
|
296
296
|
| `scrollToIndexes(params)` | `void` | Scroll to specific row/column index. Uses the current layout, also when called from a layout effect right after an update (v3.0+). |
|
|
297
|
+
| `autosizeColumn(field)` | `void` | Fit a column to its header and the cells in the current render window, clamped to `minWidth` (default 50px) / `maxWidth`; same as double-clicking its resize handle. No-op for `resizable: false` or a column that is not rendered. A flex column gets a fixed width (v3.1.0+). |
|
|
298
|
+
| `autosizeColumns(fields?)` | `void` | `autosizeColumn` for several columns; every column when `fields` is omitted (v3.1.0+). |
|
|
297
299
|
| `getAllColumns()` | `GridColDef[]` | Get all defined columns. |
|
|
298
300
|
| `getAggregationResult()` | `Record<string, unknown> \| null` | Get current aggregation results (`null` without an `aggregationModel`, and in pivot mode). Live in a parent's `useLayoutEffect` on mount and current after every update (v3.0+; before, it was installed in a passive effect). The same holds for `getAggregationModel()` and `getGroupedExportRows()`. |
|
|
299
301
|
| `getAggregationModel()` | `GridAggregationModel \| null` | Get the active aggregation configuration. |
|
|
@@ -391,8 +393,9 @@ Span values are floored; `Infinity` means "to the end"; `NaN`, `0` and negative
|
|
|
391
393
|
| Property | Type | Default | Description |
|
|
392
394
|
| :--- | :--- | :--- | :--- |
|
|
393
395
|
| `sortable` | `boolean` | `true` | Enable/disable sorting for this column. |
|
|
396
|
+
| `sortComparator` | `(v1, v2, params1, params2) => number` | — | Custom ascending comparator for client-side sorting (v3.1.0+). `v1`/`v2` are `valueGetter` values **including `null`/`undefined`**; `params` are `GridSortCellParams` `{ id, field, row, value }`. Negated for `desc`, chained in multi-sort, used for group ordering under row grouping, tree-data siblings and pivot row labels. Throw / `NaN` reads as 0 (dev warning once). Ignored with `sortingMode="server"`. |
|
|
394
397
|
| `filterable` | `boolean` | `true` | Enable/disable filtering for this column. `false` also removes it from the quick-filter search. |
|
|
395
|
-
| `resizable` | `boolean` | `true` | Allow the user to drag-resize this column. |
|
|
398
|
+
| `resizable` | `boolean` | `true` | Allow the user to drag-resize this column; double-click the handle (or Enter on it) to auto-size to content (v3.1.0+). |
|
|
396
399
|
| `hideable` | `boolean` | `true` | Allow the user to hide this column from the UI. `false` removes it from the Columns panel (unless `showNonHideableColumns`) and removes **Hide Column** from its column menu. |
|
|
397
400
|
| `pinnable` | `boolean` | `true` | Allow this column to be pinned via the UI. `false` removes the pin actions from its column menu. Does not affect drag-reordering (v3.0+). |
|
|
398
401
|
| `disableColumnMenu` | `boolean` | `false` | Hide the column header kebab/context menu. |
|
|
@@ -1443,6 +1446,15 @@ interface PdfExportOptions {
|
|
|
1443
1446
|
headerTextColor?: string;
|
|
1444
1447
|
fontSize?: number;
|
|
1445
1448
|
font?: { name: string; data: string; boldData?: string }; // v3.0+
|
|
1449
|
+
onProgress?: (p: PdfExportProgress) => void; // v3.2+
|
|
1450
|
+
signal?: AbortSignal; // v3.2+
|
|
1451
|
+
maxRows?: number; // v3.2+, default 20000
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
interface PdfExportProgress {
|
|
1455
|
+
phase: 'prepare' | 'render' | 'save';
|
|
1456
|
+
done: number;
|
|
1457
|
+
total: number;
|
|
1446
1458
|
}
|
|
1447
1459
|
```
|
|
1448
1460
|
|
|
@@ -1463,3 +1475,8 @@ interface PdfExportOptions {
|
|
|
1463
1475
|
| `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (`#rrggbb` / `#rgb`) |
|
|
1464
1476
|
| `fontSize` | `number` | `9` | Body cell font size in points |
|
|
1465
1477
|
| `font` | `{ name, data, boldData? }` | — | Base64 TrueType font for text outside Latin-1 (₹, CJK, Cyrillic…). Without it, such characters are replaced with `?` (v3.0+) |
|
|
1478
|
+
| `onProgress` | `(p: PdfExportProgress) => void` | — | Called per slice with `{ phase, done, total }`; phases `prepare` → `render` → `save`, `done` monotonic per phase and ending at `total` (v3.2+) |
|
|
1479
|
+
| `signal` | `AbortSignal` | — | Abort → the promise rejects with `DOMException` `'AbortError'`, no file saved. The final jsPDF save step cannot be interrupted (v3.2+) |
|
|
1480
|
+
| `maxRows` | `number` | `20000` | Above this many table rows a dev-mode `console.warn` recommends `exportToCsv` / `exportToExcelAdvanced`. Never throws; `Infinity` silences it (v3.2+) |
|
|
1481
|
+
|
|
1482
|
+
The export yields to the event loop between ~30 ms slices while formatting and drawing (v3.2+), so the page stays responsive; only jsPDF's final save is one synchronous step. See [PDF Export › Large exports](./features/pdf-export.md#large-exports).
|
|
@@ -56,6 +56,7 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
56
56
|
| 29 | `useGridSpanning` | `hooks/features/useGridSpanning.ts` | Row/column spans |
|
|
57
57
|
| 30 | `useGridClipboardApi` | `hooks/core/useGridClipboardApi.ts` | Ctrl/Cmd+C and `copySelectedRows` |
|
|
58
58
|
| 31 | `useGridScrollToIndexesApi` | `hooks/core/useGridScrollToIndexesApi.ts` | `scrollToIndexes` |
|
|
59
|
+
| 31a | `useGridColumnAutosize` | `hooks/core/useGridColumnAutosize.ts` | `autosizeColumn` / `autosizeColumns` on the API, and the resize handle's auto-size handler (v3.1) |
|
|
59
60
|
| 32 | `useGridViewport` | `hooks/core/useGridViewport.ts` | Scroll sync, viewport measurement, viewport callback ref |
|
|
60
61
|
| 33 | `useGridVirtualization` | `hooks/core/useGridVirtualization.ts` | Render window |
|
|
61
62
|
| 34 | `useGridSortHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Header sort / shift-sort |
|
|
@@ -8,7 +8,8 @@ OpenGridX supports dynamic column resizing, allowing users to adjust the width o
|
|
|
8
8
|
- **Keyboard**: with a column header focused, **Alt+ArrowRight** / **Alt+ArrowLeft** widen / narrow the column by 10px (hold **Shift** for 50px). The handle is a `role="separator"` that reports the width in `aria-valuenow` (and `aria-valuemin` / `aria-valuemax`).
|
|
9
9
|
- **Constraints**: `minWidth` and `maxWidth` from your `GridColDef` apply. Without `minWidth` a column can be narrowed to 50px; a column that is already narrower (a 30px icon column) is never snapped up to 50px. Without `maxWidth` there is no upper limit.
|
|
10
10
|
- **Clicks are not resizes**: pressing and releasing the handle without moving it changes nothing (a flex column stays flex), and ending a resize with the pointer over the header never sorts the column.
|
|
11
|
-
- **Double-click**
|
|
11
|
+
- **Double-click to auto-size** (v3.1.0+): double-clicking the handle fits the column to its content — the header (title, sort icon, menu button) and the cells rendered right now, i.e. the rows and pinned rows in the current virtualization window, not the whole data set. The width is clamped to `minWidth` (default 50px) and `maxWidth`; a flex column gets a fixed width, like after a manual drag. The double-click never sorts. With the handle focused, **Enter** does the same. Columns with `resizable: false` have no handle and are never auto-sized.
|
|
12
|
+
- **Programmatic auto-size**: `apiRef.current.autosizeColumn(field)` and `apiRef.current.autosizeColumns(fields?)` (every column when `fields` is omitted) use the same measurement. The new width lands in `columnWidths` state like a drag, so `onStateChange` reports it; there is no dedicated resize callback.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -34,6 +35,17 @@ const columns: GridColDef[] = [
|
|
|
34
35
|
];
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
### Auto-size to content
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const apiRef = useGridApiRef();
|
|
42
|
+
|
|
43
|
+
<DataGrid apiRef={apiRef} rows={rows} columns={columns} />
|
|
44
|
+
<button onClick={() => apiRef.current.autosizeColumns()}>Fit all columns</button>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Measurement reads the DOM (each cell's `.ogx__cell-content` laid out at `max-content`, plus the cell's padding and borders), so only rendered rows count: scroll first, or turn off virtualization for a small grid, if off-screen rows are wider. Cells that span several columns (`colSpan`) are skipped.
|
|
48
|
+
|
|
37
49
|
---
|
|
38
50
|
|
|
39
51
|
## ⚙️ How it Works
|
|
@@ -16,7 +16,7 @@ npm install jspdf jspdf-autotable
|
|
|
16
16
|
> await exportToPdf(rows, columns, { fileName: 'invoices', font: { name: 'NotoSans', data: notoSansBase64 } });
|
|
17
17
|
> ```
|
|
18
18
|
|
|
19
|
-
> **Large datasets:**
|
|
19
|
+
> **Large datasets:** time and file size grow with every row (50,000 grouped rows: several seconds and a file of 75–120 MB). Since v3.2 the export is sliced so the page stays responsive, and it reports progress and can be cancelled, but for more than a few thousand rows CSV (`exportToCsv`) or `.xlsx` (`exportToExcelAdvanced`) is still the better format. See [Large exports](#large-exports).
|
|
20
20
|
|
|
21
21
|
## Basic Usage
|
|
22
22
|
|
|
@@ -97,6 +97,42 @@ await exportToPdf(
|
|
|
97
97
|
| `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (`#rrggbb` or `#rgb`). Invalid values fall back to the default |
|
|
98
98
|
| `fontSize` | `number` | `9` | Body cell font size in points |
|
|
99
99
|
| `font` | `{ name: string; data: string; boldData?: string }` | — | A TrueType font (base64 `.ttf`) for text outside Latin-1 (v3.0+). See [Non-Latin text](#non-latin-text-and-unicode-fonts) |
|
|
100
|
+
| `onProgress` | `(p: PdfExportProgress) => void` | — | Progress: `{ phase: 'prepare' \| 'render' \| 'save', done, total }` (v3.2+). See [Large exports](#large-exports) |
|
|
101
|
+
| `signal` | `AbortSignal` | — | Cancels the export: the promise rejects with a `DOMException` named `'AbortError'` and nothing is saved (v3.2+) |
|
|
102
|
+
| `maxRows` | `number` | `20000` | Table-row count above which a development-mode warning recommends CSV / `.xlsx`. The export still runs; `Infinity` silences it (v3.2+) |
|
|
103
|
+
|
|
104
|
+
## Large exports
|
|
105
|
+
|
|
106
|
+
`exportToPdf` returns a `Promise<void>`. Since v3.2 it works in slices of about 30 ms and yields to the event loop between them, both while formatting the rows and while drawing the table, so the page keeps painting and handling input. Pass `onProgress` for a progress bar and `signal` for a Cancel button:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
110
|
+
const [progress, setProgress] = useState<PdfExportProgress | null>(null);
|
|
111
|
+
|
|
112
|
+
const onExport = async () => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
controllerRef.current = controller;
|
|
115
|
+
try {
|
|
116
|
+
await exportToPdf(rows, columns, {
|
|
117
|
+
groupedRows: apiRef.current.getGroupedExportRows() ?? undefined,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
onProgress: setProgress,
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error;
|
|
123
|
+
} finally {
|
|
124
|
+
setProgress(null);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// <button onClick={() => controllerRef.current?.abort()}>Cancel</button>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Phases.** `prepare` formats the rows (`total` = rows, or grouped entries with `groupedRows`); `render` draws the table (`total` = table body rows); `save` is reported as `0/1` before the file is written and `1/1` after. Within a phase `done` never decreases and ends at `total`.
|
|
131
|
+
- **Cancel.** When `signal` aborts, the promise rejects with `DOMException` `'AbortError'` at the next slice boundary and no file is saved. An already aborted signal rejects before jsPDF is loaded.
|
|
132
|
+
- **The last step still blocks.** jsPDF builds the whole file in one synchronous call when saving; that step cannot be split or cancelled. It is short next to the rest (about 0.4–0.6 s for 50,000 grouped rows in Chromium), but it grows with the file.
|
|
133
|
+
- **Same output.** The table is drawn as a series of `jspdf-autotable` calls that continue on the same page, with fixed column widths, the header repeated on every page and the footer only at the end. Pages, row positions, shading and page breaks are the same as a single call; the file is marginally larger (repeated style operators).
|
|
134
|
+
- **`maxRows` (default 20,000).** Above it a console warning in development recommends `exportToCsv` or `exportToExcelAdvanced`; nothing is thrown, so existing exports keep working. 20,000 rows is roughly where a PDF passes 30 MB and a few seconds to build, and where viewers start to struggle to open it. Pass a larger value (or `Infinity`) to silence the warning when a large PDF is intended.
|
|
135
|
+
- **Total time.** Slicing makes an export slightly slower overall, not faster. Formatting is a small part of the work; drawing the table dominates.
|
|
100
136
|
|
|
101
137
|
## Totals and filter summary
|
|
102
138
|
|
|
@@ -148,7 +184,7 @@ Pass the `font` option with a Unicode TrueType font. See [Non-Latin text](#non-l
|
|
|
148
184
|
|
|
149
185
|
**Export is slow or the file is huge**
|
|
150
186
|
|
|
151
|
-
PDF is not suited to tens of thousands of rows (see
|
|
187
|
+
PDF is not suited to tens of thousands of rows. Show progress and a Cancel button (see [Large exports](#large-exports)), or use CSV or `.xlsx` for large exports.
|
|
152
188
|
|
|
153
189
|
**Error: "exportToPdf requires 'jspdf' and 'jspdf-autotable'"**
|
|
154
190
|
|
|
@@ -48,6 +48,32 @@ Each active sort column shows a numbered priority badge (`1`, `2`, `3`…) next
|
|
|
48
48
|
- A column that mixes kinds of value (numbers and strings, say) is ordered by kind first — numbers, dates, booleans, strings — so the order does not depend on the input order.
|
|
49
49
|
- Ties keep the rows' original order.
|
|
50
50
|
|
|
51
|
+
### Custom comparator: `sortComparator` (v3.1.0+)
|
|
52
|
+
|
|
53
|
+
Give a column `sortComparator(v1, v2, params1, params2)` to replace the built-in comparison for that column. Return a negative number when `v1` sorts before `v2` **in ascending order**, a positive number when after, `0` when equal.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const SIZE_ORDER: Record<string, number> = { S: 0, M: 1, L: 2, XL: 3 };
|
|
57
|
+
|
|
58
|
+
const columns: GridColDef<Product>[] = [
|
|
59
|
+
{
|
|
60
|
+
field: 'size',
|
|
61
|
+
sortComparator: (a, b) => (SIZE_ORDER[String(a)] ?? 99) - (SIZE_ORDER[String(b)] ?? 99),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `v1` / `v2` are the cell values **after `valueGetter`** (the same values the built-in sort reads). `params1` / `params2` are `{ id, field, row, value }` (`GridSortCellParams`), for tie-breaking on other fields of the row.
|
|
67
|
+
- **The comparator sees every value, including `null` and `undefined`**, so you decide where empty values go. The built-in "empty values last in asc" rule does not apply to a column with a comparator. (Why: a custom order often has its own idea of "empty" — `'N/A'`, `0`, a sentinel — and hiding nulls from the comparator would make those impossible to place consistently with real nulls.)
|
|
68
|
+
- **Direction**: write the ascending order only. For `desc` the grid negates the result, so values the comparator puts last in asc (nulls, typically) come first in desc — the same as the built-in behaviour.
|
|
69
|
+
- **Multi-sort**: the comparator is one key in the chain; when it returns `0` the next sort key decides, then the original row order.
|
|
70
|
+
- **Row grouping**: sorting by the grouping column (or by the grouping/label column) orders the groups by their grouping value using the grouping column's comparator; for a group row, `params.row` is the synthetic group row. Leaf rows inside a group use the sorted column's comparator.
|
|
71
|
+
- **Tree data**: siblings are sorted with the comparator at every level. Auto-created parents (no row of their own) are compared by label with the built-in comparison.
|
|
72
|
+
- **Pivot**: sorting a pivot row-label column uses that source column's comparator on the label values.
|
|
73
|
+
- Everything that reuses the pipeline order uses it too: `apiRef.getAllFilteredRows()`, exports, clipboard.
|
|
74
|
+
- A comparator that throws, or returns `NaN`, reads as `0` (equal) with a one-time dev warning per column; the grid does not crash.
|
|
75
|
+
- **Ignored with `sortingMode="server"`**: the server sorts, the grid shows `rows` as given.
|
|
76
|
+
|
|
51
77
|
```tsx
|
|
52
78
|
// Controlled multi-sort — set programmatically or drive from UI with multiSort:
|
|
53
79
|
const [sortModel, setSortModel] = useState<GridSortItem[]>([
|
|
@@ -284,7 +284,7 @@ Multi-sort:
|
|
|
284
284
|
|
|
285
285
|
**Who is affected:** apps with snapshot tests of sorted output, or server code that expects the old client order.
|
|
286
286
|
|
|
287
|
-
**Fix it:** update the snapshots.
|
|
287
|
+
**Fix it:** update the snapshots. Since **v3.1.0** a column can take a custom `sortComparator(v1, v2, params1, params2)` (see `docs/features/sorting-pagination.md`), which is the simplest way to get a custom order. On 3.0.x there is no per-column comparator; to sort by a different key, return that key from a `valueGetter` and format it for display with `valueFormatter` (or sort on the server with `sortingMode="server"`):
|
|
288
288
|
|
|
289
289
|
```tsx
|
|
290
290
|
import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
|
|
@@ -6,6 +6,8 @@ export interface ColumnResizeHandleProps {
|
|
|
6
6
|
field: string;
|
|
7
7
|
currentWidth: number;
|
|
8
8
|
onResize: (field: string, newWidth: number) => void;
|
|
9
|
+
/** Fits the column to its content: called on double-click and on Enter. */
|
|
10
|
+
onAutosize?: (field: string) => void;
|
|
9
11
|
/** Defaults to 50 (the layout's default minimum), or the current width if that is smaller. */
|
|
10
12
|
minWidth?: number;
|
|
11
13
|
/** Defaults to no limit. */
|
|
@@ -43,7 +45,7 @@ function suppressNextClick(): void {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
46
|
-
const { field, currentWidth, onResize, minWidth, maxWidth, edge = 'end' } = props;
|
|
48
|
+
const { field, currentWidth, onResize, onAutosize, minWidth, maxWidth, edge = 'end' } = props;
|
|
47
49
|
const [isDragging, setIsDragging] = useState(false);
|
|
48
50
|
const activeRef = useRef<ActiveResize | null>(null);
|
|
49
51
|
|
|
@@ -103,17 +105,24 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
103
105
|
}, [field, onResize, widthAt]);
|
|
104
106
|
|
|
105
107
|
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
108
|
+
if (event.key === 'Enter' && onAutosize) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
onAutosize(field);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
106
114
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
107
115
|
event.preventDefault();
|
|
108
116
|
event.stopPropagation();
|
|
109
117
|
const step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
|
|
110
118
|
onResize(field, clampResizeWidth(currentWidth + step, currentWidth, minWidth, maxWidth));
|
|
111
|
-
}, [field, currentWidth, onResize, minWidth, maxWidth]);
|
|
119
|
+
}, [field, currentWidth, onResize, onAutosize, minWidth, maxWidth]);
|
|
112
120
|
|
|
113
121
|
const handleDoubleClick = useCallback((event: React.MouseEvent) => {
|
|
114
122
|
event.preventDefault();
|
|
115
123
|
event.stopPropagation();
|
|
116
|
-
|
|
124
|
+
onAutosize?.(field);
|
|
125
|
+
}, [field, onAutosize]);
|
|
117
126
|
|
|
118
127
|
const classNames = [
|
|
119
128
|
'ogx-column-resize-handle',
|
|
@@ -51,6 +51,7 @@ import { useStableColumns } from '../../hooks/core/useStableColumns';
|
|
|
51
51
|
import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
|
|
52
52
|
import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
|
|
53
53
|
import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
|
|
54
|
+
import { useGridColumnAutosize } from '../../hooks/core/useGridColumnAutosize';
|
|
54
55
|
import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
|
|
55
56
|
import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
|
|
56
57
|
import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
|
|
@@ -500,6 +501,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
500
501
|
disableKeyboardShortcut: disableClipboardCopy,
|
|
501
502
|
});
|
|
502
503
|
useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
|
|
504
|
+
const handleColumnAutosize = useGridColumnAutosize<R>({ apiRef, containerRef, columns: orderedColumns, onColumnResize: handleColumnResize });
|
|
503
505
|
|
|
504
506
|
const { scrollTop, scrollLeft, overscanRows, handleScroll, setViewportElement } = useGridViewport({
|
|
505
507
|
onRowsScrollEnd, overscanRowCount, rowCount: layout.unpinnedRowsLength, autoHeight, setDimensions, viewportRef,
|
|
@@ -779,6 +781,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
779
781
|
multiSort={multiSort}
|
|
780
782
|
|
|
781
783
|
onColumnResize={handleColumnResize}
|
|
784
|
+
onColumnAutosize={handleColumnAutosize}
|
|
782
785
|
columnWidths={columnWidths}
|
|
783
786
|
pinnedColumns={effectivePinnedColumns}
|
|
784
787
|
|
|
@@ -43,6 +43,8 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
43
43
|
onSort?: (field: string, direction: GridSortDirection) => void;
|
|
44
44
|
sortModel?: Array<{ field: string; sort: 'asc' | 'desc' }>;
|
|
45
45
|
onColumnResize?: (field: string, newWidth: number) => void;
|
|
46
|
+
/** Fits a column to its content (double-click or Enter on its resize handle). */
|
|
47
|
+
onColumnAutosize?: (field: string) => void;
|
|
46
48
|
columnWidths?: Record<string, number>;
|
|
47
49
|
pinnedColumns?: GridColumnPinning;
|
|
48
50
|
hasDetailPanel?: boolean;
|
|
@@ -81,6 +83,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
81
83
|
onSort,
|
|
82
84
|
sortModel = [],
|
|
83
85
|
onColumnResize,
|
|
86
|
+
onColumnAutosize,
|
|
84
87
|
columnWidths = {},
|
|
85
88
|
pinnedColumns,
|
|
86
89
|
hasDetailPanel = false,
|
|
@@ -525,6 +528,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
525
528
|
field={colDef.field}
|
|
526
529
|
currentWidth={getRenderedColumnWidth(colDef, columnWidths)}
|
|
527
530
|
onResize={onColumnResize}
|
|
531
|
+
onAutosize={onColumnAutosize}
|
|
528
532
|
minWidth={colDef.minWidth}
|
|
529
533
|
maxWidth={colDef.maxWidth}
|
|
530
534
|
edge={pinnedPosition === 'right' ? 'start' : 'end'}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { MutableRefObject, RefObject } from 'react';
|
|
3
|
+
import type { GridApi, GridColDef, GridValidRowModel } from '../../types';
|
|
4
|
+
import { clampAutosizeWidth, measureColumnContentWidth } from '../../utils/columnAutosize';
|
|
5
|
+
|
|
6
|
+
export interface UseGridColumnAutosizeParams<R extends GridValidRowModel> {
|
|
7
|
+
apiRef: MutableRefObject<GridApi>;
|
|
8
|
+
/** The grid root (`.ogx`). */
|
|
9
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
10
|
+
/** Every column the grid knows (hidden ones are measured as not rendered and left alone). */
|
|
11
|
+
columns: readonly GridColDef<R>[];
|
|
12
|
+
/** The width setter a manual resize uses. */
|
|
13
|
+
onColumnResize: (field: string, width: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Auto-sizing of columns to their content: the header and the cells in the current render window,
|
|
18
|
+
* measured in the DOM, clamped to `minWidth` / `maxWidth`. Columns with `resizable: false` are left
|
|
19
|
+
* alone. A flex column gets a fixed width, like after a manual resize. Installs `autosizeColumn` /
|
|
20
|
+
* `autosizeColumns` on the API and returns `autosizeColumn` for the resize handle.
|
|
21
|
+
*/
|
|
22
|
+
export function useGridColumnAutosize<R extends GridValidRowModel>(params: UseGridColumnAutosizeParams<R>): (field: string) => void {
|
|
23
|
+
const { apiRef } = params;
|
|
24
|
+
const latestRef = useRef(params);
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
latestRef.current = params;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const autosizeColumns = useCallback((fields?: readonly string[]) => {
|
|
30
|
+
const { containerRef, columns, onColumnResize } = latestRef.current;
|
|
31
|
+
const root = containerRef.current;
|
|
32
|
+
if (!root) return;
|
|
33
|
+
// Measure every column first: the reads all see the same layout, then the widths are set.
|
|
34
|
+
const widths: Array<[string, number]> = [];
|
|
35
|
+
for (const field of fields ?? columns.map(c => c.field)) {
|
|
36
|
+
const colDef = columns.find(c => c.field === field);
|
|
37
|
+
if (!colDef || colDef.resizable === false) continue;
|
|
38
|
+
const measured = measureColumnContentWidth(root, field);
|
|
39
|
+
if (measured !== null) widths.push([field, clampAutosizeWidth(measured, colDef.minWidth, colDef.maxWidth)]);
|
|
40
|
+
}
|
|
41
|
+
for (const [field, width] of widths) onColumnResize(field, width);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const autosizeColumn = useCallback((field: string) => autosizeColumns([field]), [autosizeColumns]);
|
|
45
|
+
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
apiRef.current.autosizeColumn = autosizeColumn;
|
|
48
|
+
apiRef.current.autosizeColumns = autosizeColumns;
|
|
49
|
+
}, [apiRef, autosizeColumn, autosizeColumns]);
|
|
50
|
+
|
|
51
|
+
return autosizeColumn;
|
|
52
|
+
}
|
|
@@ -103,8 +103,8 @@ export function useGridRowPipeline<R extends GridRowModel>(
|
|
|
103
103
|
const sortedUnpinnedRows = useMemo<R[]>(() => {
|
|
104
104
|
if (activeHierarchyHandlers) return unpinnedRows;
|
|
105
105
|
if (sortingMode === 'server') return unpinnedRows;
|
|
106
|
-
return sortRows(unpinnedRows, sortModel, columnLookup) as R[];
|
|
107
|
-
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, columnLookup]);
|
|
106
|
+
return sortRows(unpinnedRows, sortModel, columnLookup, getRowId) as R[];
|
|
107
|
+
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, columnLookup, getRowId]);
|
|
108
108
|
|
|
109
109
|
const isClientPaged = pagination && paginationMode !== 'server';
|
|
110
110
|
const pageSize = normalizePageSize(effectivePaginationModel.pageSize);
|
|
@@ -362,10 +362,20 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
362
362
|
|
|
363
363
|
const readSortValue = (id: GridRowId, sortItem: GridSortItem): GridSortValue => {
|
|
364
364
|
const node = treeNodes.get(id);
|
|
365
|
-
|
|
365
|
+
const groupVariants = groupRowVariants.get(id);
|
|
366
|
+
if (node && groupVariants) {
|
|
366
367
|
const groupingField = node.groupingField ?? '';
|
|
367
368
|
if (sortItem.field === groupingField || sortItem.field === labelField) {
|
|
368
|
-
|
|
369
|
+
const groupingColDef = columnLookup?.byField.get(groupingField);
|
|
370
|
+
const comparator = groupingColDef?.sortComparator;
|
|
371
|
+
return {
|
|
372
|
+
value: node.groupingValue,
|
|
373
|
+
type: groupingColDef?.type,
|
|
374
|
+
comparator,
|
|
375
|
+
params: comparator
|
|
376
|
+
? { id, field: groupingField, row: groupVariants.inline, value: node.groupingValue }
|
|
377
|
+
: undefined,
|
|
378
|
+
};
|
|
369
379
|
}
|
|
370
380
|
if (node.aggregatedValues && Object.prototype.hasOwnProperty.call(node.aggregatedValues, sortItem.field)) {
|
|
371
381
|
return { value: node.aggregatedValues[sortItem.field], type: columnLookup?.byField.get(sortItem.field)?.type };
|
|
@@ -374,7 +384,14 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
374
384
|
}
|
|
375
385
|
const row = rowLookup.get(id);
|
|
376
386
|
const colDef = columnLookup?.byField.get(sortItem.field);
|
|
377
|
-
|
|
387
|
+
const value = row ? getCellValue(row, sortItem.field, colDef) : undefined;
|
|
388
|
+
const comparator = colDef?.sortComparator;
|
|
389
|
+
return {
|
|
390
|
+
value,
|
|
391
|
+
type: colDef?.type,
|
|
392
|
+
comparator,
|
|
393
|
+
params: comparator && row ? { id, field: sortItem.field, row, value } : undefined,
|
|
394
|
+
};
|
|
378
395
|
};
|
|
379
396
|
|
|
380
397
|
const result: R[] = [];
|
package/lib/hooks/useTreeData.ts
CHANGED
|
@@ -333,7 +333,14 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
|
|
|
333
333
|
}
|
|
334
334
|
const row = rowLookup.get(id);
|
|
335
335
|
const colDef = columnLookup?.byField.get(sortItem.field);
|
|
336
|
-
|
|
336
|
+
const value = row ? getCellValue(row, sortItem.field, colDef) : undefined;
|
|
337
|
+
const comparator = colDef?.sortComparator;
|
|
338
|
+
return {
|
|
339
|
+
value,
|
|
340
|
+
type: colDef?.type,
|
|
341
|
+
comparator,
|
|
342
|
+
params: comparator && row ? { id, field: sortItem.field, row, value } : undefined,
|
|
343
|
+
};
|
|
337
344
|
};
|
|
338
345
|
|
|
339
346
|
const result: GridRowModel[] = [];
|
package/lib/index.ts
CHANGED
|
@@ -89,6 +89,7 @@ export type {
|
|
|
89
89
|
GridRenderEditCellParams,
|
|
90
90
|
GridRenderHeaderParams,
|
|
91
91
|
GridValueGetterParams,
|
|
92
|
+
GridSortCellParams,
|
|
92
93
|
GridValueSetterParams,
|
|
93
94
|
GridValueFormatterParams,
|
|
94
95
|
GridRowParams,
|
|
@@ -135,4 +136,5 @@ export type {
|
|
|
135
136
|
GridRowMeta,
|
|
136
137
|
GridLocaleText,
|
|
137
138
|
PdfExportOptions,
|
|
139
|
+
PdfExportProgress,
|
|
138
140
|
} from './types';
|
package/lib/types/index.ts
CHANGED
|
@@ -172,6 +172,16 @@ export interface GridColDef<R extends GridValidRowModel = GridRowModel> {
|
|
|
172
172
|
* @since v3.0
|
|
173
173
|
*/
|
|
174
174
|
valueSetter?(params: GridValueSetterParams<R>): R;
|
|
175
|
+
/**
|
|
176
|
+
* Custom ascending comparator for client-side sorting by this column, used instead of the built-in
|
|
177
|
+
* type-aware comparison. `v1` / `v2` are the cell values (`valueGetter` applied) and **include
|
|
178
|
+
* `null` / `undefined`**, so the comparator decides where empty values go. The grid reverses the
|
|
179
|
+
* result for 'desc' and chains it with the other sort keys in a multi-sort. Row grouping uses the
|
|
180
|
+
* grouping column's comparator to order groups by their grouping value. A comparator that throws
|
|
181
|
+
* or returns NaN is read as 0 (with a one-time dev warning). Ignored with `sortingMode="server"`.
|
|
182
|
+
* @since v3.1
|
|
183
|
+
*/
|
|
184
|
+
sortComparator?(v1: unknown, v2: unknown, params1: GridSortCellParams<R>, params2: GridSortCellParams<R>): number;
|
|
175
185
|
/** Function to format a value into a human-readable string. */
|
|
176
186
|
valueFormatter?(params: GridValueFormatterParams<R>): string;
|
|
177
187
|
/** Custom component or element to render in the cell. */
|
|
@@ -262,6 +272,18 @@ export interface GridColumnGroup {
|
|
|
262
272
|
export type GridColumnGroupingModel = GridColumnGroup[];
|
|
263
273
|
|
|
264
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Parameters passed to `GridColDef.sortComparator` for each side of a comparison. For a row-grouping
|
|
277
|
+
* group row, `row` is the synthetic group row and `value` its grouping value.
|
|
278
|
+
* @since v3.1
|
|
279
|
+
*/
|
|
280
|
+
export interface GridSortCellParams<R extends GridValidRowModel = GridRowModel> {
|
|
281
|
+
id: GridRowId;
|
|
282
|
+
field: string;
|
|
283
|
+
row: R;
|
|
284
|
+
value: unknown;
|
|
285
|
+
}
|
|
286
|
+
|
|
265
287
|
/** Parameters passed to the `valueGetter` function. */
|
|
266
288
|
export interface GridValueGetterParams<R extends GridValidRowModel = GridRowModel> {
|
|
267
289
|
/** The row object. */
|
|
@@ -505,6 +527,37 @@ export interface PdfExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
|
505
527
|
* Has no effect when the grid has no active `rowGroupingModel`, or when `selectedRows` is non-empty.
|
|
506
528
|
*/
|
|
507
529
|
groupedRows?: GridGroupedExportRow[];
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Called as the export advances: `prepare` (formatting rows; `total` = source rows or grouped
|
|
533
|
+
* entries), `render` (drawing table rows; `total` = table body rows) and `save` (`0/1` before the
|
|
534
|
+
* file is written, `1/1` after). `done` never decreases within a phase and ends at `total`.
|
|
535
|
+
* @since v3.2
|
|
536
|
+
*/
|
|
537
|
+
onProgress?: (progress: PdfExportProgress) => void;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Cancels the export. When aborted, the returned promise rejects with a `DOMException` named
|
|
541
|
+
* `'AbortError'` and no file is saved. Checked between slices of work, so cancellation takes
|
|
542
|
+
* effect within one slice; the final save step cannot be interrupted once started.
|
|
543
|
+
* @since v3.2
|
|
544
|
+
*/
|
|
545
|
+
signal?: AbortSignal;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Table-row count above which a development-mode warning recommends `exportToCsv` or
|
|
549
|
+
* `exportToExcelAdvanced`. The export still runs (nothing is thrown). Pass `Infinity` to silence it.
|
|
550
|
+
* Default: 20000
|
|
551
|
+
* @since v3.2
|
|
552
|
+
*/
|
|
553
|
+
maxRows?: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Progress reported by `PdfExportOptions.onProgress`. @since v3.2 */
|
|
557
|
+
export interface PdfExportProgress {
|
|
558
|
+
phase: 'prepare' | 'render' | 'save';
|
|
559
|
+
done: number;
|
|
560
|
+
total: number;
|
|
508
561
|
}
|
|
509
562
|
|
|
510
563
|
/**
|
|
@@ -1127,6 +1180,16 @@ export interface GridApi<R extends GridValidRowModel = GridRowModel> {
|
|
|
1127
1180
|
/** Scrolls the grid viewport to a specific row or column index. */
|
|
1128
1181
|
scrollToIndexes: (params: { rowIndex?: number; colIndex?: number }) => void;
|
|
1129
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* Sizes a column to fit its header and the cells rendered right now (the virtualization window),
|
|
1185
|
+
* clamped to `minWidth` (default 50px) / `maxWidth`, like double-clicking its resize handle.
|
|
1186
|
+
* Does nothing for `resizable: false` columns or a column that is not rendered.
|
|
1187
|
+
* A flex column gets a fixed width. @since v3.1
|
|
1188
|
+
*/
|
|
1189
|
+
autosizeColumn: (field: string) => void;
|
|
1190
|
+
/** `autosizeColumn` for several columns (default: every column). @since v3.1 */
|
|
1191
|
+
autosizeColumns: (fields?: readonly string[]) => void;
|
|
1192
|
+
|
|
1130
1193
|
/**
|
|
1131
1194
|
* Programmatically copies all currently selected rows to the clipboard as TSV: every selected
|
|
1132
1195
|
* row that passes the filter (other pages, collapsed groups and pinned rows included), with the
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DEFAULT_MIN_COLUMN_WIDTH } from '../components/ColumnResizeHandle/clampResizeWidth';
|
|
2
|
+
|
|
3
|
+
/** The grid root an element belongs to, so a nested grid (in a detail panel) is never measured. */
|
|
4
|
+
const ROOT_SELECTOR = '.ogx';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The width `content` needs to show without clipping, plus the horizontal padding and borders of
|
|
8
|
+
* `cell`. `content` is laid out at `max-content` for the read and restored right after, so the
|
|
9
|
+
* reading works for a column that is too wide (shrinks) as well as one that is too narrow (grows).
|
|
10
|
+
*/
|
|
11
|
+
function measureCell(cell: HTMLElement, content: HTMLElement): number {
|
|
12
|
+
const saved = content.style.cssText;
|
|
13
|
+
content.style.width = 'max-content';
|
|
14
|
+
content.style.minWidth = '0';
|
|
15
|
+
content.style.maxWidth = 'none';
|
|
16
|
+
content.style.flex = 'none';
|
|
17
|
+
const contentWidth = content.getBoundingClientRect().width;
|
|
18
|
+
content.style.cssText = saved;
|
|
19
|
+
|
|
20
|
+
const style = getComputedStyle(cell);
|
|
21
|
+
const extra = [style.paddingLeft, style.paddingRight, style.borderLeftWidth, style.borderRightWidth]
|
|
22
|
+
.reduce((sum, value) => sum + (parseFloat(value) || 0), 0);
|
|
23
|
+
return contentWidth + extra;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The width a column needs to fit its header and the cells rendered right now (the virtualization
|
|
28
|
+
* window, pinned rows included), measured in the DOM. Cells spanning several columns and cells
|
|
29
|
+
* hidden by a row span are skipped. Returns null when nothing of the column is rendered.
|
|
30
|
+
*/
|
|
31
|
+
export function measureColumnContentWidth(root: HTMLElement, field: string): number | null {
|
|
32
|
+
const escaped = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(field) : field.replace(/"/g, '\\"');
|
|
33
|
+
const ownedByRoot = (el: Element) => el.closest(ROOT_SELECTOR) === root;
|
|
34
|
+
let width: number | null = null;
|
|
35
|
+
const take = (w: number) => { width = width === null ? w : Math.max(width, w); };
|
|
36
|
+
|
|
37
|
+
root.querySelectorAll<HTMLElement>(`[role="columnheader"][data-field="${escaped}"]`).forEach(header => {
|
|
38
|
+
if (!ownedByRoot(header)) return;
|
|
39
|
+
const content = header.querySelector<HTMLElement>('.ogx__header-cell-content');
|
|
40
|
+
if (content) take(measureCell(header, content));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
root.querySelectorAll<HTMLElement>(`[role="gridcell"][data-field="${escaped}"]`).forEach(cell => {
|
|
44
|
+
if (!ownedByRoot(cell) || cell.hasAttribute('aria-colspan')) return;
|
|
45
|
+
const content = cell.querySelector<HTMLElement>(':scope > .ogx__cell-content');
|
|
46
|
+
if (content) take(measureCell(cell, content));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return width === null ? null : Math.ceil(width);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Clamps an autosized width to the column's `minWidth` (default 50px) and `maxWidth`. */
|
|
53
|
+
export function clampAutosizeWidth(width: number, minWidth?: number, maxWidth?: number): number {
|
|
54
|
+
const min = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
55
|
+
return Math.max(min, Math.min(maxWidth ?? Infinity, width));
|
|
56
|
+
}
|