@opencorestack/opengridx 3.9.0 → 3.10.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 (65) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +11 -7
  3. package/dist/index.d.ts +55 -0
  4. package/dist/opengridx.es.js +6346 -6279
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +4 -0
  9. package/docs/architecture/datagrid-orchestration.md +4 -2
  10. package/docs/architecture/use-grid-virtualization.md +2 -0
  11. package/docs/components/pagination.md +1 -1
  12. package/docs/features/ai-toolkit.md +1 -0
  13. package/docs/features/rtl.md +1 -1
  14. package/docs/performance.md +19 -1
  15. package/docs/roadmap.md +26 -5
  16. package/lib/components/Cell/Cell.tsx +2 -1
  17. package/lib/components/DataGrid/DataGrid.tsx +19 -18
  18. package/lib/components/DataGrid/GridListView.tsx +1 -1
  19. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  20. package/lib/components/DataGrid/GridViewport.tsx +2 -1
  21. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  22. package/lib/components/Header/Header.tsx +4 -3
  23. package/lib/components/Row/Row.tsx +6 -2
  24. package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
  25. package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
  26. package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
  27. package/lib/components/Toolbar/GridToolbar.tsx +7 -380
  28. package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
  29. package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
  30. package/lib/hooks/core/gridKeyDown.ts +272 -0
  31. package/lib/hooks/core/useDataGrid.ts +13 -5
  32. package/lib/hooks/core/useGridApiMethods.ts +2 -2
  33. package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
  34. package/lib/hooks/core/useGridCellSelection.ts +17 -297
  35. package/lib/hooks/core/useGridColumnLookup.ts +2 -1
  36. package/lib/hooks/core/useGridColumns.tsx +3 -2
  37. package/lib/hooks/core/useGridControlledState.ts +3 -2
  38. package/lib/hooks/core/useGridHierarchy.ts +1 -1
  39. package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
  40. package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
  41. package/lib/hooks/core/useGridRowInteractions.ts +1 -1
  42. package/lib/hooks/core/useGridRowSelection.ts +11 -2
  43. package/lib/hooks/core/useGridToolbarProps.ts +3 -2
  44. package/lib/hooks/features/useGridAiAssistant.ts +3 -2
  45. package/lib/hooks/useColumnReorder.ts +2 -1
  46. package/lib/hooks/useTreeData.ts +4 -2
  47. package/lib/types/locale.ts +22 -0
  48. package/lib/utils/aggregation/index.ts +16 -0
  49. package/lib/utils/aiAssistant.ts +46 -15
  50. package/lib/utils/export/excelCells.ts +244 -0
  51. package/lib/utils/export/exportCsv.ts +179 -0
  52. package/lib/utils/export/exportExcelHtml.ts +220 -0
  53. package/lib/utils/export/exportJson.ts +179 -0
  54. package/lib/utils/export/exportRows.ts +85 -0
  55. package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
  56. package/lib/utils/export/index.ts +5 -695
  57. package/lib/utils/export/printGrid.ts +282 -0
  58. package/lib/utils/gridLocale.ts +30 -0
  59. package/lib/utils/selectionSets.ts +36 -0
  60. package/lib/utils/untypedColumns.ts +16 -0
  61. package/llms.txt +3 -1
  62. package/package.json +6 -2
  63. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
  64. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
  65. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
@@ -0,0 +1,82 @@
1
+ import ReactDOM from 'react-dom';
2
+ import type { GridColDef, GridFilterModel } from '../../types';
3
+ import { FilterPanel } from '../FilterPanel/FilterPanel';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
5
+ import { FilterIcon } from './ToolbarIcons';
6
+ import { useToolbarPanel } from './useToolbarPanel';
7
+
8
+ /** The toolbar's Filters panel: `FilterPanel` with a count, Clear all and Close. */
9
+ export function FilterPanelWrapper({
10
+ anchorRef,
11
+ columns,
12
+ filterModel,
13
+ onFilterModelChange,
14
+ onClose
15
+ }: {
16
+ anchorRef: React.RefObject<HTMLElement | null>;
17
+ columns: GridColDef[];
18
+ filterModel: GridFilterModel;
19
+ onFilterModelChange: (model: GridFilterModel) => void;
20
+ onClose: () => void;
21
+ }) {
22
+ const locale = useGridLocale();
23
+ // Escape closes it; a press outside does not: users type in its inputs, and it has a Close button.
24
+ const { panelRef, position: pos, portalContainer } = useToolbarPanel(anchorRef, onClose, { closeOnOutsideClick: false });
25
+
26
+ const activeFilterCount = filterModel.items?.length ?? 0;
27
+
28
+ const handleClearAll = () => {
29
+ onFilterModelChange({ ...filterModel, items: [], logicOperator: 'and' });
30
+ };
31
+
32
+ const panel = (
33
+ <div
34
+ ref={panelRef}
35
+ className="ogx-toolbar__panel ogx-toolbar__panel--filter"
36
+ role="dialog"
37
+ aria-label={locale.toolbarFiltersLabel}
38
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
39
+ >
40
+ <div className="ogx-toolbar__panel-header">
41
+ <span className="ogx-toolbar__panel-title">
42
+ <FilterIcon />
43
+ {locale.toolbarFilters}
44
+ {activeFilterCount > 0 && (
45
+ <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
46
+ )}
47
+ </span>
48
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
49
+ {activeFilterCount > 0 && (
50
+ <button
51
+ type="button"
52
+ className="ogx-toolbar__clear-btn"
53
+ onClick={handleClearAll}
54
+ title={locale.toolbarFiltersClearAllTooltip}
55
+ >
56
+ {locale.toolbarFiltersClearAll}
57
+ </button>
58
+ )}
59
+ <button
60
+ type="button"
61
+ className="ogx-toolbar__close-btn"
62
+ onClick={onClose}
63
+ title={locale.toolbarFiltersCloseTooltip}
64
+ aria-label={locale.toolbarFiltersCloseLabel}
65
+ >
66
+ {locale.toolbarFiltersClose}
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
72
+ <FilterPanel
73
+ columns={columns}
74
+ filterModel={filterModel}
75
+ onFilterModelChange={onFilterModelChange}
76
+ />
77
+ </div>
78
+ </div>
79
+ );
80
+
81
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
82
+ }
@@ -1,18 +1,16 @@
1
- import React, { useState, useRef, useEffect, useCallback, useLayoutEffect, useContext } from 'react';
2
- import ReactDOM from 'react-dom';
1
+ import React, { useState, useRef, useEffect, useCallback, useContext } from 'react';
3
2
  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 { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
3
+ import { getOfferedAggregationFunctions } from '../../utils/aggregation';
8
4
  import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
9
5
  import { PivotPanel, PivotIcon } from './PivotPanel';
10
6
  import { GlobalSearch } from './GlobalSearch';
11
- import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
12
- import { FilterPanel } from '../FilterPanel/FilterPanel';
13
7
  import { GridTooltip } from '../Tooltip/Tooltip';
14
8
  import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
15
9
  import { useGridLocale } from '../DataGrid/gridLocaleContext';
10
+ import { SigmaIcon, ViewColumnIcon, FilterIcon } from './ToolbarIcons';
11
+ import { AggregationPanel } from './AggregationPanel';
12
+ import { ColumnsPanelWrapper } from './ColumnsPanelWrapper';
13
+ import { FilterPanelWrapper } from './FilterPanelWrapper';
16
14
 
17
15
  /** Props passed to a custom toolbar button renderer. */
18
16
  export interface ToolbarButtonRenderProps {
@@ -99,379 +97,8 @@ export interface GridToolbarProps {
99
97
  aiAssistantLabel?: string;
100
98
  }
101
99
 
102
- const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
103
-
104
- function isBuiltInAggregationFunction(fn: string): fn is BuiltInAggFn {
105
- return Object.prototype.hasOwnProperty.call(AGGREGATION_FUNCTIONS, fn);
106
- }
107
-
108
- /**
109
- * The summary functions offered for a column: its `availableAggregationFunctions` when set,
110
- * otherwise every built-in one. Names the aggregation engine does not implement are dropped,
111
- * because choosing one would show the label in the header but never compute a value.
112
- */
113
- function getAggregationFunctions(col: GridColDef): BuiltInAggFn[] {
114
- if (!col.availableAggregationFunctions) return BUILT_IN_AGGREGATION_FUNCTIONS;
115
- return Array.from(new Set(col.availableAggregationFunctions.filter(isBuiltInAggregationFunction)));
116
- }
117
-
118
100
  type ToolbarPanel = 'summaries' | 'pivot' | 'columns' | 'filters';
119
101
 
120
- function SigmaIcon() {
121
- return (
122
- <svg
123
- width="18" height="18" viewBox="0 0 24 24"
124
- fill="none" stroke="currentColor" strokeWidth="2"
125
- strokeLinecap="round" strokeLinejoin="round"
126
- aria-hidden="true"
127
- >
128
- <path d="M18 4H6l6 8-6 8h12" />
129
- </svg>
130
- );
131
- }
132
-
133
- function ViewColumnIcon() {
134
- return (
135
- <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none">
136
- <rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="1.8" />
137
- <rect x="8.5" y="4" width="7" height="16" fill="currentColor" opacity="0.25" />
138
- <line x1="8.5" y1="4" x2="8.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
139
- <line x1="15.5" y1="4" x2="15.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
140
- <line x1="2" y1="9" x2="22" y2="9" stroke="currentColor" strokeWidth="1.2" />
141
- <line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" strokeWidth="1.2" />
142
- </svg>
143
- );
144
- }
145
-
146
- function FilterIcon() {
147
- return (
148
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
149
- <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
150
- </svg>
151
- );
152
- }
153
-
154
- type PanelPosition = GridPanelPosition;
155
-
156
- function AggregationPanel({
157
- anchorRef,
158
- aggregationModel,
159
- aggregableColumns,
160
- onFunctionChange,
161
- onClearAll,
162
- onClose,
163
- activeCount,
164
- }: {
165
- anchorRef: React.RefObject<HTMLElement | null>;
166
- aggregationModel: GridAggregationModel;
167
- aggregableColumns: GridColDef[];
168
- onFunctionChange: (field: string, fn: string) => void;
169
- onClearAll: () => void;
170
- onClose: () => void;
171
- activeCount: number;
172
- }) {
173
- const locale = useGridLocale();
174
- const panelRef = useRef<HTMLDivElement>(null);
175
- const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
176
- const [portalContainer, setPortalContainer] = useState<Element | null>(null);
177
-
178
- useLayoutEffect(() => {
179
- setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
180
- }, [anchorRef]);
181
-
182
- const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
183
-
184
- useEffect(() => {
185
- setPos(computePos());
186
- const update = () => setPos(computePos());
187
- window.addEventListener('scroll', update, true);
188
- window.addEventListener('resize', update);
189
- return () => {
190
- window.removeEventListener('scroll', update, true);
191
- window.removeEventListener('resize', update);
192
- };
193
- }, [computePos]);
194
-
195
- useEffect(() => {
196
- function handleClick(e: MouseEvent) {
197
- if (
198
- panelRef.current && !panelRef.current.contains(e.target as Node) &&
199
- anchorRef.current && !anchorRef.current.contains(e.target as Node)
200
- ) {
201
- onClose();
202
- }
203
- }
204
- document.addEventListener('mousedown', handleClick);
205
- return () => document.removeEventListener('mousedown', handleClick);
206
- }, [anchorRef, onClose]);
207
-
208
- useEffect(() => {
209
- function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
210
- document.addEventListener('keydown', onKey);
211
- return () => document.removeEventListener('keydown', onKey);
212
- }, [onClose]);
213
-
214
- const panel = (
215
- <div
216
- ref={panelRef}
217
- className="ogx-toolbar__panel"
218
- role="dialog"
219
- aria-label={locale.toolbarSummariesPanelLabel}
220
- style={{ top: pos.top, right: pos.right, left: pos.left, width: 400, maxWidth: 'calc(100vw - 24px)' }}
221
- >
222
- <div className="ogx-toolbar__panel-header">
223
- <span className="ogx-toolbar__panel-title">
224
- <SigmaIcon />
225
- {locale.toolbarSummaries}
226
- </span>
227
- {activeCount > 0 && (
228
- <button
229
- type="button"
230
- className="ogx-toolbar__clear-btn"
231
- onClick={onClearAll}
232
- title={locale.toolbarSummariesClearAllTooltip}
233
- >
234
- {locale.toolbarSummariesClearAll}
235
- </button>
236
- )}
237
- </div>
238
-
239
- <div className="ogx-toolbar__panel-body">
240
- {aggregableColumns.length === 0 ? (
241
- <p className="ogx-toolbar__empty">
242
- {locale.toolbarSummariesEmpty}<br />
243
- {`${locale.toolbarSummariesEmptyHintBefore} `}<code>aggregable: true</code>{` ${locale.toolbarSummariesEmptyHintAfter}`}
244
- </p>
245
- ) : (
246
- aggregableColumns.map((col) => {
247
- const currentFn = aggregationModel[col.field] || 'none';
248
- return (
249
- <div key={col.field} className="ogx-toolbar__agg-row">
250
- <span className="ogx-toolbar__agg-label" title={col.field}>
251
- {col.headerName || col.field}
252
- </span>
253
- <div className="ogx-toolbar__agg-pills">
254
- {['none', ...getAggregationFunctions(col)].map((fn) => (
255
- <button
256
- type="button"
257
- key={fn}
258
- className={`ogx-toolbar__pill${currentFn === fn ? ' ogx-toolbar__pill--active' : ''}`}
259
- onClick={() => onFunctionChange(col.field, fn)}
260
- aria-pressed={currentFn === fn}
261
- >
262
- {locale.toolbarAggregationFunction(fn)}
263
- </button>
264
- ))}
265
- </div>
266
- </div>
267
- );
268
- })
269
- )}
270
- </div>
271
- </div>
272
- );
273
-
274
- return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
275
- }
276
-
277
- function ColumnsPanelWrapper({
278
- anchorRef,
279
- columns,
280
- visibleColumns,
281
- onVisibilityChange,
282
- onShowAll,
283
- onHideAll,
284
- onColumnReorder,
285
- onColumnOrderReset,
286
- onClose,
287
- showNonHideableColumns,
288
- }: {
289
- anchorRef: React.RefObject<HTMLElement | null>;
290
- columns: GridColDef[];
291
- visibleColumns: Set<string>;
292
- onVisibilityChange: (field: string, isVisible: boolean) => void;
293
- onShowAll: () => void;
294
- onHideAll: () => void;
295
- onColumnReorder?: (fromField: string, toField: string) => void;
296
- onColumnOrderReset?: () => void;
297
- onClose: () => void;
298
- showNonHideableColumns?: boolean;
299
- }) {
300
- const locale = useGridLocale();
301
- const panelRef = useRef<HTMLDivElement>(null);
302
- const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
303
- const [portalContainer, setPortalContainer] = useState<Element | null>(null);
304
-
305
- useLayoutEffect(() => {
306
- setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
307
- }, [anchorRef]);
308
-
309
- const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
310
-
311
- useEffect(() => {
312
- setPos(computePos());
313
- const update = () => setPos(computePos());
314
- window.addEventListener('scroll', update, true);
315
- window.addEventListener('resize', update);
316
- return () => {
317
- window.removeEventListener('scroll', update, true);
318
- window.removeEventListener('resize', update);
319
- };
320
- }, [computePos]);
321
-
322
- useEffect(() => {
323
- function handleClick(e: MouseEvent) {
324
- if (
325
- panelRef.current && !panelRef.current.contains(e.target as Node) &&
326
- anchorRef.current && !anchorRef.current.contains(e.target as Node)
327
- ) {
328
- onClose();
329
- }
330
- }
331
- document.addEventListener('mousedown', handleClick);
332
- return () => document.removeEventListener('mousedown', handleClick);
333
- }, [anchorRef, onClose]);
334
-
335
- useEffect(() => {
336
- function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); }
337
- document.addEventListener('keydown', onKey);
338
- return () => document.removeEventListener('keydown', onKey);
339
- }, [onClose]);
340
-
341
- const panel = (
342
- <div
343
- ref={panelRef}
344
- className="ogx-toolbar__panel"
345
- role="dialog"
346
- aria-label={locale.toolbarColumnsPanelLabel}
347
- style={{ top: pos.top, right: pos.right, left: pos.left, width: 280, padding: 0 }}
348
- >
349
- <ColumnVisibilityPanel
350
- columns={columns}
351
- visibleColumns={visibleColumns}
352
- onVisibilityChange={onVisibilityChange}
353
- onShowAll={onShowAll}
354
- onHideAll={onHideAll}
355
- onColumnReorder={onColumnReorder}
356
- onColumnOrderReset={onColumnOrderReset}
357
- showNonHideableColumns={showNonHideableColumns}
358
- />
359
- </div>
360
- );
361
-
362
- return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
363
- }
364
-
365
- function FilterPanelWrapper({
366
- anchorRef,
367
- columns,
368
- filterModel,
369
- onFilterModelChange,
370
- onClose
371
- }: {
372
- anchorRef: React.RefObject<HTMLElement | null>;
373
- columns: GridColDef[];
374
- filterModel: GridFilterModel;
375
- onFilterModelChange: (model: GridFilterModel) => void;
376
- onClose: () => void;
377
- }) {
378
- const locale = useGridLocale();
379
- const panelRef = useRef<HTMLDivElement>(null);
380
- const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
381
- // Stable ref for onClose — prevents the mousedown effect from
382
- // tearing down and re-registering on every filterModel update,
383
- // which could fire the close callback during the re-register window.
384
- const onCloseRef = useRef(onClose);
385
- useLayoutEffect(() => { onCloseRef.current = onClose; }, [onClose]);
386
- const [portalContainer, setPortalContainer] = useState<Element | null>(null);
387
-
388
- useLayoutEffect(() => {
389
- setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
390
- }, [anchorRef]);
391
-
392
- const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
393
-
394
- useEffect(() => {
395
- setPos(computePos());
396
- const update = () => setPos(computePos());
397
- window.addEventListener('scroll', update, true);
398
- window.addEventListener('resize', update);
399
- return () => {
400
- window.removeEventListener('scroll', update, true);
401
- window.removeEventListener('resize', update);
402
- };
403
- }, [computePos]);
404
-
405
- // Only Escape key closes the filter panel automatically.
406
- // Click-outside is intentionally removed: users need to type in filter
407
- // inputs without the panel closing. There is an explicit Close button.
408
- useEffect(() => {
409
- function handleKey(e: KeyboardEvent) {
410
- if (e.key === 'Escape') onCloseRef.current();
411
- }
412
- document.addEventListener('keydown', handleKey);
413
- return () => document.removeEventListener('keydown', handleKey);
414
- // Empty deps — listener is registered once and uses the stable ref
415
- }, []);
416
-
417
- const activeFilterCount = filterModel.items?.length ?? 0;
418
-
419
- const handleClearAll = () => {
420
- onFilterModelChange({ ...filterModel, items: [], logicOperator: 'and' });
421
- };
422
-
423
- const panel = (
424
- <div
425
- ref={panelRef}
426
- className="ogx-toolbar__panel ogx-toolbar__panel--filter"
427
- role="dialog"
428
- aria-label={locale.toolbarFiltersLabel}
429
- style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
430
- >
431
- <div className="ogx-toolbar__panel-header">
432
- <span className="ogx-toolbar__panel-title">
433
- <FilterIcon />
434
- {locale.toolbarFilters}
435
- {activeFilterCount > 0 && (
436
- <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
437
- )}
438
- </span>
439
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
440
- {activeFilterCount > 0 && (
441
- <button
442
- type="button"
443
- className="ogx-toolbar__clear-btn"
444
- onClick={handleClearAll}
445
- title={locale.toolbarFiltersClearAllTooltip}
446
- >
447
- {locale.toolbarFiltersClearAll}
448
- </button>
449
- )}
450
- <button
451
- type="button"
452
- className="ogx-toolbar__close-btn"
453
- onClick={() => onCloseRef.current()}
454
- title={locale.toolbarFiltersCloseTooltip}
455
- aria-label={locale.toolbarFiltersCloseLabel}
456
- >
457
- {locale.toolbarFiltersClose}
458
- </button>
459
- </div>
460
- </div>
461
-
462
- <div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
463
- <FilterPanel
464
- columns={columns}
465
- filterModel={filterModel}
466
- onFilterModelChange={onFilterModelChange}
467
- />
468
- </div>
469
- </div>
470
- );
471
-
472
- return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
473
- }
474
-
475
102
  const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
476
103
  // Module-level default, so `columns` is the same frozen value on every render (React Compiler).
477
104
  const NO_COLUMNS: GridColDef[] = [];
@@ -576,7 +203,7 @@ export function GridToolbar({
576
203
  const baseFields = baseColumns ? new Set(baseColumns.map((c) => c.field)) : null;
577
204
  const aggregableColumns = columns.filter(
578
205
  (c) => c.aggregable !== false && (c.type === 'number' || c.aggregable === true)
579
- && getAggregationFunctions(c).length > 0
206
+ && getOfferedAggregationFunctions(c).length > 0
580
207
  && (!baseFields || baseFields.has(c.field))
581
208
  );
582
209
 
@@ -0,0 +1,35 @@
1
+ // Icons of the toolbar buttons and panel titles.
2
+
3
+ export function SigmaIcon() {
4
+ return (
5
+ <svg
6
+ width="18" height="18" viewBox="0 0 24 24"
7
+ fill="none" stroke="currentColor" strokeWidth="2"
8
+ strokeLinecap="round" strokeLinejoin="round"
9
+ aria-hidden="true"
10
+ >
11
+ <path d="M18 4H6l6 8-6 8h12" />
12
+ </svg>
13
+ );
14
+ }
15
+
16
+ export function ViewColumnIcon() {
17
+ return (
18
+ <svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none">
19
+ <rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="1.8" />
20
+ <rect x="8.5" y="4" width="7" height="16" fill="currentColor" opacity="0.25" />
21
+ <line x1="8.5" y1="4" x2="8.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
22
+ <line x1="15.5" y1="4" x2="15.5" y2="20" stroke="currentColor" strokeWidth="1.8" />
23
+ <line x1="2" y1="9" x2="22" y2="9" stroke="currentColor" strokeWidth="1.2" />
24
+ <line x1="2" y1="14" x2="22" y2="14" stroke="currentColor" strokeWidth="1.2" />
25
+ </svg>
26
+ );
27
+ }
28
+
29
+ export function FilterIcon() {
30
+ return (
31
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
32
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
33
+ </svg>
34
+ );
35
+ }
@@ -0,0 +1,71 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { getViewportWidth } from '../../utils/viewport';
3
+ import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
4
+
5
+ export interface UseToolbarPanelOptions {
6
+ /** Close when the user presses the mouse outside the panel and its button. Default: true. */
7
+ closeOnOutsideClick?: boolean;
8
+ }
9
+
10
+ export interface UseToolbarPanelResult {
11
+ panelRef: React.RefObject<HTMLDivElement | null>;
12
+ /** Where to place the panel: under its button, kept on screen (`getPanelPosition`). */
13
+ position: GridPanelPosition;
14
+ /** The element to portal into: the enclosing theme provider (so its tokens apply), else `document.body`. */
15
+ portalContainer: Element | null;
16
+ }
17
+
18
+ /**
19
+ * What every toolbar panel needs: a portal target, a position under its button that follows page
20
+ * scroll and resize, and closing on Escape (and, by default, on a press outside it). `onClose` is
21
+ * read through a ref, so the listeners are registered once and a re-render never leaves a window in
22
+ * which a close could be lost or fired twice.
23
+ */
24
+ export function useToolbarPanel(
25
+ anchorRef: React.RefObject<HTMLElement | null>,
26
+ onClose: () => void,
27
+ { closeOnOutsideClick = true }: UseToolbarPanelOptions = {},
28
+ ): UseToolbarPanelResult {
29
+ const panelRef = useRef<HTMLDivElement>(null);
30
+ const [position, setPosition] = useState<GridPanelPosition>({ top: 0, right: 0 });
31
+ const [portalContainer, setPortalContainer] = useState<Element | null>(null);
32
+ const onCloseRef = useRef(onClose);
33
+ useLayoutEffect(() => { onCloseRef.current = onClose; });
34
+
35
+ useLayoutEffect(() => {
36
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
37
+ }, [anchorRef]);
38
+
39
+ const computePosition = useCallback(() => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
40
+
41
+ useEffect(() => {
42
+ setPosition(computePosition());
43
+ const update = () => setPosition(computePosition());
44
+ window.addEventListener('scroll', update, true);
45
+ window.addEventListener('resize', update);
46
+ return () => {
47
+ window.removeEventListener('scroll', update, true);
48
+ window.removeEventListener('resize', update);
49
+ };
50
+ }, [computePosition]);
51
+
52
+ useEffect(() => {
53
+ if (!closeOnOutsideClick) return undefined;
54
+ function handleMouseDown(e: MouseEvent) {
55
+ const target = e.target as Node;
56
+ if (panelRef.current && !panelRef.current.contains(target) && anchorRef.current && !anchorRef.current.contains(target)) {
57
+ onCloseRef.current();
58
+ }
59
+ }
60
+ document.addEventListener('mousedown', handleMouseDown);
61
+ return () => document.removeEventListener('mousedown', handleMouseDown);
62
+ }, [anchorRef, closeOnOutsideClick]);
63
+
64
+ useEffect(() => {
65
+ function handleKey(e: KeyboardEvent) { if (e.key === 'Escape') onCloseRef.current(); }
66
+ document.addEventListener('keydown', handleKey);
67
+ return () => document.removeEventListener('keydown', handleKey);
68
+ }, []);
69
+
70
+ return { panelRef, position, portalContainer };
71
+ }