@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,16 +1,24 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { GridColDef, GridRowModel, GridRowId, GridColumnPinning, GridDetailPanelHeight } from '../../types';
3
- import { isColumnPinned } from '../../utils/pinning';
3
+ import { getPinnedColumnGroups, SYSTEM_COLUMN_WIDTH } from '../../utils/pinning';
4
+ import { clampColumnWidth } from '../../utils/columnWidth';
5
+ import { callGetDetailPanelHeight, getDetailPanelLayoutHeight } from '../../utils/detailPanel';
4
6
 
5
7
  export interface UseLayoutParams<R extends GridRowModel> {
6
8
  rowHeight: number;
7
9
  pagination: boolean;
8
10
  paginatedUnpinnedRows: R[];
9
11
  sortedUnpinnedRows: R[];
12
+ /** Rows whose detail panel is open; the caller leaves out synthetic group rows (getDetailPanelRowIds). */
10
13
  expandedRowIds: Set<GridRowId>;
11
14
  getDetailPanelHeight?: (params: { row: R; id: GridRowId; rowIndex: number }) => GridDetailPanelHeight;
15
+ /** Rendered heights of `'auto'` detail panels, by row id (measured by the panels themselves). */
16
+ detailPanelHeights?: Map<GridRowId, number>;
12
17
  pinnedTopRowsLength: number;
13
18
  pinnedBottomRowsLength: number;
19
+ /** The pinned rows themselves; when given, their expanded detail panels count towards the pinned heights. */
20
+ pinnedTopRows?: R[];
21
+ pinnedBottomRows?: R[];
14
22
  visibleOrderedColumns: GridColDef<R>[];
15
23
  pinnedColumns?: GridColumnPinning;
16
24
  columnWidths: Record<string, number>;
@@ -24,15 +32,21 @@ export interface UseLayoutParams<R extends GridRowModel> {
24
32
  paginationMode: string;
25
33
  isLoading: boolean;
26
34
  pageSize: number;
35
+ /** Resolves a row's id (getRowId) for the expanded-detail-panel lookup; defaults to `row.id`. */
36
+ getRowId?: (row: R) => GridRowId;
27
37
  }
28
38
 
39
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
40
+
29
41
  export interface LayoutResult<R extends GridRowModel> {
30
42
  rowHeights: number[];
31
43
  cumulativeHeights: number[];
32
44
  unpinnedRowsHeight: number;
33
45
  pinnedTopHeight: number;
34
46
  pinnedBottomHeight: number;
47
+ /** Left-pinned columns in render order (`pinnedColumns.left` order) with resolved widths. */
35
48
  leftPinnedCols: (GridColDef<R> & { width: number; zIndex: number })[];
49
+ /** Right-pinned columns in render order (`pinnedColumns.right` order) with resolved widths. */
36
50
  rightPinnedCols: (GridColDef<R> & { width: number; zIndex: number })[];
37
51
  unpinnedColsWithWidth: (GridColDef<R> & { width: number })[];
38
52
  unpinnedColWidths: number[];
@@ -41,7 +55,10 @@ export interface LayoutResult<R extends GridRowModel> {
41
55
  leftWidth: number;
42
56
  rightWidth: number;
43
57
  unpinnedTotalWidth: number;
58
+ /** Width of all system columns (drag handle, detail toggle, checkbox). */
44
59
  systemColumnsWidth: number;
60
+ /** Width of the system columns that stay sticky on horizontal scroll. */
61
+ pinnedSystemColumnsWidth: number;
45
62
  pinnedTopRowsLength: number;
46
63
  pinnedBottomRowsLength: number;
47
64
  unpinnedRowsLength: number;
@@ -62,6 +79,144 @@ function parseWidth(width: number | string | undefined): { type: 'fixed' | 'perc
62
79
  return { type: 'fixed', value: 100 };
63
80
  }
64
81
 
82
+ type WidthSpec =
83
+ | { kind: 'fixed'; width: number }
84
+ | { kind: 'percentage'; percentage: number }
85
+ | { kind: 'flex'; flex: number };
86
+
87
+ /**
88
+ * Resolves every visible column to a pixel width. Pinned and unpinned columns follow the same
89
+ * rules, and every width is clamped to the column's minWidth / maxWidth (as the cell CSS is):
90
+ * - a number (or a manual resize) is used as is;
91
+ * - percentages are all taken of the same base: the data area left after the fixed-width columns;
92
+ * - flex and 'auto' columns share what is left after that.
93
+ */
94
+ function resolveColumnWidths<R extends GridRowModel>(
95
+ columns: GridColDef<R>[],
96
+ columnWidths: Record<string, number>,
97
+ dataAreaWidth: number,
98
+ ): { widths: Map<string, number>; flexByField: Map<string, number> } {
99
+ const specs = new Map<string, WidthSpec>();
100
+ columns.forEach(col => {
101
+ const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
102
+ if (!hasManualWidth && col.flex && col.flex > 0) {
103
+ specs.set(col.field, { kind: 'flex', flex: col.flex });
104
+ return;
105
+ }
106
+ const parsed = parseWidth(columnWidths[col.field] ?? col.width);
107
+ if (parsed.type === 'fixed') {
108
+ specs.set(col.field, { kind: 'fixed', width: clampColumnWidth(parsed.value, col.minWidth, col.maxWidth) });
109
+ } else if (parsed.type === 'percentage') {
110
+ specs.set(col.field, { kind: 'percentage', percentage: parsed.value });
111
+ } else {
112
+ specs.set(col.field, { kind: 'flex', flex: 1 });
113
+ }
114
+ });
115
+
116
+ // Widths the columns need before any sharing, so an overflowing grid scrolls instead of squashing.
117
+ const naturalWidth = columns.reduce((sum, col) => {
118
+ const spec = specs.get(col.field)!;
119
+ if (spec.kind === 'fixed') return sum + spec.width;
120
+ if (spec.kind === 'percentage') return sum + 100;
121
+ if (col.flex && col.flex > 0) {
122
+ const minWidth = col.minWidth ?? 150;
123
+ return sum + (minWidth || (typeof col.width === 'number' ? col.width : 150));
124
+ }
125
+ return sum + (col.minWidth ?? 150);
126
+ }, 0);
127
+
128
+ const availableWidth = Math.max(dataAreaWidth, naturalWidth);
129
+ const fixedWidth = columns.reduce((sum, col) => {
130
+ const spec = specs.get(col.field)!;
131
+ return spec.kind === 'fixed' ? sum + spec.width : sum;
132
+ }, 0);
133
+
134
+ const widths = new Map<string, number>();
135
+ const percentageBase = Math.max(0, availableWidth - fixedWidth);
136
+ let percentageTotal = 0;
137
+ columns.forEach(col => {
138
+ const spec = specs.get(col.field)!;
139
+ if (spec.kind === 'fixed') {
140
+ widths.set(col.field, spec.width);
141
+ } else if (spec.kind === 'percentage') {
142
+ const raw = (spec.percentage / 100) * percentageBase;
143
+ const width = clampColumnWidth(raw > 0 ? raw : 100, col.minWidth ?? 50, col.maxWidth);
144
+ widths.set(col.field, width);
145
+ percentageTotal += width;
146
+ }
147
+ });
148
+
149
+ const freeSpace = Math.max(0, percentageBase - percentageTotal);
150
+ const flexByField = new Map<string, number>();
151
+ const flexItems = columns.flatMap(col => {
152
+ const spec = specs.get(col.field)!;
153
+ if (spec.kind !== 'flex') return [];
154
+ flexByField.set(col.field, spec.flex);
155
+ return [{
156
+ field: col.field,
157
+ flex: spec.flex,
158
+ minWidth: col.minWidth ?? 50,
159
+ maxWidth: col.maxWidth ?? Infinity,
160
+ frozen: false,
161
+ computedWidth: 0,
162
+ }];
163
+ });
164
+
165
+ // Distributes the free space by flex weight, freezing items that hit their min/max and
166
+ // redistributing, the way CSS flexbox resolves flexible lengths.
167
+ let iterations = 0;
168
+ const maxIterations = flexItems.length * 2;
169
+ while (iterations < maxIterations) {
170
+ iterations++;
171
+ const unfrozen = flexItems.filter(f => !f.frozen);
172
+ if (unfrozen.length === 0) break;
173
+
174
+ const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
175
+ const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
176
+ const currentFreeSpace = Math.max(0, freeSpace - frozenWidthTotal);
177
+
178
+ if (unfrozenFlexTotal <= 0) {
179
+ unfrozen.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
180
+ break;
181
+ }
182
+
183
+ const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
184
+ let totalViolation = 0;
185
+ const minViolators: typeof flexItems = [];
186
+ const maxViolators: typeof flexItems = [];
187
+
188
+ unfrozen.forEach(f => {
189
+ const rawWidth = pixelsPerFlex * f.flex;
190
+ if (rawWidth < f.minWidth) {
191
+ totalViolation += f.minWidth - rawWidth;
192
+ minViolators.push(f);
193
+ } else if (rawWidth > f.maxWidth) {
194
+ totalViolation += f.maxWidth - rawWidth;
195
+ maxViolators.push(f);
196
+ } else {
197
+ f.computedWidth = rawWidth;
198
+ }
199
+ });
200
+
201
+ if (minViolators.length === 0 && maxViolators.length === 0) {
202
+ unfrozen.forEach(f => { f.frozen = true; });
203
+ break;
204
+ }
205
+
206
+ if (totalViolation > 0) {
207
+ minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
208
+ } else if (totalViolation < 0) {
209
+ maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
210
+ } else {
211
+ minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
212
+ maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
213
+ }
214
+ }
215
+ flexItems.forEach(f => widths.set(f.field, f.computedWidth));
216
+
217
+ return { widths, flexByField };
218
+ }
219
+
65
220
  export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): LayoutResult<R> {
66
221
  const {
67
222
  rowHeight,
@@ -70,8 +225,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
70
225
  sortedUnpinnedRows,
71
226
  expandedRowIds,
72
227
  getDetailPanelHeight,
228
+ detailPanelHeights,
73
229
  pinnedTopRowsLength,
74
230
  pinnedBottomRowsLength,
231
+ pinnedTopRows,
232
+ pinnedBottomRows,
75
233
  visibleOrderedColumns,
76
234
  pinnedColumns,
77
235
  columnWidths,
@@ -79,22 +237,25 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
79
237
  checkboxSelection,
80
238
  hasDetailPanel,
81
239
  rowReordering,
240
+ pinCheckboxColumn,
241
+ pinExpandColumn,
82
242
  paginationMode,
83
243
  isLoading,
84
244
  pageSize,
245
+ getRowId = defaultGetRowId,
85
246
  } = params;
86
247
 
87
248
  return useMemo<LayoutResult<R>>(() => {
88
249
  const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
89
250
 
90
- const rowHeights = unpinnedRows.map((row, index) => {
91
- let height = rowHeight;
92
- if (expandedRowIds.has(row.id)) {
93
- const detailHeight = getDetailPanelHeight?.({ row, id: row.id, rowIndex: pinnedTopRowsLength + index }) ?? 200;
94
- height += typeof detailHeight === 'number' ? detailHeight : parseInt(String(detailHeight), 10) || 200;
95
- }
96
- return height;
97
- });
251
+ const heightOf = (row: R, rowIndex: number) => {
252
+ const id = getRowId(row);
253
+ if (!expandedRowIds.has(id)) return rowHeight;
254
+ const panelHeight = callGetDetailPanelHeight(getDetailPanelHeight, { row, id, rowIndex });
255
+ return rowHeight + getDetailPanelLayoutHeight(panelHeight, detailPanelHeights?.get(id));
256
+ };
257
+
258
+ const rowHeights = unpinnedRows.map((row, index) => heightOf(row, pinnedTopRowsLength + index));
98
259
 
99
260
  const cumulativeHeights = rowHeights.reduce((acc, height, index) => {
100
261
  acc.push((acc[index - 1] || 0) + height);
@@ -105,199 +266,51 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
105
266
  ? Math.min(pageSize, 20)
106
267
  : 0;
107
268
  const unpinnedRowsHeight = (cumulativeHeights[cumulativeHeights.length - 1] || 0) + skeletonCount * rowHeight;
108
- const pinnedTopHeight = pinnedTopRowsLength * rowHeight;
109
- const pinnedBottomHeight = pinnedBottomRowsLength * rowHeight;
110
-
111
- const rawLeftPinnedCols: GridColDef<R>[] = [];
112
- const rawRightPinnedCols: GridColDef<R>[] = [];
113
- const unpinnedCols: GridColDef<R>[] = [];
114
- const unpinnedColWidths: number[] = [];
115
-
116
- visibleOrderedColumns.forEach(col => {
117
- const pinned = isColumnPinned(col.field, pinnedColumns);
118
- if (pinned === 'left') {
119
- rawLeftPinnedCols.push(col);
120
- } else if (pinned === 'right') {
121
- rawRightPinnedCols.push(col);
122
- } else {
123
- unpinnedCols.push(col);
124
- }
125
- });
126
-
127
- const systemColumnsWidth = (checkboxSelection ? 48 : 0) + (hasDetailPanel ? 48 : 0) + (rowReordering ? 48 : 0);
128
-
129
- const getColWidth = (c: GridColDef<R>) => columnWidths[c.field] ?? c.width;
130
-
131
- const leftWidth = rawLeftPinnedCols.reduce((sum, c) => {
132
- const parsed = parseWidth(getColWidth(c));
133
- return sum + (parsed.type === 'fixed' ? parsed.value : 100);
134
- }, 0);
135
- const rightWidth = rawRightPinnedCols.reduce((sum, c) => {
136
- const parsed = parseWidth(getColWidth(c));
137
- return sum + (parsed.type === 'fixed' ? parsed.value : 100);
138
- }, 0);
139
-
140
- const naturalFlexWidth = unpinnedCols.reduce((sum, col) => {
141
- const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
142
- if (!hasManualWidth && col.flex && col.flex > 0) {
143
- const minWidth = col.minWidth ?? 150;
144
- const w = typeof col.width === 'number' ? col.width : 150;
145
- return sum + (minWidth || w);
146
- }
147
- const parsed = parseWidth(getColWidth(col));
148
- if (parsed.type === 'fixed') return sum + parsed.value;
149
- if (parsed.type === 'percentage') return sum + 100;
150
- return sum + (col.minWidth ?? 150);
151
- }, 0);
152
-
153
- const availableWidth = Math.max(
154
- viewportWidth - systemColumnsWidth - leftWidth - rightWidth,
155
- naturalFlexWidth
269
+ // Pinned rows are indexed from 0 within their section (as GridPinnedRows renders them).
270
+ const pinnedBlockHeight = (rows: R[] | undefined, count: number) =>
271
+ rows ? rows.reduce((sum, row, index) => sum + heightOf(row, index), 0) : count * rowHeight;
272
+ const pinnedTopHeight = pinnedBlockHeight(pinnedTopRows, pinnedTopRowsLength);
273
+ const pinnedBottomHeight = pinnedBlockHeight(pinnedBottomRows, pinnedBottomRowsLength);
274
+
275
+ // Pinned columns render in pinnedColumns.left / .right order; unpinned ones in column order.
276
+ const { left: rawLeftPinnedCols, center: unpinnedCols, right: rawRightPinnedCols } =
277
+ getPinnedColumnGroups(visibleOrderedColumns, pinnedColumns);
278
+
279
+ const systemColumnsWidth =
280
+ (checkboxSelection ? SYSTEM_COLUMN_WIDTH : 0) +
281
+ (hasDetailPanel ? SYSTEM_COLUMN_WIDTH : 0) +
282
+ (rowReordering ? SYSTEM_COLUMN_WIDTH : 0);
283
+ const pinnedSystemColumnsWidth =
284
+ (rowReordering ? SYSTEM_COLUMN_WIDTH : 0) +
285
+ (hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0) +
286
+ (checkboxSelection && pinCheckboxColumn ? SYSTEM_COLUMN_WIDTH : 0);
287
+
288
+ const { widths, flexByField } = resolveColumnWidths(
289
+ [...rawLeftPinnedCols, ...unpinnedCols, ...rawRightPinnedCols],
290
+ columnWidths,
291
+ viewportWidth - systemColumnsWidth,
156
292
  );
157
-
158
- const fixedWidthCols: Array<{ col: GridColDef<R>; width: number }> = [];
159
- const percentageCols: Array<{ col: GridColDef<R>; percentage: number }> = [];
160
- const flexCols: Array<{ col: GridColDef<R>; flex: number }> = [];
161
-
162
- unpinnedCols.forEach(col => {
163
- const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
164
- const parsed = parseWidth(getColWidth(col));
165
-
166
- if (!hasManualWidth && col.flex && col.flex > 0) {
167
- flexCols.push({ col, flex: col.flex });
168
- return;
169
- }
170
-
171
- if (parsed.type === 'fixed') {
172
- fixedWidthCols.push({ col, width: parsed.value });
173
- } else if (parsed.type === 'percentage') {
174
- percentageCols.push({ col, percentage: parsed.value });
175
- } else {
176
- flexCols.push({ col, flex: 1 });
177
- }
178
- });
179
-
180
- const fixedWidth = fixedWidthCols.reduce((sum, { width }) => sum + width, 0);
181
- let remainingSpaceForFlex = Math.max(0, availableWidth - fixedWidth);
182
-
183
- const percentageWidthMap = new Map<string, number>();
184
- percentageCols.forEach(({ col, percentage }) => {
185
- const calculatedWidth = (percentage / 100) * remainingSpaceForFlex;
186
- percentageWidthMap.set(col.field, calculatedWidth);
187
- remainingSpaceForFlex -= calculatedWidth;
188
- });
189
- remainingSpaceForFlex = Math.max(0, remainingSpaceForFlex);
190
-
191
- const finalFlexWidths = new Map<string, number>();
192
- const flexItems = flexCols.map(f => ({
193
- ...f,
194
- minWidth: f.col.minWidth ?? 50,
195
- maxWidth: f.col.maxWidth ?? Infinity,
196
- frozen: false,
197
- computedWidth: 0
198
- }));
199
-
200
- const solveFlexAllocation = () => {
201
- let iterations = 0;
202
- const maxIterations = flexItems.length * 2;
203
-
204
- while (iterations < maxIterations) {
205
- iterations++;
206
- const unfrozen = flexItems.filter(f => !f.frozen);
207
- if (unfrozen.length === 0) break;
208
-
209
- const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
210
- const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
211
- const currentFreeSpace = Math.max(0, remainingSpaceForFlex - frozenWidthTotal);
212
-
213
- if (unfrozenFlexTotal <= 0) {
214
- unfrozen.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
215
- break;
216
- }
217
-
218
- const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
219
- let totalViolation = 0;
220
- const minViolators: typeof flexItems = [];
221
- const maxViolators: typeof flexItems = [];
222
-
223
- unfrozen.forEach(f => {
224
- const rawWidth = pixelsPerFlex * f.flex;
225
- if (rawWidth < f.minWidth) {
226
- totalViolation += f.minWidth - rawWidth;
227
- minViolators.push(f);
228
- } else if (rawWidth > f.maxWidth) {
229
- totalViolation += f.maxWidth - rawWidth;
230
- maxViolators.push(f);
231
- } else {
232
- f.computedWidth = rawWidth;
233
- }
234
- });
235
-
236
- if (minViolators.length === 0 && maxViolators.length === 0) {
237
- unfrozen.forEach(f => f.frozen = true);
238
- break;
239
- }
240
-
241
- if (totalViolation > 0) {
242
- minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
243
- } else if (totalViolation < 0) {
244
- maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
245
- } else {
246
- minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
247
- maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
248
- }
249
- }
250
- };
251
-
252
- solveFlexAllocation();
253
- flexItems.forEach(f => finalFlexWidths.set(f.col.field, f.computedWidth));
254
-
255
- const computedWidthMap = new Map<string, number>();
256
- unpinnedCols.forEach(col => {
257
- let computedWidth: number;
258
- if (finalFlexWidths.has(col.field)) {
259
- computedWidth = finalFlexWidths.get(col.field)!;
260
- } else {
261
- const fixedCol = fixedWidthCols.find(f => f.col.field === col.field);
262
- const pctCol = percentageCols.find(p => p.col.field === col.field);
263
- if (fixedCol) {
264
- computedWidth = fixedCol.width;
265
- } else if (pctCol) {
266
- const rawPctWidth = percentageWidthMap.get(col.field) || 0;
267
- computedWidth = Math.max(col.minWidth ?? 50, rawPctWidth || 100);
268
- } else {
269
- computedWidth = 100;
270
- }
271
- }
272
- unpinnedColWidths.push(computedWidth);
273
- computedWidthMap.set(col.field, computedWidth);
274
- });
275
-
276
- const unpinnedColsWithWidth = unpinnedCols.map(col => {
277
- const flexItem = flexItems.find(f => f.col.field === col.field);
278
- return {
279
- ...col,
280
- width: computedWidthMap.get(col.field) ?? (typeof col.width === 'number' ? col.width : 100),
281
- flex: flexItem ? flexItem.flex : col.flex
282
- };
293
+ const widthOf = (col: GridColDef<R>) => widths.get(col.field) ?? 100;
294
+ const withWidth = (col: GridColDef<R>) => ({
295
+ ...col,
296
+ width: widthOf(col),
297
+ flex: flexByField.get(col.field) ?? col.flex,
283
298
  });
284
299
 
285
300
  const baseLeftZ = 11;
286
- const leftPinnedCols = rawLeftPinnedCols.map((col, i) => {
287
- const parsed = parseWidth(getColWidth(col));
288
- return { ...col, width: parsed.type === 'fixed' ? parsed.value : 100, zIndex: baseLeftZ + i };
289
- });
301
+ const leftPinnedCols = rawLeftPinnedCols.map((col, i) => ({ ...withWidth(col), zIndex: baseLeftZ + i }));
290
302
 
291
303
  const baseRightZ = 11;
292
- const rightPinnedCols = rawRightPinnedCols.map((col, i) => {
293
- const parsed = parseWidth(getColWidth(col));
294
- return {
295
- ...col,
296
- width: parsed.type === 'fixed' ? parsed.value : 100,
297
- zIndex: baseRightZ + (rawRightPinnedCols.length - 1 - i)
298
- };
299
- });
304
+ const rightPinnedCols = rawRightPinnedCols.map((col, i) => ({
305
+ ...withWidth(col),
306
+ zIndex: baseRightZ + (rawRightPinnedCols.length - 1 - i),
307
+ }));
308
+
309
+ const unpinnedColsWithWidth = unpinnedCols.map(withWidth);
310
+ const unpinnedColWidths = unpinnedColsWithWidth.map(col => col.width);
300
311
 
312
+ const leftWidth = leftPinnedCols.reduce((sum, col) => sum + col.width, 0);
313
+ const rightWidth = rightPinnedCols.reduce((sum, col) => sum + col.width, 0);
301
314
  const unpinnedTotalWidth = unpinnedColWidths.reduce((sum, w) => sum + w, 0);
302
315
  const totalWidth = leftWidth + unpinnedTotalWidth + rightWidth + systemColumnsWidth;
303
316
  const unpinnedAccWidths = unpinnedColWidths.reduce((acc, w, i) => {
@@ -321,6 +334,7 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
321
334
  rightWidth,
322
335
  unpinnedTotalWidth,
323
336
  systemColumnsWidth,
337
+ pinnedSystemColumnsWidth,
324
338
  pinnedTopRowsLength,
325
339
  pinnedBottomRowsLength,
326
340
  unpinnedRowsLength: unpinnedRows.length,
@@ -332,8 +346,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
332
346
  sortedUnpinnedRows,
333
347
  expandedRowIds,
334
348
  getDetailPanelHeight,
349
+ detailPanelHeights,
335
350
  pinnedTopRowsLength,
336
351
  pinnedBottomRowsLength,
352
+ pinnedTopRows,
353
+ pinnedBottomRows,
337
354
  visibleOrderedColumns,
338
355
  pinnedColumns,
339
356
  columnWidths,
@@ -341,8 +358,11 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
341
358
  checkboxSelection,
342
359
  hasDetailPanel,
343
360
  rowReordering,
361
+ pinCheckboxColumn,
362
+ pinExpandColumn,
344
363
  paginationMode,
345
364
  isLoading,
346
365
  pageSize,
366
+ getRowId,
347
367
  ]);
348
368
  }
@@ -0,0 +1,41 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import type React from 'react';
3
+ import type { GridRowScrollEndParams } from '../../types';
4
+ import { getRowsScrollEndParams } from '../../utils/scroll/scrollEnd';
5
+
6
+ /**
7
+ * A scroll handler that calls `onRowsScrollEnd` once per arrival near the end of a row container.
8
+ * It re-arms when the container leaves the threshold or `rowCount` changes (new rows loaded), so a
9
+ * consumer that loads the next page from the callback is not flooded with calls while it loads.
10
+ * The grid viewport has the same rule built into useGridScrollSync; the list view uses this hook.
11
+ */
12
+ export function useRowsScrollEndLatch(
13
+ onRowsScrollEnd: ((params: GridRowScrollEndParams) => void) | undefined,
14
+ rowCount: number,
15
+ ): ((event: React.UIEvent<HTMLElement>) => void) | undefined {
16
+ const armedRef = useRef(true);
17
+ const callbackRef = useRef(onRowsScrollEnd);
18
+
19
+ useEffect(() => {
20
+ callbackRef.current = onRowsScrollEnd;
21
+ }, [onRowsScrollEnd]);
22
+
23
+ useEffect(() => {
24
+ armedRef.current = true;
25
+ }, [rowCount]);
26
+
27
+ const handleScroll = useCallback((event: React.UIEvent<HTMLElement>) => {
28
+ const params = getRowsScrollEndParams(event.currentTarget);
29
+ if (!params) {
30
+ armedRef.current = true;
31
+ return;
32
+ }
33
+ if (!armedRef.current) return;
34
+ const callback = callbackRef.current;
35
+ if (!callback) return;
36
+ armedRef.current = false;
37
+ callback(params);
38
+ }, []);
39
+
40
+ return onRowsScrollEnd ? handleScroll : undefined;
41
+ }
@@ -0,0 +1,34 @@
1
+ import { useState } from 'react';
2
+
3
+ function shallowEqualObjects(a: object, b: object): boolean {
4
+ if (a === b) return true;
5
+ const aKeys = Object.keys(a);
6
+ if (aKeys.length !== Object.keys(b).length) return false;
7
+ const aRecord = a as Record<string, unknown>;
8
+ const bRecord = b as Record<string, unknown>;
9
+ return aKeys.every(key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(aRecord[key], bRecord[key]));
10
+ }
11
+
12
+ /** Same length, and each column has the same own properties with identical values (functions by identity). */
13
+ export function columnsShallowEqual<C extends object>(a: readonly C[], b: readonly C[]): boolean {
14
+ if (a === b) return true;
15
+ if (a.length !== b.length) return false;
16
+ return a.every((col, index) => shallowEqualObjects(col, b[index]));
17
+ }
18
+
19
+ /**
20
+ * The `columns` prop with a stable identity: while each column definition is shallowly equal to
21
+ * the previous one (same properties, same values, callbacks compared by identity), the previous
22
+ * array is returned. An inline `columns={[...]}` (a new but equal array on every parent render)
23
+ * then does not re-run the filter, sort, aggregation, grouping and spanning passes, which are all
24
+ * memoised on column identity. A changed property, including a new inline callback, is a new array.
25
+ *
26
+ * The previous array is held in state and replaced during render (React's "adjust state when a
27
+ * prop changes" pattern), so no ref is written during render.
28
+ */
29
+ export function useStableColumns<T extends readonly object[]>(columns: T): T {
30
+ const [stable, setStable] = useState(columns);
31
+ const same = columnsShallowEqual(stable, columns);
32
+ if (!same) setStable(columns);
33
+ return same ? stable : columns;
34
+ }