@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  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 -166
  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 +6 -7
  29. package/docs/components/datagrid.md +15 -13
  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 +90 -38
  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 +126 -39
  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 +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -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 +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
  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 +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -0,0 +1,77 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { RefObject } from 'react';
3
+ import type { GridToolbarHost } from './gridToolbarHostContext';
4
+
5
+ export interface UseGridColumnsPanelResult {
6
+ /** Provide through `GridToolbarHostContext` around the toolbar slot. */
7
+ toolbarHost: GridToolbarHost;
8
+ /** Passed to the toolbar as `forceColumnsOpen`. */
9
+ toolbarPanelRequested: boolean;
10
+ /** Passed to the toolbar as `onColumnsPanelClose`. */
11
+ closeToolbarPanel: () => void;
12
+ /** Whether the standalone Columns panel (used when no toolbar can show one) is open. */
13
+ standalonePanelOpen: boolean;
14
+ closeStandalonePanel: () => void;
15
+ /** Attach to the standalone panel; a mousedown outside it closes it. */
16
+ standalonePanelRef: RefObject<HTMLDivElement | null>;
17
+ /** The column menu's **Manage columns** action. */
18
+ openColumnsPanel: () => void;
19
+ }
20
+
21
+ /**
22
+ * Opens the Columns panel for the column menu: in the toolbar when a `GridToolbar` has registered
23
+ * through `GridToolbarHostContext`, otherwise as the standalone panel, which closes on Escape or a
24
+ * mousedown outside it.
25
+ */
26
+ export function useGridColumnsPanel(): UseGridColumnsPanelResult {
27
+ const [toolbarPanelRequested, setToolbarPanelRequested] = useState(false);
28
+ const [standalonePanelOpen, setStandalonePanelOpen] = useState(false);
29
+ const standalonePanelRef = useRef<HTMLDivElement>(null);
30
+ const toolbarHostCountRef = useRef(0);
31
+
32
+ const toolbarHost = useMemo<GridToolbarHost>(() => ({
33
+ registerColumnsPanel: () => {
34
+ toolbarHostCountRef.current += 1;
35
+ return () => { toolbarHostCountRef.current -= 1; };
36
+ },
37
+ }), []);
38
+
39
+ const openColumnsPanel = useCallback(() => {
40
+ if (toolbarHostCountRef.current > 0) {
41
+ setToolbarPanelRequested(true);
42
+ } else {
43
+ setStandalonePanelOpen(true);
44
+ }
45
+ }, []);
46
+
47
+ const closeToolbarPanel = useCallback(() => setToolbarPanelRequested(false), []);
48
+ const closeStandalonePanel = useCallback(() => setStandalonePanelOpen(false), []);
49
+
50
+ useEffect(() => {
51
+ if (!standalonePanelOpen) return;
52
+ function handleMouseDown(e: MouseEvent) {
53
+ if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
54
+ setStandalonePanelOpen(false);
55
+ }
56
+ }
57
+ function handleKeyDown(e: KeyboardEvent) {
58
+ if (e.key === 'Escape') setStandalonePanelOpen(false);
59
+ }
60
+ document.addEventListener('mousedown', handleMouseDown);
61
+ document.addEventListener('keydown', handleKeyDown);
62
+ return () => {
63
+ document.removeEventListener('mousedown', handleMouseDown);
64
+ document.removeEventListener('keydown', handleKeyDown);
65
+ };
66
+ }, [standalonePanelOpen]);
67
+
68
+ return {
69
+ toolbarHost,
70
+ toolbarPanelRequested,
71
+ closeToolbarPanel,
72
+ standalonePanelOpen,
73
+ closeStandalonePanel,
74
+ standalonePanelRef,
75
+ openColumnsPanel,
76
+ };
77
+ }
@@ -1,13 +1,16 @@
1
- import { useState, useCallback, useMemo } from 'react';
1
+ import { useState, useCallback, useMemo, useLayoutEffect, useRef } from 'react';
2
2
  import type {
3
3
  GridRowId,
4
4
  GridSortItem,
5
+ GridFilterModel,
5
6
  GridColumnPinning,
6
7
  GridPaginationModel,
7
8
  GridAggregationModel,
8
9
  GridPivotModel,
9
10
  } from '../../types';
10
- import type { GridInitialState } from '../../state/types';
11
+ import type { GridInitialState, GridDensityState } from '../../state/types';
12
+
13
+ export type GridDensity = GridDensityState['density'];
11
14
 
12
15
  export interface UseGridControlledStateParams {
13
16
  initialState?: GridInitialState;
@@ -15,6 +18,9 @@ export interface UseGridControlledStateParams {
15
18
  sortModel?: GridSortItem[];
16
19
  onSortModelChange?: (model: GridSortItem[]) => void;
17
20
 
21
+ filterModel?: GridFilterModel;
22
+ onFilterModelChange?: (model: GridFilterModel) => void;
23
+
18
24
  aggregationModel?: GridAggregationModel;
19
25
  onAggregationModelChange?: (model: GridAggregationModel) => void;
20
26
 
@@ -29,9 +35,13 @@ export interface UseGridControlledStateParams {
29
35
 
30
36
  paginationModel?: GridPaginationModel;
31
37
  onPaginationModelChange?: (model: GridPaginationModel) => void;
38
+ /** Used to pick the uncontrolled default page size: 100 when offered, else the first option. */
39
+ pageSizeOptions?: number[];
32
40
 
33
41
  rowSelectionModel?: GridRowId[];
34
42
  onRowSelectionModelChange?: (model: GridRowId[]) => void;
43
+
44
+ density?: GridDensity;
35
45
  }
36
46
 
37
47
  export interface UseGridControlledStateReturn {
@@ -39,6 +49,14 @@ export interface UseGridControlledStateReturn {
39
49
  sortModel: GridSortItem[];
40
50
  isSortControlled: boolean;
41
51
  setInternalSortModel: React.Dispatch<React.SetStateAction<GridSortItem[]>>;
52
+ /** Sets the internal model when uncontrolled and fires onSortModelChange. */
53
+ handleSortModelChange: (model: GridSortItem[]) => void;
54
+
55
+ // filter
56
+ filterModel: GridFilterModel;
57
+ isFilterControlled: boolean;
58
+ /** Sets the internal model when uncontrolled and fires onFilterModelChange. */
59
+ handleFilterModelChange: (model: GridFilterModel) => void;
42
60
 
43
61
  // aggregation
44
62
  aggregationModel: GridAggregationModel;
@@ -65,14 +83,37 @@ export interface UseGridControlledStateReturn {
65
83
  selectedRowIds: Set<GridRowId>;
66
84
  isSelectionControlled: boolean;
67
85
  setInternalRowSelectionModel: React.Dispatch<React.SetStateAction<GridRowId[]>>;
86
+ /** Sets the internal model when uncontrolled and fires onRowSelectionModelChange. */
87
+ handleRowSelectionModelChange: (model: GridRowId[]) => void;
88
+ /**
89
+ * The model passed to `handleRowSelectionModelChange` since the last commit, or null when
90
+ * there is none: a change made in this tick that has not rendered yet (read from
91
+ * onRowSelectionModelChange or right after apiRef.selectRows). Stable identity.
92
+ */
93
+ getPendingRowSelectionModel: () => GridRowId[] | null;
94
+
95
+ // density (prop, else initialState.density, else 'standard')
96
+ density: GridDensity;
68
97
  }
69
98
 
70
99
  const EMPTY_PIVOT_MODEL: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
100
+ const DEFAULT_PAGE_SIZE = 100;
101
+
102
+ /** The uncontrolled page size must be one the page-size select can show. */
103
+ export function getDefaultPageSize(pageSizeOptions?: number[]): number {
104
+ if (!pageSizeOptions || pageSizeOptions.length === 0 || pageSizeOptions.includes(DEFAULT_PAGE_SIZE)) {
105
+ return DEFAULT_PAGE_SIZE;
106
+ }
107
+ return pageSizeOptions[0];
108
+ }
71
109
 
72
110
  export function useGridControlledState(params: UseGridControlledStateParams): UseGridControlledStateReturn {
73
111
  const {
74
112
  initialState,
75
113
  sortModel: propSortModel,
114
+ onSortModelChange,
115
+ filterModel: propFilterModel,
116
+ onFilterModelChange,
76
117
  aggregationModel: propAggregationModel,
77
118
  onAggregationModelChange,
78
119
  columnVisibilityModel: propColumnVisibilityModel,
@@ -83,8 +124,10 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
83
124
  onPivotModelChange,
84
125
  paginationModel: propPaginationModel,
85
126
  onPaginationModelChange,
127
+ pageSizeOptions,
86
128
  rowSelectionModel: propRowSelectionModel,
87
129
  onRowSelectionModelChange,
130
+ density: propDensity,
88
131
  } = params;
89
132
 
90
133
  // ── Sort ─────────────────────────────────────────────────────────────────
@@ -94,12 +137,36 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
94
137
  const isSortControlled = propSortModel !== undefined;
95
138
  const sortModel = isSortControlled ? propSortModel! : internalSortModel;
96
139
 
140
+ const handleSortModelChange = useCallback((model: GridSortItem[]) => {
141
+ if (!isSortControlled) setInternalSortModel(model);
142
+ onSortModelChange?.(model);
143
+ }, [isSortControlled, onSortModelChange]);
144
+
145
+ // ── Filter ───────────────────────────────────────────────────────────────
146
+ const [internalFilterModel, setInternalFilterModel] = useState<GridFilterModel>(
147
+ () => initialState?.filter?.filterModel ?? { items: [] }
148
+ );
149
+ // `!= null` so a JS consumer's `filterModel={null}` behaves like "not passed", as before.
150
+ const isFilterControlled = propFilterModel != null;
151
+ const filterModel = isFilterControlled ? propFilterModel : internalFilterModel;
152
+
153
+ const handleFilterModelChange = useCallback((model: GridFilterModel) => {
154
+ if (!isFilterControlled) setInternalFilterModel(model);
155
+ onFilterModelChange?.(model);
156
+ }, [isFilterControlled, onFilterModelChange]);
157
+
97
158
  // ── Aggregation ──────────────────────────────────────────────────────────
98
159
  const isAggregationControlled = propAggregationModel !== undefined;
99
160
  const [internalAggregationModel, setInternalAggregationModel] = useState<GridAggregationModel>(
100
161
  () => propAggregationModel ?? {}
101
162
  );
102
- const aggregationModel = isAggregationControlled ? propAggregationModel! : internalAggregationModel;
163
+ // Identity follows content: `aggregationModel={{ salary: 'sum' }}` is a new object on every parent
164
+ // render, and the fetches and memos keyed on the model must not rerun for an unchanged model.
165
+ const aggregationModelKey = JSON.stringify(isAggregationControlled ? propAggregationModel! : internalAggregationModel);
166
+ const aggregationModel = useMemo<GridAggregationModel>(
167
+ () => JSON.parse(aggregationModelKey) as GridAggregationModel,
168
+ [aggregationModelKey],
169
+ );
103
170
 
104
171
  const handleAggregationModelChange = useCallback((model: GridAggregationModel) => {
105
172
  if (!isAggregationControlled) setInternalAggregationModel(model);
@@ -146,7 +213,7 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
146
213
  // ── Pagination ───────────────────────────────────────────────────────────
147
214
  const isPaginationControlled = propPaginationModel !== undefined;
148
215
  const [internalPaginationModel, setInternalPaginationModel] = useState<GridPaginationModel>(
149
- () => initialState?.pagination?.paginationModel ?? propPaginationModel ?? { page: 0, pageSize: 100 }
216
+ () => initialState?.pagination?.paginationModel ?? propPaginationModel ?? { page: 0, pageSize: getDefaultPageSize(pageSizeOptions) }
150
217
  );
151
218
  const effectivePaginationModel = isPaginationControlled ? propPaginationModel : internalPaginationModel;
152
219
 
@@ -161,13 +228,35 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
161
228
  const rowSelectionModel = isSelectionControlled ? propRowSelectionModel! : internalRowSelectionModel;
162
229
  const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
163
230
 
164
- // onRowSelectionModelChange is consumed by the caller directly (they call it when selection changes)
165
- void onRowSelectionModelChange;
231
+ // Cleared on every commit: the rendered model is current again (a controlled parent may
232
+ // also have rejected the change).
233
+ const pendingRowSelectionRef = useRef<GridRowId[] | null>(null);
234
+ useLayoutEffect(() => {
235
+ pendingRowSelectionRef.current = null;
236
+ });
237
+ const getPendingRowSelectionModel = useCallback(() => pendingRowSelectionRef.current, []);
238
+
239
+ const handleRowSelectionModelChange = useCallback((model: GridRowId[]) => {
240
+ pendingRowSelectionRef.current = model;
241
+ if (!isSelectionControlled) setInternalRowSelectionModel(model);
242
+ onRowSelectionModelChange?.(model);
243
+ }, [isSelectionControlled, onRowSelectionModelChange]);
244
+
245
+ // ── Density ──────────────────────────────────────────────────────────────
246
+ // No built-in density UI exists, so there is no internal setter: the prop wins,
247
+ // otherwise the persisted/initial density applies.
248
+ const [initialDensity] = useState<GridDensity>(() => initialState?.density?.density ?? 'standard');
249
+ const density = propDensity ?? initialDensity;
166
250
 
167
251
  return {
168
252
  sortModel: sortModel as GridSortItem[],
169
253
  isSortControlled,
170
254
  setInternalSortModel: setInternalSortModel as React.Dispatch<React.SetStateAction<GridSortItem[]>>,
255
+ handleSortModelChange,
256
+
257
+ filterModel,
258
+ isFilterControlled,
259
+ handleFilterModelChange,
171
260
 
172
261
  aggregationModel,
173
262
  handleAggregationModelChange,
@@ -188,5 +277,9 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
188
277
  selectedRowIds,
189
278
  isSelectionControlled,
190
279
  setInternalRowSelectionModel,
280
+ handleRowSelectionModelChange,
281
+ getPendingRowSelectionModel,
282
+
283
+ density,
191
284
  };
192
285
  }
@@ -0,0 +1,51 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { useDetailPanelHeights } from '../features/useDetailPanelHeights';
3
+ import { getDetailPanelRowIds } from '../../utils/detailPanel';
4
+ import type { GridRowId, GridRowMeta } from '../../types';
5
+
6
+ export interface UseGridDetailPanelParams {
7
+ /** Whether `getDetailPanelContent` was passed: the grid then has an expand column. */
8
+ hasDetailPanel: boolean;
9
+ /** `detailPanelExpandedRowIds` prop (controlled when defined). */
10
+ controlledExpandedRowIds: Set<GridRowId> | undefined;
11
+ onDetailPanelExpandedRowIdsChange: ((expandedRowIds: Set<GridRowId>) => void) | undefined;
12
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
13
+ }
14
+
15
+ export interface UseGridDetailPanelResult {
16
+ hasDetailPanel: boolean;
17
+ /** What renders and is laid out: synthetic group rows never show a detail panel. */
18
+ expandedRowIds: Set<GridRowId>;
19
+ handleDetailPanelToggle: (rowId: GridRowId) => void;
20
+ /** Measured heights of `'auto'` panels. */
21
+ detailPanelHeights: Map<GridRowId, number>;
22
+ reportDetailPanelHeight: (rowId: GridRowId, height: number) => void;
23
+ }
24
+
25
+ /** Detail-panel expansion (controlled or uncontrolled) and the measured heights of `'auto'` panels. */
26
+ export function useGridDetailPanel(params: UseGridDetailPanelParams): UseGridDetailPanelResult {
27
+ const { hasDetailPanel, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange, rowMetaMap } = params;
28
+
29
+ const [internalExpandedRowIds, setInternalExpandedRowIds] = useState<Set<GridRowId>>(new Set());
30
+ const requestedExpandedRowIds = controlledExpandedRowIds ?? internalExpandedRowIds;
31
+ const expandedRowIds = useMemo(() => getDetailPanelRowIds(requestedExpandedRowIds, rowMetaMap), [requestedExpandedRowIds, rowMetaMap]);
32
+
33
+ const handleDetailPanelToggle = useCallback((rowId: GridRowId) => {
34
+ // Synthetic group / subtotal rows have no detail panel (keyboard Space on the expand cell lands here too).
35
+ if (rowMetaMap.get(rowId)?.isGroupRow) return;
36
+ const newExpandedRowIds = new Set(requestedExpandedRowIds);
37
+ if (newExpandedRowIds.has(rowId)) {
38
+ newExpandedRowIds.delete(rowId);
39
+ } else {
40
+ newExpandedRowIds.add(rowId);
41
+ }
42
+ if (controlledExpandedRowIds === undefined) {
43
+ setInternalExpandedRowIds(newExpandedRowIds);
44
+ }
45
+ onDetailPanelExpandedRowIdsChange?.(newExpandedRowIds);
46
+ }, [requestedExpandedRowIds, rowMetaMap, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange]);
47
+
48
+ const { detailPanelHeights, reportDetailPanelHeight } = useDetailPanelHeights();
49
+
50
+ return { hasDetailPanel, expandedRowIds, handleDetailPanelToggle, detailPanelHeights, reportDetailPanelHeight };
51
+ }
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
+ import type { RefObject } from 'react';
2
3
 
3
4
  export interface UseGridDevWarningsParams {
4
5
  paginationRequested: boolean;
@@ -7,15 +8,73 @@ export interface UseGridDevWarningsParams {
7
8
  viewportHeight: number;
8
9
  renderedRowCount: number;
9
10
  totalRowCount: number;
11
+ /** `pinnedRows` is set while tree data or row grouping is active, where it has no effect. */
12
+ pinnedRowsIgnored?: boolean;
13
+ /** Pixel height of the scrollable content. */
14
+ scrollHeight?: number;
15
+ /** `listView` is set but `listViewColumn` is not, so the grid view is shown. */
16
+ listViewWithoutColumn?: boolean;
17
+ /** Number of items the list view renders (0 when the list view is not shown). It does not virtualize. */
18
+ listViewRowCount?: number;
10
19
  }
11
20
 
12
21
  // Below this many rows, rendering everything is harmless, so an unbounded container is not worth a warning.
13
22
  const UNBOUNDED_WARN_THRESHOLD = 200;
14
23
 
24
+ /** The list view renders every item; above this many the page slows down noticeably. */
25
+ export const LIST_VIEW_WARN_THRESHOLD = 2000;
26
+
27
+ /**
28
+ * Browsers cap element heights (Firefox at about 17.9M px, Chromium and Safari at about 33.5M px):
29
+ * rows laid out below the cap cannot be scrolled to.
30
+ */
31
+ export const MAX_SCROLLABLE_HEIGHT = 17_800_000;
32
+
15
33
  export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
16
- const { paginationRequested, isRowGrouping, autoHeight, viewportHeight, renderedRowCount, totalRowCount } = params;
34
+ const { paginationRequested, isRowGrouping, autoHeight, viewportHeight, renderedRowCount, totalRowCount, pinnedRowsIgnored = false, scrollHeight = 0, listViewWithoutColumn = false, listViewRowCount = 0 } = params;
17
35
  const warnedPaginationRef = useRef(false);
18
36
  const warnedUnboundedRef = useRef(false);
37
+ const warnedPinnedRowsRef = useRef(false);
38
+ const warnedHeightRef = useRef(false);
39
+ const warnedListViewRef = useRef(false);
40
+ const warnedListViewSizeRef = useRef(false);
41
+
42
+ useEffect(() => {
43
+ if (process.env.NODE_ENV === 'production' || warnedListViewSizeRef.current || listViewRowCount <= LIST_VIEW_WARN_THRESHOLD) return;
44
+ warnedListViewSizeRef.current = true;
45
+ console.warn(
46
+ `[OpenGridX] The list view is rendering all ${listViewRowCount} items: it does not virtualize. ` +
47
+ 'Above a few thousand items use `pagination`, server-side loading, or the grid view. See docs/features/list-view.md.'
48
+ );
49
+ }, [listViewRowCount]);
50
+
51
+ useEffect(() => {
52
+ if (process.env.NODE_ENV === 'production' || warnedListViewRef.current || !listViewWithoutColumn) return;
53
+ warnedListViewRef.current = true;
54
+ console.warn(
55
+ '[OpenGridX] `listView` is set without a `listViewColumn`, so the grid view is shown instead. ' +
56
+ 'Pass `listViewColumn` to render list items. See docs/features/list-view.md.'
57
+ );
58
+ }, [listViewWithoutColumn]);
59
+
60
+ useEffect(() => {
61
+ if (process.env.NODE_ENV === 'production' || warnedPinnedRowsRef.current || !pinnedRowsIgnored) return;
62
+ warnedPinnedRowsRef.current = true;
63
+ console.warn(
64
+ '[OpenGridX] `pinnedRows` is ignored while treeData or rowGroupingModel is active: the rows stay in their ' +
65
+ 'place in the hierarchy. See docs/features/pinning.md.'
66
+ );
67
+ }, [pinnedRowsIgnored]);
68
+
69
+ useEffect(() => {
70
+ if (process.env.NODE_ENV === 'production' || warnedHeightRef.current || scrollHeight <= MAX_SCROLLABLE_HEIGHT) return;
71
+ warnedHeightRef.current = true;
72
+ console.warn(
73
+ `[OpenGridX] The grid content is ${Math.round(scrollHeight)}px tall. Browsers cap element heights ` +
74
+ '(about 17.9M px in Firefox, 33.5M px in Chrome and Safari), so the last rows may not be reachable by scrolling. ' +
75
+ 'Use pagination, server-side or infinite loading, or a smaller rowHeight. See docs/features/virtualization.md.'
76
+ );
77
+ }, [scrollHeight]);
19
78
 
20
79
  useEffect(() => {
21
80
  if (process.env.NODE_ENV === 'production' || warnedPaginationRef.current) return;
@@ -30,13 +89,53 @@ export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
30
89
 
31
90
  useEffect(() => {
32
91
  if (process.env.NODE_ENV === 'production' || warnedUnboundedRef.current || autoHeight) return;
33
- if (viewportHeight > 0 && totalRowCount > UNBOUNDED_WARN_THRESHOLD && renderedRowCount >= totalRowCount) {
92
+ // Really unbounded: every row is rendered and the viewport is at least as tall as the rows, so it never scrolls.
93
+ if (viewportHeight > 0 && totalRowCount > UNBOUNDED_WARN_THRESHOLD && renderedRowCount >= totalRowCount && viewportHeight >= scrollHeight) {
34
94
  warnedUnboundedRef.current = true;
35
95
  console.warn(
36
96
  `[OpenGridX] The grid viewport is ${Math.round(viewportHeight)}px tall and is rendering all ${totalRowCount} rows, ` +
37
- 'so row virtualization is effectively off. The grid container has no bounded height: it grows to fit its ' +
38
- 'content. Give it a fixed height, or if it is a flex child add `min-height: 0`. See docs/features/virtualization.md.'
97
+ 'so row virtualization is effectively off: the grid fills its container, and the container grows to fit every row. ' +
98
+ 'Give the container a definite height (for example `height: 600px` or `100vh`). If the container, or any ancestor ' +
99
+ 'up to the sized one, is a flex or grid item, add `min-height: 0` to it. Alternatively pass a `height` prop, ' +
100
+ 'or `autoHeight` if rendering every row is intended. See docs/features/virtualization.md.'
39
101
  );
40
102
  }
41
- }, [autoHeight, viewportHeight, renderedRowCount, totalRowCount]);
103
+ }, [autoHeight, viewportHeight, renderedRowCount, totalRowCount, scrollHeight]);
104
+ }
105
+
106
+ /**
107
+ * True when the grid's stylesheet does not reach `root` (the `.ogx` element). The stylesheet lays the
108
+ * root out as a flex column; without it the root is a plain block and the viewport does not scroll.
109
+ * jsdom applies no library CSS, so it is never reported there.
110
+ */
111
+ export function isGridStylesheetMissing(root: HTMLElement): boolean {
112
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return false;
113
+ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) return false;
114
+ return window.getComputedStyle(root).display !== 'flex';
115
+ }
116
+
117
+ let warnedStylesheet = false;
118
+
119
+ /**
120
+ * Dev-only: warns once per page when a grid renders without `@opencorestack/opengridx/styles`.
121
+ * The JS bundle does not load the stylesheet, and without it the viewport grows to fit every row,
122
+ * which silently turns virtualization off.
123
+ */
124
+ export function useGridStylesheetWarning(rootRef: RefObject<HTMLElement | null>): void {
125
+ useEffect(() => {
126
+ if (process.env.NODE_ENV === 'production' || warnedStylesheet) return;
127
+ const root = rootRef.current;
128
+ if (!root || !isGridStylesheetMissing(root)) return;
129
+ warnedStylesheet = true;
130
+ console.warn(
131
+ '[OpenGridX] The grid stylesheet is not loaded, so the grid is unstyled and renders every row ' +
132
+ "(its viewport does not scroll). Add `import '@opencorestack/opengridx/styles'` once in your app root. " +
133
+ 'See README.md, Getting Started.'
134
+ );
135
+ }, [rootRef]);
136
+ }
137
+
138
+ /** Test helper: lets the stylesheet warning fire again. */
139
+ export function resetGridStylesheetWarning(): void {
140
+ warnedStylesheet = false;
42
141
  }
@@ -0,0 +1,50 @@
1
+ import { useCallback } from 'react';
2
+ import type { RefObject } from 'react';
3
+ import type { FocusedCell } from '../../utils/focus';
4
+ import type { GridCellParams, GridRowId, GridRowModel } from '../../types';
5
+
6
+ /**
7
+ * Keyboard-mode flag: toggled via a DOM class name on the grid root, with no React state, so
8
+ * the focus ring appears instantly without a re-render cycle.
9
+ */
10
+ export function useGridKeyboardMode(containerRef: RefObject<HTMLDivElement | null>): (on: boolean) => void {
11
+ return useCallback((on: boolean) => {
12
+ containerRef.current?.classList.toggle('ogx--kb', on);
13
+ }, [containerRef]);
14
+ }
15
+
16
+ export interface UseGridPointerFocusHandlersParams<R extends GridRowModel> {
17
+ ensureGridFocus: () => void;
18
+ setKeyboardMode: (on: boolean) => void;
19
+ setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
20
+ getRowId: (row: GridRowModel) => GridRowId;
21
+ onCellClick: ((params: GridCellParams<R>) => void) | undefined;
22
+ }
23
+
24
+ export interface UseGridPointerFocusHandlersResult<R extends GridRowModel> {
25
+ handleCellClick: (params: GridCellParams<R>) => void;
26
+ handleHeaderClick: (field: string) => void;
27
+ }
28
+
29
+ /** Cell and header clicks move grid focus there and leave keyboard mode. */
30
+ export function useGridPointerFocusHandlers<R extends GridRowModel>(
31
+ params: UseGridPointerFocusHandlersParams<R>
32
+ ): UseGridPointerFocusHandlersResult<R> {
33
+ const { ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId, onCellClick } = params;
34
+
35
+ const handleCellClick = useCallback((cellParams: GridCellParams<R>) => {
36
+ // Focus stays where the click put it (the cell, or a control rendered inside it).
37
+ ensureGridFocus();
38
+ setKeyboardMode(false);
39
+ setFocusedCell({ id: getRowId(cellParams.row), field: cellParams.field, rowIndex: cellParams.rowIndex });
40
+ onCellClick?.(cellParams);
41
+ }, [onCellClick, setKeyboardMode, setFocusedCell, ensureGridFocus, getRowId]);
42
+
43
+ const handleHeaderClick = useCallback((field: string) => {
44
+ ensureGridFocus();
45
+ setFocusedCell({ id: null, field });
46
+ setKeyboardMode(false);
47
+ }, [ensureGridFocus, setFocusedCell, setKeyboardMode]);
48
+
49
+ return { handleCellClick, handleHeaderClick };
50
+ }
@@ -0,0 +1,62 @@
1
+ import { useMemo } from 'react';
2
+ import type { GridColDef, GridColumnPinning, GridRowModel } from '../../types';
3
+
4
+ export interface UseGridGroupingColumnParams<R extends GridRowModel> {
5
+ /** The columns before the grouping column is added (the pivot's columns in pivot mode). */
6
+ baseColumns: GridColDef<R>[];
7
+ pinnedColumns: GridColumnPinning;
8
+ /** Show the synthetic `__group__` column (see `resolveGridModes`). */
9
+ hasGroupingColumn: boolean;
10
+ groupingColDef: Partial<GridColDef<R>> | undefined;
11
+ isTreeData: boolean;
12
+ getTreeDataPath: ((row: R) => string[]) | undefined;
13
+ }
14
+
15
+ export interface UseGridGroupingColumnResult<R extends GridRowModel> {
16
+ /** `baseColumns`, with the `__group__` column first when it is shown. */
17
+ activeColumns: GridColDef<R>[];
18
+ /** `pinnedColumns`, with `__group__` pinned first on the left when it is shown. */
19
+ effectivePinnedColumns: GridColumnPinning;
20
+ }
21
+
22
+ /**
23
+ * The dedicated `__group__` column that `groupingColDef` asks for while row grouping or tree
24
+ * data is active: prepended at position 0 and auto-pinned left.
25
+ *
26
+ * Both results MUST be memoized: a new array/object reference every render cascades through
27
+ * useRowGrouping's memos and effects into an infinite setState loop (Maximum update depth exceeded).
28
+ */
29
+ export function useGridGroupingColumn<R extends GridRowModel>(
30
+ params: UseGridGroupingColumnParams<R>
31
+ ): UseGridGroupingColumnResult<R> {
32
+ const { baseColumns, pinnedColumns, hasGroupingColumn, groupingColDef, isTreeData, getTreeDataPath } = params;
33
+
34
+ const effectivePinnedColumns = useMemo(() => (
35
+ hasGroupingColumn
36
+ ? { ...pinnedColumns, left: ['__group__', ...((pinnedColumns?.left ?? []).filter(f => f !== '__group__'))] }
37
+ : pinnedColumns
38
+ ), [hasGroupingColumn, pinnedColumns]);
39
+
40
+ const activeColumns = useMemo(() => (
41
+ hasGroupingColumn
42
+ ? [
43
+ {
44
+ headerName: 'Group',
45
+ width: 220,
46
+ // Tree data: a row's own entry in the hierarchy is the last segment of its path.
47
+ ...(isTreeData && getTreeDataPath ? { valueGetter: ({ row }: { row: R }) => { const path = getTreeDataPath(row); return path[path.length - 1]; } } : {}),
48
+ ...groupingColDef,
49
+ field: '__group__',
50
+ hideable: false,
51
+ sortable: false,
52
+ filterable: false,
53
+ pinnable: false,
54
+ exportable: false,
55
+ } as unknown as GridColDef<R>,
56
+ ...baseColumns,
57
+ ]
58
+ : baseColumns
59
+ ), [groupingColDef, hasGroupingColumn, isTreeData, getTreeDataPath, baseColumns]);
60
+
61
+ return { activeColumns, effectivePinnedColumns };
62
+ }