@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
@@ -3,8 +3,9 @@
3
3
  align-items: center;
4
4
  gap: var(--ogx-spacing-sm);
5
5
  padding: var(--ogx-spacing-sm) var(--ogx-spacing-sm);
6
- border-bottom: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
7
- background: var(--ogx-grid-header-background);
6
+ border-bottom: var(--ogx-border-width-thin) solid var(--ogx-toolbar-border, var(--ogx-grid-border-color));
7
+ background: var(--ogx-toolbar-background, var(--ogx-grid-header-background));
8
+ color: var(--ogx-toolbar-text, var(--ogx-grid-header-text));
8
9
  min-height: 44px;
9
10
  position: relative;
10
11
  z-index: 100;
@@ -37,17 +38,17 @@
37
38
  padding: 0;
38
39
  border: none;
39
40
  border-radius: 8px;
40
- background: transparent;
41
- color: var(--ogx-grid-header-text);
41
+ background: var(--ogx-toolbar-btn-bg, transparent);
42
+ color: var(--ogx-toolbar-btn-text, var(--ogx-grid-header-text));
42
43
  cursor: pointer;
43
44
  transition: background 0.15s, color 0.15s;
44
45
  position: relative;
45
46
  }
46
47
  .ogx-toolbar__icon-btn:hover {
47
- background: var(--ogx-grid-header-hover-background);
48
+ background: var(--ogx-toolbar-btn-hover, var(--ogx-grid-header-hover-background));
48
49
  }
49
50
  .ogx-toolbar__icon-btn--active {
50
- background: var(--ogx-grid-header-hover-background);
51
+ background: var(--ogx-toolbar-btn-hover, var(--ogx-grid-header-hover-background));
51
52
  outline: none;
52
53
  }
53
54
  .ogx-toolbar__icon-btn:focus {
@@ -106,7 +107,7 @@
106
107
  border: var(--ogx-border-width-thin) solid var(--ogx-border-color);
107
108
  border-radius: var(--ogx-border-radius-sm);
108
109
  background: transparent;
109
- color: var(--ogx-color-error, #ef4444);
110
+ color: var(--ogx-toolbar-btn-danger-text, var(--ogx-color-error, #ef4444));
110
111
  font-family: var(--ogx-font-family);
111
112
  font-size: var(--ogx-font-size-xs);
112
113
  font-weight: 500;
@@ -114,8 +115,8 @@
114
115
  transition: background 0.15s, border-color 0.15s;
115
116
  }
116
117
  .ogx-toolbar__clear-btn:hover {
117
- background: var(--ogx-toolbar-btn-danger-bg, rgba(239, 68, 68, 0.08));
118
- border-color: var(--ogx-color-error, #ef4444);
118
+ background: var(--ogx-toolbar-btn-danger-hover, rgba(239, 68, 68, 0.15));
119
+ border-color: var(--ogx-toolbar-btn-danger-text, var(--ogx-color-error, #ef4444));
119
120
  }
120
121
  .ogx-toolbar__close-btn {
121
122
  padding: 2px 10px;
@@ -189,8 +190,8 @@
189
190
  padding: 3px 6px;
190
191
  border: var(--ogx-border-width-thin) solid var(--ogx-overlay-border, var(--ogx-border-color));
191
192
  border-radius: 999px;
192
- background: transparent;
193
- color: var(--ogx-grid-row-text);
193
+ background: var(--ogx-toolbar-chip-bg, transparent);
194
+ color: var(--ogx-toolbar-chip-text, var(--ogx-grid-row-text));
194
195
  font-family: var(--ogx-font-family-mono, monospace);
195
196
  font-size: 10px;
196
197
  font-weight: 500;
@@ -1,6 +1,10 @@
1
- import React, { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback, useLayoutEffect, useContext } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel } from '../../types';
4
+ import { AGGREGATION_FUNCTIONS } from '../../utils/aggregation';
5
+ import type { BuiltInAggFn } from '../../utils/aggregation';
6
+ import { getViewportWidth } from '../../utils/viewport';
7
+ import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
4
8
  import { PivotPanel, PivotIcon } from './PivotPanel';
5
9
  import { GlobalSearch } from './GlobalSearch';
6
10
  import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
@@ -78,7 +82,23 @@ export interface GridToolbarProps {
78
82
  showNonHideableColumns?: boolean;
79
83
  }
80
84
 
81
- const AGGREGATION_FUNCTIONS = ['none', 'sum', 'avg', 'count', 'min', 'max'] as const;
85
+ const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
86
+
87
+ function isBuiltInAggregationFunction(fn: string): fn is BuiltInAggFn {
88
+ return Object.prototype.hasOwnProperty.call(AGGREGATION_FUNCTIONS, fn);
89
+ }
90
+
91
+ /**
92
+ * The summary functions offered for a column: its `availableAggregationFunctions` when set,
93
+ * otherwise every built-in one. Names the aggregation engine does not implement are dropped,
94
+ * because choosing one would show the label in the header but never compute a value.
95
+ */
96
+ function getAggregationFunctions(col: GridColDef): BuiltInAggFn[] {
97
+ if (!col.availableAggregationFunctions) return BUILT_IN_AGGREGATION_FUNCTIONS;
98
+ return Array.from(new Set(col.availableAggregationFunctions.filter(isBuiltInAggregationFunction)));
99
+ }
100
+
101
+ type ToolbarPanel = 'summaries' | 'pivot' | 'columns' | 'filters';
82
102
 
83
103
  function SigmaIcon() {
84
104
  return (
@@ -135,7 +155,7 @@ function AggregationPanel({
135
155
  }) {
136
156
  const panelRef = useRef<HTMLDivElement>(null);
137
157
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
138
- const [portalContainer, setPortalContainer] = useState<Element>(document.body);
158
+ const [portalContainer, setPortalContainer] = useState<Element | null>(null);
139
159
 
140
160
  useLayoutEffect(() => {
141
161
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
@@ -146,7 +166,7 @@ function AggregationPanel({
146
166
  const rect = anchorRef.current.getBoundingClientRect();
147
167
  return {
148
168
  top: rect.bottom + 6,
149
- right: window.innerWidth - rect.right,
169
+ right: getViewportWidth() - rect.right,
150
170
  };
151
171
  }, [anchorRef]);
152
172
 
@@ -195,6 +215,7 @@ function AggregationPanel({
195
215
  </span>
196
216
  {activeCount > 0 && (
197
217
  <button
218
+ type="button"
198
219
  className="ogx-toolbar__clear-btn"
199
220
  onClick={onClearAll}
200
221
  title="Clear all summaries"
@@ -219,8 +240,9 @@ function AggregationPanel({
219
240
  {col.headerName || col.field}
220
241
  </span>
221
242
  <div className="ogx-toolbar__agg-pills">
222
- {AGGREGATION_FUNCTIONS.map((fn) => (
243
+ {['none', ...getAggregationFunctions(col)].map((fn) => (
223
244
  <button
245
+ type="button"
224
246
  key={fn}
225
247
  className={`ogx-toolbar__pill${currentFn === fn ? ' ogx-toolbar__pill--active' : ''}`}
226
248
  onClick={() => onFunctionChange(col.field, fn)}
@@ -238,7 +260,7 @@ function AggregationPanel({
238
260
  </div>
239
261
  );
240
262
 
241
- return ReactDOM.createPortal(panel, portalContainer);
263
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
242
264
  }
243
265
 
244
266
  function ColumnsPanelWrapper({
@@ -266,7 +288,7 @@ function ColumnsPanelWrapper({
266
288
  }) {
267
289
  const panelRef = useRef<HTMLDivElement>(null);
268
290
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
269
- const [portalContainer, setPortalContainer] = useState<Element>(document.body);
291
+ const [portalContainer, setPortalContainer] = useState<Element | null>(null);
270
292
 
271
293
  useLayoutEffect(() => {
272
294
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
@@ -277,7 +299,7 @@ function ColumnsPanelWrapper({
277
299
  const rect = anchorRef.current.getBoundingClientRect();
278
300
  return {
279
301
  top: rect.bottom + 6,
280
- right: window.innerWidth - rect.right,
302
+ right: getViewportWidth() - rect.right,
281
303
  };
282
304
  }, [anchorRef]);
283
305
 
@@ -305,6 +327,12 @@ function ColumnsPanelWrapper({
305
327
  return () => document.removeEventListener('mousedown', handleClick);
306
328
  }, [anchorRef, onClose]);
307
329
 
330
+ useEffect(() => {
331
+ function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
332
+ document.addEventListener('keydown', onKey);
333
+ return () => document.removeEventListener('keydown', onKey);
334
+ }, [onClose]);
335
+
308
336
  const panel = (
309
337
  <div
310
338
  ref={panelRef}
@@ -326,7 +354,7 @@ function ColumnsPanelWrapper({
326
354
  </div>
327
355
  );
328
356
 
329
- return ReactDOM.createPortal(panel, portalContainer);
357
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
330
358
  }
331
359
 
332
360
  function FilterPanelWrapper({
@@ -349,7 +377,7 @@ function FilterPanelWrapper({
349
377
  // which could fire the close callback during the re-register window.
350
378
  const onCloseRef = useRef(onClose);
351
379
  useLayoutEffect(() => { onCloseRef.current = onClose; }, [onClose]);
352
- const [portalContainer, setPortalContainer] = useState<Element>(document.body);
380
+ const [portalContainer, setPortalContainer] = useState<Element | null>(null);
353
381
 
354
382
  useLayoutEffect(() => {
355
383
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
@@ -360,7 +388,7 @@ function FilterPanelWrapper({
360
388
  const rect = anchorRef.current.getBoundingClientRect();
361
389
  return {
362
390
  top: rect.bottom + 6,
363
- right: window.innerWidth - rect.right,
391
+ right: getViewportWidth() - rect.right,
364
392
  };
365
393
  }, [anchorRef]);
366
394
 
@@ -412,6 +440,7 @@ function FilterPanelWrapper({
412
440
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
413
441
  {activeFilterCount > 0 && (
414
442
  <button
443
+ type="button"
415
444
  className="ogx-toolbar__clear-btn"
416
445
  onClick={handleClearAll}
417
446
  title="Clear all filters"
@@ -420,6 +449,7 @@ function FilterPanelWrapper({
420
449
  </button>
421
450
  )}
422
451
  <button
452
+ type="button"
423
453
  className="ogx-toolbar__close-btn"
424
454
  onClick={() => onCloseRef.current()}
425
455
  title="Close filters"
@@ -440,7 +470,7 @@ function FilterPanelWrapper({
440
470
  </div>
441
471
  );
442
472
 
443
- return ReactDOM.createPortal(panel, portalContainer);
473
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
444
474
  }
445
475
 
446
476
  const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
@@ -471,20 +501,35 @@ export function GridToolbar({
471
501
  className,
472
502
  showNonHideableColumns,
473
503
  }: GridToolbarProps) {
474
- const [aggOpen, setAggOpen] = useState(false);
475
- const [pivotOpen, setPivotOpen] = useState(false);
476
- const [colsOpen, setColsOpen] = useState(false);
477
- const [filterOpen, setFilterOpen] = useState(false);
504
+ // At most one panel is open. Every way the Columns panel closes (its button, a custom
505
+ // button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
506
+ // grid can ask for it again with forceColumnsOpen.
507
+ const [openPanel, setOpenPanel] = useState<ToolbarPanel | null>(null);
508
+ const aggOpen = openPanel === 'summaries';
509
+ const pivotOpen = openPanel === 'pivot';
510
+ const colsOpen = openPanel === 'columns';
511
+ const filterOpen = openPanel === 'filters';
512
+
513
+ const switchPanel = (next: ToolbarPanel | null) => {
514
+ if (openPanel === 'columns' && next !== 'columns') onColumnsPanelClose?.();
515
+ setOpenPanel(next);
516
+ };
517
+ const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
518
+ const closePanel = (panel: ToolbarPanel) => { if (openPanel === panel) switchPanel(null); };
478
519
 
479
520
  useEffect(() => {
480
- if (forceColumnsOpen) {
481
- setColsOpen(true);
482
- setAggOpen(false);
483
- setPivotOpen(false);
484
- setFilterOpen(false);
485
- }
521
+ if (forceColumnsOpen) setOpenPanel('columns');
486
522
  }, [forceColumnsOpen]);
487
523
 
524
+ // Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
525
+ // when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
526
+ const toolbarHost = useContext(GridToolbarHostContext);
527
+ const hostsColumnsPanel = forceColumnsOpen !== undefined && Boolean(onColumnVisibilityModelChange);
528
+ useEffect(() => {
529
+ if (!toolbarHost || !hostsColumnsPanel) return undefined;
530
+ return toolbarHost.registerColumnsPanel();
531
+ }, [toolbarHost, hostsColumnsPanel]);
532
+
488
533
  const aggButtonRef = useRef<HTMLButtonElement>(null);
489
534
  const pivotButtonRef = useRef<HTMLButtonElement>(null);
490
535
  const colsButtonRef = useRef<HTMLButtonElement>(null);
@@ -501,8 +546,13 @@ export function GridToolbar({
501
546
  || currentPivotModel.columnFields.length > 0
502
547
  || currentPivotModel.valueFields.length > 0;
503
548
 
549
+ // aggregationModel keys name source columns, so generated pivot columns (absent from baseColumns)
550
+ // are never offered: a summary on them would do nothing and leave a stray key in the model.
551
+ const baseFields = baseColumns ? new Set(baseColumns.map((c) => c.field)) : null;
504
552
  const aggregableColumns = columns.filter(
505
553
  (c) => c.aggregable !== false && (c.type === 'number' || c.aggregable === true)
554
+ && getAggregationFunctions(c).length > 0
555
+ && (!baseFields || baseFields.has(c.field))
506
556
  );
507
557
 
508
558
  const activeCount = Object.keys(aggregationModel).filter(
@@ -530,7 +580,9 @@ export function GridToolbar({
530
580
  const handleSearchChange = useCallback((value: string) => {
531
581
  if (!onFilterModelChange) return;
532
582
 
533
- const values = value ? [value] : [];
583
+ // Each whitespace-separated word is its own term; a row matches when every term is found
584
+ // in some column, so "john london" finds first = John, city = London.
585
+ const values = value.trim().split(/\s+/).filter(Boolean);
534
586
  const nextModel = {
535
587
  ...(filterModel || {}),
536
588
  quickFilterValues: values,
@@ -539,7 +591,7 @@ export function GridToolbar({
539
591
  onFilterModelChange(nextModel);
540
592
  }, [filterModel, onFilterModelChange]);
541
593
 
542
- const searchValue = filterModel?.quickFilterValues?.[0] || '';
594
+ const searchValue = (filterModel?.quickFilterValues ?? []).join(' ');
543
595
  const activeFilterCount = (filterModel?.items?.length || 0);
544
596
 
545
597
  // Column Visibility Logic
@@ -604,22 +656,20 @@ export function GridToolbar({
604
656
  <div ref={colsWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
605
657
  {renderColumnsButton
606
658
  ? renderColumnsButton({
607
- onClick: () => { setColsOpen(o => !o); setAggOpen(false); setPivotOpen(false); setFilterOpen(false); },
659
+ onClick: () => togglePanel('columns'),
608
660
  isOpen: colsOpen,
609
661
  activeCount: columns.length - visibleColumns.size,
610
662
  })
611
663
  : (
612
664
  <GridTooltip title="Columns">
613
665
  <button
666
+ type="button"
614
667
  ref={colsButtonRef}
615
668
  className={`ogx-toolbar__icon-btn${colsOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
616
669
  aria-label="Manage columns"
617
- onClick={() => {
618
- setColsOpen(!colsOpen);
619
- setAggOpen(false);
620
- setPivotOpen(false);
621
- setFilterOpen(false);
622
- }}
670
+ aria-haspopup="dialog"
671
+ aria-expanded={colsOpen}
672
+ onClick={() => togglePanel('columns')}
623
673
  >
624
674
  <ViewColumnIcon />
625
675
  </button>
@@ -636,7 +686,7 @@ export function GridToolbar({
636
686
  onHideAll={handleHideAllColumns}
637
687
  onColumnReorder={onColumnReorder}
638
688
  onColumnOrderReset={onColumnOrderReset}
639
- onClose={() => { setColsOpen(false); onColumnsPanelClose?.(); }}
689
+ onClose={() => closePanel('columns')}
640
690
  showNonHideableColumns={showNonHideableColumns}
641
691
  />
642
692
  )}
@@ -648,22 +698,20 @@ export function GridToolbar({
648
698
  <div ref={filterWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
649
699
  {renderFilterButton
650
700
  ? renderFilterButton({
651
- onClick: () => { setFilterOpen(o => !o); setAggOpen(false); setPivotOpen(false); setColsOpen(false); },
701
+ onClick: () => togglePanel('filters'),
652
702
  isOpen: filterOpen,
653
703
  activeCount: activeFilterCount,
654
704
  })
655
705
  : (
656
706
  <GridTooltip title="Filters">
657
707
  <button
708
+ type="button"
658
709
  ref={filterButtonRef}
659
710
  className={`ogx-toolbar__icon-btn${filterOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
660
711
  aria-label="Advanced filters"
661
- onClick={() => {
662
- setFilterOpen(!filterOpen);
663
- setAggOpen(false);
664
- setPivotOpen(false);
665
- setColsOpen(false);
666
- }}
712
+ aria-haspopup="dialog"
713
+ aria-expanded={filterOpen}
714
+ onClick={() => togglePanel('filters')}
667
715
  >
668
716
  <FilterIcon />
669
717
  {activeFilterCount > 0 && (
@@ -679,7 +727,7 @@ export function GridToolbar({
679
727
  columns={columns}
680
728
  filterModel={filterModel || { items: [] }}
681
729
  onFilterModelChange={onFilterModelChange}
682
- onClose={() => setFilterOpen(false)}
730
+ onClose={() => closePanel('filters')}
683
731
  />
684
732
  )}
685
733
  </div>
@@ -690,16 +738,13 @@ export function GridToolbar({
690
738
  <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
691
739
  <GridTooltip title="Pivot">
692
740
  <button
741
+ type="button"
693
742
  ref={pivotButtonRef}
694
743
  className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
695
744
  aria-label="Configure pivot"
745
+ aria-haspopup="dialog"
696
746
  aria-expanded={pivotOpen}
697
- onClick={() => {
698
- setPivotOpen(!pivotOpen);
699
- setAggOpen(false);
700
- setColsOpen(false);
701
- setFilterOpen(false);
702
- }}
747
+ onClick={() => togglePanel('pivot')}
703
748
  >
704
749
  <PivotIcon />
705
750
  {pivotActive && (
@@ -714,55 +759,54 @@ export function GridToolbar({
714
759
  columns={baseColumns ?? columns}
715
760
  model={currentPivotModel}
716
761
  onChange={(m) => { onPivotModelChange?.(m); }}
717
- onClose={() => setPivotOpen(false)}
762
+ onClose={() => closePanel('pivot')}
718
763
  />
719
764
  )}
720
765
  </div>
721
766
  )}
722
767
 
723
768
  {/* Summaries button */}
724
- <div ref={aggWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
725
- {renderAggregationButton
726
- ? renderAggregationButton({
727
- onClick: () => { setAggOpen(o => !o); setPivotOpen(false); setColsOpen(false); setFilterOpen(false); },
728
- isOpen: aggOpen,
729
- activeCount,
730
- })
731
- : (
732
- <GridTooltip title="Summaries">
733
- <button
734
- ref={aggButtonRef}
735
- className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
736
- aria-label="Configure summaries"
737
- aria-expanded={aggOpen}
738
- onClick={() => {
739
- setAggOpen(!aggOpen);
740
- setPivotOpen(false);
741
- setColsOpen(false);
742
- setFilterOpen(false);
743
- }}
744
- >
745
- <SigmaIcon />
746
- {activeCount > 0 && (
747
- <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
748
- )}
749
- </button>
750
- </GridTooltip>
751
- )
752
- }
753
-
754
- {aggOpen && (
755
- <AggregationPanel
756
- anchorRef={renderAggregationButton ? aggWrapperRef : aggButtonRef}
757
- aggregationModel={aggregationModel}
758
- aggregableColumns={aggregableColumns}
759
- onFunctionChange={handleFunctionChange}
760
- onClearAll={clearAllAgg}
761
- onClose={() => setAggOpen(false)}
762
- activeCount={activeCount}
763
- />
764
- )}
765
- </div>
769
+ {onAggregationModelChange && (
770
+ <div ref={aggWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
771
+ {renderAggregationButton
772
+ ? renderAggregationButton({
773
+ onClick: () => togglePanel('summaries'),
774
+ isOpen: aggOpen,
775
+ activeCount,
776
+ })
777
+ : (
778
+ <GridTooltip title="Summaries">
779
+ <button
780
+ type="button"
781
+ ref={aggButtonRef}
782
+ className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
783
+ aria-label="Configure summaries"
784
+ aria-haspopup="dialog"
785
+ aria-expanded={aggOpen}
786
+ onClick={() => togglePanel('summaries')}
787
+ >
788
+ <SigmaIcon />
789
+ {activeCount > 0 && (
790
+ <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
791
+ )}
792
+ </button>
793
+ </GridTooltip>
794
+ )
795
+ }
796
+
797
+ {aggOpen && (
798
+ <AggregationPanel
799
+ anchorRef={renderAggregationButton ? aggWrapperRef : aggButtonRef}
800
+ aggregationModel={aggregationModel}
801
+ aggregableColumns={aggregableColumns}
802
+ onFunctionChange={handleFunctionChange}
803
+ onClearAll={clearAllAgg}
804
+ onClose={() => closePanel('summaries')}
805
+ activeCount={activeCount}
806
+ />
807
+ )}
808
+ </div>
809
+ )}
766
810
 
767
811
  {/* Export button slot */}
768
812
  {renderExportButton && renderExportButton()}