@opencorestack/opengridx 3.9.0 → 3.10.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +11 -7
  3. package/dist/index.d.ts +55 -0
  4. package/dist/opengridx.es.js +6346 -6279
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +4 -0
  9. package/docs/architecture/datagrid-orchestration.md +4 -2
  10. package/docs/architecture/use-grid-virtualization.md +2 -0
  11. package/docs/components/pagination.md +1 -1
  12. package/docs/features/ai-toolkit.md +1 -0
  13. package/docs/features/rtl.md +1 -1
  14. package/docs/performance.md +19 -1
  15. package/docs/roadmap.md +26 -5
  16. package/lib/components/Cell/Cell.tsx +2 -1
  17. package/lib/components/DataGrid/DataGrid.tsx +19 -18
  18. package/lib/components/DataGrid/GridListView.tsx +1 -1
  19. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  20. package/lib/components/DataGrid/GridViewport.tsx +2 -1
  21. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  22. package/lib/components/Header/Header.tsx +4 -3
  23. package/lib/components/Row/Row.tsx +6 -2
  24. package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
  25. package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
  26. package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
  27. package/lib/components/Toolbar/GridToolbar.tsx +7 -380
  28. package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
  29. package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
  30. package/lib/hooks/core/gridKeyDown.ts +272 -0
  31. package/lib/hooks/core/useDataGrid.ts +13 -5
  32. package/lib/hooks/core/useGridApiMethods.ts +2 -2
  33. package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
  34. package/lib/hooks/core/useGridCellSelection.ts +17 -297
  35. package/lib/hooks/core/useGridColumnLookup.ts +2 -1
  36. package/lib/hooks/core/useGridColumns.tsx +3 -2
  37. package/lib/hooks/core/useGridControlledState.ts +3 -2
  38. package/lib/hooks/core/useGridHierarchy.ts +1 -1
  39. package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
  40. package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
  41. package/lib/hooks/core/useGridRowInteractions.ts +1 -1
  42. package/lib/hooks/core/useGridRowSelection.ts +11 -2
  43. package/lib/hooks/core/useGridToolbarProps.ts +3 -2
  44. package/lib/hooks/features/useGridAiAssistant.ts +3 -2
  45. package/lib/hooks/useColumnReorder.ts +2 -1
  46. package/lib/hooks/useTreeData.ts +4 -2
  47. package/lib/types/locale.ts +22 -0
  48. package/lib/utils/aggregation/index.ts +16 -0
  49. package/lib/utils/aiAssistant.ts +46 -15
  50. package/lib/utils/export/excelCells.ts +244 -0
  51. package/lib/utils/export/exportCsv.ts +179 -0
  52. package/lib/utils/export/exportExcelHtml.ts +220 -0
  53. package/lib/utils/export/exportJson.ts +179 -0
  54. package/lib/utils/export/exportRows.ts +85 -0
  55. package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
  56. package/lib/utils/export/index.ts +5 -695
  57. package/lib/utils/export/printGrid.ts +282 -0
  58. package/lib/utils/gridLocale.ts +30 -0
  59. package/lib/utils/selectionSets.ts +36 -0
  60. package/lib/utils/untypedColumns.ts +16 -0
  61. package/llms.txt +3 -1
  62. package/package.json +6 -2
  63. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
  64. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
  65. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
@@ -0,0 +1,272 @@
1
+ import { isRtl, logicalArrowKey } from '../../utils/direction';
2
+ import { resolveCellEditable } from '../../utils/editing';
3
+ import { getCellValue } from '../../utils/values';
4
+ import { isSyntheticRowId } from '../../utils/syntheticRows';
5
+ import {
6
+ CHECKBOX_FIELD,
7
+ EXPAND_FIELD,
8
+ REORDER_FIELD,
9
+ classifyKeyTarget,
10
+ findFocusTarget,
11
+ isSystemField,
12
+ } from '../../utils/focus';
13
+ import type { FocusedCell } from '../../utils/focus';
14
+ import { isRangeColumnField } from '../../utils/cellSelection';
15
+ import type {
16
+ GridRowModel,
17
+ GridRowId,
18
+ GridColDef,
19
+ GridSortDirection,
20
+ } from '../../types';
21
+ import type { UseGridKeyboardNavigationParams } from './useGridKeyboardNavigation';
22
+
23
+ // The grid's keydown handling (arrows, Home / End, PageUp / PageDown, Enter, Escape, Space, Tab,
24
+ // Ctrl/Cmd+A, sort and column-menu keys, header filter row, cell range keys). A plain function over an
25
+ // explicit context: useGridKeyboardNavigation builds the context from its inputs and calls it, so the
26
+ // handler's identity and dependencies are unchanged (R6, 3.10).
27
+
28
+ /** Everything the keydown handling reads: the hook's inputs (defaults applied) and its focus state. */
29
+ export interface GridKeyDownContext<R extends GridRowModel> extends Pick<UseGridKeyboardNavigationParams<R>,
30
+ 'allRenderableRows' | 'navigationColumns' | 'editingHandlers' | 'selectedRowIds' | 'handleSelectionChange' | 'handleDetailPanelToggle' | 'setKeyboardMode' | 'sortModel' | 'handleSort' | 'handleSortAdd' | 'isCellEditable' | 'pagination' | 'pageSize' | 'rowMetaMap' | 'onRowActivate' | 'toggleRowExpansion' | 'handleSelectAll' | 'getSpanOrigin' | 'cellSelection'> {
31
+ multiSort: boolean;
32
+ rowSelectionEnabled: boolean;
33
+ multipleRowSelectionEnabled: boolean;
34
+ headerFilters: boolean;
35
+ getRowId: (row: R) => GridRowId;
36
+ focusedCell: FocusedCell | null;
37
+ focusedRowIndex: number;
38
+ getDefaultFocus: () => FocusedCell | null;
39
+ scrollCellIntoView: (rowIndex: number, field: string) => void;
40
+ setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
41
+ }
42
+
43
+ /** Keys that move focus between cells. */
44
+ export const NAVIGATION_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown']);
45
+
46
+ export function handleGridKeyDown<R extends GridRowModel>(event: React.KeyboardEvent, context: GridKeyDownContext<R>): void {
47
+ const { focusedCell, focusedRowIndex, allRenderableRows, navigationColumns, editingHandlers, selectedRowIds, handleSelectionChange, handleDetailPanelToggle, setKeyboardMode, sortModel, handleSort, handleSortAdd, multiSort, isCellEditable, pagination, pageSize, rowMetaMap, onRowActivate, toggleRowExpansion, handleSelectAll, rowSelectionEnabled, multipleRowSelectionEnabled, getDefaultFocus, scrollCellIntoView, getSpanOrigin, getRowId, cellSelection, headerFilters, setFocusedCell } = context;
48
+ const viewport = event.currentTarget as HTMLElement;
49
+ const targetKind = classifyKeyTarget(event.target, viewport);
50
+ if (targetKind === 'foreign') return;
51
+ setKeyboardMode(true);
52
+
53
+ // While an IME is composing, Enter / Escape / arrows confirm or navigate candidates.
54
+ if (event.nativeEvent?.isComposing || event.keyCode === 229) return;
55
+
56
+ // In a right-to-left grid ArrowLeft moves to the column on the left: the next column.
57
+ const key = logicalArrowKey(event.key, isRtl(viewport));
58
+ const isSpace = key === ' ' || key === 'Spacebar';
59
+ const isEditing = Boolean(editingHandlers.editingCell);
60
+
61
+ // Row positions: rows from 0, the column header row at `headerRow`, and the header filter
62
+ // row (when rendered) at -1, between the header and the rows.
63
+ const headerRow = headerFilters ? -2 : -1;
64
+ const moveTo = (r: number, c: number) => {
65
+ const col = navigationColumns[c];
66
+ if (!col || r < headerRow || r >= allRenderableRows.length) return;
67
+ if (r === headerRow) setFocusedCell({ id: null, field: col.field });
68
+ else if (r < 0) setFocusedCell({ id: null, field: col.field, headerFilter: true });
69
+ else setFocusedCell({ id: getRowId(allRenderableRows[r]), field: col.field, rowIndex: r });
70
+ scrollCellIntoView(r, col.field);
71
+ };
72
+
73
+ if (!focusedCell) {
74
+ if (!NAVIGATION_KEYS.has(key)) return;
75
+ const initial = getDefaultFocus();
76
+ if (!initial) return;
77
+ event.preventDefault();
78
+ setFocusedCell(initial);
79
+ scrollCellIntoView(initial.rowIndex ?? -1, initial.field);
80
+ return;
81
+ }
82
+
83
+ const { id, field } = focusedCell;
84
+ const onHeaderFilter = id === null && Boolean(focusedCell.headerFilter);
85
+ const rowIndex = id !== null ? focusedRowIndex : onHeaderFilter ? -1 : headerRow;
86
+ const colIndex = navigationColumns.findIndex(c => c.field === field);
87
+ const systemColumnCount = navigationColumns.filter(c => isSystemField(c.field)).length;
88
+ const isSyntheticRow = (rowId: GridRowId) => isSyntheticRowId(rowId, rowMetaMap);
89
+ // Same rule and starting value as double-click (see Row / resolveCellEditable).
90
+ const isEditableCell = (row: R, r: number, c: number) => {
91
+ const colDef = navigationColumns[c] as GridColDef<R>;
92
+ if (isSystemField(colDef.field)) return false;
93
+ return resolveCellEditable({
94
+ row,
95
+ field: colDef.field,
96
+ value: getCellValue(row, colDef.field, colDef),
97
+ colDef,
98
+ rowIndex: r,
99
+ colIndex: c - systemColumnCount,
100
+ rowMeta: rowMetaMap?.get(getRowId(row)),
101
+ }, isCellEditable);
102
+ };
103
+
104
+ // Next editable data cell in reading order, for Tab while editing.
105
+ const findNextEditable = (deltaCol: number) => {
106
+ let r = rowIndex;
107
+ let c = colIndex;
108
+ const maxSteps = (allRenderableRows.length + 1) * navigationColumns.length;
109
+ for (let steps = 0; steps < maxSteps; steps++) {
110
+ c += deltaCol;
111
+ if (c >= navigationColumns.length) { c = 0; r++; }
112
+ else if (c < 0) { c = navigationColumns.length - 1; r--; }
113
+ if (r < 0 || r >= allRenderableRows.length) return null;
114
+
115
+ const row = allRenderableRows[r];
116
+ // Cells covered by a span are not stops; their origin is.
117
+ if (getSpanOrigin?.(getRowId(row), navigationColumns[c].field)) continue;
118
+ if (isEditableCell(row, r, c)) return { r, c };
119
+ }
120
+ return null;
121
+ };
122
+
123
+ if (isEditing) {
124
+ if (key === 'Enter') {
125
+ event.preventDefault();
126
+ editingHandlers.stopCellEdit();
127
+ } else if (key === 'Escape') {
128
+ event.preventDefault();
129
+ editingHandlers.stopCellEdit({ cancel: true });
130
+ } else if (key === 'Tab' && id !== null) {
131
+ // No next editable cell: the browser moves focus on and the editor commits on blur.
132
+ const next = findNextEditable(event.shiftKey ? -1 : 1);
133
+ if (next) {
134
+ event.preventDefault();
135
+ editingHandlers.stopCellEdit();
136
+ moveTo(next.r, next.c);
137
+ }
138
+ }
139
+ return;
140
+ }
141
+
142
+ // Tab is not captured outside edit mode: the grid is a single tab stop and Tab leaves it
143
+ // (or reaches focusable content inside cells and detail panels).
144
+ if (key === 'Escape' && cellSelection?.collapseSelection()) {
145
+ event.preventDefault();
146
+ return;
147
+ }
148
+ if (key === 'Tab' || key === 'Escape') return;
149
+ // Enter and Space activate a control rendered inside a cell.
150
+ if (targetKind === 'control' && (key === 'Enter' || isSpace)) return;
151
+
152
+ if ((event.ctrlKey || event.metaKey) && (key === 'a' || key === 'A')) {
153
+ if (cellSelection) {
154
+ if (cellSelection.selectAllCells()) event.preventDefault();
155
+ return;
156
+ }
157
+ if (!rowSelectionEnabled || !multipleRowSelectionEnabled || !handleSelectAll) return;
158
+ event.preventDefault();
159
+ handleSelectAll(true);
160
+ return;
161
+ }
162
+
163
+ // A filter cell handles Enter, Space and typing itself (HeaderFilterCell).
164
+ if (onHeaderFilter && (key === 'Enter' || isSpace)) return;
165
+ if (id === null) {
166
+ if (key === 'Enter' || isSpace) {
167
+ event.preventDefault();
168
+ if (field === CHECKBOX_FIELD) {
169
+ // Same as clicking the select-all checkbox.
170
+ findFocusTarget(viewport, focusedCell, -1)?.click();
171
+ return;
172
+ }
173
+ const col = navigationColumns[colIndex];
174
+ if (!col || isSystemField(field) || col.sortable === false) return;
175
+ const currentSort = sortModel.find(item => item.field === field);
176
+ const direction: GridSortDirection = currentSort ? (currentSort.sort === 'asc' ? 'desc' : null) : 'asc';
177
+ if ((multiSort || event.shiftKey) && handleSortAdd) handleSortAdd(field, direction);
178
+ else handleSort(field, direction);
179
+ return;
180
+ }
181
+ } else if (key === 'Enter' || isSpace) {
182
+ event.preventDefault();
183
+ const row = allRenderableRows[rowIndex];
184
+ if (field === CHECKBOX_FIELD) {
185
+ handleSelectionChange(id, !selectedRowIds.has(id));
186
+ return;
187
+ }
188
+ if (field === EXPAND_FIELD) {
189
+ if (!event.shiftKey) handleDetailPanelToggle(id);
190
+ return;
191
+ }
192
+ if (isSpace) {
193
+ // Shift+Space selects the row; plain Space only must not scroll the viewport.
194
+ if (event.shiftKey && rowSelectionEnabled && !isSyntheticRow(id)) {
195
+ handleSelectionChange(id, !selectedRowIds.has(id));
196
+ }
197
+ return;
198
+ }
199
+ if (field === REORDER_FIELD) return;
200
+ if (isEditableCell(row, rowIndex, colIndex)) {
201
+ editingHandlers.startCellEdit({ id, field, value: getCellValue(row, field, navigationColumns[colIndex] as GridColDef<R>) });
202
+ return;
203
+ }
204
+ // A row with children toggles, so keyboard users can expand a tree-data parent even
205
+ // though clicking it selects it. Others: the keyboard equivalent of clicking the row.
206
+ if (toggleRowExpansion && rowMetaMap?.get(id)?.hasChildren) {
207
+ toggleRowExpansion(id);
208
+ return;
209
+ }
210
+ onRowActivate?.({ row, id, rowIndex });
211
+ return;
212
+ }
213
+
214
+ // Treegrid shortcuts: Alt+ArrowRight expands, Alt+ArrowLeft collapses the focused row.
215
+ if (event.altKey && (key === 'ArrowRight' || key === 'ArrowLeft') && toggleRowExpansion && id !== null) {
216
+ event.preventDefault();
217
+ const meta = rowMetaMap?.get(id);
218
+ if (meta?.hasChildren && Boolean(meta.isExpanded) !== (key === 'ArrowRight')) toggleRowExpansion(id);
219
+ return;
220
+ }
221
+
222
+ if (!NAVIGATION_KEYS.has(key)) return;
223
+ // Prevent native scrolling even when the focus cannot move (grid edges).
224
+ event.preventDefault();
225
+
226
+ const lastRow = allRenderableRows.length - 1;
227
+ const lastCol = navigationColumns.length - 1;
228
+ const withModifier = event.ctrlKey || event.metaKey;
229
+ const page = pagination ? pageSize : 10;
230
+
231
+ // Shift+navigation on a data cell extends the cell range; focus stays on its anchor.
232
+ if (cellSelection && event.shiftKey && id !== null && isRangeColumnField(field)
233
+ && cellSelection.extendSelection(key, withModifier, page, focusedCell)) {
234
+ return;
235
+ }
236
+
237
+ const step = (r: number, c: number): { r: number; c: number } => {
238
+ switch (key) {
239
+ case 'ArrowRight': return c + 1 > lastCol ? { r: r + 1, c: 0 } : { r, c: c + 1 };
240
+ case 'ArrowLeft': return c - 1 < 0 ? { r: r - 1, c: lastCol } : { r, c: c - 1 };
241
+ case 'ArrowDown': return { r: r + 1, c };
242
+ case 'ArrowUp': return { r: r - 1, c };
243
+ case 'Home': return { r: withModifier ? headerRow : r, c: 0 };
244
+ case 'End': return { r: withModifier ? lastRow : r, c: lastCol };
245
+ case 'PageUp': return { r: Math.max(headerRow, r - page), c };
246
+ default: return { r: Math.min(lastRow, r + page), c };
247
+ }
248
+ };
249
+ const inGrid = ({ r, c }: { r: number; c: number }) => r >= headerRow && r <= lastRow && c >= 0 && c <= lastCol;
250
+ const spanOriginAt = ({ r, c }: { r: number; c: number }) =>
251
+ r >= 0 ? getSpanOrigin?.(getRowId(allRenderableRows[r]), navigationColumns[c].field) ?? null : null;
252
+
253
+ let next = step(rowIndex, colIndex);
254
+ // Arrow keys step over the cells of the focused span (its own merged area) ...
255
+ if (key.startsWith('Arrow')) {
256
+ for (let origin = inGrid(next) ? spanOriginAt(next) : null;
257
+ origin && origin.rowId === id && origin.field === field;
258
+ origin = inGrid(next) ? spanOriginAt(next) : null) {
259
+ next = step(next.r, next.c);
260
+ }
261
+ }
262
+ if (!inGrid(next)) return;
263
+ // ... then land on the origin of whatever span covers the target cell.
264
+ const origin = spanOriginAt(next);
265
+ if (origin) {
266
+ const originRow = allRenderableRows.findIndex(r => getRowId(r) === origin.rowId);
267
+ const originCol = navigationColumns.findIndex(c => c.field === origin.field);
268
+ if (originRow !== -1 && originCol !== -1) next = { r: originRow, c: originCol };
269
+ }
270
+ if (next.r === rowIndex && next.c === colIndex) return;
271
+ moveTo(next.r, next.c);
272
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  GridApi
10
10
  } from '../../types';
11
11
  import { createGridApiPlaceholder } from './gridApiPlaceholder';
12
+ import { untypedColumn, untypedColumns } from '../../utils/untypedColumns';
12
13
 
13
14
  // Sort, filter, pagination and selection deliberately have no slice here: they live in
14
15
  // useGridControlledState, which is what the grid renders from. (Copies of them in this
@@ -76,14 +77,14 @@ function createInitialState<R extends GridRowModel>(rows: R[], getRowId: GetRowI
76
77
  const orderedFields: string[] = [];
77
78
 
78
79
  columns.forEach(col => {
79
- columnLookup.set(col.field, col as unknown as GridColDef);
80
+ columnLookup.set(col.field, untypedColumn(col));
80
81
  orderedFields.push(col.field);
81
82
  });
82
83
 
83
84
  return {
84
85
  rows: buildRowsState(rows, getRowId),
85
86
  columns: {
86
- all: columns as unknown as GridColDef[],
87
+ all: untypedColumns(columns),
87
88
  lookup: columnLookup,
88
89
  orderedFields,
89
90
  columnVisibilityModel
@@ -287,8 +288,13 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
287
288
 
288
289
  // `_preserveRowCount` is still accepted from callers written against the old signature;
289
290
  // replacing rows never changes the server row count any more.
290
- const setRows = useCallback((rowsOrUpdater: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]), _preserveRowCount?: boolean) => {
291
- dispatch({ type: 'SET_ROWS', payload: rowsOrUpdater, getRowId: getRowIdRef.current });
291
+ // Typed with the grid's own row type `R`. The store holds those very objects (never copies), so the
292
+ // stored rows read back here are `R` again: this is the one place the store's untyped rows narrow.
293
+ const setRows = useCallback((rowsOrUpdater: R[] | ((prev: R[]) => R[]), _preserveRowCount?: boolean) => {
294
+ const payload = typeof rowsOrUpdater === 'function'
295
+ ? (prev: GridRowModel[]) => rowsOrUpdater(prev as R[])
296
+ : rowsOrUpdater;
297
+ dispatch({ type: 'SET_ROWS', payload, getRowId: getRowIdRef.current });
292
298
  }, []);
293
299
 
294
300
  /** Replaces the row stored under `id` (a committed edit); does nothing if that row is gone. */
@@ -297,7 +303,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
297
303
  }, []);
298
304
 
299
305
  /** Replaces several stored rows in one update (a batch edit); ids no longer stored are ignored. */
300
- const replaceRows = useCallback((rowsById: ReadonlyMap<GridRowId, GridRowModel>) => {
306
+ const replaceRows = useCallback((rowsById: ReadonlyMap<GridRowId, R>) => {
301
307
  if (rowsById.size > 0) dispatch({ type: 'REPLACE_ROWS', rows: rowsById });
302
308
  }, []);
303
309
 
@@ -334,6 +340,8 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
334
340
 
335
341
  return {
336
342
  state,
343
+ /** The stored rows by id, typed with the grid's row type (see `setRows`). */
344
+ rowsLookup: (state.rows.idRowsLookup as ReadonlyMap<GridRowId, GridRowModel>) as ReadonlyMap<GridRowId, R>,
337
345
  dispatch,
338
346
  apiRef,
339
347
  setRows,
@@ -29,7 +29,7 @@ export interface UseGridApiMethodsParams {
29
29
  paginationModel: GridPaginationModel;
30
30
  onPaginationModelChange: (model: GridPaginationModel) => void;
31
31
 
32
- selectedRowIds: Set<GridRowId>;
32
+ selectedRowIds: ReadonlySet<GridRowId>;
33
33
  /**
34
34
  * A selection change made since the last commit (see useGridControlledState), or null. It
35
35
  * wins over `selectedRowIds`, so the API is current inside onRowSelectionModelChange.
@@ -103,7 +103,7 @@ export function useGridApiMethods(params: UseGridApiMethodsParams): void {
103
103
  api.setPage = (page) => setPaginationModel({ ...latest().paginationModel, page });
104
104
  api.setPageSize = (pageSize) => setPaginationModel({ ...latest().paginationModel, pageSize, page: 0 });
105
105
 
106
- const currentSelection = (): Set<GridRowId> => {
106
+ const currentSelection = (): ReadonlySet<GridRowId> => {
107
107
  const pending = latest().getPendingRowSelectionModel?.();
108
108
  return pending ? new Set(pending) : latest().selectedRowIds;
109
109
  };