@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,44 +1,62 @@
1
1
  import { useRef, useState, useCallback, useEffect } from 'react';
2
2
  import type { GridRowScrollEndParams } from '../../types';
3
+ import { getRowsScrollEndParams } from '../../utils/scroll/scrollEnd';
3
4
 
4
5
  export interface UseGridScrollSyncParams {
5
6
  onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
6
7
  /** Minimum overscan rows — the adaptive algorithm never goes below this. Defaults to 3. */
7
8
  overscanRowCount?: number;
9
+ /**
10
+ * Number of scrolling (center) rows. A change re-arms `onRowsScrollEnd` and re-checks whether
11
+ * the end of the rows is in view (e.g. the first page does not fill the viewport).
12
+ */
13
+ rowCount?: number;
14
+ /** The viewport grows to its content and never scrolls itself: only scroll events can signal the end. */
15
+ autoHeight?: boolean;
8
16
  }
9
17
 
10
18
  export interface UseGridScrollSyncResult {
11
19
  scrollTop: number;
12
20
  scrollLeft: number;
13
- /** Dynamically computed overscan row count based on current scroll velocity. */
21
+ /** Overscan row count: the velocity-based value, never below `overscanRowCount`. */
14
22
  overscanRows: number;
15
23
  handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
24
+ /**
25
+ * Callback ref for the viewport element. When the viewport remounts (list view switched off
26
+ * again), the last scroll position is restored onto the new element so the render window and
27
+ * the DOM agree.
28
+ */
29
+ attachViewport: (element: HTMLDivElement | null) => void;
16
30
  }
17
31
 
18
32
  /** Maps scroll velocity (px/ms) to an overscan row count. */
19
- function velocityToOverscan(velocity: number, minOverscan: number): number {
20
- let base: number;
21
- if (velocity < 0.5) base = 3;
22
- else if (velocity < 3) base = 5;
23
- else if (velocity < 15) base = 12;
24
- else if (velocity < 40) base = 20;
25
- else base = 30;
26
- return Math.max(base, minOverscan);
33
+ function velocityToOverscan(velocity: number): number {
34
+ if (velocity < 0.5) return 3;
35
+ if (velocity < 3) return 5;
36
+ if (velocity < 15) return 12;
37
+ if (velocity < 40) return 20;
38
+ return 30;
27
39
  }
28
40
 
29
41
  export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrollSyncResult {
30
- const { onRowsScrollEnd, overscanRowCount = 3 } = params;
42
+ const { onRowsScrollEnd, overscanRowCount = 3, rowCount = 0, autoHeight = false } = params;
31
43
 
32
44
  const pendingRef = useRef({ scrollTop: 0, scrollLeft: 0, velocity: 0 });
33
45
  const prevRef = useRef({ scrollTop: 0, scrollLeft: 0, time: 0 });
34
46
  const rafRef = useRef<number | null>(null);
35
47
  const decayRef = useRef<ReturnType<typeof setTimeout> | null>(null);
48
+ const elementRef = useRef<HTMLDivElement | null>(null);
49
+ /** Cleared when onRowsScrollEnd fires; set again once the viewport leaves the threshold or the rows change. */
50
+ const armedRef = useRef(true);
51
+ const onRowsScrollEndRef = useRef(onRowsScrollEnd);
36
52
 
37
- const [scrollState, setScrollState] = useState({
38
- scrollTop: 0,
39
- scrollLeft: 0,
40
- overscanRows: overscanRowCount,
41
- });
53
+ // velocityOverscan is 0 at rest; the prop floor is applied during render, so a new
54
+ // overscanRowCount takes effect without waiting for a scroll.
55
+ const [scrollState, setScrollState] = useState({ scrollTop: 0, scrollLeft: 0, velocityOverscan: 0 });
56
+
57
+ useEffect(() => {
58
+ onRowsScrollEndRef.current = onRowsScrollEnd;
59
+ }, [onRowsScrollEnd]);
42
60
 
43
61
  useEffect(() => {
44
62
  return () => {
@@ -47,6 +65,20 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
47
65
  };
48
66
  }, []);
49
67
 
68
+ /** Fires onRowsScrollEnd once per arrival within the threshold of the end. */
69
+ const checkRowsScrollEnd = useCallback((target: HTMLElement) => {
70
+ const endParams = getRowsScrollEndParams(target);
71
+ if (!endParams) {
72
+ armedRef.current = true;
73
+ return;
74
+ }
75
+ if (!armedRef.current) return;
76
+ const callback = onRowsScrollEndRef.current;
77
+ if (!callback) return;
78
+ armedRef.current = false;
79
+ callback(endParams);
80
+ }, []);
81
+
50
82
  const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
51
83
  const target = event.currentTarget;
52
84
  const now = performance.now();
@@ -56,6 +88,7 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
56
88
  + Math.abs(target.scrollLeft - prevRef.current.scrollLeft);
57
89
  const deltaTime = now - prevRef.current.time;
58
90
  const velocity = deltaTime > 0 ? deltaPos / deltaTime : 0;
91
+ const movedVertically = target.scrollTop !== prevRef.current.scrollTop;
59
92
 
60
93
  prevRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, time: now };
61
94
  pendingRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, velocity };
@@ -64,36 +97,51 @@ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrol
64
97
  rafRef.current = requestAnimationFrame(() => {
65
98
  rafRef.current = null;
66
99
  const { scrollTop, scrollLeft, velocity: v } = pendingRef.current;
67
- setScrollState({
68
- scrollTop,
69
- scrollLeft,
70
- overscanRows: velocityToOverscan(v, overscanRowCount),
71
- });
100
+ setScrollState({ scrollTop, scrollLeft, velocityOverscan: velocityToOverscan(v) });
72
101
  });
73
102
 
74
103
  // Decay back to the minimum overscan 200ms after scrolling stops
75
104
  if (decayRef.current !== null) clearTimeout(decayRef.current);
76
105
  decayRef.current = setTimeout(() => {
77
106
  decayRef.current = null;
78
- setScrollState(prev => ({ ...prev, overscanRows: overscanRowCount }));
107
+ setScrollState(prev => (prev.velocityOverscan === 0 ? prev : { ...prev, velocityOverscan: 0 }));
79
108
  }, 200);
80
109
 
81
- if (onRowsScrollEnd) {
82
- const { scrollTop, scrollHeight, clientHeight } = target;
83
- if (scrollHeight - scrollTop - clientHeight < 100) {
84
- onRowsScrollEnd({
85
- visibleTop: scrollTop,
86
- visibleBottom: scrollTop + clientHeight,
87
- viewportHeight: clientHeight,
88
- });
89
- }
110
+ // Horizontal-only scrolling never reaches (or leaves) the end of the rows.
111
+ if (movedVertically) checkRowsScrollEnd(target);
112
+ }, [checkRowsScrollEnd]);
113
+
114
+ // New rows re-arm the callback; if the end is still in view (the rows do not fill the
115
+ // viewport), no scroll event can happen, so check here.
116
+ useEffect(() => {
117
+ armedRef.current = true;
118
+ const el = elementRef.current;
119
+ if (!el || autoHeight || rowCount <= 0 || el.clientHeight === 0) return;
120
+ checkRowsScrollEnd(el);
121
+ }, [rowCount, autoHeight, checkRowsScrollEnd]);
122
+
123
+ const attachViewport = useCallback((element: HTMLDivElement | null) => {
124
+ elementRef.current = element;
125
+ if (!element) return;
126
+ const { scrollTop, scrollLeft } = pendingRef.current;
127
+ if (scrollTop === 0 && scrollLeft === 0) return;
128
+ element.scrollTop = scrollTop;
129
+ element.scrollLeft = scrollLeft;
130
+ // The browser clamps to the new content. If it did, sync the state to what the element
131
+ // shows (no scroll event fires when the position ends up unchanged).
132
+ const actual = { scrollTop: element.scrollTop, scrollLeft: element.scrollLeft };
133
+ if (actual.scrollTop !== scrollTop || actual.scrollLeft !== scrollLeft) {
134
+ pendingRef.current = { ...pendingRef.current, ...actual };
135
+ prevRef.current = { ...prevRef.current, ...actual };
136
+ setScrollState(prev => ({ ...prev, ...actual }));
90
137
  }
91
- }, [onRowsScrollEnd, overscanRowCount]);
138
+ }, []);
92
139
 
93
140
  return {
94
141
  scrollTop: scrollState.scrollTop,
95
142
  scrollLeft: scrollState.scrollLeft,
96
- overscanRows: scrollState.overscanRows,
143
+ overscanRows: Math.max(scrollState.velocityOverscan, overscanRowCount),
97
144
  handleScroll,
145
+ attachViewport,
98
146
  };
99
147
  }
@@ -0,0 +1,45 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import type { MutableRefObject, RefObject } from 'react';
3
+ import { scrollRowIntoView, scrollColumnIntoView } from '../../utils/scroll';
4
+ import type { LayoutResult } from './useLayout';
5
+ import type { GridApi, GridRowModel } from '../../types';
6
+
7
+ export interface UseGridScrollToIndexesApiParams<R extends GridRowModel> {
8
+ apiRef: MutableRefObject<GridApi>;
9
+ viewportRef: RefObject<HTMLDivElement | null>;
10
+ layout: LayoutResult<R>;
11
+ rowHeight: number;
12
+ }
13
+
14
+ /**
15
+ * Installs `scrollToIndexes` on the API, in a layout effect reading the latest layout from a ref,
16
+ * so it is live in a parent's layout effect and never scrolls by a previous render's layout.
17
+ */
18
+ export function useGridScrollToIndexesApi<R extends GridRowModel>(params: UseGridScrollToIndexesApiParams<R>): void {
19
+ const { apiRef } = params;
20
+ const latestRef = useRef(params);
21
+ useLayoutEffect(() => {
22
+ latestRef.current = params;
23
+ });
24
+
25
+ useLayoutEffect(() => {
26
+ apiRef.current.scrollToIndexes = ({ rowIndex, colIndex }) => {
27
+ const { viewportRef, layout, rowHeight } = latestRef.current;
28
+ const el = viewportRef.current;
29
+ if (!el) return;
30
+
31
+ if (rowIndex !== undefined && rowIndex >= 0) {
32
+ scrollRowIntoView(el, rowIndex, layout.cumulativeHeights, layout.pinnedBottomHeight, rowHeight);
33
+ }
34
+
35
+ if (colIndex !== undefined && colIndex >= 0) {
36
+ // colIndex is an index into all data columns: leftPinned + unpinned + rightPinned
37
+ const allDataCols = [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols];
38
+ const targetCol = allDataCols[colIndex];
39
+ if (!targetCol) return;
40
+ // A pinned column (index -1) is always visible: nothing to scroll.
41
+ scrollColumnIntoView(el, layout.unpinnedColsWithWidth.findIndex(c => c.field === targetCol.field), layout);
42
+ }
43
+ };
44
+ }, [apiRef]);
45
+ }
@@ -1,5 +1,5 @@
1
1
  import { useRef, useEffect, useLayoutEffect } from 'react';
2
- import type { GridState } from '../../state/types';
2
+ import type { GridState, GridDensityState } from '../../state/types';
3
3
  import type {
4
4
  GridSortItem,
5
5
  GridFilterModel,
@@ -7,6 +7,23 @@ import type {
7
7
  GridColumnPinning,
8
8
  } from '../../types';
9
9
 
10
+ /** Grid-owned columns that must never leak into the public state (and so into storage). */
11
+ const SYNTHETIC_FIELDS: ReadonlySet<string> = new Set(['__group__']);
12
+ const isUserField = (field: string) => !SYNTHETIC_FIELDS.has(field);
13
+
14
+ function withoutSyntheticKeys<T>(record: Record<string, T>): Record<string, T> {
15
+ const keys = Object.keys(record);
16
+ if (keys.every(isUserField)) return record;
17
+ return Object.fromEntries(Object.entries(record).filter(([field]) => isUserField(field)));
18
+ }
19
+
20
+ function userPinnedColumns(pinned: GridColumnPinning): GridColumnPinning {
21
+ const result: GridColumnPinning = { ...pinned };
22
+ if (pinned.left) result.left = pinned.left.filter(isUserField);
23
+ if (pinned.right) result.right = pinned.right.filter(isUserField);
24
+ return result;
25
+ }
26
+
10
27
  export interface UseGridStateSnapshotParams {
11
28
  onStateChange?: (state: GridState) => void;
12
29
  sortModel: GridSortItem[];
@@ -15,9 +32,17 @@ export interface UseGridStateSnapshotParams {
15
32
  columnWidths: Record<string, number>;
16
33
  effectiveColumnOrder: string[];
17
34
  columnVisibilityModel: Record<string, boolean>;
35
+ /** The user's pins (not the grid's effective pins, which add the synthetic group column). */
18
36
  pinnedColumns: GridColumnPinning;
37
+ density: GridDensityState['density'];
19
38
  }
20
39
 
40
+ /**
41
+ * Emits the public GridState through onStateChange whenever its *value* changes.
42
+ * Inline model props get a new identity on every parent render, so the comparison is
43
+ * structural; otherwise an onStateChange that stores state in the parent re-renders
44
+ * the parent, which creates new inline props, which fires onStateChange again, forever.
45
+ */
21
46
  export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
22
47
  const {
23
48
  onStateChange,
@@ -28,6 +53,7 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
28
53
  effectiveColumnOrder,
29
54
  columnVisibilityModel,
30
55
  pinnedColumns,
56
+ density,
31
57
  } = params;
32
58
 
33
59
  const onStateChangeRef = useRef(onStateChange);
@@ -35,6 +61,8 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
35
61
  onStateChangeRef.current = onStateChange;
36
62
  });
37
63
 
64
+ const lastEmittedRef = useRef<string | null>(null);
65
+
38
66
  useEffect(() => {
39
67
  if (!onStateChangeRef.current) return;
40
68
 
@@ -43,13 +71,19 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
43
71
  filter: { filterModel },
44
72
  pagination: { paginationModel: effectivePaginationModel },
45
73
  columns: {
46
- columnWidths,
47
- columnOrder: effectiveColumnOrder,
48
- columnVisibilityModel,
49
- pinnedColumns,
74
+ columnWidths: withoutSyntheticKeys(columnWidths),
75
+ columnOrder: effectiveColumnOrder.filter(isUserField),
76
+ columnVisibilityModel: withoutSyntheticKeys(columnVisibilityModel),
77
+ pinnedColumns: userPinnedColumns(pinnedColumns),
50
78
  },
79
+ density: { density },
51
80
  };
52
81
 
82
+ // The snapshot only holds plain data, so its JSON is a faithful value key.
83
+ const serialized = JSON.stringify(snapshot);
84
+ if (serialized === lastEmittedRef.current) return;
85
+ lastEmittedRef.current = serialized;
86
+
53
87
  onStateChangeRef.current(snapshot);
54
88
  }, [
55
89
  sortModel,
@@ -59,5 +93,6 @@ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
59
93
  effectiveColumnOrder,
60
94
  columnVisibilityModel,
61
95
  pinnedColumns,
96
+ density,
62
97
  ]);
63
98
  }
@@ -0,0 +1,49 @@
1
+ import { useContext } from 'react';
2
+ import { GridThemeContext } from '../../theme/gridThemeContext';
3
+ import type { GridDensity } from './useGridControlledState';
4
+
5
+ const DEFAULT_ROW_HEIGHT = 52;
6
+ const DEFAULT_HEADER_HEIGHT = 56;
7
+ const COMPACT_ROW_HEIGHT = 32;
8
+ const COMFORTABLE_ROW_HEIGHT = 72;
9
+
10
+ /** A theme height in pixels (`'36px'` or `'36'`); anything else (rem, calc, …) is ignored. */
11
+ export function parseThemePixels(value: string | undefined): number | undefined {
12
+ if (value === undefined) return undefined;
13
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(px)?\s*$/.exec(value);
14
+ if (!match) return undefined;
15
+ const px = Number(match[1]);
16
+ return px > 0 ? px : undefined;
17
+ }
18
+
19
+ export interface UseGridThemeDimensionsParams {
20
+ rowHeight?: number;
21
+ headerHeight?: number;
22
+ density: GridDensity;
23
+ }
24
+
25
+ /**
26
+ * The row and header heights the grid lays out and renders with. The grid drives virtualization
27
+ * from these numbers, so a DataGridThemeProvider's `grid.rowHeight*` / `grid.headerHeight` are read
28
+ * here rather than left to CSS (which the grid's own inline heights would override). Precedence:
29
+ * the `rowHeight` / `headerHeight` props, then the theme, then the defaults. `density="compact"` /
30
+ * `"comfortable"` use the theme's compact / comfortable height, else 32 / 72.
31
+ */
32
+ export function useGridThemeDimensions(params: UseGridThemeDimensionsParams): { rowHeight: number; headerHeight: number } {
33
+ const { rowHeight, headerHeight, density } = params;
34
+ const grid = useContext(GridThemeContext)?.grid;
35
+
36
+ let effectiveRowHeight: number;
37
+ if (density === 'compact') {
38
+ effectiveRowHeight = parseThemePixels(grid?.rowHeightCompact) ?? COMPACT_ROW_HEIGHT;
39
+ } else if (density === 'comfortable') {
40
+ effectiveRowHeight = parseThemePixels(grid?.rowHeightComfortable) ?? COMFORTABLE_ROW_HEIGHT;
41
+ } else {
42
+ effectiveRowHeight = rowHeight ?? parseThemePixels(grid?.rowHeightStandard) ?? DEFAULT_ROW_HEIGHT;
43
+ }
44
+
45
+ return {
46
+ rowHeight: effectiveRowHeight,
47
+ headerHeight: headerHeight ?? parseThemePixels(grid?.headerHeight) ?? DEFAULT_HEADER_HEIGHT,
48
+ };
49
+ }
@@ -0,0 +1,97 @@
1
+ import { useMemo } from 'react';
2
+ import type { MutableRefObject } from 'react';
3
+ import { canReorderWithinColumnGroups } from '../../utils/columnGroups';
4
+ import type {
5
+ DataGridProps,
6
+ GridAggregationModel,
7
+ GridApi,
8
+ GridColDef,
9
+ GridColumnGroupingModel,
10
+ GridColumnVisibilityModel,
11
+ GridFilterModel,
12
+ GridPivotModel,
13
+ GridRowModel,
14
+ GridToolbarSlotProps,
15
+ } from '../../types';
16
+
17
+ type ToolbarSlot = NonNullable<DataGridProps['slots']>['toolbar'];
18
+ type ToolbarSlotProps = NonNullable<DataGridProps['slotProps']>['toolbar'];
19
+
20
+ export interface UseGridToolbarPropsParams<R extends GridRowModel> {
21
+ /** `slots.toolbar`: nothing is built without one. */
22
+ toolbar: ToolbarSlot;
23
+ /** `slotProps.toolbar`, merged over the grid's props. */
24
+ toolbarSlotProps: ToolbarSlotProps;
25
+ apiRef: MutableRefObject<GridApi>;
26
+ /** Columns in display order (`useGridColumns().orderedColumns`). */
27
+ orderedColumns: GridColDef<R>[];
28
+ /** The `columns` prop. */
29
+ baseColumns: GridColDef<R>[];
30
+ aggregationModel: GridAggregationModel;
31
+ onAggregationModelChange: (model: GridAggregationModel) => void;
32
+ /** The toolbar gets the pivot model only when the consumer uses the pivot (`pivotMode`, `pivotModel` or `onPivotModelChange`). */
33
+ pivotMode: boolean;
34
+ pivotModelProp: GridPivotModel | undefined;
35
+ onPivotModelChangeProp: ((model: GridPivotModel) => void) | undefined;
36
+ pivotModel: GridPivotModel;
37
+ onPivotModelChange: (model: GridPivotModel) => void;
38
+ filterModel: GridFilterModel;
39
+ onFilterModelChange: (model: GridFilterModel) => void;
40
+ columnVisibilityModel: GridColumnVisibilityModel;
41
+ onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
42
+ disableColumnReorder: boolean;
43
+ columnGroupingModel: GridColumnGroupingModel | undefined;
44
+ moveColumn: (fromField: string, toField: string) => void;
45
+ resetColumnOrder: () => void;
46
+ /** `useGridColumnsPanel().toolbarPanelRequested`. */
47
+ forceColumnsOpen: boolean;
48
+ onColumnsPanelClose: () => void;
49
+ }
50
+
51
+ /** The props the grid passes to `slots.toolbar`, or null when there is no toolbar slot. */
52
+ export function useGridToolbarProps<R extends GridRowModel>(
53
+ params: UseGridToolbarPropsParams<R>
54
+ ): (GridToolbarSlotProps & Record<string, unknown>) | null {
55
+ const {
56
+ toolbar, toolbarSlotProps, apiRef, orderedColumns, baseColumns, aggregationModel, onAggregationModelChange,
57
+ pivotMode, pivotModelProp, onPivotModelChangeProp, pivotModel, onPivotModelChange, filterModel,
58
+ onFilterModelChange, columnVisibilityModel, onColumnVisibilityModelChange, disableColumnReorder,
59
+ columnGroupingModel, moveColumn, resetColumnOrder, forceColumnsOpen, onColumnsPanelClose,
60
+ } = params;
61
+
62
+ return useMemo(() => {
63
+ if (!toolbar) return null;
64
+ const reorderHandler = disableColumnReorder
65
+ ? undefined
66
+ : (fromField: string, toField: string) => {
67
+ if (!canReorderWithinColumnGroups(columnGroupingModel, fromField, toField)) return;
68
+ moveColumn(fromField, toField);
69
+ };
70
+ return {
71
+ apiRef,
72
+ columns: orderedColumns as unknown as GridColDef[],
73
+ baseColumns: baseColumns as unknown as GridColDef[],
74
+ aggregationModel,
75
+ onAggregationModelChange,
76
+ ...(pivotMode || pivotModelProp || onPivotModelChangeProp ? {
77
+ pivotModel,
78
+ onPivotModelChange,
79
+ } : {}),
80
+ filterModel,
81
+ onFilterModelChange,
82
+ columnVisibilityModel,
83
+ onColumnVisibilityModelChange,
84
+ onColumnReorder: reorderHandler,
85
+ onColumnOrderReset: disableColumnReorder ? undefined : resetColumnOrder,
86
+ forceColumnsOpen,
87
+ onColumnsPanelClose,
88
+ ...toolbarSlotProps,
89
+ };
90
+ }, [
91
+ toolbar, disableColumnReorder, moveColumn, resetColumnOrder, orderedColumns, apiRef,
92
+ baseColumns, aggregationModel, onAggregationModelChange, pivotMode,
93
+ pivotModelProp, onPivotModelChangeProp, pivotModel, onPivotModelChange,
94
+ filterModel, onFilterModelChange, columnVisibilityModel,
95
+ onColumnVisibilityModelChange, forceColumnsOpen, onColumnsPanelClose, toolbarSlotProps, columnGroupingModel,
96
+ ]);
97
+ }
@@ -0,0 +1,38 @@
1
+ import { useCallback } from 'react';
2
+ import type { MutableRefObject } from 'react';
3
+ import { useGridScrollSync, type UseGridScrollSyncParams } from './useGridScrollSync';
4
+ import { useGridViewportSize } from './useGridViewportSize';
5
+
6
+ export interface UseGridViewportParams extends UseGridScrollSyncParams {
7
+ /** Receives the viewport's measured content size. */
8
+ setDimensions: (width: number, height: number) => void;
9
+ /** Set to the mounted viewport element. */
10
+ viewportRef: MutableRefObject<HTMLDivElement | null>;
11
+ }
12
+
13
+ export interface UseGridViewportResult {
14
+ scrollTop: number;
15
+ scrollLeft: number;
16
+ overscanRows: number;
17
+ handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
18
+ /**
19
+ * Callback ref for the viewport: every viewport that mounts (list view switched off again)
20
+ * is measured and gets its scroll position back.
21
+ */
22
+ setViewportElement: (el: HTMLDivElement | null) => void;
23
+ }
24
+
25
+ /** The grid viewport: scroll position and velocity overscan, size measurement, and its callback ref. */
26
+ export function useGridViewport(params: UseGridViewportParams): UseGridViewportResult {
27
+ const { setDimensions, viewportRef, ...scrollParams } = params;
28
+
29
+ const { scrollTop, scrollLeft, overscanRows, handleScroll, attachViewport } = useGridScrollSync(scrollParams);
30
+ const observeViewportSize = useGridViewportSize(setDimensions);
31
+ const setViewportElement = useCallback((el: HTMLDivElement | null) => {
32
+ viewportRef.current = el;
33
+ observeViewportSize(el);
34
+ attachViewport(el);
35
+ }, [viewportRef, observeViewportSize, attachViewport]);
36
+
37
+ return { scrollTop, scrollLeft, overscanRows, handleScroll, setViewportElement };
38
+ }
@@ -0,0 +1,26 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * Returns a callback ref that measures the element it is attached to with a ResizeObserver and
5
+ * reports its content size. Every element that mounts is observed (the grid viewport unmounts in
6
+ * list view and a new one mounts when list view is switched off); a detached element is released.
7
+ */
8
+ export function useGridViewportSize(
9
+ setDimensions: (width: number, height: number) => void,
10
+ ): (element: HTMLElement | null) => void {
11
+ const observerRef = useRef<ResizeObserver | null>(null);
12
+
13
+ return useCallback((element: HTMLElement | null) => {
14
+ observerRef.current?.disconnect();
15
+ observerRef.current = null;
16
+ if (!element || typeof ResizeObserver === 'undefined') return;
17
+ const observer = new ResizeObserver((entries) => {
18
+ for (const entry of entries) {
19
+ const { width, height } = entry.contentRect;
20
+ setDimensions(width, height);
21
+ }
22
+ });
23
+ observer.observe(element);
24
+ observerRef.current = observer;
25
+ }, [setDimensions]);
26
+ }
@@ -13,13 +13,21 @@ export interface UseGridVisibleRowsParams<R extends GridRowModel> {
13
13
  paginatedUnpinnedRows: R[];
14
14
  sortedUnpinnedRows: R[];
15
15
  pagination: boolean;
16
+ /** Resolves a row's id (rows are not guaranteed to carry it in `row.id`); defaults to `row.id`. */
17
+ getRowId?: (row: R) => GridRowId;
16
18
  }
17
19
 
18
20
  export interface GridVisibleRow<R extends GridRowModel> {
19
21
  row: R;
22
+ /** The row's id (getRowId), resolved once for everything that renders the row. */
23
+ id: GridRowId;
20
24
  rowIndex: number;
25
+ /** Set on the rows of the top / bottom pinned sections, which render outside the scrolling rows. */
26
+ pinned?: 'top' | 'bottom';
21
27
  }
22
28
 
29
+ const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
30
+
23
31
  export function useGridVisibleRows<R extends GridRowModel>(
24
32
  params: UseGridVisibleRowsParams<R>
25
33
  ): GridVisibleRow<R>[] {
@@ -30,6 +38,7 @@ export function useGridVisibleRows<R extends GridRowModel>(
30
38
  paginatedUnpinnedRows,
31
39
  sortedUnpinnedRows,
32
40
  pagination,
41
+ getRowId = defaultGetRowId,
33
42
  } = params;
34
43
 
35
44
  return useMemo<GridVisibleRow<R>[]>(() => {
@@ -37,30 +46,34 @@ export function useGridVisibleRows<R extends GridRowModel>(
37
46
  const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
38
47
  const topPinnedCount = pinnedTopRows.length;
39
48
 
40
- const topPinned = pinnedTopRows.map((row, index) => ({ row, rowIndex: index }));
49
+ const topPinned = pinnedTopRows.map((row, index) => ({ row, id: getRowId(row), rowIndex: index, pinned: 'top' as const }));
41
50
  const bottomPinned = pinnedBottomRows.map((row, index) => ({
42
51
  row,
52
+ id: getRowId(row),
43
53
  rowIndex: topPinnedCount + centerRows.length + index,
54
+ pinned: 'bottom' as const,
44
55
  }));
45
56
 
46
- const centerStartIndex = Math.max(0, firstRowIndex - topPinnedCount);
47
- const centerEndIndex = Math.min(centerRows.length, lastRowIndex - topPinnedCount + 1);
57
+ // renderContext indices come from the unpinned-row layout, so they index centerRows directly.
58
+ const centerStartIndex = Math.max(0, firstRowIndex);
59
+ const centerEndIndex = Math.min(centerRows.length, lastRowIndex + 1);
48
60
 
49
61
  const centerVisible = centerRows
50
62
  .slice(centerStartIndex, centerEndIndex)
51
63
  .map((row, index) => ({
52
64
  row,
65
+ id: getRowId(row),
53
66
  rowIndex: topPinnedCount + centerStartIndex + index,
54
67
  }));
55
68
 
56
- const combined = [...topPinned, ...centerVisible, ...bottomPinned];
69
+ const combined: GridVisibleRow<R>[] = [...topPinned, ...centerVisible, ...bottomPinned];
57
70
 
58
71
  // Deduplication guard: a row ID in both pinnedRows and rows would cause React key collisions
59
72
  const seenIds = new Set<GridRowId>();
60
73
  return combined.filter(item => {
61
- if (seenIds.has(item.row.id)) return false;
62
- seenIds.add(item.row.id);
74
+ if (seenIds.has(item.id)) return false;
75
+ seenIds.add(item.id);
63
76
  return true;
64
77
  });
65
- }, [renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination]);
78
+ }, [renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination, getRowId]);
66
79
  }