@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
@@ -0,0 +1,115 @@
1
+ import type { GridRowId } from '../../types';
2
+
3
+ /** Synthetic navigation columns rendered before the data columns, in render order. */
4
+ export const REORDER_FIELD = '__reorder_col__';
5
+ export const EXPAND_FIELD = '__expand_col__';
6
+ export const CHECKBOX_FIELD = '__checkbox_col__';
7
+
8
+ export const isSystemField = (field: string): boolean =>
9
+ field === REORDER_FIELD || field === EXPAND_FIELD || field === CHECKBOX_FIELD;
10
+
11
+ /** The grid's logical focus position. */
12
+ export interface FocusedCell {
13
+ /** Row id of the focused body cell, or `null` when a column header cell is focused. */
14
+ id: GridRowId | null;
15
+ field: string;
16
+ /** Index in the renderable rows when focus was set; used to pick a neighbour if the row disappears. */
17
+ rowIndex?: number;
18
+ }
19
+
20
+ export const isSameCell = (a: FocusedCell | null, b: FocusedCell | null): boolean =>
21
+ a === b || (a !== null && b !== null && a.id === b.id && a.field === b.field);
22
+
23
+ /**
24
+ * Where a keydown came from, from the grid's point of view:
25
+ * - `grid`: the viewport or a cell / header cell itself — the grid handles the key;
26
+ * - `control`: a button, link or checkbox inside a cell — navigation keys move grid focus, but
27
+ * Enter and Space are left to the control;
28
+ * - `editor`: inside the cell being edited;
29
+ * - `foreign`: anything the grid must leave alone — text inputs rendered in cells, detail-panel
30
+ * content, portals such as the column menu.
31
+ */
32
+ export type GridKeyTarget = 'grid' | 'control' | 'editor' | 'foreign';
33
+
34
+ const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'color']);
35
+ const TEXT_ROLES = new Set(['textbox', 'searchbox', 'combobox', 'listbox', 'slider', 'spinbutton']);
36
+
37
+ /** True for elements that use typing or arrow keys themselves. */
38
+ export function isTextEntryElement(el: Element): boolean {
39
+ const tag = el.tagName;
40
+ if (tag === 'INPUT') return !NON_TEXT_INPUT_TYPES.has((el as HTMLInputElement).type);
41
+ if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
42
+ if ((el as HTMLElement).isContentEditable || el.closest('[contenteditable]:not([contenteditable="false"])')) return true;
43
+ const role = el.getAttribute('role');
44
+ return role !== null && TEXT_ROLES.has(role);
45
+ }
46
+
47
+ const isElement = (target: EventTarget | null): target is Element =>
48
+ target !== null && (target as Node).nodeType === 1;
49
+
50
+ /** The cell or header cell of `viewport` (not of a grid nested inside it) that contains `el`, if any. */
51
+ function owningCell(el: Element, viewport: HTMLElement): HTMLElement | null {
52
+ const panel = el.closest('.ogx__detail-panel');
53
+ if (panel && viewport.contains(panel)) return null;
54
+ const cell = el.closest<HTMLElement>('[role="gridcell"], [role="columnheader"]');
55
+ if (!cell || cell.closest('.ogx__viewport') !== viewport) return null;
56
+ return cell;
57
+ }
58
+
59
+ export function classifyKeyTarget(target: EventTarget | null, viewport: HTMLElement): GridKeyTarget {
60
+ if (target === viewport) return 'grid';
61
+ if (!isElement(target) || !viewport.contains(target)) return 'foreign';
62
+ const cell = owningCell(target, viewport);
63
+ if (!cell) return 'foreign';
64
+ if (cell.classList.contains('ogx__cell--editing')) return 'editor';
65
+ if (cell === target) return 'grid';
66
+ if (isTextEntryElement(target)) return 'foreign';
67
+ // The select-all checkbox input is the header cell's own focus target.
68
+ if (cell.classList.contains('ogx__header-cell--checkbox')) return 'grid';
69
+ return 'control';
70
+ }
71
+
72
+ /** Resolves the cell that holds `target` to a focus position, or `null` when it is not in a cell. */
73
+ export function focusedCellFromElement<T>(
74
+ target: EventTarget | null,
75
+ viewport: HTMLElement,
76
+ rows: ReadonlyArray<T>,
77
+ getRowId: (row: T) => GridRowId
78
+ ): FocusedCell | null {
79
+ if (!isElement(target)) return null;
80
+ const cell = owningCell(target, viewport);
81
+ const field = cell?.dataset.field;
82
+ if (!cell || !field) return null;
83
+ if (cell.getAttribute('role') === 'columnheader') {
84
+ return cell.parentElement?.classList.contains('ogx__header') ? { id: null, field } : null;
85
+ }
86
+ const rowIndex = Number(cell.parentElement?.getAttribute('data-rowindex'));
87
+ const row = Number.isInteger(rowIndex) ? rows[rowIndex] : undefined;
88
+ return row ? { id: getRowId(row), field, rowIndex } : null;
89
+ }
90
+
91
+ function childWithField(parent: Element | null, field: string): HTMLElement | null {
92
+ if (!parent) return null;
93
+ for (const child of Array.from(parent.children)) {
94
+ if ((child as HTMLElement).dataset.field === field) return child as HTMLElement;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /** The DOM element that should hold focus for `cell`, or `null` while it is not rendered. */
100
+ export function findFocusTarget(viewport: HTMLElement, cell: FocusedCell, rowIndex: number): HTMLElement | null {
101
+ if (cell.id === null) {
102
+ // The header sits in the sticky top block together with the top-pinned rows.
103
+ const header = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-top > .ogx__header-wrap > .ogx__header');
104
+ const el = childWithField(header, cell.field);
105
+ if (el && cell.field === CHECKBOX_FIELD) return el.querySelector('input') ?? el;
106
+ return el;
107
+ }
108
+ if (rowIndex < 0) return null;
109
+ const row = viewport.querySelector(
110
+ `:scope > .ogx__content > .ogx__virtual-container > .ogx__rows > [data-rowindex="${rowIndex}"],` +
111
+ `:scope > .ogx__content > .ogx__sticky-top > .ogx__pinned-rows > [data-rowindex="${rowIndex}"],` +
112
+ `:scope > .ogx__content > .ogx__sticky-bottom > .ogx__pinned-rows > [data-rowindex="${rowIndex}"]`
113
+ );
114
+ return childWithField(row, cell.field);
115
+ }
@@ -0,0 +1,35 @@
1
+ import type { GridRowId, GridRowMeta, GridRowModel } from '../types';
2
+
3
+ export interface CollectAllFilteredRowsParams<R extends GridRowModel> {
4
+ /** The active tree-data / row-grouping handlers, or null when the rows are flat. */
5
+ hierarchyHandlers: { getVisibleRows: (options?: { expandAll?: boolean; labelField?: string }) => R[] | null } | null;
6
+ /** The column that shows the hierarchy, so sorting by it orders the rows as on screen. */
7
+ hierarchyField?: string;
8
+ pinnedTopRows: R[];
9
+ /** Every unpinned row that passes the filter, sorted, across all pages. */
10
+ sortedUnpinnedRows: R[];
11
+ pinnedBottomRows: R[];
12
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
13
+ getRowId: (row: GridRowModel) => GridRowId;
14
+ /**
15
+ * With a hierarchy: the rows that pass the filter (the set select-all and the footer use).
16
+ * Tree-data ancestors shown only to give a match its context are left out. Omitted: every
17
+ * non-synthetic row of the expanded hierarchy is returned.
18
+ */
19
+ dataRows?: readonly R[];
20
+ }
21
+
22
+ /**
23
+ * `GridApi.getAllFilteredRows()`: every data row that passes the filter, in display order,
24
+ * ignoring pagination and hierarchy expansion. Flat rows keep their pinned rows; with a
25
+ * hierarchy every group is expanded, and synthetic group rows and tree-data ancestors that do
26
+ * not match the filter themselves (shown only for context) are left out, so the result is the
27
+ * set select-all and the aggregation footer act on.
28
+ */
29
+ export function collectAllFilteredRows<R extends GridRowModel>(params: CollectAllFilteredRowsParams<R>): R[] {
30
+ const { hierarchyHandlers, pinnedTopRows, sortedUnpinnedRows, pinnedBottomRows, rowMetaMap, getRowId, hierarchyField, dataRows } = params;
31
+ const hierarchyRows = hierarchyHandlers?.getVisibleRows({ expandAll: true, labelField: hierarchyField });
32
+ if (!hierarchyRows) return [...pinnedTopRows, ...sortedUnpinnedRows, ...pinnedBottomRows];
33
+ const matching = dataRows ? new Set<R>(dataRows) : null;
34
+ return hierarchyRows.filter(row => !rowMetaMap.get(getRowId(row))?.isGroupRow && (!matching || matching.has(row)));
35
+ }
@@ -0,0 +1,77 @@
1
+ import type { GridRowGroupingModel } from '../types';
2
+
3
+ /**
4
+ * Stable empty grouping model: used when `rowGroupingModel` is not passed and while pivot mode
5
+ * is active. It must keep its identity across renders, because the hierarchy hooks memoize on it.
6
+ */
7
+ export const EMPTY_ROW_GROUPING_MODEL: GridRowGroupingModel = [];
8
+
9
+ export interface ResolveGridModesParams {
10
+ /** `rowGroupingModel` prop (or `EMPTY_ROW_GROUPING_MODEL`). */
11
+ rowGroupingModel: GridRowGroupingModel;
12
+ /** `treeData` prop. */
13
+ treeData: boolean;
14
+ /** Whether `getTreeDataPath` was passed. */
15
+ hasTreeDataPath: boolean;
16
+ /** Whether the pivot is producing the rows (`useGridPivot().isActive`). */
17
+ isPivotActive: boolean;
18
+ /** `rowReordering` prop. */
19
+ rowReordering: boolean;
20
+ /** Whether `groupingColDef` was passed. */
21
+ hasGroupingColDef: boolean;
22
+ /** `pagination` prop. */
23
+ pagination: boolean;
24
+ paginationMode: 'client' | 'server' | 'infinite';
25
+ /** `listView` prop. */
26
+ listView: boolean;
27
+ /** Whether `listViewColumn` was passed. */
28
+ hasListViewColumn: boolean;
29
+ }
30
+
31
+ export interface GridModes {
32
+ /** Row drag-reorder is on (never over generated pivot rows). */
33
+ rowReordering: boolean;
34
+ /** The grouping model the hierarchy hooks see: empty while pivoting. */
35
+ hierarchyRowGroupingModel: GridRowGroupingModel;
36
+ /** `treeData` asked for and not overridden by the pivot. */
37
+ isTreeDataRequested: boolean;
38
+ /** Tree data is built: requested and `getTreeDataPath` is present. */
39
+ isTreeData: boolean;
40
+ isRowGrouping: boolean;
41
+ isHierarchyEnabled: boolean;
42
+ /** The synthetic `__group__` column is shown. */
43
+ hasGroupingColumn: boolean;
44
+ /** Pagination is on: requested, not grouping, and not infinite scroll. */
45
+ pagination: boolean;
46
+ /** List view is shown: requested and `listViewColumn` is present. */
47
+ listView: boolean;
48
+ }
49
+
50
+ /**
51
+ * Which grid features are in effect, from the props that request them. Features override each
52
+ * other here and nowhere else:
53
+ * - pivot rows are generated, so row-reorder indices over them would mean nothing to the consumer;
54
+ * - pivot rows are already grouped by the pivot row fields, so tree data and row grouping would regroup them;
55
+ * - without `getTreeDataPath` there is no tree to build: the rows are shown flat (useTreeData warns);
56
+ * - infinite scroll loads rows as the user scrolls: it has no pages to show or slice;
57
+ * - list view needs a `listViewColumn` to render its items; without one the grid view is shown
58
+ * (useGridDevWarnings says so) instead of an empty container.
59
+ */
60
+ export function resolveGridModes(params: ResolveGridModesParams): GridModes {
61
+ const { isPivotActive } = params;
62
+ const hierarchyRowGroupingModel = isPivotActive ? EMPTY_ROW_GROUPING_MODEL : params.rowGroupingModel;
63
+ const isTreeDataRequested = params.treeData && !isPivotActive;
64
+ const isTreeData = isTreeDataRequested && params.hasTreeDataPath;
65
+ const isRowGrouping = hierarchyRowGroupingModel.length > 0;
66
+ return {
67
+ rowReordering: params.rowReordering && !isPivotActive,
68
+ hierarchyRowGroupingModel,
69
+ isTreeDataRequested,
70
+ isTreeData,
71
+ isRowGrouping,
72
+ isHierarchyEnabled: isTreeData || isRowGrouping,
73
+ hasGroupingColumn: params.hasGroupingColDef && (isRowGrouping || isTreeData),
74
+ pagination: params.pagination && !isRowGrouping && params.paginationMode !== 'infinite',
75
+ listView: params.listView && params.hasListViewColumn,
76
+ };
77
+ }
@@ -0,0 +1,91 @@
1
+ import type {
2
+ GridAggregationModel,
3
+ GridAggregationResult,
4
+ GridColDef,
5
+ GridGroupedExportRow,
6
+ GridRowId,
7
+ GridRowMeta,
8
+ GridRowModel,
9
+ } from '../../types';
10
+ import { computeAggregations } from '../aggregation';
11
+
12
+ export interface BuildGroupedExportRowsParams<R extends GridRowModel> {
13
+ /**
14
+ * The grouped rows in display order with every group expanded: group rows followed by
15
+ * their filtered, sorted descendants (useRowGrouping's getVisibleRows({ expandAll: true })).
16
+ */
17
+ rows: R[];
18
+ getRowId: (row: R) => GridRowId;
19
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
20
+ columns: GridColDef<R>[];
21
+ aggregationModel: GridAggregationModel;
22
+ /** The footer's grand total (already over the filtered data rows); omitted when null. */
23
+ aggregationResult: GridAggregationResult | null;
24
+ /** Groups whose subtotal is hidden (getAggregationPosition returned null for them); no subtotal is written. */
25
+ isSubtotalHidden?: (groupId: GridRowId) => boolean;
26
+ }
27
+
28
+ interface OpenGroup<R> {
29
+ id: GridRowId;
30
+ header: GridGroupedExportRow;
31
+ leaves: R[];
32
+ }
33
+
34
+ /**
35
+ * Flattens the grouping tree for export: group-header, its leaves and nested groups, then
36
+ * its subtotal, and a grand total at the end. The collapsed/expanded state on screen does
37
+ * not matter, and subtotals are computed over the group's filtered leaves (the rows the
38
+ * export contains), so every subtotal agrees with the rows above it.
39
+ */
40
+ export function buildGroupedExportRows<R extends GridRowModel>(params: BuildGroupedExportRowsParams<R>): GridGroupedExportRow[] {
41
+ const { rows, getRowId, rowMetaMap, columns, aggregationModel, aggregationResult, isSubtotalHidden } = params;
42
+ const hasAggregation = Object.keys(aggregationModel).length > 0;
43
+ const columnsLookup = new Map(columns.map(col => [col.field, col]));
44
+
45
+ const result: GridGroupedExportRow[] = [];
46
+ const open: OpenGroup<R>[] = [];
47
+
48
+ const closeGroupsFrom = (depth: number) => {
49
+ while (open.length > 0 && (open[open.length - 1].header.depth >= depth)) {
50
+ const group = open.pop()!;
51
+ if (hasAggregation && !isSubtotalHidden?.(group.id)) {
52
+ result.push({
53
+ type: 'group-subtotal',
54
+ depth: group.header.depth,
55
+ groupField: group.header.groupField,
56
+ groupValue: group.header.groupValue,
57
+ groupLabel: group.header.groupLabel,
58
+ aggregatedValues: computeAggregations(group.leaves, aggregationModel, columnsLookup, 'getGroupedExportRows'),
59
+ });
60
+ }
61
+ }
62
+ };
63
+
64
+ for (const row of rows) {
65
+ const id = getRowId(row);
66
+ const meta = rowMetaMap.get(id);
67
+ if (meta?.isGroupFooter) continue;
68
+ if (meta?.isGroupRow) {
69
+ const depth = meta.treeDepth ?? 0;
70
+ closeGroupsFrom(depth);
71
+ const header: GridGroupedExportRow = {
72
+ type: 'group-header',
73
+ depth,
74
+ groupField: meta.groupingField,
75
+ groupValue: meta.groupingValue,
76
+ groupLabel: meta.groupLabel,
77
+ };
78
+ result.push(header);
79
+ open.push({ id, header, leaves: [] });
80
+ } else {
81
+ result.push({ type: 'leaf', depth: meta?.treeDepth ?? open.length, row });
82
+ open.forEach(group => group.leaves.push(row));
83
+ }
84
+ }
85
+ closeGroupsFrom(0);
86
+
87
+ if (hasAggregation && aggregationResult) {
88
+ result.push({ type: 'grand-total', depth: 0, aggregatedValues: aggregationResult });
89
+ }
90
+ return result;
91
+ }
@@ -0,0 +1,56 @@
1
+ import type { GridLocaleText } from '../../types';
2
+
3
+ /**
4
+ * Page-size and page-index arithmetic shared by the row pipeline, the pager and the list view,
5
+ * so all three agree on which page is shown.
6
+ */
7
+
8
+ /** A usable page size: a whole number of at least 1. A size of 0, a negative size or NaN would divide by zero. */
9
+ export function normalizePageSize(pageSize: number): number {
10
+ return pageSize >= 1 ? Math.floor(pageSize) : 1;
11
+ }
12
+
13
+ /** The number of pages needed for `rowCount` rows; at least 1, so an empty grid still has page 1. */
14
+ export function getPageCount(rowCount: number, pageSize: number): number {
15
+ return Math.max(1, Math.ceil(Math.max(0, rowCount) / normalizePageSize(pageSize)));
16
+ }
17
+
18
+ export interface GetPaginationRowCountParams {
19
+ paginationMode: 'client' | 'server' | 'infinite';
20
+ /** Whether a `dataSource` is set: its response's `rowCount` then wins over the prop. */
21
+ hasDataSource: boolean;
22
+ /** The server total from the last `dataSource` response, if any. */
23
+ dataSourceRowCount: number | null | undefined;
24
+ /** The `rowCount` prop. */
25
+ rowCountProp: number | undefined;
26
+ /** The filtered, unpinned rows the grid holds. */
27
+ clientRowCount: number;
28
+ }
29
+
30
+ /**
31
+ * What the pager pages through. Client: the filtered, unpinned rows (pinned rows show on every
32
+ * page). Server: the server total, from the dataSource response or the rowCount prop, falling
33
+ * back to the rows the grid holds.
34
+ */
35
+ export function getPaginationRowCount(params: GetPaginationRowCountParams): number {
36
+ const { paginationMode, hasDataSource, dataSourceRowCount, rowCountProp, clientRowCount } = params;
37
+ if (paginationMode !== 'server') return clientRowCount;
38
+ return (hasDataSource ? dataSourceRowCount ?? rowCountProp : rowCountProp) ?? clientRowCount;
39
+ }
40
+
41
+ /** The pager's strings out of the grid's `localeText` (undefined when there is none). */
42
+ export function pickPaginationLocaleText(
43
+ localeText: GridLocaleText | undefined
44
+ ): Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'> | undefined {
45
+ return localeText ? {
46
+ paginationRowsPerPage: localeText.paginationRowsPerPage,
47
+ paginationOf: localeText.paginationOf,
48
+ paginationPage: localeText.paginationPage,
49
+ } : undefined;
50
+ }
51
+
52
+ /** `page` limited to the valid range `0 … pageCount - 1`. */
53
+ export function clampPage(page: number, pageCount: number): number {
54
+ if (!Number.isFinite(page)) return 0;
55
+ return Math.min(Math.max(0, Math.floor(page)), Math.max(0, pageCount - 1));
56
+ }
@@ -1,5 +1,9 @@
1
1
 
2
2
  import type { GridColDef, GridRowModel, GridColumnPinning, GridRowPinning, GridPinnedPosition, GridRowId } from '../../types';
3
+ import { getRenderedColumnWidth } from '../columnWidth';
4
+
5
+ /** Width of each built-in system column (drag handle, detail-panel toggle, checkbox). */
6
+ export const SYSTEM_COLUMN_WIDTH = 48;
3
7
 
4
8
  export function isColumnPinned(field: string, pinnedColumns?: GridColumnPinning): GridPinnedPosition | null {
5
9
  if (!pinnedColumns) return null;
@@ -15,6 +19,11 @@ export function isColumnPinned(field: string, pinnedColumns?: GridColumnPinning)
15
19
  return null;
16
20
  }
17
21
 
22
+ /**
23
+ * Splits columns into left-pinned, unpinned and right-pinned groups. The unpinned group keeps
24
+ * column order; the pinned groups follow the order of `pinnedColumns.left` / `.right`, which is
25
+ * the order the grid renders them in.
26
+ */
18
27
  export function getPinnedColumnGroups<R extends GridRowModel = GridRowModel>(
19
28
  columns: GridColDef<R>[],
20
29
  pinnedColumns?: GridColumnPinning
@@ -62,6 +71,12 @@ export function getPinnedColumnGroups<R extends GridRowModel = GridRowModel>(
62
71
  return { left, center, right };
63
72
  }
64
73
 
74
+ /**
75
+ * Sticky `left` / `right` offsets of the pinned columns, in pixels. Offsets follow the order of
76
+ * `columns`, which must be the order the cells are rendered in (the grid renders pinned columns
77
+ * in `pinnedColumns.left` / `.right` order), and use each column's rendered width (see
78
+ * `getRenderedColumnWidth`). Left offsets start after the pinned system columns.
79
+ */
65
80
  export function calculatePinnedPositions<R extends GridRowModel = GridRowModel>(
66
81
  columns: GridColDef<R>[],
67
82
  columnWidths: Record<string, number>,
@@ -76,39 +91,52 @@ export function calculatePinnedPositions<R extends GridRowModel = GridRowModel>(
76
91
 
77
92
  if (!pinnedColumns) return positions;
78
93
 
79
- const groups = getPinnedColumnGroups(columns, pinnedColumns);
80
-
81
- let leftOffset = 0;
82
-
83
- if (rowReordering) {
84
- leftOffset += 48;
85
- }
86
-
87
- if (hasDetailPanel && pinExpandColumn) {
88
- leftOffset += 48;
89
- }
90
-
91
- if (checkboxSelection && pinCheckboxColumn) {
92
- leftOffset += 48;
93
- }
94
+ let leftOffset =
95
+ (rowReordering ? SYSTEM_COLUMN_WIDTH : 0) +
96
+ (hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0) +
97
+ (checkboxSelection && pinCheckboxColumn ? SYSTEM_COLUMN_WIDTH : 0);
98
+ const right: GridColDef<R>[] = [];
94
99
 
95
- groups.left.forEach(col => {
96
- positions[col.field] = leftOffset;
97
- const width = columnWidths[col.field] ?? col.width ?? 100;
98
- leftOffset += width;
100
+ columns.forEach(col => {
101
+ if (col.isSpacer) return;
102
+ const side = isColumnPinned(col.field, pinnedColumns);
103
+ if (side === 'left') {
104
+ positions[col.field] = leftOffset;
105
+ leftOffset += getRenderedColumnWidth(col, columnWidths);
106
+ } else if (side === 'right') {
107
+ right.push(col);
108
+ }
99
109
  });
100
110
 
101
111
  let rightOffset = 0;
102
-
103
- [...groups.right].reverse().forEach(col => {
104
- const width = columnWidths[col.field] ?? col.width ?? 100;
105
- positions[col.field] = rightOffset;
106
- rightOffset += width;
107
- });
112
+ for (let i = right.length - 1; i >= 0; i--) {
113
+ positions[right[i].field] = rightOffset;
114
+ rightOffset += getRenderedColumnWidth(right[i], columnWidths);
115
+ }
108
116
 
109
117
  return positions;
110
118
  }
111
119
 
120
+ /**
121
+ * The last rendered left-pinned column and the first rendered right-pinned column: the cells that
122
+ * carry the pinned-section edge classes. `columns` is in render order; spacers are skipped.
123
+ */
124
+ export function getPinnedEdgeFields<R extends GridRowModel = GridRowModel>(
125
+ columns: GridColDef<R>[],
126
+ pinnedColumns?: GridColumnPinning
127
+ ): { lastLeft: string | null; firstRight: string | null } {
128
+ let lastLeft: string | null = null;
129
+ let firstRight: string | null = null;
130
+ if (!pinnedColumns) return { lastLeft, firstRight };
131
+ for (const col of columns) {
132
+ if (col.isSpacer) continue;
133
+ const side = isColumnPinned(col.field, pinnedColumns);
134
+ if (side === 'left') lastLeft = col.field;
135
+ else if (side === 'right' && firstRight === null) firstRight = col.field;
136
+ }
137
+ return { lastLeft, firstRight };
138
+ }
139
+
112
140
  export function isColumnPinnable<R extends GridRowModel = GridRowModel>(
113
141
  colDef: GridColDef<R>
114
142
  ): boolean {
@@ -166,8 +194,7 @@ export function getPinnedColumnsWidth(
166
194
  const col = columns.find(c => c.field === field);
167
195
  if (!col) return total;
168
196
 
169
- const width = columnWidths[field] ?? col.width ?? 100;
170
- return total + width;
197
+ return total + getRenderedColumnWidth(col, columnWidths);
171
198
  }, 0);
172
199
  }
173
200
 
@@ -190,7 +217,8 @@ export function isRowPinned(
190
217
 
191
218
  export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
192
219
  rows: R[],
193
- pinnedRows?: GridRowPinning
220
+ pinnedRows?: GridRowPinning,
221
+ getRowId: (row: R) => GridRowId = (row) => row.id
194
222
  ): {
195
223
  top: R[];
196
224
  center: R[];
@@ -205,7 +233,7 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
205
233
  }
206
234
 
207
235
  rows.forEach(row => {
208
- const pinnedPosition = isRowPinned(row.id, pinnedRows);
236
+ const pinnedPosition = isRowPinned(getRowId(row), pinnedRows);
209
237
 
210
238
  if (pinnedPosition === 'top') {
211
239
  top.push(row);
@@ -218,16 +246,16 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
218
246
 
219
247
  if (pinnedRows.top) {
220
248
  top.sort((a, b) => {
221
- const aIndex = pinnedRows.top!.indexOf(a.id);
222
- const bIndex = pinnedRows.top!.indexOf(b.id);
249
+ const aIndex = pinnedRows.top!.indexOf(getRowId(a));
250
+ const bIndex = pinnedRows.top!.indexOf(getRowId(b));
223
251
  return aIndex - bIndex;
224
252
  });
225
253
  }
226
254
 
227
255
  if (pinnedRows.bottom) {
228
256
  bottom.sort((a, b) => {
229
- const aIndex = pinnedRows.bottom!.indexOf(a.id);
230
- const bIndex = pinnedRows.bottom!.indexOf(b.id);
257
+ const aIndex = pinnedRows.bottom!.indexOf(getRowId(a));
258
+ const bIndex = pinnedRows.bottom!.indexOf(getRowId(b));
231
259
  return aIndex - bIndex;
232
260
  });
233
261
  }
@@ -280,3 +308,19 @@ export function getPinnedRowsCount(
280
308
  const pinnedIds = position === 'top' ? pinnedRows.top : pinnedRows.bottom;
281
309
  return pinnedIds?.length || 0;
282
310
  }
311
+
312
+ /**
313
+ * The column pinning model after the column menu pins `field` to `side` (appended after the
314
+ * columns already pinned there) or unpins it (`null`). The field is removed from the other side.
315
+ */
316
+ export function pinColumnTo(
317
+ pinnedColumns: GridColumnPinning | undefined,
318
+ field: string,
319
+ side: GridPinnedPosition | null
320
+ ): GridColumnPinning {
321
+ const cleanLeft = (pinnedColumns?.left ?? []).filter(f => f !== field);
322
+ const cleanRight = (pinnedColumns?.right ?? []).filter(f => f !== field);
323
+ if (side === 'left') return { left: [...cleanLeft, field], right: cleanRight };
324
+ if (side === 'right') return { left: cleanLeft, right: [...cleanRight, field] };
325
+ return { left: cleanLeft, right: cleanRight };
326
+ }