@opencorestack/opengridx 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Row } from '../Row/Row';
3
+ import { DetailPanelContent } from '../DetailPanel/DetailPanelContent';
4
+ import { callGetDetailPanelHeight, resolveDetailPanelHeight } from '../../utils/detailPanel';
3
5
  import type {
4
6
  GridRowModel,
5
7
  GridRowId,
@@ -13,6 +15,14 @@ import type {
13
15
  GridRowMeta,
14
16
  } from '../../types';
15
17
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
18
+ import type { GridEditingState } from '../../hooks/features/useGridEditing';
19
+
20
+ interface PinnedRowEditingHandlers {
21
+ editingCell: GridEditingState['editingCell'];
22
+ startCellEdit: (params: { id: GridRowId; field: string; value: unknown }) => void;
23
+ stopCellEdit: (params?: { cancel?: boolean; id?: GridRowId; field?: string }) => void;
24
+ setEditCellValue: (params: { id: GridRowId; field: string; value: unknown }) => void;
25
+ }
16
26
 
17
27
  export interface GridPinnedRowsProps<R extends GridRowModel> {
18
28
  rows: R[];
@@ -21,6 +31,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
21
31
  selectedRowIds: Set<GridRowId>;
22
32
  checkboxSelection: boolean;
23
33
  onRowClick: (params: GridRowParams<R>) => void;
34
+ onRowDoubleClick?: (params: GridRowParams<R>) => void;
24
35
  onCellClick: (params: GridCellParams<R>) => void;
25
36
  onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
26
37
  columnWidths: Record<string, number>;
@@ -31,15 +42,34 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
31
42
  getDetailPanelContent?: (params: GridDetailPanelParams<R>) => React.ReactNode;
32
43
  getDetailPanelHeight?: (params: GridDetailPanelParams<R>) => GridDetailPanelHeight;
33
44
  onDetailPanelToggle: (rowId: GridRowId) => void;
45
+ /** Receives the rendered height of `'auto'` detail panels. */
46
+ onDetailPanelHeightChange?: (rowId: GridRowId, height: number) => void;
34
47
  pinCheckboxColumn?: boolean;
35
48
  pinExpandColumn?: boolean;
36
- focusedCell: { id: GridRowId; field: string } | null;
49
+ focusedCell: { id: GridRowId | null; field: string } | null;
37
50
  colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
38
51
  rowSpanningCaches?: RowSpanningCaches;
39
52
  rowHeight: number;
40
53
  rowMetaMap: Map<GridRowId, GridRowMeta>;
54
+ /** Resolves a row's id; defaults to `row.id`. */
55
+ getRowId?: (row: R) => GridRowId;
56
+ /** Index of the first of these rows in the grid's renderable rows (top-pinned + centre + bottom-pinned). */
57
+ rowIndexOffset?: number;
58
+ /** `aria-rowindex` of the first of these rows. */
59
+ ariaRowIndexBase?: number;
60
+ columnIndexMap?: Map<string, number>;
61
+ /** Pinned rows are edited like any other row. */
62
+ editingHandlers?: PinnedRowEditingHandlers;
63
+ isCellEditable?: (params: GridCellParams<R>) => boolean;
64
+ /**
65
+ * Renders the (empty, non-draggable) reorder cell so the row lines up with the header and the
66
+ * centre rows. Pinned rows cannot be dragged.
67
+ */
68
+ rowReordering?: boolean;
41
69
  }
42
70
 
71
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
72
+
43
73
  export function GridPinnedRows<R extends GridRowModel>({
44
74
  rows,
45
75
  position,
@@ -47,6 +77,7 @@ export function GridPinnedRows<R extends GridRowModel>({
47
77
  selectedRowIds,
48
78
  checkboxSelection,
49
79
  onRowClick,
80
+ onRowDoubleClick,
50
81
  onCellClick,
51
82
  onSelectionChange,
52
83
  columnWidths,
@@ -57,6 +88,7 @@ export function GridPinnedRows<R extends GridRowModel>({
57
88
  getDetailPanelContent,
58
89
  getDetailPanelHeight,
59
90
  onDetailPanelToggle,
91
+ onDetailPanelHeightChange,
60
92
  pinCheckboxColumn,
61
93
  pinExpandColumn,
62
94
  focusedCell,
@@ -64,39 +96,61 @@ export function GridPinnedRows<R extends GridRowModel>({
64
96
  rowSpanningCaches,
65
97
  rowHeight,
66
98
  rowMetaMap,
99
+ getRowId = defaultGetRowId,
100
+ rowIndexOffset = 0,
101
+ ariaRowIndexBase,
102
+ columnIndexMap,
103
+ editingHandlers,
104
+ isCellEditable,
105
+ rowReordering = false,
67
106
  }: GridPinnedRowsProps<R>) {
68
107
  if (rows.length === 0) return null;
69
108
 
70
109
  return (
71
110
  <div className={`ogx__pinned-rows ogx__pinned-rows--${position}`} role="rowgroup">
72
- {rows.map((row, index) => (
111
+ {rows.map((row, index) => {
112
+ const id = getRowId(row);
113
+ const rowIndex = rowIndexOffset + index;
114
+ return (
73
115
  <Row<R>
74
- key={row.id}
116
+ key={id}
75
117
  row={row}
118
+ rowId={id}
76
119
  columns={columns}
77
- rowIndex={index}
78
- isSelected={selectedRowIds.has(row.id)}
120
+ rowIndex={rowIndex}
121
+ ariaRowIndex={ariaRowIndexBase !== undefined ? ariaRowIndexBase + index : undefined}
122
+ columnIndexMap={columnIndexMap}
123
+ isSelected={selectedRowIds.has(id)}
79
124
  checkboxSelection={checkboxSelection}
80
125
  onRowClick={onRowClick}
126
+ onRowDoubleClick={onRowDoubleClick}
81
127
  onCellClick={onCellClick}
82
128
  onSelectionChange={onSelectionChange}
83
129
  columnWidths={columnWidths}
84
130
  pinnedColumns={pinnedColumns}
85
131
  pinnedRows={pinnedRows}
86
132
  hasDetailPanel={hasDetailPanel}
87
- isDetailPanelExpanded={expandedRowIds.has(row.id)}
88
- detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: index }) : null}
89
- detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: index }) || 200}
133
+ isDetailPanelExpanded={expandedRowIds.has(id)}
134
+ detailPanelContent={expandedRowIds.has(id) && getDetailPanelContent ? <DetailPanelContent<R> getContent={getDetailPanelContent} params={{ row, id, rowIndex }} /> : null}
135
+ detailPanelHeight={expandedRowIds.has(id) ? resolveDetailPanelHeight(callGetDetailPanelHeight(getDetailPanelHeight, { row, id, rowIndex })) : undefined}
136
+ onDetailPanelHeightChange={onDetailPanelHeightChange}
90
137
  onDetailPanelToggle={onDetailPanelToggle}
91
138
  pinCheckboxColumn={pinCheckboxColumn}
92
139
  pinExpandColumn={pinExpandColumn}
93
- focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
140
+ focusedCellField={focusedCell?.id === id ? focusedCell.field : null}
94
141
  colspanMap={colspanMap}
95
142
  rowSpanningCaches={rowSpanningCaches}
96
143
  rowHeight={rowHeight}
97
- rowMeta={rowMetaMap.get(row.id)}
144
+ rowMeta={rowMetaMap.get(id)}
145
+ editingCell={editingHandlers?.editingCell}
146
+ onEditStart={editingHandlers?.startCellEdit}
147
+ onEditStop={editingHandlers?.stopCellEdit}
148
+ onEditCellValueChange={editingHandlers?.setEditCellValue}
149
+ isCellEditable={isCellEditable}
150
+ rowReordering={rowReordering}
98
151
  />
99
- ))}
152
+ );
153
+ })}
100
154
  </div>
101
155
  );
102
156
  }
@@ -1,67 +1,67 @@
1
1
  import React, { useState, useLayoutEffect } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
4
- import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../../types';
4
+ import type { GridColDef, GridRowModel, GridColumnGroupingModel } from '../../types';
5
+ import { canReorderWithinColumnGroups } from '../../utils/columnGroups';
6
+ import { getViewportWidth } from '../../utils/viewport';
5
7
 
6
8
  export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
7
9
  isOpen: boolean;
8
10
  containerRef: React.RefObject<HTMLDivElement | null>;
9
11
  panelRef?: React.RefObject<HTMLDivElement | null>;
10
- effectiveColumns: GridColDef<R>[];
12
+ /** Every column, in the grid's current column order. */
13
+ columns: GridColDef<R>[];
11
14
  columnVisibilityModel: Record<string, boolean>;
12
- effectiveColumnOrder: string[];
13
- columnOrder?: string[];
14
15
  disableColumnReorder: boolean;
15
16
  onClose: () => void;
16
17
  onColumnVisibilityChange: (model: Record<string, boolean>) => void;
17
- onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
18
- setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
18
+ /** Moves `fromField` to the position of `toField` in the column order. */
19
+ onColumnMove: (fromField: string, toField: string) => void;
20
+ /** Restores the columns' definition order (the panel's Reset). */
21
+ onColumnOrderReset: () => void;
22
+ /** When set, a column can only be moved within its own column group. */
23
+ columnGroupingModel?: GridColumnGroupingModel;
24
+ }
25
+
26
+ interface PanelPlacement {
27
+ top: number;
28
+ right: number;
29
+ target: Element;
19
30
  }
20
31
 
21
32
  export function GridStandaloneColumnPanel<R extends GridRowModel>({
22
33
  isOpen,
23
34
  containerRef,
24
35
  panelRef,
25
- effectiveColumns,
36
+ columns,
26
37
  columnVisibilityModel,
27
- effectiveColumnOrder,
28
- columnOrder,
29
38
  disableColumnReorder,
30
39
  onClose,
31
40
  onColumnVisibilityChange,
32
- onColumnOrderChange,
33
- setInternalColumnOrder,
41
+ onColumnMove,
42
+ onColumnOrderReset,
43
+ columnGroupingModel,
34
44
  }: GridStandaloneColumnPanelProps<R>) {
35
- const [panelTop, setPanelTop] = useState<number>(16);
36
- const [panelRight, setPanelRight] = useState<number>(16);
37
- const [portalTarget, setPortalTarget] = useState<Element>(() => document.body);
45
+ // Resolved from the DOM after mount, never during render, so the grid renders on the server.
46
+ const [placement, setPlacement] = useState<PanelPlacement | null>(null);
38
47
 
39
48
  useLayoutEffect(() => {
40
49
  if (!isOpen || !containerRef.current) return;
41
50
  const rect = containerRef.current.getBoundingClientRect();
42
- setPanelTop(rect.top + 8);
43
- setPanelRight(window.innerWidth - rect.right + 8);
44
- setPortalTarget(containerRef.current.closest('.ogx-theme-provider') || document.body);
51
+ setPlacement({
52
+ top: rect.top + 8,
53
+ right: getViewportWidth() - rect.right + 8,
54
+ target: containerRef.current.closest('.ogx-theme-provider') || document.body,
55
+ });
45
56
  }, [isOpen, containerRef]);
46
57
 
47
- if (!isOpen) return null;
58
+ if (!isOpen || !placement) return null;
48
59
 
49
60
  const handleReorder = disableColumnReorder
50
61
  ? undefined
51
62
  : (fromField: string, toField: string) => {
52
- const currentOrder = [...effectiveColumnOrder];
53
- const fromIdx = currentOrder.indexOf(fromField);
54
- const toIdx = currentOrder.indexOf(toField);
55
- if (fromIdx === -1 || toIdx === -1) return;
56
- const newOrder = [...currentOrder];
57
- newOrder.splice(fromIdx, 1);
58
- newOrder.splice(toIdx, 0, fromField);
59
- if (!columnOrder) setInternalColumnOrder(newOrder);
60
- onColumnOrderChange?.({
61
- oldIndex: fromIdx,
62
- targetIndex: toIdx,
63
- column: effectiveColumns.find(c => c.field === fromField) as unknown as GridColDef,
64
- });
63
+ if (!canReorderWithinColumnGroups(columnGroupingModel, fromField, toField)) return;
64
+ onColumnMove(fromField, toField);
65
65
  };
66
66
 
67
67
  return ReactDOM.createPortal(
@@ -69,13 +69,14 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
69
69
  ref={panelRef}
70
70
  style={{
71
71
  position: 'fixed',
72
- top: panelTop,
73
- right: panelRight,
72
+ top: placement.top,
73
+ right: placement.right,
74
74
  zIndex: 9999,
75
75
  display: 'inline-block',
76
76
  }}
77
77
  >
78
78
  <button
79
+ type="button"
79
80
  onClick={onClose}
80
81
  style={{
81
82
  position: 'absolute',
@@ -99,9 +100,9 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
99
100
  aria-label="Close"
100
101
  >×</button>
101
102
  <ColumnVisibilityPanel<R>
102
- columns={effectiveColumns}
103
+ columns={columns}
103
104
  visibleColumns={new Set(
104
- effectiveColumns
105
+ columns
105
106
  .filter(col => columnVisibilityModel[col.field] !== false)
106
107
  .map(col => col.field)
107
108
  )}
@@ -110,17 +111,18 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
110
111
  }}
111
112
  onShowAll={() => {
112
113
  const next = { ...columnVisibilityModel };
113
- effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = true; });
114
+ columns.forEach(col => { if (col.hideable !== false) next[col.field] = true; });
114
115
  onColumnVisibilityChange(next);
115
116
  }}
116
117
  onHideAll={() => {
117
118
  const next = { ...columnVisibilityModel };
118
- effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
119
+ columns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
119
120
  onColumnVisibilityChange(next);
120
121
  }}
121
122
  onColumnReorder={handleReorder}
123
+ onColumnOrderReset={disableColumnReorder ? undefined : onColumnOrderReset}
122
124
  />
123
125
  </div>,
124
- portalTarget
126
+ placement.target
125
127
  );
126
128
  }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import type { GridToolbarSlotProps as GridToolbarSlotComponentProps } from '../../types';
3
+
4
+ type ToolbarProps = GridToolbarSlotComponentProps & Record<string, unknown>;
5
+ type ToolbarRender = (props: ToolbarProps) => React.ReactNode;
6
+
7
+ interface GridToolbarSlotProps {
8
+ component: React.ComponentType<ToolbarProps>;
9
+ toolbarProps: ToolbarProps;
10
+ }
11
+
12
+ interface InlineToolbarProps {
13
+ render: ToolbarRender;
14
+ toolbarProps: ToolbarProps;
15
+ }
16
+
17
+ /** A plain function component: not a class, and not a memo / forwardRef / lazy object. */
18
+ function isPlainFunctionComponent(component: React.ComponentType<ToolbarProps>): component is ToolbarRender {
19
+ if (typeof component !== 'function') return false;
20
+ const proto: unknown = component.prototype;
21
+ return !(typeof proto === 'object' && proto !== null && 'isReactComponent' in proto);
22
+ }
23
+
24
+ /**
25
+ * Runs a plain function toolbar inside its own fiber. The caller keys this element on the
26
+ * function's source, so the fiber (and every hook in it) survives when the consumer redefines
27
+ * the same toolbar inline on each render, but remounts when a different toolbar is passed.
28
+ */
29
+ function InlineToolbar({ render, toolbarProps }: InlineToolbarProps) {
30
+ return <>{render(toolbarProps)}</>;
31
+ }
32
+
33
+ /**
34
+ * Renders `slots.toolbar`.
35
+ *
36
+ * Consumers often write the toolbar as an inline function inside their own component, which
37
+ * gives it a new identity on every render. Rendering that as `<Toolbar />` would remount it on
38
+ * every keystroke and lose GridToolbar's state (open panels, typed search). A plain function is
39
+ * therefore called from a wrapper keyed on its source text: the same code keeps its state, and
40
+ * a switch to a toolbar with different code (and different hooks) remounts cleanly.
41
+ *
42
+ * Class, `React.memo`, `forwardRef` and `lazy` components cannot be called as functions, so they
43
+ * are rendered as ordinary elements.
44
+ */
45
+ export function GridToolbarSlot({ component, toolbarProps }: GridToolbarSlotProps) {
46
+ if (isPlainFunctionComponent(component)) {
47
+ return <InlineToolbar key={`fn:${component.toString()}`} render={component} toolbarProps={toolbarProps} />;
48
+ }
49
+ const Toolbar = component;
50
+ return <Toolbar {...toolbarProps} />;
51
+ }
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import { Row } from '../Row/Row';
3
+ import { DetailPanelContent } from '../DetailPanel/DetailPanelContent';
3
4
  import { SkeletonRow } from '../SkeletonRow';
4
- import { isRowPinned } from '../../utils/pinning';
5
+ import { callGetDetailPanelHeight, resolveDetailPanelHeight } from '../../utils/detailPanel';
5
6
  import type {
6
7
  GridRowModel,
7
8
  GridRowId,
@@ -9,7 +10,6 @@ import type {
9
10
  GridRowParams,
10
11
  GridCellParams,
11
12
  GridColumnPinning,
12
- GridRowPinning,
13
13
  GridDetailPanelParams,
14
14
  GridDetailPanelHeight,
15
15
  GridRowMeta,
@@ -22,7 +22,7 @@ import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/us
22
22
  interface RowEditingHandlers {
23
23
  editingCell: GridEditingState['editingCell'];
24
24
  startCellEdit: (params: { id: GridRowId; field: string; value: unknown }) => void;
25
- stopCellEdit: (params?: { cancel?: boolean }) => void;
25
+ stopCellEdit: (params?: { cancel?: boolean; id?: GridRowId; field?: string }) => void;
26
26
  setEditCellValue: (params: { id: GridRowId; field: string; value: unknown }) => void;
27
27
  }
28
28
 
@@ -38,9 +38,9 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
38
38
  hasDetailPanel: boolean;
39
39
  rowReordering: boolean;
40
40
  rowHeight: number;
41
- pinnedRows?: GridRowPinning;
42
41
  selectedRowIds: Set<GridRowId>;
43
42
  onRowClick: (params: GridRowParams<R>) => void;
43
+ onRowDoubleClick?: (params: GridRowParams<R>) => void;
44
44
  onCellClick: (params: GridCellParams<R>) => void;
45
45
  onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
46
46
  columnWidths: Record<string, number>;
@@ -49,11 +49,14 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
49
49
  getDetailPanelContent?: (params: GridDetailPanelParams<R>) => React.ReactNode;
50
50
  getDetailPanelHeight?: (params: GridDetailPanelParams<R>) => GridDetailPanelHeight;
51
51
  onDetailPanelToggle: (rowId: GridRowId) => void;
52
+ /** Receives the rendered height of `'auto'` detail panels. */
53
+ onDetailPanelHeightChange?: (rowId: GridRowId, height: number) => void;
52
54
  pinCheckboxColumn?: boolean;
53
55
  pinExpandColumn?: boolean;
54
56
  rowReorderHandlers: UseRowReorderReturn;
55
57
  editingHandlers: RowEditingHandlers;
56
- focusedCell: { id: GridRowId; field: string } | null;
58
+ isCellEditable?: (params: GridCellParams<R>) => boolean;
59
+ focusedCell: { id: GridRowId | null; field: string } | null;
57
60
  colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
58
61
  rowSpanningCaches?: RowSpanningCaches;
59
62
  paginationMode: 'client' | 'server' | 'infinite';
@@ -61,7 +64,13 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
61
64
  sortedUnpinnedRowCount: number;
62
65
  infiniteScrollSkeletonCount: number;
63
66
  unpinnedRowsLength: number;
67
+ /** Last center row index in the render window: skeleton rows render only once it reaches the end of the data. */
68
+ lastRenderedRowIndex: number;
64
69
  rowMetaMap: Map<GridRowId, GridRowMeta>;
70
+ loadingOverlay?: React.ReactNode;
71
+ /** Added to a row's rowIndex to get its `aria-rowindex` (header rows and earlier pages included). */
72
+ ariaRowIndexOffset?: number;
73
+ columnIndexMap?: Map<string, number>;
65
74
  }
66
75
 
67
76
  export function GridVirtualRows<R extends GridRowModel>({
@@ -76,9 +85,9 @@ export function GridVirtualRows<R extends GridRowModel>({
76
85
  hasDetailPanel,
77
86
  rowReordering,
78
87
  rowHeight,
79
- pinnedRows,
80
88
  selectedRowIds,
81
89
  onRowClick,
90
+ onRowDoubleClick,
82
91
  onCellClick,
83
92
  onSelectionChange,
84
93
  columnWidths,
@@ -87,10 +96,12 @@ export function GridVirtualRows<R extends GridRowModel>({
87
96
  getDetailPanelContent,
88
97
  getDetailPanelHeight,
89
98
  onDetailPanelToggle,
99
+ onDetailPanelHeightChange,
90
100
  pinCheckboxColumn,
91
101
  pinExpandColumn,
92
102
  rowReorderHandlers,
93
103
  editingHandlers,
104
+ isCellEditable,
94
105
  focusedCell,
95
106
  colspanMap,
96
107
  rowSpanningCaches,
@@ -99,7 +110,11 @@ export function GridVirtualRows<R extends GridRowModel>({
99
110
  sortedUnpinnedRowCount,
100
111
  infiniteScrollSkeletonCount,
101
112
  unpinnedRowsLength,
113
+ lastRenderedRowIndex,
102
114
  rowMetaMap,
115
+ loadingOverlay,
116
+ ariaRowIndexOffset,
117
+ columnIndexMap,
103
118
  }: GridVirtualRowsProps<R>) {
104
119
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
105
120
  ? baseColumns
@@ -117,11 +132,12 @@ export function GridVirtualRows<R extends GridRowModel>({
117
132
  })) as GridColDef<R>[];
118
133
  })();
119
134
 
120
- const centerRows = visibleRows
121
- .filter(({ row }) => !isRowPinned(row.id, pinnedRows))
122
- .filter((item, index, self) =>
123
- index === self.findIndex(t => t.row.id === item.row.id)
124
- );
135
+ // Pinned rows render in GridPinnedRows; ids are already unique (useGridVisibleRows de-duplicates).
136
+ const centerRows = visibleRows.filter(item => !item.pinned);
137
+ // Placeholders stand in for the next page, after the last data row: only draw them once the
138
+ // render window reaches it, or they would follow the window into the middle of the data.
139
+ const showInfiniteSkeletons = paginationMode === 'infinite' && dataSourceLoading && sortedUnpinnedRowCount > 0
140
+ && lastRenderedRowIndex >= unpinnedRowsLength - 1;
125
141
 
126
142
  return (
127
143
  <div
@@ -134,7 +150,9 @@ export function GridVirtualRows<R extends GridRowModel>({
134
150
  style={{ transform: `translateY(${offsetTop}px)` }}
135
151
  role="rowgroup"
136
152
  >
137
- {effectiveLoading && visibleRows.length === 0 ? (
153
+ {effectiveLoading && visibleRows.length === 0 && loadingOverlay ? (
154
+ <div className="ogx__loading-overlay" style={{ width: viewportWidth || undefined }}>{loadingOverlay}</div>
155
+ ) : effectiveLoading && visibleRows.length === 0 ? (
138
156
  Array.from({ length: 10 }).map((_, index) => (
139
157
  <SkeletonRow
140
158
  key={`skeleton-${index}`}
@@ -146,48 +164,53 @@ export function GridVirtualRows<R extends GridRowModel>({
146
164
  />
147
165
  ))
148
166
  ) : (
149
- centerRows.map(({ row, rowIndex: actualIndex }) => (
167
+ centerRows.map(({ row, id, rowIndex: actualIndex }) => (
150
168
  <Row<R>
151
- key={row.id}
169
+ key={id}
152
170
  row={row}
171
+ rowId={id}
153
172
  columns={virtualColumns}
154
173
  rowIndex={actualIndex}
155
- isSelected={selectedRowIds.has(row.id)}
174
+ ariaRowIndex={ariaRowIndexOffset !== undefined ? actualIndex + ariaRowIndexOffset : undefined}
175
+ columnIndexMap={columnIndexMap}
176
+ isSelected={selectedRowIds.has(id)}
156
177
  checkboxSelection={checkboxSelection}
157
178
  onRowClick={onRowClick}
179
+ onRowDoubleClick={onRowDoubleClick}
158
180
  onCellClick={onCellClick}
159
181
  onSelectionChange={onSelectionChange}
160
182
  columnWidths={columnWidths}
161
183
  pinnedColumns={pinnedColumns}
162
- pinnedRows={pinnedRows}
163
184
  hasDetailPanel={hasDetailPanel}
164
- isDetailPanelExpanded={expandedRowIds.has(row.id)}
165
- detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: actualIndex }) : null}
166
- detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: actualIndex }) || 200}
185
+ isDetailPanelExpanded={expandedRowIds.has(id)}
186
+ detailPanelContent={expandedRowIds.has(id) && getDetailPanelContent ? <DetailPanelContent<R> getContent={getDetailPanelContent} params={{ row, id, rowIndex: actualIndex }} /> : null}
187
+ detailPanelHeight={expandedRowIds.has(id) ? resolveDetailPanelHeight(callGetDetailPanelHeight(getDetailPanelHeight, { row, id, rowIndex: actualIndex })) : undefined}
188
+ onDetailPanelHeightChange={onDetailPanelHeightChange}
167
189
  onDetailPanelToggle={onDetailPanelToggle}
168
190
  pinCheckboxColumn={pinCheckboxColumn}
169
191
  pinExpandColumn={pinExpandColumn}
170
192
  rowReordering={rowReordering}
171
- onDragStart={rowReorderHandlers.onDragStart}
193
+ onDragStart={rowReorderHandlers.canReorderRow(id) ? rowReorderHandlers.onDragStart : undefined}
172
194
  onDragOver={rowReorderHandlers.onDragOver}
173
195
  onDragEnd={rowReorderHandlers.onDragEnd}
174
196
  onDrop={rowReorderHandlers.onDrop}
175
- isDragging={rowReorderHandlers.draggedRowId === row.id}
176
- isDragOver={rowReorderHandlers.dragOverRowId === row.id}
197
+ isDragging={rowReorderHandlers.draggedRowId === id}
198
+ isDragOver={rowReorderHandlers.dragOverRowId === id}
177
199
  editingCell={editingHandlers.editingCell}
178
200
  onEditStart={editingHandlers.startCellEdit}
179
201
  onEditStop={editingHandlers.stopCellEdit}
180
202
  onEditCellValueChange={editingHandlers.setEditCellValue}
181
- focusedCellField={focusedCell != null && focusedCell.id === row.id ? focusedCell.field : null}
203
+ isCellEditable={isCellEditable}
204
+ focusedCellField={focusedCell != null && focusedCell.id === id ? focusedCell.field : null}
182
205
  colspanMap={colspanMap}
183
206
  rowSpanningCaches={rowSpanningCaches}
184
207
  rowHeight={rowHeight}
185
- rowMeta={rowMetaMap.get(row.id)}
208
+ rowMeta={rowMetaMap.get(id)}
186
209
  />
187
210
  ))
188
211
  )}
189
212
 
190
- {paginationMode === 'infinite' && dataSourceLoading && sortedUnpinnedRowCount > 0 && (
213
+ {showInfiniteSkeletons && (
191
214
  <div className="ogx__skeleton-group">
192
215
  {Array.from({ length: infiniteScrollSkeletonCount }).map((_, i) => (
193
216
  <Row<R>
@@ -28,9 +28,3 @@
28
28
  .ogx__detail-panel {
29
29
  animation: detailPanelExpand 0.2s ease-out;
30
30
  }
31
- @media (prefers-color-scheme: dark) {
32
- .ogx__detail-panel {
33
- background: var(--ogx-color-gray-800);
34
- border-color: var(--ogx-color-gray-700);
35
- }
36
- }
@@ -10,23 +10,52 @@ export interface DetailPanelProps<R extends GridRowModel = GridRowModel> {
10
10
  content: React.ReactNode;
11
11
  height: GridDetailPanelHeight;
12
12
  isExpanded: boolean;
13
+ /** Referenced by the expand cell's `aria-controls`. */
14
+ id?: string;
15
+ /** Number of grid columns the panel spans (`aria-colspan` of its cell). */
16
+ colSpan?: number;
17
+ /**
18
+ * Called with the panel's rendered height when `height` is `'auto'`: once when it mounts and
19
+ * again whenever its size changes, so the grid can lay the rows below it out correctly.
20
+ */
21
+ onHeightChange?: (rowId: GridRowId, height: number) => void;
13
22
  }
14
23
 
15
24
  export function DetailPanel<R extends GridRowModel = GridRowModel>(props: DetailPanelProps<R>) {
16
- const { content, height, isExpanded } = props;
25
+ const { rowId, content, height, isExpanded, onHeightChange, id, colSpan } = props;
26
+ const panelRef = React.useRef<HTMLDivElement>(null);
27
+ const isAuto = height === 'auto';
28
+
29
+ // Measured before paint, so the grid re-lays out rows under the panel before they are shown.
30
+ React.useLayoutEffect(() => {
31
+ const el = panelRef.current;
32
+ if (!el || !isAuto || !isExpanded || !onHeightChange) return;
33
+ const report = () => {
34
+ // Not laid out (hidden ancestor, or no layout engine): keep the previous height.
35
+ if (el.getClientRects().length === 0) return;
36
+ onHeightChange(rowId, el.offsetHeight);
37
+ };
38
+ report();
39
+ if (typeof ResizeObserver === 'undefined') return;
40
+ const observer = new ResizeObserver(report);
41
+ observer.observe(el);
42
+ return () => observer.disconnect();
43
+ }, [isAuto, isExpanded, onHeightChange, rowId]);
17
44
 
18
45
  if (!isExpanded) {
19
46
  return null;
20
47
  }
21
48
 
22
49
  const style: React.CSSProperties = {
23
- height: height === 'auto' ? 'auto' : `${height}px`,
24
- overflow: height === 'auto' ? 'visible' : 'auto'
50
+ height: isAuto ? 'auto' : `${height}px`,
51
+ overflow: isAuto ? 'visible' : 'auto'
25
52
  };
26
53
 
54
+ // A detail panel sits between rows of the grid, so it is exposed as a row with one cell
55
+ // spanning every column (a bare div is not allowed inside role="rowgroup").
27
56
  return (
28
- <div className="ogx__detail-panel" style={style}>
29
- <div className="ogx__detail-panel-content">
57
+ <div ref={panelRef} className="ogx__detail-panel" style={style} role="row" id={id}>
58
+ <div className="ogx__detail-panel-content" role="gridcell" aria-colspan={colSpan}>
30
59
  {content}
31
60
  </div>
32
61
  </div>
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { CellErrorBoundary } from '../Cell/CellErrorBoundary';
3
+ import type { GridDetailPanelParams, GridRowModel } from '../../types';
4
+
5
+ interface DetailPanelContentProps<R extends GridRowModel> {
6
+ getContent: (params: GridDetailPanelParams<R>) => React.ReactNode;
7
+ params: GridDetailPanelParams<R>;
8
+ }
9
+
10
+ /**
11
+ * Calls `getDetailPanelContent` while rendering, below an error boundary, so a throwing callback
12
+ * shows an error marker in that one panel instead of unmounting the grid. The row lists render it
13
+ * only for an expanded row, so the callback never runs for collapsed rows.
14
+ */
15
+ export function DetailPanelContent<R extends GridRowModel>({ getContent, params }: DetailPanelContentProps<R>) {
16
+ return <CellErrorBoundary field="detail panel" renderFn={() => getContent(params)} resetKey={params.row} />;
17
+ }