@opencorestack/opengridx 3.2.2 → 3.3.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 +22 -0
- package/README.md +12 -1
- package/dist/index.d.ts +173 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +5472 -4841
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +15 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +34 -2
- package/docs/README.md +1 -0
- package/docs/architecture/datagrid-orchestration.md +13 -5
- package/docs/components/header.md +25 -0
- package/docs/customization/theming.md +3 -1
- package/docs/features/cell-selection.md +131 -0
- package/docs/features/clipboard.md +2 -0
- package/docs/features/keyboard-navigation.md +14 -2
- package/docs/roadmap.md +64 -7
- package/lib/components/Cell/Cell.css +30 -0
- package/lib/components/Cell/Cell.tsx +25 -13
- package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
- package/lib/components/CellSelectionStats/CellSelectionStats.css +31 -0
- package/lib/components/CellSelectionStats/CellSelectionStats.tsx +32 -0
- package/lib/components/CellSelectionStats/GridCellSelectionStatsArea.tsx +24 -0
- package/lib/components/DataGrid/DataGrid.tsx +61 -1
- package/lib/components/DataGrid/GridLiveRegion.tsx +8 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +9 -0
- package/lib/components/DataGrid/GridToolbarSlot.tsx +5 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +9 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +2 -1
- package/lib/components/Header/Header.css +12 -0
- package/lib/components/Header/Header.tsx +17 -4
- package/lib/components/Row/Row.tsx +38 -1
- package/lib/components/Toolbar/GridToolbar.tsx +10 -4
- package/lib/components/ui/Checkbox.tsx +10 -5
- package/lib/hooks/core/gridApiPlaceholder.ts +6 -0
- package/lib/hooks/core/useGridCellSelection.ts +603 -0
- package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
- package/lib/hooks/core/useGridClipboardApi.ts +8 -2
- package/lib/hooks/core/useGridControlledState.ts +38 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +8 -4
- package/lib/hooks/features/useAggregation.ts +8 -6
- package/lib/hooks/features/useGridClipboard.ts +47 -2
- package/lib/hooks/features/useGridEditing.ts +16 -7
- package/lib/index.ts +9 -0
- package/lib/styles/base/variables.css +6 -1
- package/lib/styles/opengridx.css +1 -0
- package/lib/theme/DataGridThemeProvider.tsx +4 -0
- package/lib/theme/presets.ts +8 -0
- package/lib/theme/types.ts +4 -0
- package/lib/types/index.ts +130 -0
- package/lib/utils/attempt.ts +14 -0
- package/lib/utils/cellSelection.ts +238 -0
- package/lib/utils/headerWrap.ts +23 -0
- package/llms.txt +17 -2
- package/package.json +7 -3
package/docs/API_REFERENCE.md
CHANGED
|
@@ -17,6 +17,7 @@ The main component for displaying and interacting with data.
|
|
|
17
17
|
| `getRowId` | `(row: R) => GridRowId` | `row.id` | Returns a unique identifier for each row. It only keys the grid's internal store: rows reach `renderCell`, events, `apiRef` and `processRowUpdate` unchanged, and the id is never written to `row.id` (v3.0+). Pass it to export functions as `getRowId` when you export `selectedRows`. It must not throw: an id is a programming contract, so a throw is not contained (for a `dataSource` row it fails that request with the error overlay). |
|
|
18
18
|
| `rowHeight` | `number` | `52` | Height of each row in pixels. Without it, an enclosing `DataGridThemeProvider`'s `grid.rowHeightStandard` applies (v3.0+). |
|
|
19
19
|
| `headerHeight` | `number` | `56` | Height of the header row. Without it, the theme's `grid.headerHeight` applies (v3.0+). |
|
|
20
|
+
| `wrapHeaderText` | `boolean` | `false` | Wraps header titles onto several lines instead of cutting them with an ellipsis (v3.3.0+). The header keeps `headerHeight`: a title takes as many lines as fit (2 at 56px, 3 at 72px, 4 at 88px) and ends in an ellipsis if it is still longer; raise `headerHeight` to show more lines. Wrapped headers get `ogx__header-cell--wrap` and their full title as the `title` tooltip (unless `description` is set). `GridColDef.wrapHeaderText` overrides it per column. See [Header](components/header.md). |
|
|
20
21
|
| `autoHeight` | `boolean` | `false` | Adjust grid height to match row total. |
|
|
21
22
|
| `overscanRowCount` | `number` | `3` | Minimum rows rendered outside the visible viewport. The grid adapts this upward automatically based on scroll velocity — this prop sets the floor. |
|
|
22
23
|
| `loading` | `boolean` | `false` | With no rows, the body shows skeleton rows (or `slots.loadingOverlay`). With rows already shown, they stay and a progress bar runs along the top of the grid (or `slots.loadingOverlay` is shown over them). Works in list view too. |
|
|
@@ -30,7 +31,7 @@ The main component for displaying and interacting with data.
|
|
|
30
31
|
| `height` | `number \| string` | `undefined` | Total height of the grid. Without it the grid fills its container (`height: 100%`, or `flex: 1 1 auto; min-height: 0` as a flex item; v3.0.1+), and grows to fit every row in an auto-height container. |
|
|
31
32
|
| `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Visual row density: compact = the theme's `grid.rowHeightCompact` or 32 px, standard = `rowHeight`, comfortable = the theme's `grid.rowHeightComfortable` or 72 px. |
|
|
32
33
|
| `initialState` | `GridInitialState` | `undefined` | Starting state (sort, filter, pagination, columns, density), read on mount. Seeds the grid's uncontrolled state; see [State Persistence](features/state-persistence.md). |
|
|
33
|
-
| `slots` | `GridSlots` | — | Custom component overrides: `toolbar`, `pagination`, `noRowsOverlay`, `loadingOverlay`, `footer`. See [`GridSlots`](#gridslots-and-gridslotprops) and the [Slots API](customization/slots-api.md). |
|
|
34
|
+
| `slots` | `GridSlots` | — | Custom component overrides: `toolbar`, `pagination`, `noRowsOverlay`, `loadingOverlay`, `footer`, `cellSelectionStats`. See [`GridSlots`](#gridslots-and-gridslotprops) and the [Slots API](customization/slots-api.md). |
|
|
34
35
|
| `slotProps` | `GridSlotProps` | — | Props passed to the slots, keyed like `slots`. `slotProps.toolbar` is typed as `Partial<GridToolbarProps>` (plus any extra keys your own toolbar reads), so `GridToolbar` render props are type-checked (v3.0+). |
|
|
35
36
|
| `filterModel` | `GridFilterModel` | `undefined` | Active filters (controlled). Omit it to let the grid keep its own filter state, seeded from `initialState.filter` and changed by the toolbar or `apiRef.setFilterModel` (v3.0+). |
|
|
36
37
|
| `sortModel` | `GridSortItem[]` | `undefined` | Active sorting. |
|
|
@@ -61,6 +62,26 @@ The main component for displaying and interacting with data.
|
|
|
61
62
|
| `disableMultipleRowSelection` | `boolean` | `false` | When `true`, at most one row can be selected at a time — by click, checkbox, Space or `apiRef` — and the header select-all checkbox is not shown. |
|
|
62
63
|
| `disableClipboardCopy` | `boolean` | `false` | When `true`, Ctrl+C / Cmd+C does not copy the selected rows, so the page or your own handler owns the shortcut. `apiRef.current.copySelectedRows()` still works. See [Clipboard](features/clipboard.md). |
|
|
63
64
|
|
|
65
|
+
#### Cell Range Selection (v3.3)
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| :--- | :--- | :--- | :--- |
|
|
69
|
+
| `cellSelection` | `boolean` | `false` | Spreadsheet-style cell ranges: drag (with auto-scroll), Shift+click, Shift+arrow / Home / End / PageUp / PageDown, Ctrl/Cmd+A (every data cell of the page) and Escape (collapse). While on, Ctrl/Cmd+C copies the range (the focused cell at minimum) as TSV without a header line; `apiRef.current.copySelectedRows()` still copies rows. Row selection is not changed by ranges. See [Cell Range Selection](features/cell-selection.md). |
|
|
70
|
+
| `cellSelectionModel` | `GridCellSelectionModel` | — | Controlled cell selection (zero or one range). Ignored while `cellSelection` is off. |
|
|
71
|
+
| `onCellSelectionModelChange` | `(model: GridCellSelectionModel, details: { reason: GridCellSelectionReason }) => void` | — | Fired when the cell selection changes. `reason`: `'pointer'`, `'keyboard'`, `'selectAll'`, `'api'`, `'clear'` or `'dataChange'` (a corner's row or column is no longer displayed, so the range was cleared). |
|
|
72
|
+
| `showCellSelectionStats` | `boolean` | `false` | Status bar under the grid (above the pagination area) with Count of non-empty cells and Sum / Average of the numeric cells, while more than one cell is selected. Replaceable through `slots.cellSelectionStats`. Needs `cellSelection`. |
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
interface GridCellCoordinates { id: GridRowId; field: string }
|
|
76
|
+
interface GridCellRange { anchor: GridCellCoordinates; head: GridCellCoordinates } // anchor = active cell
|
|
77
|
+
type GridCellSelectionModel = readonly GridCellRange[];
|
|
78
|
+
type GridCellSelectionReason = 'pointer' | 'keyboard' | 'selectAll' | 'api' | 'clear' | 'dataChange';
|
|
79
|
+
interface GridCellSelectionChangeDetails { reason: GridCellSelectionReason }
|
|
80
|
+
interface GridSelectedCell { id: GridRowId; field: string; value: unknown }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Public CSS: `ogx__cell--range`, `ogx__cell--range-top` / `-bottom` / `-left` / `-right`, `ogx--range-dragging` (root, during a drag), `--ogx-range-background`, `--ogx-range-border`. Range cells carry `aria-selected="true"`.
|
|
84
|
+
|
|
64
85
|
#### Column Visibility
|
|
65
86
|
|
|
66
87
|
| Prop | Type | Default | Description |
|
|
@@ -210,6 +231,7 @@ The types of the `slots` and `slotProps` props (v3.0+ exports). Each slot is typ
|
|
|
210
231
|
| `pagination` | The built-in `Pagination` bar (shown with `pagination`). | `GridPaginationSlotProps` (= `PaginationProps`: `page`, `pageSize`, `rowCount`, `pageSizeOptions`, `onPageChange`, `onPageSizeChange`, `localeText`), then `slotProps.pagination`. |
|
|
211
232
|
| `noRowsOverlay` | The empty-state icon and label (shown when there are no rows and the grid is not loading). | `slotProps.noRowsOverlay` only (`GridOverlaySlotProps`). |
|
|
212
233
|
| `loadingOverlay` | The loading indicator: the skeleton rows while loading with no rows, the progress bar while loading with rows shown (the slot is then shown over them). | `slotProps.loadingOverlay` only (`GridOverlaySlotProps`). |
|
|
234
|
+
| `cellSelectionStats` | The status bar of `showCellSelectionStats` (v3.3). Rendered while more than one cell is selected. | `GridCellSelectionStatsSlotProps`: `apiRef`, `cellCount`, `rowCount`, `columnCount`, `count`, `numericCount`, `sum`, `average` (`null` without numeric cells), then `slotProps.cellSelectionStats`. |
|
|
213
235
|
| `footer` | The pagination area. | `GridFooterSlotProps`: `apiRef`, `aggregationModel`, `aggregationResult`, `rowCount`, `pagination`, `paginationModel`, `pageSizeOptions`, `onPaginationModelChange`, then `slotProps.footer`. `rowCount` is the built-in pager's count (pinned rows excluded, v3.0+); under tree data or row grouping, the filtered data rows. |
|
|
214
236
|
|
|
215
237
|
```ts
|
|
@@ -301,6 +323,12 @@ Access these methods via the `apiRef` prop.
|
|
|
301
323
|
| `getAggregationModel()` | `GridAggregationModel \| null` | Get the active aggregation configuration. |
|
|
302
324
|
| `getAllFilteredRows()` | `R[]` | Get every row that passes the filter, sorted, regardless of pagination, including pinned rows. Under row grouping and tree data it returns the data rows (no group rows) in hierarchy order with every group expanded, ordered like the screen (also when sorting by the hierarchy column; v3.0+). With tree data and a filter, ancestors shown only to give a match its context are left out: the result is the set select-all and the aggregation footer act on (v3.0+). Use for full-dataset exports. |
|
|
303
325
|
| `getGroupedExportRows()` | `GridGroupedExportRow[] \| null` | Get a flat ordered list reflecting the active row-grouping tree (group-header, leaf, subtotal, grand-total), sorted and filtered like the screen. Collapsed groups are included with their rows, and subtotals are computed over each group's exported rows. Group-header and subtotal entries carry `groupLabel`, the label the grid shows (v3.0+). Returns `null` when row grouping is not active. |
|
|
326
|
+
| `getCellSelectionModel()` | `GridCellSelectionModel` | The cell selection (`[]` while `cellSelection` is off). Includes a change made earlier in the same tick (v3.3). |
|
|
327
|
+
| `setCellSelectionModel(model)` | `void` | Replace the cell selection (only the first range is kept); focus moves to its anchor. `reason: 'api'` (v3.3). |
|
|
328
|
+
| `selectCellRange(anchor, head)` | `void` | Select the rectangle between two `GridCellCoordinates`; `anchor` becomes the active cell (v3.3). |
|
|
329
|
+
| `clearCellSelection()` | `void` | Empty the cell selection (`reason: 'clear'`) (v3.3). |
|
|
330
|
+
| `getSelectedCells()` | `GridSelectedCell[]` | Cells of the range in display order with values read through `valueGetter`. Synthetic rows and span-covered positions are left out (v3.3). |
|
|
331
|
+
| `copySelectedCells()` | `Promise<void>` | Copy the range (the focused cell when nothing else is selected) as TSV without a header line. Resolves without writing when there is nothing to copy; rejects when the clipboard write fails (v3.3). |
|
|
304
332
|
| `copySelectedRows()` | `Promise<void>` | Copy every selected row that passes the filter (other pages, collapsed groups and pinned rows included) as TSV, with the visible columns in screen order. Resolves without writing when no selected row is found; rejects when the clipboard write fails. |
|
|
305
333
|
|
|
306
334
|
---
|
|
@@ -346,6 +374,7 @@ Defines the behavior and appearance of a single column.
|
|
|
346
374
|
| `flex` | `number` | — | Flex grow factor — distributes remaining space proportionally. When both are set, `flex` wins over `width` until the user resizes the column. |
|
|
347
375
|
| `align` | `'left' \| 'center' \| 'right'` | `'left'` | Horizontal alignment of cell content. |
|
|
348
376
|
| `headerAlign` | `'left' \| 'center' \| 'right'` | `align` | Horizontal alignment of the header cell content. Falls back to the column's `align`, then `'left'`. |
|
|
377
|
+
| `wrapHeaderText` | `boolean` | grid's `wrapHeaderText` | Wraps this column's header title (v3.3.0+). Overrides the grid prop either way: `false` keeps one line with an ellipsis in a wrapping grid. |
|
|
349
378
|
| `zIndex` | `number` | — | CSS `z-index` for the column's cells and header cell (pinned cells default to 3). |
|
|
350
379
|
|
|
351
380
|
#### Data & Type
|
|
@@ -438,6 +467,7 @@ The grid's building blocks are exported with their props types (the props types
|
|
|
438
467
|
| `Header` | `HeaderProps` | The column header rows. See [Header](components/header.md). |
|
|
439
468
|
| `Row` | `RowProps` | One body row. See [Row](components/row.md). |
|
|
440
469
|
| `Cell` | `CellProps` | One body cell. See [Cell](components/cell.md). |
|
|
470
|
+
| `CellSelectionStats` | `CellSelectionStatsProps` | The default status bar of `showCellSelectionStats` (v3.3), e.g. to wrap in a `cellSelectionStats` slot. |
|
|
441
471
|
| `Skeleton` | `SkeletonProps` | Loading placeholder (`rows`, `columns`, both default 5). |
|
|
442
472
|
| `Button`, `Input`, `Checkbox` | `ButtonProps`, `InputProps`, `CheckboxProps` | The grid's form controls. `Button` defaults to `type="button"`. |
|
|
443
473
|
|
|
@@ -453,7 +483,7 @@ Context provider for overriding the grid's visual system.
|
|
|
453
483
|
- **`colors`**: `primary`, `primaryDark`, `primaryLight`, `primaryFocus`, `secondary*`, `success`, `warning`, `error`, `info`, `white`, `black`, `gray` (`{ 50 … 900 }`). Toolbar, menu, selection and focus accents derive from `primary*` unless set directly.
|
|
454
484
|
- **`typography`**: `fontFamily`, `fontFamilyMono`, `fontSizeXs` … `fontSizeXl`.
|
|
455
485
|
- **`spacing`**: `xs` … `xxl`. **`borders`**: `widthThin/Medium/Thick`, `radiusSm` … `radiusXl`, `color`, `colorHover`. **`shadows`**: `sm` … `xl`.
|
|
456
|
-
- **`grid`**: surfaces (`background`, `borderColor`, `headerBackground`, `headerText`, `headerHoverBackground`, `headerSortedBackground`, `rowText`, `rowHoverBackground`, `rowAlternateBackground`, `rowSelectedBackground`, `rowSelectedHoverBackground`, `cellFocusBorder`, `pinnedLeftShadow`, `pinnedRightShadow`, `checkboxBg`, `checkboxBorder`), sizing (`rowHeightCompact`, `rowHeightStandard`, `rowHeightComfortable`, `headerHeight` in px, read by the grid's layout; the `rowHeight` / `headerHeight` props win), `cellPaddingX`, `cellPaddingY`, `cellFontSize`, `headerFontSize`.
|
|
486
|
+
- **`grid`**: surfaces (`background`, `borderColor`, `headerBackground`, `headerText`, `headerHoverBackground`, `headerSortedBackground`, `rowText`, `rowHoverBackground`, `rowAlternateBackground`, `rowSelectedBackground`, `rowSelectedHoverBackground`, `cellFocusBorder`, `rangeBackground`, `rangeBorder` (v3.3), `pinnedLeftShadow`, `pinnedRightShadow`, `checkboxBg`, `checkboxBorder`), sizing (`rowHeightCompact`, `rowHeightStandard`, `rowHeightComfortable`, `headerHeight` in px, read by the grid's layout; the `rowHeight` / `headerHeight` props win), `cellPaddingX`, `cellPaddingY`, `cellFontSize`, `headerFontSize`.
|
|
457
487
|
- **`toolbar`**, **`overlays`**, **`scrollbar`** (`thumbColor`, `trackColor`, `size`), **`skeleton`** (`baseColor`, `highlightColor`), **`transitions`**.
|
|
458
488
|
|
|
459
489
|
Presets: `darkTheme`, `roseTheme`, `emeraldTheme`, `amberTheme`, `compactTheme`. See [Theming](customization/theming.md).
|
|
@@ -690,6 +720,8 @@ interface GridLocaleText {
|
|
|
690
720
|
paginationOf?: (from: number, to: number, count: number) => string;
|
|
691
721
|
paginationPage?: (page: number, pageCount: number) => string;
|
|
692
722
|
noRowsLabel?: string;
|
|
723
|
+
/** v3.3: live-region text after a cell range change. Default "12 cells selected, 3 rows by 4 columns". */
|
|
724
|
+
cellSelectionAnnouncement?: (cellCount: number, rowCount: number, columnCount: number) => string;
|
|
693
725
|
}
|
|
694
726
|
```
|
|
695
727
|
|
package/docs/README.md
CHANGED
|
@@ -37,6 +37,7 @@ In-depth guides on the grid's functional capabilities.
|
|
|
37
37
|
- **[Loading & Performance](features/loading-states.md)** - Skeletons, Lazy, and Infinite loading
|
|
38
38
|
- **[Row Selection](features/selection.md)** - Multi & single choice selection
|
|
39
39
|
- **[Clipboard](features/clipboard.md)** - Copy and Paste grid data
|
|
40
|
+
- **[Cell Range Selection](features/cell-selection.md)** - Spreadsheet-style cell ranges, copy and a sum / average status bar
|
|
40
41
|
- **[Pinning](features/pinning.md)** - Sticky columns and rows
|
|
41
42
|
- **[State Persistence](features/state-persistence.md)** - Save/Restore grid configuration
|
|
42
43
|
- **[Aggregation & Pivot](features/aggregation-pivot.md)** - Data summarization and analytics
|
|
@@ -17,6 +17,9 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
17
17
|
`useGridAggregationApi` and `useGridScrollToIndexesApi` install once per `apiRef` in a layout
|
|
18
18
|
effect and read the latest values from a ref refreshed in a layout effect, so the methods are
|
|
19
19
|
live in a parent's layout effect and never answer from a previous render.
|
|
20
|
+
- `useGridCellSelection` runs after `useGridSpanning` (it needs `getSpanOrigin` and the span caches) and
|
|
21
|
+
before `useGridKeyboardNavigation`, which receives its Shift-key handlers. `useGridCellSelectionApi` runs
|
|
22
|
+
after the keyboard hook because it reads and sets `focusedCell`.
|
|
20
23
|
- `useGridLiveRowSelection` prunes the selection, and `useGridPageCorrection` corrects a page past the
|
|
21
24
|
end, in effects that run before the later hooks' effects.
|
|
22
25
|
|
|
@@ -25,7 +28,7 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
25
28
|
| # | Hook / util | File | Produces |
|
|
26
29
|
| :- | :--- | :--- | :--- |
|
|
27
30
|
| 0 | `useStableColumns` | `hooks/core/useStableColumns.ts` | The `columns` prop with a stable identity while every column is shallowly equal (inline columns do not re-run the row passes) |
|
|
28
|
-
| 1 | `useGridControlledState` | `hooks/core/useGridControlledState.ts` | Controlled/uncontrolled sort, filter, aggregation, visibility, pinning, pivot, pagination, selection, density |
|
|
31
|
+
| 1 | `useGridControlledState` | `hooks/core/useGridControlledState.ts` | Controlled/uncontrolled sort, filter, aggregation, visibility, pinning, pivot, pagination, selection, density, cell selection (v3.3) |
|
|
29
32
|
| 2 | `useGridPivot` | `hooks/features/useGridPivot.ts` | Pivot rows/columns (or the source ones) and the pipeline filter/sort models |
|
|
30
33
|
| 3 | `resolveGridModes` (pure) | `utils/gridModes.ts` | Which features are in effect: tree data, row grouping, pagination, row reordering, list view, grouping column |
|
|
31
34
|
| 4 | `useGridThemeDimensions` | `hooks/core/useGridThemeDimensions.ts` | Row and header heights (props → theme provider → defaults) |
|
|
@@ -54,14 +57,16 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
54
57
|
| 27 | `useGridAggregationApi` | `hooks/core/useGridAggregationApi.ts` | `getAggregationResult`, `getAggregationModel`, `getGroupedExportRows` |
|
|
55
58
|
| 28 | `useLayout` | `hooks/core/useLayout.ts` | Row heights, pinned column split, widths |
|
|
56
59
|
| 29 | `useGridSpanning` | `hooks/features/useGridSpanning.ts` | Row/column spans |
|
|
57
|
-
|
|
|
60
|
+
| 29a | `useGridCellSelection` | `hooks/core/useGridCellSelection.ts` | Cell range (v3.3): the model's range resolved against `allRenderableRows` and the data columns of `navigationColumns` (grown over spans), the render rectangle, pointer drag with auto-scroll and Shift+click, the Shift-key handlers for the keyboard hook, copy text, status-bar totals and the debounced live-region text. Inert while `cellSelection` is off |
|
|
61
|
+
| 30 | `useGridClipboardApi` | `hooks/core/useGridClipboardApi.ts` | Ctrl/Cmd+C and `copySelectedRows`; with `cellSelection` the shortcut copies the range (`getCellRangeCopyText`) |
|
|
58
62
|
| 31 | `useGridScrollToIndexesApi` | `hooks/core/useGridScrollToIndexesApi.ts` | `scrollToIndexes` |
|
|
59
63
|
| 31a | `useGridColumnAutosize` | `hooks/core/useGridColumnAutosize.ts` | `autosizeColumn` / `autosizeColumns` on the API, and the resize handle's auto-size handler (v3.1) |
|
|
60
64
|
| 32 | `useGridViewport` | `hooks/core/useGridViewport.ts` | Scroll sync, viewport measurement, viewport callback ref |
|
|
61
65
|
| 33 | `useGridVirtualization` | `hooks/core/useGridVirtualization.ts` | Render window |
|
|
62
66
|
| 34 | `useGridSortHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Header sort / shift-sort |
|
|
63
|
-
| 35 | `useGridKeyboardNavigation` | `hooks/core/useGridKeyboardNavigation.ts` | Focus and keyboard handling |
|
|
67
|
+
| 35 | `useGridKeyboardNavigation` | `hooks/core/useGridKeyboardNavigation.ts` | Focus and keyboard handling; takes `cellSelection.keyboard` (Shift+navigation, Escape, Ctrl/Cmd+A) |
|
|
64
68
|
| 36 | `useGridPointerFocusHandlers` | `hooks/core/useGridFocusHandlers.ts` | Cell and header click focus |
|
|
69
|
+
| 36a | `useGridCellSelectionApi` | `hooks/core/useGridCellSelectionApi.ts` | Keeps the range anchor and the focused cell together (layout effect: a new anchor moves focus; focus moved to another data cell collapses the range), clears a range whose corner is gone (`'dataChange'`), and installs `getCellSelectionModel`, `setCellSelectionModel`, `selectCellRange`, `clearCellSelection`, `getSelectedCells`, `copySelectedCells` (once per `apiRef`, reading the latest values from a ref) |
|
|
65
70
|
| 37 | `useGridColumnMenuHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Column menu Hide / Pin (`pinColumnTo` in `utils/pinning`) |
|
|
66
71
|
| 38 | `useGridAriaRows` | `hooks/core/useGridAriaRows.ts` | `aria-rowcount` and `aria-rowindex` bases |
|
|
67
72
|
| 39 | `useGridSpanRowWindow` | `hooks/features/useGridSpanRenderWindow.ts` | Render window widened to whole row spans |
|
|
@@ -77,6 +82,9 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
77
82
|
|
|
78
83
|
`GridToolbarSlot`, `GridStandaloneColumnPanel`, `GridListView` (list view) or the viewport
|
|
79
84
|
(`Header`, `GridPinnedRows` top, `GridEmptyState`, `GridVirtualRows`, `GridPinnedRows` bottom,
|
|
80
|
-
`GridAggregationFooter`), then `slots.footer` or `GridPaginationArea`, `GridLiveRegion`,
|
|
85
|
+
`GridAggregationFooter`), `GridCellSelectionStatsArea` (`showCellSelectionStats` with `cellSelection`), then `slots.footer` or `GridPaginationArea`, `GridLiveRegion`,
|
|
81
86
|
`GridLoadingOverlay` and `GridErrorOverlay`. The props all three row renderers take alike are built
|
|
82
|
-
once in `DataGrid.tsx` (`rowRenderProps`) and spread into each.
|
|
87
|
+
once in `DataGrid.tsx` (`rowRenderProps`) and spread into each. `rowRenderProps.cellRange` is the cell range's
|
|
88
|
+
render rectangle; each row renderer turns it into a per-row `cellRange` (`getRowCellRange`), `null` for rows
|
|
89
|
+
outside it, and `Row` is memoised with a value comparison of that prop, so a drag only re-renders the rows the
|
|
90
|
+
range enters or leaves.
|
|
@@ -24,6 +24,31 @@ Manages the column headers, sorting triggers, column resizing, and column groupi
|
|
|
24
24
|
| `focusedCell` | `{ id: GridRowId \| null; field: string } \| null` | The grid's focus position. `id: null` means the header cell of `field` is focused (v3.0; it used to be the string `'HEADER'`, which a real row id could collide with). DOM focus is moved by the grid, not by `<Header />`. |
|
|
25
25
|
| `columnIndexMap` | `Map<string, number>` | Position of each visible data column in render order. Used for `aria-colindex` and the `colIndex` passed to `renderHeader`, so they match the body cells and do not change with horizontal scrolling. |
|
|
26
26
|
|
|
27
|
+
## ↩️ Wrapped Header Text
|
|
28
|
+
|
|
29
|
+
By default a header title stays on one line and is cut with an ellipsis. Pass `wrapHeaderText` to `<DataGrid>` (or set `GridColDef.wrapHeaderText` on a column) and long titles wrap instead (v3.3.0+):
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const columns: GridColDef[] = [
|
|
33
|
+
// Keeps one line with an ellipsis even though the grid wraps
|
|
34
|
+
{ field: 'fund', headerName: 'Fund name and share class', wrapHeaderText: false },
|
|
35
|
+
{ field: 'nav', headerName: 'Net asset value per share (USD)', width: 120 },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
<DataGrid rows={rows} columns={columns} wrapHeaderText headerHeight={72} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **The header keeps `headerHeight`.** It does not grow to fit the title (sticky offsets and virtualization rely on a fixed header height). A wrapped title takes as many lines as fit — 2 at the default 56px, 3 at 72px, 4 at 88px (16px lines inside 8px vertical padding) — and ends in an ellipsis on the last line if it is still longer. Raise `headerHeight` to show more lines. When the column shows an aggregation label under its title, the title gets one line less.
|
|
42
|
+
- **Precedence:** a column's `wrapHeaderText` wins over the grid prop, in both directions.
|
|
43
|
+
- **Tooltip:** a wrapped header shows its full title as the `title` tooltip, unless the column has a `description`, which is shown instead. Only string titles are used; a `renderHeader` result gets no automatic tooltip.
|
|
44
|
+
- **Layout:** the sort icon, the menu button and the resize handle stay where they are and stay clickable; only the title wraps.
|
|
45
|
+
- **Auto-size:** double-clicking the resize handle (or `apiRef.current.autosizeColumn`) still measures the title on one line, so the column grows until the title no longer wraps.
|
|
46
|
+
- **Class:** wrapped header cells get `ogx__header-cell--wrap`; the line count is set inline as `-webkit-line-clamp` on `.ogx__header-cell-title`.
|
|
47
|
+
- **Themes:** the line count assumes the default header font (13px) and cell padding (8px). With a theme that changes `headerFontSize` or `cellPaddingY`, the title is still capped to the header height, but the last visible line may be cut without an ellipsis.
|
|
48
|
+
- Column group header rows are not affected.
|
|
49
|
+
|
|
50
|
+
`Header` takes the same `wrapHeaderText` prop and a `headerHeight` (default 56) when used on its own.
|
|
51
|
+
|
|
27
52
|
## 📐 Column Grouping
|
|
28
53
|
|
|
29
54
|
The `<Header />` dynamically calculates the nesting depth of your `columnGroupingModel` and renders one group row per level above the main column headers. Group cells are sized from `allColumns`, so they match their member columns' resolved widths; cells over pinned columns are sticky. See [Column Group Headers](../API_REFERENCE.md#️-column-group-headers) for the full behaviour.
|
|
@@ -83,7 +83,7 @@ interface GridTheme {
|
|
|
83
83
|
spacing?: GridThemeSpacing;
|
|
84
84
|
borders?: GridThemeBorders;
|
|
85
85
|
shadows?: GridThemeShadows;
|
|
86
|
-
grid?: GridThemeGrid; // surfaces, heights (px), cellPaddingX/Y, cellFontSize, headerFontSize, …
|
|
86
|
+
grid?: GridThemeGrid; // surfaces, heights (px), cellPaddingX/Y, cellFontSize, headerFontSize, rangeBackground, rangeBorder, …
|
|
87
87
|
toolbar?: GridThemeToolbar;
|
|
88
88
|
overlays?: GridThemeOverlays;
|
|
89
89
|
scrollbar?: GridThemeScrollbar;
|
|
@@ -213,6 +213,8 @@ All sub-theme types (`GridThemeColors`, `GridThemeGrayScale`, `GridThemeTypograp
|
|
|
213
213
|
| `--ogx-grid-row-selected-background` | `#eff6ff` | `#1e3a5f` | Selected row fill |
|
|
214
214
|
| `--ogx-grid-row-selected-hover-background` | `#dbeafe` | `#1d4ed8` | Selected row hover |
|
|
215
215
|
| `--ogx-grid-cell-focus-border` | `#3b82f6` | `#60a5fa` | Keyboard focus ring |
|
|
216
|
+
| `--ogx-range-background` | `rgba(59, 130, 246, 0.12)` | `rgba(96, 165, 250, 0.18)` | Tint over a selected cell range (`cellSelection`, v3.3). Theme key `grid.rangeBackground` |
|
|
217
|
+
| `--ogx-range-border` | `var(--ogx-color-primary)` | `#60a5fa` | Outline of a selected cell range, drawn as an inset box-shadow on `ogx__cell--range-top/-bottom/-left/-right` (v3.3). Theme key `grid.rangeBorder` |
|
|
216
218
|
| `--ogx-grid-pinned-left-shadow` | `4px 0 24px...` | (darker) | Shadow on left-pinned columns |
|
|
217
219
|
| `--ogx-grid-pinned-right-shadow` | `-4px 0 24px...` | (darker) | Shadow on right-pinned columns |
|
|
218
220
|
| `--ogx-checkbox-bg` | — | `#1e293b` | Checkbox background (dark only) |
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Cell Range Selection
|
|
2
|
+
|
|
3
|
+
*Since v3.3.0*
|
|
4
|
+
|
|
5
|
+
Users select a rectangle of cells the way they do in Excel or Google Sheets, by dragging, Shift+clicking or pressing Shift+arrow keys, and copy it with Ctrl/Cmd+C. An optional status bar shows the count, sum and average of the selection.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { DataGrid } from '@opencorestack/opengridx';
|
|
9
|
+
|
|
10
|
+
<DataGrid
|
|
11
|
+
rows={rows}
|
|
12
|
+
columns={columns}
|
|
13
|
+
cellSelection
|
|
14
|
+
showCellSelectionStats
|
|
15
|
+
height={500}
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Everything is off unless `cellSelection` is set. Without it the grid behaves exactly as before.
|
|
20
|
+
|
|
21
|
+
## Gestures
|
|
22
|
+
|
|
23
|
+
| Input | Result |
|
|
24
|
+
| :--- | :--- |
|
|
25
|
+
| Click | Focus the cell. The range is that cell. A plain click still selects the row unless `disableRowSelectionOnClick` is set. |
|
|
26
|
+
| Press and drag | Range from the pressed cell to the cell under the pointer. Holding the pointer near or past an edge of the body scrolls the grid, over virtualized rows and columns. |
|
|
27
|
+
| Shift+click | Extend from the anchor (the active cell) to the clicked cell. |
|
|
28
|
+
| Shift+Arrow | Move the far corner (head) by one cell. It does **not** wrap to the next row, though plain arrows still do. |
|
|
29
|
+
| Shift+Home / Shift+End | Head to the first / last column of its row. |
|
|
30
|
+
| Ctrl+Shift+Home / End (Cmd on macOS) | Head to the first / last cell of the page. |
|
|
31
|
+
| Shift+PageUp / Shift+PageDown | Head one page up / down. |
|
|
32
|
+
| Ctrl/Cmd+A | Select every data cell on the page. The header checkbox still selects all rows. |
|
|
33
|
+
| Escape | Collapse the range to the anchor (when not editing). |
|
|
34
|
+
| Ctrl/Cmd+C | Copy the range as TSV. |
|
|
35
|
+
| Typing / Enter / F2 | Edit the anchor cell, as before. |
|
|
36
|
+
|
|
37
|
+
Drag and Shift ranges never change the **row** selection, and the checkbox column keeps selecting rows.
|
|
38
|
+
|
|
39
|
+
## The model
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
interface GridCellCoordinates { id: GridRowId; field: string }
|
|
43
|
+
interface GridCellRange { anchor: GridCellCoordinates; head: GridCellCoordinates }
|
|
44
|
+
type GridCellSelectionModel = readonly GridCellRange[]; // zero or one range in 3.3
|
|
45
|
+
type GridCellSelectionReason = 'pointer' | 'keyboard' | 'selectAll' | 'api' | 'clear' | 'dataChange';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `anchor` is the active cell. It keeps DOM focus and it is where an edit starts. `head` is the corner that moves, and the one that is scrolled into view. A single focused cell is a range whose `anchor` equals its `head`. It is not drawn as a range, but it is what Ctrl+C copies.
|
|
49
|
+
- Corners are row **ids** and column **fields**, not indices. The rectangle is worked out from the current display order on every render, so after a sort or filter it follows the two corner rows (as Google Sheets does). If a corner's row or column is no longer displayed (filtered, on another page, removed or hidden), the range is cleared with `reason: 'dataChange'`.
|
|
50
|
+
- Controlled or uncontrolled, like the other models:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const [model, setModel] = useState<GridCellSelectionModel>([]);
|
|
54
|
+
|
|
55
|
+
<DataGrid
|
|
56
|
+
cellSelection
|
|
57
|
+
cellSelectionModel={model}
|
|
58
|
+
onCellSelectionModelChange={(next, { reason }) => setModel(next)}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Which cells take part
|
|
63
|
+
|
|
64
|
+
- **Columns:** only data columns, in screen order: left-pinned, then centre, then right-pinned. The checkbox, detail-panel, reorder and grouping (`__group__`) columns never join a range, and clicking them works as before.
|
|
65
|
+
- **Rows:** pinned top and bottom rows take part in screen order. Group headers, subtotals, tree auto-parents and the pivot Grand Total are highlighted when they fall inside the rectangle, but they are **skipped by copy** and by `getSelectedCells()`.
|
|
66
|
+
- **Spans:** a range that touches part of a `colSpan` or `rowSpan` grows to cover the whole span, repeatedly until no span crosses its edge (as Excel does with merged cells). In a copy the span's value sits at its origin and the covered positions are empty.
|
|
67
|
+
- **Pages:** a range covers the rows of the current page.
|
|
68
|
+
|
|
69
|
+
## Copy
|
|
70
|
+
|
|
71
|
+
With `cellSelection` on, Ctrl/Cmd+C copies the range (the focused cell when nothing else is selected):
|
|
72
|
+
|
|
73
|
+
- values formatted the way the cells show them (`valueGetter`, then `valueFormatter`);
|
|
74
|
+
- tabs between columns, line breaks between rows, **no header line**, and fields quoted where Excel needs it;
|
|
75
|
+
- `exportable: false` columns are copied when they are in the range, because the user chose them on screen.
|
|
76
|
+
|
|
77
|
+
Row copy stays available through `apiRef.current.copySelectedRows()`, which still includes the header line. `disableClipboardCopy` turns the shortcut off for both. Above 100,000 cells a development warning suggests `exportToCsv` instead. Nothing is truncated.
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
| Method | Description |
|
|
82
|
+
| :--- | :--- |
|
|
83
|
+
| `getCellSelectionModel()` | The current model (`[]` when `cellSelection` is off). |
|
|
84
|
+
| `setCellSelectionModel(model)` | Replace the model (only the first range is kept). Focus moves to its anchor. |
|
|
85
|
+
| `selectCellRange(anchor, head)` | Select the rectangle between two cells. |
|
|
86
|
+
| `clearCellSelection()` | Empty the selection (`reason: 'clear'`). |
|
|
87
|
+
| `getSelectedCells()` | `{ id, field, value }[]` of the range in display order, values read through `valueGetter`. Synthetic rows and span-covered positions are left out. |
|
|
88
|
+
| `copySelectedCells()` | Copy the range as TSV. Rejects when the clipboard write fails. |
|
|
89
|
+
|
|
90
|
+
The methods do nothing while `cellSelection` is off.
|
|
91
|
+
|
|
92
|
+
## Status bar
|
|
93
|
+
|
|
94
|
+
`showCellSelectionStats` adds a strip under the grid (above the pagination area). While more than one cell is selected it shows:
|
|
95
|
+
|
|
96
|
+
- **Count**: non-empty cells of data rows;
|
|
97
|
+
- **Sum** and **Average**: of the numeric cells. Both are hidden when there are none.
|
|
98
|
+
|
|
99
|
+
These use the shared aggregation functions. The strip keeps its height while one cell or none is selected, so the grid does not jump. Replace its content with `slots.cellSelectionStats`, which receives `GridCellSelectionStatsSlotProps` (`cellCount`, `rowCount`, `columnCount`, `count`, `numericCount`, `sum`, `average`, `apiRef`) plus `slotProps.cellSelectionStats`. The default component is exported as `CellSelectionStats`.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
function MyStats({ count, sum }: GridCellSelectionStatsSlotProps) {
|
|
103
|
+
return <span>{count} cells, total {sum ?? 0}</span>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
<DataGrid cellSelection showCellSelectionStats slots={{ cellSelectionStats: MyStats }} />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Styling
|
|
110
|
+
|
|
111
|
+
| Class / variable | Meaning |
|
|
112
|
+
| :--- | :--- |
|
|
113
|
+
| `ogx__cell--range` | Every cell in the range (tinted). |
|
|
114
|
+
| `ogx__cell--range-top` / `-bottom` / `-left` / `-right` | Edge cells. The outline is an inset `box-shadow`, so nothing moves. |
|
|
115
|
+
| `ogx--range-dragging` | On the grid root during a pointer drag (`user-select: none`). |
|
|
116
|
+
| `--ogx-range-background` | Range tint. Theme key `grid.rangeBackground`. |
|
|
117
|
+
| `--ogx-range-border` | Range outline. Theme key `grid.rangeBorder`. |
|
|
118
|
+
|
|
119
|
+
All five built-in themes define both variables. A custom `DataGridThemeProvider` theme derives them from `colors.primary` unless it sets them.
|
|
120
|
+
|
|
121
|
+
## Accessibility
|
|
122
|
+
|
|
123
|
+
- Cells in the range have `aria-selected="true"`, and the grid is `aria-multiselectable`.
|
|
124
|
+
- DOM focus stays on the anchor. The grid stays a single Tab stop.
|
|
125
|
+
- After each change the polite live region announces the size, for example "12 cells selected, 3 rows by 4 columns", debounced to 300 ms. Translate it with `localeText.cellSelectionAnnouncement(cells, rows, columns)`.
|
|
126
|
+
|
|
127
|
+
## Not in 3.3
|
|
128
|
+
|
|
129
|
+
Paste, Delete-to-clear and undo (planned for 3.4), the fill handle, several ranges at once (Ctrl+drag), touch selection (on touch a drag scrolls) and ranges across pages.
|
|
130
|
+
|
|
131
|
+
See also: [Clipboard](clipboard.md), [Keyboard Navigation](keyboard-navigation.md), [Theming](../customization/theming.md).
|
|
@@ -17,6 +17,8 @@ Selection and copying follow standard spreadsheet metaphors:
|
|
|
17
17
|
| :--- | :--- |
|
|
18
18
|
| `Ctrl + C` / `Cmd + C` | Copy currently selected rows to the clipboard. |
|
|
19
19
|
|
|
20
|
+
> **Cell ranges (v3.3)**: with `cellSelection` on, Ctrl/Cmd+C copies the selected cell range instead (the focused cell at minimum), with no header line. `apiRef.current.copySelectedRows()` still copies rows. See [Cell Range Selection](cell-selection.md).
|
|
21
|
+
|
|
20
22
|
> **Note**: The shortcut only copies while focus is inside the grid (a cell, a header, the toolbar), so with several grids on a page only the one in use copies, and Ctrl+C elsewhere on the page is never taken over (v3.0). It is also ignored while an input, textarea, or another editable element has focus, and while the page has a text selection, which the browser copies as usual. `apiRef.current.copySelectedRows()` is not affected by focus.
|
|
21
23
|
>
|
|
22
24
|
> The shortcut works with Caps Lock on and on non-Latin keyboard layouts (the key in the "C" position). Ctrl+Shift+C and Ctrl+Alt+C are not copy shortcuts and are left alone. Focus stays where it was after the copy.
|
|
@@ -25,8 +25,20 @@ The grid follows the [WAI-ARIA data grid pattern](https://www.w3.org/WAI/ARIA/ap
|
|
|
25
25
|
| **Alt+ArrowRight** / **Alt+ArrowLeft** | Tree data / row grouping: expand / collapse the focused row. |
|
|
26
26
|
| **Shift+Space** | Select or deselect the focused row (when rows can be selected: `checkboxSelection`, or click selection not disabled). Synthetic group rows are not selectable. |
|
|
27
27
|
| **Space** | On the row checkbox: toggle the row. On the detail-panel toggle: expand or collapse the panel. Elsewhere it does nothing (it never scrolls the grid). |
|
|
28
|
-
| **Ctrl+A** (Cmd+A) | Select every row, when several rows can be selected. |
|
|
29
|
-
| **Ctrl+C** (Cmd+C) | Copy the selected rows (see [Clipboard](clipboard.md)). |
|
|
28
|
+
| **Ctrl+A** (Cmd+A) | Select every row, when several rows can be selected. With `cellSelection`: select every data cell of the page instead. |
|
|
29
|
+
| **Ctrl+C** (Cmd+C) | Copy the selected rows (see [Clipboard](clipboard.md)). With `cellSelection`: copy the cell range, or the focused cell. |
|
|
30
|
+
|
|
31
|
+
With [`cellSelection`](cell-selection.md) on (v3.3), on a data cell:
|
|
32
|
+
|
|
33
|
+
| Key | Action |
|
|
34
|
+
| :--- | :--- |
|
|
35
|
+
| **Shift+Arrow keys** | Extend the cell range by one cell. Unlike plain arrows they do not wrap to the next row. Focus stays on the anchor. |
|
|
36
|
+
| **Shift+Home** / **Shift+End** | Extend to the first / last column of the row. |
|
|
37
|
+
| **Ctrl+Shift+Home** / **Ctrl+Shift+End** (Cmd on macOS) | Extend to the first / last cell of the page. |
|
|
38
|
+
| **Shift+PageUp** / **Shift+PageDown** | Extend one page up / down. |
|
|
39
|
+
| **Escape** | Collapse the range to the anchor (when not editing). |
|
|
40
|
+
|
|
41
|
+
A plain arrow key after a range collapses it to the newly focused cell. Shift+Space still selects the row.
|
|
30
42
|
|
|
31
43
|
Arrow, Home/End, PageUp/PageDown and Space are always consumed, so the viewport does not scroll natively when focus cannot move further.
|
|
32
44
|
|
package/docs/roadmap.md
CHANGED
|
@@ -70,18 +70,75 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## 🗓️ Q4 2026 Roadmap (October – December)
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
* **Interactive Pivot Builder**: Drag-and-drop UI for users to dynamically create pivot tables.
|
|
77
|
-
* **Advanced Charts Integration**: Inline sparklines and trend visualization.
|
|
75
|
+
**Theme: free spreadsheet editing, with AI you control.** Range selection, paste, undo/redo and a fill handle are paid features in MUI X Premium and AG Grid Enterprise, and clipboard paste (476 👍) and cell range selection (317 👍) are among the most-requested grid features on GitHub. The research behind this plan compared ten competing grids and their AI features.
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
**Team:** one maintainer plus an AI coding assistant. The plan is sized for that: one headline feature per release, and anything that slips moves to the next release instead of stretching the current one.
|
|
78
|
+
|
|
79
|
+
**Compatibility:** everything ships in 3.x minor releases. Each new interaction is opt-in through a prop, so existing grids behave exactly as before until the feature is enabled.
|
|
80
|
+
|
|
81
|
+
### Releases
|
|
82
|
+
|
|
83
|
+
| Release | Target | Headline | Also in it |
|
|
84
|
+
| :--- | :--- | :--- | :--- |
|
|
85
|
+
| **3.3.0** ✅ | shipped 7 Oct | **Cell range selection** (opt-in `cellSelection` prop): mouse drag, Shift+click, Shift+arrows; Ctrl+C / Cmd+C copies the block as TSV | Selection status bar (count, sum, average of selected cells); wrapped header text; React Compiler compatibility check |
|
|
86
|
+
| **3.4.0** | 20 Nov | **Paste from Excel / Google Sheets** into the selection, through `processRowUpdate` (values parsed per column type, non-editable cells skipped, `onClipboardPasteError`). **Undo / redo** for cell edits and pastes (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y, `apiRef.undo()` / `redo()`) | **AI toolkit, part 1** in a new `@opencorestack/opengridx/ai` entry point: `getGridAiSchema(columns)` (JSON Schema of the filter, sort, grouping, aggregation, pivot and visibility models the columns allow) and `validateGridAiState(json, columns)` (drops unknown fields and operators, coerces values, returns errors) |
|
|
87
|
+
| **3.5.0** | 15 Dec | **AI toolkit, part 2**: `aiAssistant={{ onPrompt }}` prompt panel that calls the app's own model, shows the proposed changes as removable chips and applies them with undo; `createGridAgentTools(apiRef)` (plain tool objects for CopilotKit, the Vercel AI SDK or WebMCP) | Header filter row; **fill handle** as a stretch goal (moves to Q1 2027 if 3.3–3.4 run late) |
|
|
88
|
+
| Freeze | 19 Dec – 5 Jan | Bug fixes only | — |
|
|
89
|
+
|
|
90
|
+
### Across the quarter
|
|
91
|
+
|
|
92
|
+
* **Published benchmark**: reproducible 100k / 1M-row numbers (scroll frame rate, sort and filter time, memory) in `docs/performance.md`.
|
|
93
|
+
* **Next.js App Router guide**: using the grid from a `'use client'` component, with the stylesheet in `app/layout.tsx`.
|
|
94
|
+
* **RTL check**: verify right-to-left layouts and document the result.
|
|
95
|
+
* **Demo site**: source viewer on all examples (`DocsLayout` with Vite `?raw`) and syntax highlighting, done alongside each release's demo page.
|
|
96
|
+
|
|
97
|
+
### Definition of done for each feature
|
|
98
|
+
|
|
99
|
+
* A short design note in `docs/superpowers/specs/` before coding starts (API, keyboard behaviour, ARIA).
|
|
100
|
+
* Unit tests, browser tests in Chromium, Firefox and WebKit, and a scenario in the package smoke suite.
|
|
101
|
+
* Keyboard-only and screen-reader pass: the grid stays a single Tab stop and announces selection changes.
|
|
102
|
+
* API reference, feature guide, demo page, CHANGELOG and `llms.txt` updated (the wiki follows automatically).
|
|
103
|
+
* Bundle budget: the core bundle grows only by the feature itself; the `/ai` entry point has no dependencies and adds nothing when it is not imported.
|
|
104
|
+
|
|
105
|
+
### Rules for the AI toolkit
|
|
106
|
+
|
|
107
|
+
* OpenGridX never calls an AI service and never bundles an AI SDK. The app passes a function that calls whichever model it uses.
|
|
108
|
+
* By default only column definitions and grid state are sent, never row data. Example values are opt-in per column and documented as real data.
|
|
109
|
+
* Model output is always validated against the schema and shown to the user before it changes the grid, with one-click undo.
|
|
110
|
+
* The schema carries a `schemaVersion`, since apps will cache prompts against it.
|
|
111
|
+
|
|
112
|
+
### Risks
|
|
113
|
+
|
|
114
|
+
* **Range selection touches focus and keyboard handling**, where most v3.0 fixes landed. It is opt-in and gets the longest test cycle; if it slips, 3.4.0 moves rather than shipping paste on an unstable base.
|
|
115
|
+
* **Undo must cover rejected updates**: a `processRowUpdate` that throws or a paste that partly fails must leave the history consistent.
|
|
116
|
+
* **Wrong AI filters** that look plausible: the validator, visible chips and undo are required, not optional.
|
|
117
|
+
|
|
118
|
+
## 🔭 Next (Q1 2027)
|
|
119
|
+
|
|
120
|
+
* **Row reordering inside tree data and row groups**, touch-friendly (MUI #4821, 405 👍, not in MUI X).
|
|
121
|
+
* **Fill handle**, if it does not make 3.5.0.
|
|
122
|
+
* **Find & highlight** (Ctrl+F through cells), **right-click context menu**, **Excel-style value (set) filter**.
|
|
123
|
+
* **Calculated columns** (`[price] * [qty]` defined by users).
|
|
124
|
+
* **Interactive pivot builder**: drag-and-drop panel for pivot and grouping.
|
|
125
|
+
* **Sparklines** and a charts adapter that passes grid state to any chart library.
|
|
126
|
+
* **AI toolkit, part 3**: smart paste (unstructured text → pending rows) and `apiRef.getAiContext()` for "summarise selection"; an `opengridx-mcp` docs server for coding assistants.
|
|
127
|
+
* **Tailwind / shadcn theme preset**, sticky header against page scroll.
|
|
128
|
+
|
|
129
|
+
### Not planned
|
|
130
|
+
|
|
131
|
+
* **Spreadsheet formulas** (`=SUM(A1:A9)`): a formula engine is a product of its own; calculated columns cover most of the need.
|
|
132
|
+
* **AI on every cell**: the cost grows with each row and results are not reproducible.
|
|
133
|
+
* **Built-in semantic search or anomaly detection**: large models and app-specific logic; better built by the app on top of the grid.
|
|
134
|
+
* **Scaled scrolling past ~645,000 rows** and **list-view virtualization**: revisit if users ask.
|
|
82
135
|
|
|
83
136
|
## ✅ Implemented Features (Recent)
|
|
84
137
|
|
|
138
|
+
### 3.x releases *(2026-09-29 → 2026-10-01)*
|
|
139
|
+
- **3.0.0** correctness release (rows never modified, typed generics, React 18 support), **3.0.1** fill-height layout fix, **3.1.0** `sortComparator` and column auto-size, **3.2.0** non-blocking PDF export, **3.2.1** Safari boolean editor fix, **3.2.2** linear-time export regexes. Details in [CHANGELOG.md](../CHANGELOG.md); upgrade guide in [migration/v2-to-v3.md](migration/v2-to-v3.md).
|
|
140
|
+
- Package smoke suite against the packed tarball in React 18 and 19 apps, pull-request CI, community files and a wiki generated from these docs.
|
|
141
|
+
|
|
85
142
|
### Consumer defect fixes *(v2.1.0, 2026-09-23)*
|
|
86
143
|
- 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
144
|
- Group and tree-data expansion state survives row updates, including server-side lazily loaded children.
|
|
@@ -28,6 +28,36 @@
|
|
|
28
28
|
.ogx__cell--editable {
|
|
29
29
|
cursor: text;
|
|
30
30
|
}
|
|
31
|
+
/*
|
|
32
|
+
* Cell range selection (cellSelection, v3.3). The tint is a background image, so it also covers
|
|
33
|
+
* pinned cells' own background; the outline is an inset box-shadow, so the layout never shifts.
|
|
34
|
+
*/
|
|
35
|
+
.ogx__cell--range {
|
|
36
|
+
background-image: linear-gradient(var(--ogx-range-background), var(--ogx-range-background));
|
|
37
|
+
box-shadow:
|
|
38
|
+
var(--ogx-range-edge-top, 0 0 0 0 transparent),
|
|
39
|
+
var(--ogx-range-edge-bottom, 0 0 0 0 transparent),
|
|
40
|
+
var(--ogx-range-edge-left, 0 0 0 0 transparent),
|
|
41
|
+
var(--ogx-range-edge-right, 0 0 0 0 transparent);
|
|
42
|
+
}
|
|
43
|
+
.ogx__cell--range-top {
|
|
44
|
+
--ogx-range-edge-top: inset 0 2px 0 0 var(--ogx-range-border);
|
|
45
|
+
}
|
|
46
|
+
.ogx__cell--range-bottom {
|
|
47
|
+
--ogx-range-edge-bottom: inset 0 -2px 0 0 var(--ogx-range-border);
|
|
48
|
+
}
|
|
49
|
+
.ogx__cell--range-left {
|
|
50
|
+
--ogx-range-edge-left: inset 2px 0 0 0 var(--ogx-range-border);
|
|
51
|
+
}
|
|
52
|
+
.ogx__cell--range-right {
|
|
53
|
+
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
54
|
+
}
|
|
55
|
+
/* While a pointer drag selects cells, the browser must not select text as well. */
|
|
56
|
+
.ogx--range-dragging,
|
|
57
|
+
.ogx--range-dragging * {
|
|
58
|
+
-webkit-user-select: none;
|
|
59
|
+
user-select: none;
|
|
60
|
+
}
|
|
31
61
|
.ogx__cell--editing {
|
|
32
62
|
padding: 0;
|
|
33
63
|
overflow: visible;
|
|
@@ -6,6 +6,8 @@ import type { CellColSpanInfo } from '../../hooks/features/useGridSpanning';
|
|
|
6
6
|
import { GridEditInputCell } from './GridEditInputCell';
|
|
7
7
|
import { CellErrorBoundary } from './CellErrorBoundary';
|
|
8
8
|
import { formatValueByType } from '../../utils/values';
|
|
9
|
+
import { attempt } from '../../utils/attempt';
|
|
10
|
+
import { RANGE_BOTTOM, RANGE_CELL, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
|
|
9
11
|
|
|
10
12
|
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
11
13
|
onCellClick?: (params: GridCellParams<R>) => void;
|
|
@@ -46,6 +48,11 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
|
46
48
|
rowMeta?: GridRowMeta;
|
|
47
49
|
/** The error the column's valueGetter threw for this cell (computed by Row); shown as a cell error. */
|
|
48
50
|
valueError?: unknown;
|
|
51
|
+
/**
|
|
52
|
+
* Place of the cell in the selected cell range (`cellSelection`), as bit flags: in range 1, top
|
|
53
|
+
* edge 2, bottom 4, left 8, right 16. 0 or absent: not in a range. @since v3.3
|
|
54
|
+
*/
|
|
55
|
+
rangeFlags?: number;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
interface FormattedValue {
|
|
@@ -82,7 +89,8 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
82
89
|
rowSpan: rowSpanProp,
|
|
83
90
|
isHiddenByRowSpan,
|
|
84
91
|
rowMeta,
|
|
85
|
-
valueError
|
|
92
|
+
valueError,
|
|
93
|
+
rangeFlags = 0,
|
|
86
94
|
} = props;
|
|
87
95
|
|
|
88
96
|
// All hooks must come before any early returns (Rules of Hooks)
|
|
@@ -99,12 +107,12 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
99
107
|
// formatter, written for data rows, is only called for the values they do hold.
|
|
100
108
|
const isSyntheticRow = rowMeta?.isGroupRow === true;
|
|
101
109
|
const { formattedValue, error: formatError } = React.useMemo<FormattedValue>(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
const formatter = colDef.valueFormatter;
|
|
111
|
+
if (formatter && !(isSyntheticRow && value == null)) {
|
|
112
|
+
const result = attempt(() => formatter({ value, row, field: colDef.field }));
|
|
113
|
+
return result.ok
|
|
114
|
+
? { formattedValue: result.value }
|
|
115
|
+
: { formattedValue: '', error: result.error ?? new Error('valueFormatter failed') };
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
// Without a formatter the column type decides: locale date, Yes / No, singleSelect label.
|
|
@@ -113,12 +121,10 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
113
121
|
|
|
114
122
|
const resolvedCellClassName = React.useMemo(() => {
|
|
115
123
|
if (!colDef.cellClassName) return '';
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return '';
|
|
121
|
-
}
|
|
124
|
+
const getClassName = colDef.cellClassName;
|
|
125
|
+
if (typeof getClassName === 'function') {
|
|
126
|
+
const result = attempt(() => getClassName({ value, formattedValue, row, field: colDef.field, colDef, rowIndex, colIndex, rowMeta }));
|
|
127
|
+
return (result.ok && result.value) || '';
|
|
122
128
|
}
|
|
123
129
|
return colDef.cellClassName;
|
|
124
130
|
}, [colDef, value, formattedValue, row, rowIndex, colIndex, rowMeta]);
|
|
@@ -224,6 +230,11 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
224
230
|
isPinned && isPinnedEdge && `ogx__cell--pinned-${pinnedPosition}-${pinnedPosition === 'left' ? 'last' : 'first'}`,
|
|
225
231
|
isEditing && 'ogx__cell--editing',
|
|
226
232
|
(rowSpanProp && rowSpanProp > 1) && 'ogx__cell--spanned',
|
|
233
|
+
(rangeFlags & RANGE_CELL) !== 0 && 'ogx__cell--range',
|
|
234
|
+
(rangeFlags & RANGE_TOP) !== 0 && 'ogx__cell--range-top',
|
|
235
|
+
(rangeFlags & RANGE_BOTTOM) !== 0 && 'ogx__cell--range-bottom',
|
|
236
|
+
(rangeFlags & RANGE_LEFT) !== 0 && 'ogx__cell--range-left',
|
|
237
|
+
(rangeFlags & RANGE_RIGHT) !== 0 && 'ogx__cell--range-right',
|
|
227
238
|
resolvedCellClassName
|
|
228
239
|
].filter(Boolean).join(' ');
|
|
229
240
|
|
|
@@ -266,6 +277,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
266
277
|
data-colindex={colIndex}
|
|
267
278
|
{...(colSpan > 1 ? { 'aria-colspan': colSpan } : {})}
|
|
268
279
|
{...(rowSpan > 1 ? { 'aria-rowspan': rowSpan } : {})}
|
|
280
|
+
{...((rangeFlags & RANGE_CELL) !== 0 ? { 'aria-selected': true } : {})}
|
|
269
281
|
>
|
|
270
282
|
<div className="ogx__cell-content">
|
|
271
283
|
{isEditing && (onCellValueChange || onValueChange) && onEditStop ? (
|
|
@@ -3,7 +3,10 @@ import React from 'react';
|
|
|
3
3
|
// Must live at module scope (not inline) so React sees a stable component type.
|
|
4
4
|
// Calling renderFn() here puts the throw one level BELOW CellErrorBoundary,
|
|
5
5
|
// which is required — a component cannot catch errors thrown in its own render().
|
|
6
|
+
// `renderFn` runs the consumer's renderCell, which may call hooks, so it must run on every render:
|
|
7
|
+
// 'use no memo' stops React Compiler from memoizing the call (see GridToolbarSlot's InlineToolbar).
|
|
6
8
|
function CellRenderTarget({ renderFn }: { renderFn: () => React.ReactNode }) {
|
|
9
|
+
'use no memo';
|
|
7
10
|
return <>{renderFn()}</>;
|
|
8
11
|
}
|
|
9
12
|
|