@opencorestack/opengridx 3.2.2 → 3.4.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 (88) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +16 -2
  3. package/dist/ai.cjs +1 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +166 -0
  6. package/dist/ai.es.js +1 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +321 -3
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6483 -5291
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -13
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +132 -3
  15. package/docs/README.md +4 -1
  16. package/docs/architecture/datagrid-orchestration.md +23 -6
  17. package/docs/components/header.md +25 -0
  18. package/docs/customization/theming.md +12 -1
  19. package/docs/features/ai-toolkit.md +119 -0
  20. package/docs/features/cell-selection.md +131 -0
  21. package/docs/features/clipboard.md +90 -0
  22. package/docs/features/editing-reordering.md +50 -0
  23. package/docs/features/keyboard-navigation.md +25 -2
  24. package/docs/features/undo-redo.md +79 -0
  25. package/docs/roadmap.md +64 -7
  26. package/lib/ai/coerce.ts +90 -0
  27. package/lib/ai/columns.ts +90 -0
  28. package/lib/ai/index.ts +16 -0
  29. package/lib/ai/schema.ts +214 -0
  30. package/lib/ai/types.ts +88 -0
  31. package/lib/ai/validate.ts +286 -0
  32. package/lib/components/Cell/Cell.css +35 -0
  33. package/lib/components/Cell/Cell.tsx +25 -13
  34. package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
  35. package/lib/components/Cell/GridEditInputCell.css +12 -4
  36. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  37. package/lib/components/CellSelectionStats/CellSelectionStats.css +31 -0
  38. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +32 -0
  39. package/lib/components/CellSelectionStats/GridCellSelectionStatsArea.tsx +24 -0
  40. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  41. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  42. package/lib/components/DataGrid/DataGrid.tsx +123 -3
  43. package/lib/components/DataGrid/GridLiveRegion.tsx +8 -2
  44. package/lib/components/DataGrid/GridPinnedRows.tsx +9 -0
  45. package/lib/components/DataGrid/GridToolbarSlot.tsx +5 -0
  46. package/lib/components/DataGrid/GridVirtualRows.tsx +9 -0
  47. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  48. package/lib/components/FilterPanel/FilterPanel.tsx +5 -3
  49. package/lib/components/Header/Header.css +12 -0
  50. package/lib/components/Header/Header.tsx +17 -4
  51. package/lib/components/ListView/ListView.css +2 -4
  52. package/lib/components/ListView/ListViewRow.tsx +2 -2
  53. package/lib/components/Row/Row.tsx +38 -1
  54. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  55. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  56. package/lib/components/Toolbar/GridToolbar.tsx +10 -4
  57. package/lib/components/ui/Checkbox.tsx +26 -10
  58. package/lib/components/ui/Input.css +43 -0
  59. package/lib/components/ui/Input.tsx +24 -5
  60. package/lib/hooks/core/gridApiPlaceholder.ts +14 -1
  61. package/lib/hooks/core/useDataGrid.ts +27 -0
  62. package/lib/hooks/core/useGridCellSelection.ts +631 -0
  63. package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
  64. package/lib/hooks/core/useGridClipboardApi.ts +8 -2
  65. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  66. package/lib/hooks/core/useGridControlledState.ts +38 -0
  67. package/lib/hooks/core/useGridKeyboardNavigation.ts +32 -0
  68. package/lib/hooks/core/useGridRowInteractions.ts +8 -4
  69. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  70. package/lib/hooks/features/useAggregation.ts +8 -6
  71. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  72. package/lib/hooks/features/useGridClipboard.ts +47 -2
  73. package/lib/hooks/features/useGridEditing.ts +39 -51
  74. package/lib/index.ts +16 -0
  75. package/lib/styles/base/variables.css +6 -1
  76. package/lib/styles/opengridx.css +3 -1
  77. package/lib/theme/DataGridThemeProvider.tsx +4 -0
  78. package/lib/theme/presets.ts +8 -0
  79. package/lib/theme/types.ts +4 -0
  80. package/lib/types/index.ts +254 -0
  81. package/lib/utils/attempt.ts +14 -0
  82. package/lib/utils/cellSelection.ts +238 -0
  83. package/lib/utils/editing/commit.ts +69 -0
  84. package/lib/utils/editing/targets.ts +109 -0
  85. package/lib/utils/headerWrap.ts +23 -0
  86. package/lib/utils/parsing.ts +252 -0
  87. package/llms.txt +71 -2
  88. package/package.json +14 -4
@@ -1,5 +1,6 @@
1
1
  import { useState, useMemo, useRef, useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
+ import { Input } from '../ui/Input';
3
4
  import type { GridColDef, GridRowModel, GridValidRowModel } from '../../types';
4
5
 
5
6
 
@@ -115,9 +116,9 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
115
116
  <div className="ogx-column-visibility-panel__search-section">
116
117
  <div className="ogx-column-visibility-panel__search-container">
117
118
  <SearchIcon />
118
- <input
119
+ <Input
119
120
  autoComplete="off"
120
- className="ogx-column-visibility-panel__search-input"
121
+ inputClassName="ogx-column-visibility-panel__search-input"
121
122
  placeholder="Search"
122
123
  value={searchQuery}
123
124
  onChange={(e) => setSearchQuery(e.target.value)}
@@ -27,6 +27,9 @@ import { useDataGrid } from '../../hooks/core/useDataGrid';
27
27
  import { useRowReorder } from '../../hooks/useRowReorder';
28
28
  import { useGridHierarchy } from '../../hooks/core/useGridHierarchy';
29
29
  import { useGridEditing } from '../../hooks/features/useGridEditing';
30
+ import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
31
+ import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
32
+ import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
30
33
  import { useGridSpanning } from '../../hooks/features/useGridSpanning';
31
34
  import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
32
35
  import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
@@ -52,6 +55,9 @@ import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
52
55
  import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
53
56
  import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
54
57
  import { useGridColumnAutosize } from '../../hooks/core/useGridColumnAutosize';
58
+ import { useGridCellSelection } from '../../hooks/core/useGridCellSelection';
59
+ import { useGridCellSelectionApi } from '../../hooks/core/useGridCellSelectionApi';
60
+ import { GridCellSelectionStatsArea } from '../CellSelectionStats/GridCellSelectionStatsArea';
55
61
  import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
56
62
  import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
57
63
  import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
@@ -76,12 +82,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
76
82
  getRowId,
77
83
  rowHeight: rowHeightProp,
78
84
  headerHeight: headerHeightProp,
85
+ wrapHeaderText = false,
79
86
  autoHeight = false,
80
87
  density,
81
88
  checkboxSelection = false,
82
89
  disableRowSelectionOnClick = false,
83
90
  disableMultipleRowSelection = false,
84
91
  disableClipboardCopy = false,
92
+ cellSelection: cellSelectionProp = false,
93
+ cellSelectionModel: propCellSelectionModel,
94
+ onCellSelectionModelChange,
95
+ showCellSelectionStats = false,
96
+ disableClipboardPaste = false,
97
+ disableRangeClear = false,
98
+ onClipboardPaste,
99
+ onBeforeClipboardPaste,
100
+ undoRedo = false,
101
+ onHistoryChange,
85
102
  rowSelectionModel: propRowSelectionModel,
86
103
  onRowSelectionModelChange: propOnRowSelectionModelChange,
87
104
  onRowClick,
@@ -184,6 +201,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
184
201
  rowSelectionModel: propRowSelectionModel,
185
202
  onRowSelectionModelChange: propOnRowSelectionModelChange,
186
203
  density,
204
+ cellSelectionModel: propCellSelectionModel,
205
+ onCellSelectionModelChange,
187
206
  });
188
207
 
189
208
  const {
@@ -261,7 +280,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
261
280
  syncRows: !dataSource,
262
281
  });
263
282
  const {
264
- state, apiRef, setRows, replaceRow, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
283
+ state, apiRef, setRows, replaceRows, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
265
284
  setRowCount
266
285
  } = gridData;
267
286
 
@@ -285,6 +304,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
285
304
  getAggregationPosition, columnLookup, filterMode, sortingMode,
286
305
  });
287
306
 
307
+ // Commits (single edits and batch edits) reach the undo/redo history created further down.
308
+ const editCommitChannel = useGridEditCommitChannel();
309
+ // Batch edits (paste, range clear, undo, redo): one processRowUpdate per row, one store update.
310
+ const batchEdit = useGridBatchEdit<R>({
311
+ rowsLookup: state.rows.idRowsLookup as unknown as ReadonlyMap<GridRowId, R>,
312
+ columns: activeColumns,
313
+ processRowUpdate,
314
+ onProcessRowUpdateError,
315
+ replaceRows: replaceRows as unknown as (rows: ReadonlyMap<GridRowId, R>) => void,
316
+ onCommitted: editCommitChannel.emit,
317
+ });
318
+
288
319
  const editingHandlers = useGridEditing({
289
320
  rows: effectiveRows,
290
321
  getRowId: getRowIdOf,
@@ -296,8 +327,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
296
327
  // after an awaited processRowUpdate, by which time the rows prop (or a dataSource
297
328
  // fetch) may have replaced the rows this render saw. The returned row is stored as is
298
329
  // (the grid never writes an id onto it), even if it does not carry its key itself.
299
- replaceRow(rowId, updatedRow);
330
+ batchEdit.storeRow(rowId, updatedRow);
300
331
  },
332
+ onCommitted: (changes) => editCommitChannel.emit(changes, 'edit'),
301
333
  });
302
334
 
303
335
  // The selection without ids of removed rows. Only client-owned rows are pruned: a server page
@@ -493,12 +525,34 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
493
525
  [layout.leftPinnedCols, layout.unpinnedColsWithWidth, layout.rightPinnedCols]
494
526
  );
495
527
 
528
+ // Cell range selection (cellSelection): the range, pointer drag and Shift-keys, copy text, totals.
529
+ const cellSelection = useGridCellSelection<R>({
530
+ enabled: cellSelectionProp && !listView,
531
+ model: controlledState.cellSelectionModel,
532
+ onModelChange: controlledState.handleCellSelectionModelChange,
533
+ getPendingModel: controlledState.getPendingCellSelectionModel,
534
+ allRenderableRows,
535
+ getRowId: getRowIdOf,
536
+ navigationColumns,
537
+ columnIndexMap,
538
+ spanning,
539
+ rowMetaMap,
540
+ layout,
541
+ rowHeight: effectiveRowHeight,
542
+ pinnedTopRowCount: pinnedTopRows.length,
543
+ viewportRef,
544
+ containerRef,
545
+ localeText,
546
+ showStats: showCellSelectionStats,
547
+ });
548
+
496
549
  useGridClipboardApi({
497
550
  apiRef,
498
551
  columns: renderedDataColumns as unknown as GridColDef[],
499
552
  getRowId: getRowIdOf,
500
553
  containerRef,
501
554
  disableKeyboardShortcut: disableClipboardCopy,
555
+ getCellRangeCopyText: cellSelection.enabled ? cellSelection.getCopyText : undefined,
502
556
  });
503
557
  useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
504
558
  const handleColumnAutosize = useGridColumnAutosize<R>({ apiRef, containerRef, columns: orderedColumns, onColumnResize: handleColumnResize });
@@ -541,11 +595,61 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
541
595
  pinCheckboxColumn,
542
596
  pinExpandColumn,
543
597
  rowHeight: effectiveRowHeight,
598
+ cellSelection: cellSelection.keyboard,
544
599
  });
545
600
 
546
601
  const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
547
602
  ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId: getRowIdOf, onCellClick,
548
603
  });
604
+ // Keeps the range anchor and the focused cell together; installs the cell selection API.
605
+ useGridCellSelectionApi({
606
+ apiRef,
607
+ selection: cellSelection,
608
+ model: controlledState.cellSelectionModel,
609
+ getPendingModel: controlledState.getPendingCellSelectionModel,
610
+ focusedCell,
611
+ setFocusedCell,
612
+ containerRef,
613
+ });
614
+
615
+ // Ctrl/Cmd+V and Delete on a range (cellSelection); apiRef.pasteText.
616
+ useGridClipboardPaste<R>({
617
+ apiRef, containerRef, viewportRef,
618
+ selection: cellSelection,
619
+ disableClipboardPaste, disableRangeClear,
620
+ focusedCell,
621
+ allRenderableRows,
622
+ getRowId: getRowIdOf,
623
+ navigationColumns: navigationColumns as unknown as GridColDef<R>[],
624
+ isCellEditable,
625
+ rowMetaMap,
626
+ getSpanOrigin: spanning.getSpanOrigin,
627
+ isEditing: editingHandlers.editingCell !== null,
628
+ batchEdit,
629
+ onClipboardPaste,
630
+ onBeforeClipboardPaste,
631
+ });
632
+
633
+ // Undo / redo (undoRedo): history of committed edits, keys and apiRef methods.
634
+ useGridUndoRedo<R>({
635
+ apiRef,
636
+ enabled: Boolean(undoRedo),
637
+ limit: (typeof undoRedo === 'object' && undoRedo.limit) || UNDO_HISTORY_LIMIT,
638
+ onHistoryChange,
639
+ channel: editCommitChannel,
640
+ batchEdit,
641
+ isEditing: editingHandlers.editingCell !== null,
642
+ containerRef, viewportRef,
643
+ selection: cellSelection,
644
+ setFocusedCell,
645
+ allRenderableRows,
646
+ getRowId: getRowIdOf,
647
+ navigationColumns: navigationColumns as unknown as GridColDef<R>[],
648
+ layout,
649
+ rowHeight: effectiveRowHeight,
650
+ pinnedTopRowCount: pinnedTopRows.length,
651
+ });
652
+
549
653
  const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
550
654
  columnVisibilityModel,
551
655
  onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
@@ -658,6 +762,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
658
762
  rowSpanningCaches: spanning.rowSpanningCaches,
659
763
  rowHeight: effectiveRowHeight,
660
764
  rowMetaMap,
765
+ cellRange: cellSelection.displayRange,
766
+ cellRangeHasRowSpan: cellSelection.hasRowSpan,
661
767
  };
662
768
 
663
769
  return (
@@ -746,11 +852,13 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
746
852
  aria-label={ariaLabel || 'Data grid'}
747
853
  aria-rowcount={ariaRows.rowCount}
748
854
  aria-colcount={navigationColumns.length}
749
- aria-multiselectable={rowSelectionEnabled && !disableMultipleRowSelection}
855
+ aria-multiselectable={(rowSelectionEnabled && !disableMultipleRowSelection) || cellSelection.enabled}
750
856
  aria-busy={effectiveLoading}
751
857
  tabIndex={viewportTabIndex}
752
858
  onKeyDownCapture={() => { setKeyboardMode(true); }}
753
859
  onMouseDownCapture={handleMouseDownCapture}
860
+ onMouseDown={cellSelection.handleMouseDown}
861
+ onClickCapture={cellSelection.handleClickCapture}
754
862
  onKeyDown={handleKeyDown}
755
863
  onFocus={handleFocus}
756
864
  onBlur={handleBlur}
@@ -802,6 +910,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
802
910
  onHideColumn={handleHideColumn}
803
911
  onManageColumns={columnsPanel.openColumnsPanel}
804
912
  onPinColumn={handlePinColumn}
913
+ wrapHeaderText={wrapHeaderText}
914
+ headerHeight={headerHeight}
805
915
  />
806
916
 
807
917
  <GridPinnedRows<R>
@@ -881,6 +991,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
881
991
  </div>
882
992
  )}
883
993
 
994
+ {cellSelection.enabled && showCellSelectionStats && (
995
+ <GridCellSelectionStatsArea
996
+ stats={cellSelection.stats}
997
+ apiRef={gridData.apiRef}
998
+ slot={slots?.cellSelectionStats}
999
+ slotProps={slotProps?.cellSelectionStats}
1000
+ />
1001
+ )}
1002
+
884
1003
  {FooterSlot && (
885
1004
  <FooterSlot
886
1005
  apiRef={gridData.apiRef}
@@ -915,6 +1034,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
915
1034
  filteredRowCount={filteredRows.length}
916
1035
  dataRowCount={dataRows.length}
917
1036
  filterModel={filterModel}
1037
+ cellSelectionAnnouncement={cellSelection.announcement}
918
1038
  />
919
1039
 
920
1040
  {showLoadingOverRows && (
@@ -11,10 +11,15 @@ interface GridLiveRegionProps {
11
11
  /** Data rows that pass the filter: the "n rows found" count. */
12
12
  dataRowCount: number;
13
13
  filterModel: GridFilterModel | undefined;
14
+ /** The size of the selected cell range (`cellSelection`), already debounced. */
15
+ cellSelectionAnnouncement?: string;
14
16
  }
15
17
 
16
- /** Polite status announcements: loading, a load error, no rows, or the row count of an active filter. */
17
- export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel }: GridLiveRegionProps) {
18
+ /**
19
+ * Polite status announcements: loading, a load error, no rows, or the row count of an active filter;
20
+ * and the size of the selected cell range.
21
+ */
22
+ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement }: GridLiveRegionProps) {
18
23
  return (
19
24
  <div className="ogx-aria-live-status" role="status" aria-live="polite">
20
25
  {loading ? 'Loading data...' : ''}
@@ -26,6 +31,7 @@ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount,
26
31
  ? `${dataRowCount} ${dataRowCount === 1 ? 'row' : 'rows'} found`
27
32
  : ''
28
33
  )}
34
+ {cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
29
35
  </div>
30
36
  );
31
37
  }
@@ -15,6 +15,8 @@ import type {
15
15
  GridRowMeta,
16
16
  } from '../../types';
17
17
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
18
+ import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
19
+ import { getRowCellRange } from '../../utils/cellSelection';
18
20
  import type { GridEditingState } from '../../hooks/features/useGridEditing';
19
21
 
20
22
  interface PinnedRowEditingHandlers {
@@ -58,6 +60,10 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
58
60
  /** `aria-rowindex` of the first of these rows. */
59
61
  ariaRowIndexBase?: number;
60
62
  columnIndexMap?: Map<string, number>;
63
+ /** The selected cell range to draw (`cellSelection`), in renderable-row / data-column indices. */
64
+ cellRange?: GridDisplayCellRange | null;
65
+ /** The grid has row spans: rows tell their cells how far the range bottom is. */
66
+ cellRangeHasRowSpan?: boolean;
61
67
  /** Pinned rows are edited like any other row. */
62
68
  editingHandlers?: PinnedRowEditingHandlers;
63
69
  isCellEditable?: (params: GridCellParams<R>) => boolean;
@@ -103,6 +109,8 @@ export function GridPinnedRows<R extends GridRowModel>({
103
109
  editingHandlers,
104
110
  isCellEditable,
105
111
  rowReordering = false,
112
+ cellRange,
113
+ cellRangeHasRowSpan = false,
106
114
  }: GridPinnedRowsProps<R>) {
107
115
  if (rows.length === 0) return null;
108
116
 
@@ -148,6 +156,7 @@ export function GridPinnedRows<R extends GridRowModel>({
148
156
  onEditCellValueChange={editingHandlers?.setEditCellValue}
149
157
  isCellEditable={isCellEditable}
150
158
  rowReordering={rowReordering}
159
+ cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
151
160
  />
152
161
  );
153
162
  })}
@@ -25,8 +25,13 @@ function isPlainFunctionComponent(component: React.ComponentType<ToolbarProps>):
25
25
  * Runs a plain function toolbar inside its own fiber. The caller keys this element on the
26
26
  * function's source, so the fiber (and every hook in it) survives when the consumer redefines
27
27
  * the same toolbar inline on each render, but remounts when a different toolbar is passed.
28
+ *
29
+ * `render` calls hooks, so it must run on every render. React Compiler would memoize the call
30
+ * like any other and skip it when `render` and `toolbarProps` are unchanged, which breaks the
31
+ * toolbar's hooks; `'use no memo'` keeps this wrapper out of the compiler.
28
32
  */
29
33
  function InlineToolbar({ render, toolbarProps }: InlineToolbarProps) {
34
+ 'use no memo';
30
35
  return <>{render(toolbarProps)}</>;
31
36
  }
32
37
 
@@ -18,6 +18,8 @@ import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
18
18
  import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
19
19
  import type { GridEditingState } from '../../hooks/features/useGridEditing';
20
20
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
21
+ import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
22
+ import { getRowCellRange } from '../../utils/cellSelection';
21
23
 
22
24
  interface RowEditingHandlers {
23
25
  editingCell: GridEditingState['editingCell'];
@@ -71,6 +73,10 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
71
73
  /** Added to a row's rowIndex to get its `aria-rowindex` (header rows and earlier pages included). */
72
74
  ariaRowIndexOffset?: number;
73
75
  columnIndexMap?: Map<string, number>;
76
+ /** The selected cell range to draw (`cellSelection`), in renderable-row / data-column indices. */
77
+ cellRange?: GridDisplayCellRange | null;
78
+ /** The grid has row spans: rows tell their cells how far the range bottom is. */
79
+ cellRangeHasRowSpan?: boolean;
74
80
  }
75
81
 
76
82
  export function GridVirtualRows<R extends GridRowModel>({
@@ -115,6 +121,8 @@ export function GridVirtualRows<R extends GridRowModel>({
115
121
  loadingOverlay,
116
122
  ariaRowIndexOffset,
117
123
  columnIndexMap,
124
+ cellRange,
125
+ cellRangeHasRowSpan = false,
118
126
  }: GridVirtualRowsProps<R>) {
119
127
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
120
128
  ? baseColumns
@@ -206,6 +214,7 @@ export function GridVirtualRows<R extends GridRowModel>({
206
214
  rowSpanningCaches={rowSpanningCaches}
207
215
  rowHeight={rowHeight}
208
216
  rowMeta={rowMetaMap.get(id)}
217
+ cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
209
218
  />
210
219
  ))
211
220
  )}
@@ -115,11 +115,25 @@
115
115
  border-radius: var(--ogx-border-radius-sm);
116
116
  font-family: var(--ogx-font-family);
117
117
  font-size: var(--ogx-font-size-xs);
118
+ line-height: revert;
118
119
  background: var(--ogx-grid-background);
119
120
  color: var(--ogx-grid-row-text);
120
121
  outline: none;
121
122
  transition: border-color 0.15s, box-shadow 0.15s;
122
123
  }
124
+ /* The text/date value box renders through the shared Input: the <input> keeps its own border,
125
+ so the wrapper draws nothing and only takes the input's place in the row. */
126
+ .ogx-filter__controls .ogx-input-wrapper {
127
+ flex: 1;
128
+ min-width: 60px;
129
+ border: none;
130
+ border-radius: 0;
131
+ background: transparent;
132
+ transition: none;
133
+ }
134
+ .ogx-filter__controls .ogx-input-wrapper:focus-within {
135
+ box-shadow: none;
136
+ }
123
137
  .ogx-filter__value-input::placeholder {
124
138
  color: var(--ogx-overlay-text, var(--ogx-color-gray-400));
125
139
  opacity: 0.6;
@@ -8,6 +8,7 @@ import type {
8
8
  } from '../../types';
9
9
  import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
10
  import { toLocalDateString } from '../../utils/values';
11
+ import { Input } from '../ui/Input';
11
12
 
12
13
 
13
14
  export interface FilterPanelProps {
@@ -107,7 +108,8 @@ const FilterRow: React.FC<{
107
108
  onChange: (item: GridFilterItem | null) => void;
108
109
  }> = ({ col, item, resetToken, onChange }) => {
109
110
  const filterId = useId();
110
- const typeOperators = getOperatorsForType(col.type);
111
+ // Memoized so React Compiler treats the list as frozen and can keep the callbacks below memoized.
112
+ const typeOperators = useMemo(() => getOperatorsForType(col.type), [col.type]);
111
113
  const defaultOperator = typeOperators[0];
112
114
  const currentOperator: GridFilterOperator = item?.operator ?? defaultOperator;
113
115
  // An operator set programmatically that the column type does not list is still shown, so the
@@ -250,10 +252,10 @@ const FilterRow: React.FC<{
250
252
  );
251
253
  } else if (showValue) {
252
254
  valueControl = (
253
- <input
255
+ <Input
254
256
  id={`${filterId}-val`}
255
257
  name={`filter-val-${col.field}`}
256
- className={valueClassName}
258
+ inputClassName={valueClassName}
257
259
  type={col.type === 'date' ? 'date' : 'text'}
258
260
  value={draft ?? toInputText(item?.value, col)}
259
261
  onChange={(e) => setDraft(e.target.value)}
@@ -68,6 +68,18 @@
68
68
  font-weight: 600;
69
69
  letter-spacing: -0.01em;
70
70
  }
71
+ /* Wrapped header title (wrapHeaderText): as many lines as fit in --ogx-header-height, the line count
72
+ is set inline as -webkit-line-clamp (getHeaderLineClamp). The line-height must match
73
+ HEADER_WRAP_LINE_HEIGHT in utils/headerWrap.ts. max-height keeps the header at its height even when
74
+ a theme changes the font or padding. */
75
+ .ogx__header-cell--wrap .ogx__header-cell-title {
76
+ display: -webkit-box;
77
+ -webkit-box-orient: vertical;
78
+ white-space: normal;
79
+ overflow-wrap: break-word;
80
+ line-height: 16px;
81
+ max-height: calc(var(--ogx-header-height, 56px) - 2 * var(--ogx-grid-cell-padding-y, 8px));
82
+ }
71
83
  .ogx__header-cell-aggregation {
72
84
  font-size: 0.6rem;
73
85
  font-weight: 500;
@@ -7,6 +7,7 @@ import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '.
7
7
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
8
8
  import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
9
9
  import { ColumnMenu } from './ColumnMenu';
10
+ import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
10
11
 
11
12
 
12
13
  function MenuIcon() {
@@ -69,6 +70,10 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
69
70
  onManageColumns?: () => void;
70
71
  /** Position of each visible data column in render order, for `colIndex` and `aria-colindex`. */
71
72
  columnIndexMap?: Map<string, number>;
73
+ /** Wraps header titles onto several lines; `GridColDef.wrapHeaderText` overrides it per column. */
74
+ wrapHeaderText?: boolean;
75
+ /** Header row height in pixels; sets how many lines a wrapped title may take. Defaults to 56. */
76
+ headerHeight?: number;
72
77
  }
73
78
 
74
79
  export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
@@ -105,6 +110,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
105
110
  onPinColumn,
106
111
  onManageColumns,
107
112
  columnIndexMap,
113
+ wrapHeaderText = false,
114
+ headerHeight = 56,
108
115
  } = props;
109
116
 
110
117
  const selectAllId = useId();
@@ -411,6 +418,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
411
418
  const isPinned = pinnedPosition !== null;
412
419
  const isDragging = draggedColumn === colDef.field;
413
420
  const isDragOver = dragOverColumn === colDef.field;
421
+ const wraps = shouldWrapHeaderText(colDef.wrapHeaderText, wrapHeaderText);
422
+ const aggregationLabel = aggregationModel?.[colDef.field];
414
423
 
415
424
  const classNames = [
416
425
  'ogx__header-cell',
@@ -423,6 +432,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
423
432
  pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight && 'ogx__header-cell--pinned-right-first',
424
433
  isDragging && 'ogx__header-cell--dragging',
425
434
  isDragOver && 'ogx__header-cell--drag-over',
435
+ wraps && 'ogx__header-cell--wrap',
426
436
  isHeaderFocused(colDef.field) && 'ogx__header-cell--focused',
427
437
  colDef.headerClassName
428
438
  ].filter(Boolean).join(' ');
@@ -476,7 +486,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
476
486
  key={colDef.field}
477
487
  className={classNames}
478
488
  style={style}
479
- title={colDef.description}
489
+ title={colDef.description ?? (wraps && typeof headerContent === 'string' ? headerContent : undefined)}
480
490
  onClick={handleHeaderClick}
481
491
  role="columnheader"
482
492
  draggable={!!handleDragStart}
@@ -498,12 +508,15 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
498
508
  </div>
499
509
  )}
500
510
  <div className="ogx__header-cell-title-wrapper">
501
- <span className="ogx__header-cell-title">
511
+ <span
512
+ className="ogx__header-cell-title"
513
+ style={wraps ? { WebkitLineClamp: getHeaderLineClamp(headerHeight, !!aggregationLabel) } : undefined}
514
+ >
502
515
  {headerContent}
503
516
  </span>
504
- {aggregationModel && aggregationModel[colDef.field] && (
517
+ {aggregationLabel && (
505
518
  <span className="ogx__header-cell-aggregation">
506
- {aggregationModel[colDef.field]}
519
+ {aggregationLabel}
507
520
  </span>
508
521
  )}
509
522
  </div>
@@ -65,11 +65,9 @@
65
65
  border-right: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
66
66
  }
67
67
 
68
- .ogx-list-view__checkbox input[type="checkbox"] {
69
- width: 16px;
70
- height: 16px;
68
+ /* The row checkbox is the shared Checkbox (its <input> is visually hidden). */
69
+ .ogx-list-view__checkbox .ogx-checkbox-wrapper {
71
70
  cursor: pointer;
72
- accent-color: var(--ogx-color-primary);
73
71
  }
74
72
 
75
73
  /* ─── Cell content area ───────────────────────────────────────────────────── */
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { formatValueByType, getCellValue, getFormattedValue } from '../../utils/values';
3
3
  import { ExpandIcon } from '../ui/ExpandIcon';
4
+ import { Checkbox } from '../ui/Checkbox';
4
5
  import { CellErrorBoundary } from '../Cell/CellErrorBoundary';
5
6
  import { isSyntheticRowId } from '../../utils/syntheticRows';
6
7
  import type { GridColDef, GridRowModel, GridRowId, GridRowMeta, GridListViewColDef, GridRenderCellParams } from '../../types';
@@ -92,8 +93,7 @@ export function ListViewRow<R extends GridRowModel = GridRowModel>({
92
93
  >
93
94
  {checkboxSelection && (
94
95
  <div className="ogx-list-view__checkbox" onClick={(e) => e.stopPropagation()}>
95
- {!isSyntheticRow && <input
96
- type="checkbox"
96
+ {!isSyntheticRow && <Checkbox
97
97
  checked={isSelected}
98
98
  onChange={handleCheckboxChange}
99
99
  aria-label={`Select row ${id}`}
@@ -13,6 +13,8 @@ import { tryGetCellValue } from '../../utils/values';
13
13
  import { isSyntheticRowId } from '../../utils/syntheticRows';
14
14
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
15
15
  import { DEFAULT_DETAIL_PANEL_HEIGHT } from '../../utils/detailPanel';
16
+ import { GROUPING_COLUMN_FIELD, getCellRangeFlags, isSameRowCellRange } from '../../utils/cellSelection';
17
+ import type { GridRowCellRange } from '../../utils/cellSelection';
16
18
 
17
19
  /** Sticky system cells (drag handle, detail toggle, checkbox) sit above scrolled, spanned and focused cells. */
18
20
  const SYSTEM_CELL_Z_INDEX = 12;
@@ -96,9 +98,14 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
96
98
  columnIndexMap?: Map<string, number>;
97
99
  /** 1-based `aria-rowindex` of this row in the whole grid (header rows included). Defaults to `rowIndex + 2`. */
98
100
  ariaRowIndex?: number;
101
+ /**
102
+ * The part of the selected cell range (`cellSelection`) in this row, or `null` / absent when the
103
+ * row is outside it. @since v3.3
104
+ */
105
+ cellRange?: GridRowCellRange | null;
99
106
  }
100
107
 
101
- export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
108
+ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
102
109
  const {
103
110
  row,
104
111
  rowId: rowIdProp,
@@ -144,6 +151,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
144
151
  rowMeta,
145
152
  columnIndexMap,
146
153
  ariaRowIndex,
154
+ cellRange,
147
155
  } = props;
148
156
 
149
157
  const id = rowIdProp ?? row.id;
@@ -437,6 +445,14 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
437
445
  || (pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight);
438
446
  const rowSpan = rowSpanningCaches?.spannedCells[id]?.[colDef.field];
439
447
  const isHiddenByRowSpan = rowSpanningCaches?.hiddenCells[id]?.[colDef.field] || false;
448
+ const rangeFlags = cellRange && !isHiddenByRowSpan && colDef.field !== GROUPING_COLUMN_FIELD
449
+ ? getCellRangeFlags(
450
+ cellRange,
451
+ colIndex,
452
+ colSpanInfo && !colSpanInfo.spannedByColSpan ? colSpanInfo.cellProps.colSpan : 1,
453
+ rowSpan ?? 1,
454
+ )
455
+ : undefined;
440
456
 
441
457
  return (
442
458
  <Cell
@@ -468,6 +484,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
468
484
  isHiddenByRowSpan={isHiddenByRowSpan}
469
485
  rowMeta={rowMeta}
470
486
  valueError={valueError}
487
+ rangeFlags={rangeFlags}
471
488
  />
472
489
  );
473
490
  })}
@@ -490,3 +507,23 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
490
507
  </>
491
508
  );
492
509
  }
510
+
511
+ /**
512
+ * Shallow prop comparison, except `cellRange`, which is compared by value: a row inside a cell range
513
+ * gets a new range object on every render, but only re-renders when its part of the range changes.
514
+ */
515
+ function areRowPropsEqual<R extends GridRowModel>(prev: RowProps<R>, next: RowProps<R>): boolean {
516
+ const prevKeys = Object.keys(prev) as (keyof RowProps<R>)[];
517
+ if (prevKeys.length !== Object.keys(next).length) return false;
518
+ for (const key of prevKeys) {
519
+ if (key === 'cellRange') {
520
+ if (!isSameRowCellRange(prev.cellRange, next.cellRange)) return false;
521
+ } else if (!Object.is(prev[key], next[key])) {
522
+ return false;
523
+ }
524
+ }
525
+ return true;
526
+ }
527
+
528
+ /** One grid row. Memoised: rows whose props did not change (for example outside a dragged cell range) do not re-render. */
529
+ export const Row = React.memo(RowImpl, areRowPropsEqual) as typeof RowImpl;