@opencorestack/opengridx 0.1.7 → 0.1.9

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.
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type {
3
3
  GridFilterModel,
4
4
  GridFilterItem,
@@ -29,6 +29,37 @@ const FilterRow: React.FC<{
29
29
  const isActive = !!item;
30
30
  const currentOperator = item?.operator ?? operators[0];
31
31
 
32
+ // Local input state to prevent full filter-panel re-render on every keystroke
33
+ const [localValue, setLocalValue] = useState(item?.value ?? '');
34
+ const onChangeRef = useRef(onChange);
35
+ onChangeRef.current = onChange;
36
+
37
+ // Sync when the external item changes (e.g. operator change, clear)
38
+ const prevItemValue = useRef(item?.value);
39
+ useEffect(() => {
40
+ if (item?.value !== prevItemValue.current) {
41
+ prevItemValue.current = item?.value;
42
+ setLocalValue(item?.value ?? '');
43
+ }
44
+ }, [item?.value]);
45
+
46
+ // Debounce propagating the value upward (300ms)
47
+ useEffect(() => {
48
+ const handler = setTimeout(() => {
49
+ const currentValue = item?.value ?? '';
50
+ if (localValue !== currentValue) {
51
+ onChangeRef.current({
52
+ id: item?.id ?? `${col.field}-${Date.now()}`,
53
+ field: col.field,
54
+ operator: currentOperator as GridFilterOperator,
55
+ value: localValue,
56
+ });
57
+ }
58
+ }, 300);
59
+ return () => clearTimeout(handler);
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, [localValue]);
62
+
32
63
  const handleOperatorChange = (op: string) => {
33
64
  const noValue = NO_VALUE_OPERATORS.has(op);
34
65
  const newItem: GridFilterItem = {
@@ -40,16 +71,8 @@ const FilterRow: React.FC<{
40
71
  onChange(newItem);
41
72
  };
42
73
 
43
- const handleValueChange = (value: string) => {
44
- onChange({
45
- id: item?.id ?? `${col.field}-${Date.now()}`,
46
- field: col.field,
47
- operator: currentOperator as GridFilterOperator,
48
- value,
49
- });
50
- };
51
-
52
74
  const handleClear = () => {
75
+ setLocalValue('');
53
76
  onChange(null);
54
77
  };
55
78
 
@@ -87,7 +110,15 @@ const FilterRow: React.FC<{
87
110
  name={`filter-val-${col.field}`}
88
111
  className={`ogx-filter__value-input ogx-filter__value-select${isActive ? ' ogx-filter__value-input--active' : ''}`}
89
112
  value={String(item?.value ?? 'true')}
90
- onChange={(e) => handleValueChange(e.target.value)}
113
+ onChange={(e) => {
114
+ setLocalValue(e.target.value);
115
+ onChange({
116
+ id: item?.id ?? `${col.field}-${Date.now()}`,
117
+ field: col.field,
118
+ operator: currentOperator as GridFilterOperator,
119
+ value: e.target.value,
120
+ });
121
+ }}
91
122
  aria-label={`Filter value for ${col.headerName || col.field}`}
92
123
  >
93
124
  <option value="true">true</option>
@@ -100,10 +131,10 @@ const FilterRow: React.FC<{
100
131
  id={`ogx-filter-val-${col.field}`}
101
132
  name={`filter-val-${col.field}`}
102
133
  className={`ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`}
103
- type={col.type === 'number' ? 'number' : 'text'}
104
- value={item?.value ?? ''}
105
- onChange={(e) => handleValueChange(e.target.value)}
106
- placeholder="Value…"
134
+ type="text"
135
+ value={localValue}
136
+ onChange={(e) => setLocalValue(e.target.value)}
137
+ placeholder={col.type === 'number' ? 'Number…' : 'Value…'}
107
138
  aria-label={`Filter value for ${col.headerName || col.field}`}
108
139
  />
109
140
  )}
@@ -97,6 +97,45 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
97
97
  const expandCellRef = React.useRef<HTMLDivElement>(null);
98
98
  const checkboxCellRef = React.useRef<HTMLDivElement>(null);
99
99
 
100
+ // All hooks must come before any early returns (Rules of Hooks)
101
+ React.useLayoutEffect(() => {
102
+ if (focusedCellField === '__expand_col__' && expandCellRef.current) {
103
+ expandCellRef.current.focus({ preventScroll: true });
104
+ }
105
+ if (focusedCellField === '__checkbox_col__' && checkboxCellRef.current) {
106
+ checkboxCellRef.current.focus({ preventScroll: true });
107
+ }
108
+ }, [focusedCellField]);
109
+
110
+ const pinnedPositions = React.useMemo(() => {
111
+ return calculatePinnedPositions(
112
+ columns,
113
+ columnWidths,
114
+ pinnedColumns,
115
+ checkboxSelection,
116
+ pinCheckboxColumn,
117
+ hasDetailPanel,
118
+ pinExpandColumn,
119
+ rowReordering
120
+ );
121
+ }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
122
+
123
+ const isGroupRow = (row as any)._hasChildren === true;
124
+
125
+ const handleCellEditStart = React.useCallback((field: string, value: any) => {
126
+ if (!isGroupRow) {
127
+ onEditStart?.({ id: row.id, field, value });
128
+ }
129
+ }, [isGroupRow, row.id, onEditStart]);
130
+
131
+ const handleCellValueChange = React.useCallback((field: string, newValue: any) => {
132
+ onEditCellValueChange?.({ id: row.id, field, value: newValue });
133
+ }, [onEditCellValueChange, row.id]);
134
+
135
+ const handleEditStopWrapper = React.useCallback((cancel?: boolean) => {
136
+ onEditStop?.({ cancel });
137
+ }, [onEditStop]);
138
+
100
139
  // ── Skeleton row (shown during infinite-scroll fetch) ─────────────────────
101
140
  if ((row as any)._isSkeleton) {
102
141
  // Render shimmer placeholders — one per visible column, matching row height
@@ -117,15 +156,6 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
117
156
  );
118
157
  }
119
158
 
120
- React.useLayoutEffect(() => {
121
- if (focusedCellField === '__expand_col__' && expandCellRef.current) {
122
- expandCellRef.current.focus({ preventScroll: true });
123
- }
124
- if (focusedCellField === '__checkbox_col__' && checkboxCellRef.current) {
125
- checkboxCellRef.current.focus({ preventScroll: true });
126
- }
127
- }, [focusedCellField]);
128
-
129
159
  const handleRowClick = (event: React.MouseEvent) => {
130
160
 
131
161
  if ((event.target as HTMLElement).closest('.ogx-checkbox-wrapper, .ogx-expand-icon, .ogx-drag-handle, .ogx__edit-cell')) {
@@ -144,39 +174,10 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
144
174
  onDetailPanelToggle?.(row.id);
145
175
  };
146
176
 
147
- const pinnedPositions = React.useMemo(() => {
148
- return calculatePinnedPositions(
149
- columns,
150
- columnWidths,
151
- pinnedColumns,
152
- checkboxSelection,
153
- pinCheckboxColumn,
154
- hasDetailPanel,
155
- pinExpandColumn,
156
- rowReordering
157
- );
158
- }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
159
-
160
177
  const rowPinnedPosition = isRowPinned(row.id, pinnedRows);
161
178
  const isRowPinnedTop = rowPinnedPosition === 'top';
162
179
  const isRowPinnedBottom = rowPinnedPosition === 'bottom';
163
180
 
164
- const isGroupRow = (row as any)._hasChildren === true;
165
-
166
- const handleCellEditStart = React.useCallback((field: string, value: any) => {
167
- if (!isGroupRow) {
168
- onEditStart?.({ id: row.id, field, value });
169
- }
170
- }, [isGroupRow, row.id, onEditStart]);
171
-
172
- const handleCellValueChange = React.useCallback((field: string, newValue: any) => {
173
- onEditCellValueChange?.({ id: row.id, field, value: newValue });
174
- }, [onEditCellValueChange, row.id]);
175
-
176
- const handleEditStopWrapper = React.useCallback((cancel?: boolean) => {
177
- onEditStop?.({ cancel });
178
- }, [onEditStop]);
179
-
180
181
  const classNames = [
181
182
  'ogx__row',
182
183
  isSelected && 'ogx__row--selected',
@@ -40,7 +40,6 @@
40
40
 
41
41
  /* ─── Icon wrapper ────────────────────────────────────────────────────────── */
42
42
  .ogx-global-search__icon-wrapper {
43
- /* Fixed 34×34 square — never shrinks so the icon is always centred */
44
43
  flex: 0 0 34px;
45
44
  width: 34px;
46
45
  height: 34px;
@@ -51,7 +50,6 @@
51
50
  pointer-events: none;
52
51
  flex-shrink: 0;
53
52
  transition: color 0.15s;
54
- /* No border, no background — the unit looks seamless */
55
53
  }
56
54
  .ogx-global-search--expanded .ogx-global-search__icon-wrapper {
57
55
  color: var(--ogx-color-primary, #3b82f6);
@@ -74,7 +72,6 @@
74
72
  font-size: var(--ogx-font-size-sm, 13px);
75
73
  color: var(--ogx-grid-row-text, #1e293b);
76
74
  outline: none;
77
- /* Hide with opacity, not display:none, so focus events still fire */
78
75
  opacity: 0;
79
76
  pointer-events: none;
80
77
  transition: opacity 0.18s ease;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from 'react';
1
+ import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
2
2
 
3
3
 
4
4
  interface GlobalSearchProps {
@@ -27,48 +27,102 @@ function CloseIcon() {
27
27
  }
28
28
 
29
29
  export function GlobalSearch({ value = '', onChange, placeholder = 'Search...', debounceMs = 250 }: GlobalSearchProps) {
30
- const [isExpanded, setIsExpanded] = useState(false);
30
+ const [isExpanded, setIsExpanded] = useState(() => value.length > 0);
31
+ // Fully uncontrolled: localValue is the single source of truth.
31
32
  const [localValue, setLocalValue] = useState(value);
32
33
  const inputRef = useRef<HTMLInputElement>(null);
33
34
  const containerRef = useRef<HTMLDivElement>(null);
35
+ // Keep onChange always current without adding it to effect deps
36
+ const onChangeRef = useRef(onChange);
37
+ useLayoutEffect(() => { onChangeRef.current = onChange; }, [onChange]);
38
+ // Track the last value we emitted upward
39
+ const lastEmittedRef = useRef(value);
40
+ // Track the last incoming prop value to detect external resets
41
+ const lastIncomingValueRef = useRef(value);
42
+ // Track whether input was focused BEFORE a re-render so we can restore it
43
+ const wasFocusedRef = useRef(false);
34
44
 
35
- // Sync local value with prop
45
+ // ── Focus preservation across re-renders ─────────────────────────────────
46
+ // When the parent updates filterModel (triggered by our debounce), React
47
+ // re-renders the entire tree. Even though the input element stays mounted,
48
+ // the browser can lose focus during heavy reconciliation. We detect this
49
+ // via useLayoutEffect (runs synchronously after DOM mutations, before paint)
50
+ // and immediately restore focus without any visual flicker.
51
+ useLayoutEffect(() => {
52
+ if (wasFocusedRef.current && document.activeElement !== inputRef.current) {
53
+ inputRef.current?.focus();
54
+ }
55
+ });
56
+
57
+ // ── External reset detection ──────────────────────────────────────────────
58
+ // Only responds when the PARENT programmatically changes the value
59
+ // (e.g. "Clear all filters" button) — not from our own typing.
36
60
  useEffect(() => {
37
- setLocalValue(value);
38
- if (value && !isExpanded) {
39
- setIsExpanded(true);
61
+ if (value === lastIncomingValueRef.current) return;
62
+ lastIncomingValueRef.current = value;
63
+
64
+ if (value !== lastEmittedRef.current) {
65
+ setLocalValue(value);
66
+ lastEmittedRef.current = value;
67
+ if (!value) {
68
+ wasFocusedRef.current = false;
69
+ setIsExpanded(false);
70
+ } else {
71
+ setIsExpanded(true);
72
+ }
40
73
  }
41
- }, [value, isExpanded]);
74
+ }, [value]);
42
75
 
43
- // Debounce change
76
+ // ── Debounced upward notification ─────────────────────────────────────────
44
77
  useEffect(() => {
45
78
  const handler = setTimeout(() => {
46
- if (localValue !== value) {
47
- onChange(localValue);
79
+ if (localValue !== lastEmittedRef.current) {
80
+ lastEmittedRef.current = localValue;
81
+ onChangeRef.current(localValue);
48
82
  }
49
83
  }, debounceMs);
50
84
  return () => clearTimeout(handler);
51
- }, [localValue, onChange, debounceMs, value]);
85
+ }, [localValue, debounceMs]);
52
86
 
53
- const handleFocus = () => {
87
+ const handleExpand = useCallback(() => {
54
88
  setIsExpanded(true);
55
- };
89
+ requestAnimationFrame(() => inputRef.current?.focus());
90
+ }, []);
56
91
 
57
- const handleClear = (e: React.MouseEvent) => {
92
+ const handleClear = useCallback((e: React.MouseEvent) => {
58
93
  e.stopPropagation();
59
94
  setLocalValue('');
95
+ lastEmittedRef.current = '';
96
+ onChangeRef.current('');
60
97
  inputRef.current?.focus();
61
- onChange(''); // Immediate clear
62
- };
98
+ }, []);
63
99
 
64
- // Close on blur if empty
65
- const handleBlur = () => {
66
- if (!localValue) {
67
- setIsExpanded(false);
68
- }
69
- };
100
+ const handleFocus = useCallback(() => {
101
+ wasFocusedRef.current = true;
102
+ setIsExpanded(true);
103
+ }, []);
70
104
 
71
- // Click outside to collapse if empty
105
+ // Blur: defer 150ms to allow focus to move within the container first
106
+ // (e.g. clicking the clear button) before deciding to collapse.
107
+ const handleBlur = useCallback(() => {
108
+ wasFocusedRef.current = false;
109
+ setTimeout(() => {
110
+ if (
111
+ containerRef.current &&
112
+ document.activeElement &&
113
+ containerRef.current.contains(document.activeElement)
114
+ ) {
115
+ // Focus is still inside — mark as focused again and abort
116
+ wasFocusedRef.current = true;
117
+ return;
118
+ }
119
+ if (!localValue) {
120
+ setIsExpanded(false);
121
+ }
122
+ }, 150);
123
+ }, [localValue]);
124
+
125
+ // ── Click outside ─────────────────────────────────────────────────────────
72
126
  useEffect(() => {
73
127
  const handleClickOutside = (event: MouseEvent) => {
74
128
  if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
@@ -77,7 +131,6 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
77
131
  }
78
132
  }
79
133
  };
80
-
81
134
  document.addEventListener('mousedown', handleClickOutside);
82
135
  return () => document.removeEventListener('mousedown', handleClickOutside);
83
136
  }, [localValue]);
@@ -86,13 +139,7 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
86
139
  <div
87
140
  ref={containerRef}
88
141
  className={`ogx-global-search${isExpanded ? ' ogx-global-search--expanded' : ''}`}
89
- onClick={() => {
90
- if (!isExpanded) {
91
- setIsExpanded(true);
92
-
93
- setTimeout(() => inputRef.current?.focus(), 50);
94
- }
95
- }}
142
+ onClick={!isExpanded ? handleExpand : undefined}
96
143
  >
97
144
  <div className="ogx-global-search__icon-wrapper">
98
145
  <SearchIcon />
@@ -117,6 +117,23 @@
117
117
  background: var(--ogx-toolbar-btn-danger-bg, rgba(239, 68, 68, 0.08));
118
118
  border-color: var(--ogx-color-error, #ef4444);
119
119
  }
120
+ .ogx-toolbar__close-btn {
121
+ padding: 2px 10px;
122
+ border: var(--ogx-border-width-thin) solid var(--ogx-border-color);
123
+ border-radius: var(--ogx-border-radius-sm);
124
+ background: transparent;
125
+ color: var(--ogx-grid-header-text);
126
+ font-family: var(--ogx-font-family);
127
+ font-size: var(--ogx-font-size-xs);
128
+ font-weight: 500;
129
+ cursor: pointer;
130
+ transition: background 0.15s, border-color 0.15s;
131
+ }
132
+ .ogx-toolbar__close-btn:hover {
133
+ background: var(--ogx-grid-header-hover-background);
134
+ border-color: var(--ogx-color-primary);
135
+ color: var(--ogx-color-primary);
136
+ }
120
137
  .ogx-toolbar__panel-body {
121
138
  padding: var(--ogx-spacing-xs) 0;
122
139
  max-height: 360px;
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect, useCallback } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel } from '../../types';
4
4
  import { PivotPanel, PivotIcon } from './PivotPanel';
@@ -9,6 +9,11 @@ import { GridTooltip } from '../Tooltip/Tooltip';
9
9
 
10
10
  export interface GridToolbarProps {
11
11
  columns?: GridColDef[];
12
+ /** Original user-defined columns (before pivot transformation).
13
+ * When provided, the PivotPanel shows these instead of `columns`
14
+ * to prevent pivot-generated synthetic columns from appearing in
15
+ * 'Available Fields' and causing infinite nested aggregations. */
16
+ baseColumns?: GridColDef[];
12
17
  aggregationModel?: GridAggregationModel;
13
18
  onAggregationModelChange?: (model: GridAggregationModel) => void;
14
19
  pivotModel?: GridPivotModel;
@@ -82,6 +87,11 @@ function AggregationPanel({
82
87
  }) {
83
88
  const panelRef = useRef<HTMLDivElement>(null);
84
89
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
90
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
91
+
92
+ useLayoutEffect(() => {
93
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
94
+ }, [anchorRef]);
85
95
 
86
96
  const computePos = useCallback((): PanelPosition => {
87
97
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -180,7 +190,7 @@ function AggregationPanel({
180
190
  </div>
181
191
  );
182
192
 
183
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
193
+ return ReactDOM.createPortal(panel, portalContainer);
184
194
  }
185
195
 
186
196
  function ColumnsPanelWrapper({
@@ -204,6 +214,11 @@ function ColumnsPanelWrapper({
204
214
  }) {
205
215
  const panelRef = useRef<HTMLDivElement>(null);
206
216
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
217
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
218
+
219
+ useLayoutEffect(() => {
220
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
221
+ }, [anchorRef]);
207
222
 
208
223
  const computePos = useCallback((): PanelPosition => {
209
224
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -257,7 +272,7 @@ function ColumnsPanelWrapper({
257
272
  </div>
258
273
  );
259
274
 
260
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
275
+ return ReactDOM.createPortal(panel, portalContainer);
261
276
  }
262
277
 
263
278
  function FilterPanelWrapper({
@@ -275,6 +290,16 @@ function FilterPanelWrapper({
275
290
  }) {
276
291
  const panelRef = useRef<HTMLDivElement>(null);
277
292
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
293
+ // Stable ref for onClose — prevents the mousedown effect from
294
+ // tearing down and re-registering on every filterModel update,
295
+ // which could fire the close callback during the re-register window.
296
+ const onCloseRef = useRef(onClose);
297
+ useLayoutEffect(() => { onCloseRef.current = onClose; }, [onClose]);
298
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
299
+
300
+ useLayoutEffect(() => {
301
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
302
+ }, [anchorRef]);
278
303
 
279
304
  const computePos = useCallback((): PanelPosition => {
280
305
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -296,25 +321,17 @@ function FilterPanelWrapper({
296
321
  };
297
322
  }, [computePos]);
298
323
 
324
+ // Only Escape key closes the filter panel automatically.
325
+ // Click-outside is intentionally removed: users need to type in filter
326
+ // inputs without the panel closing. There is an explicit Close button.
299
327
  useEffect(() => {
300
- function handleClick(e: MouseEvent) {
301
- if (
302
- panelRef.current && !panelRef.current.contains(e.target as Node) &&
303
- anchorRef.current && !anchorRef.current.contains(e.target as Node)
304
- ) {
305
- onClose();
306
- }
307
- }
308
328
  function handleKey(e: KeyboardEvent) {
309
- if (e.key === 'Escape') onClose();
329
+ if (e.key === 'Escape') onCloseRef.current();
310
330
  }
311
- document.addEventListener('mousedown', handleClick);
312
331
  document.addEventListener('keydown', handleKey);
313
- return () => {
314
- document.removeEventListener('mousedown', handleClick);
315
- document.removeEventListener('keydown', handleKey);
316
- };
317
- }, [anchorRef, onClose]);
332
+ return () => document.removeEventListener('keydown', handleKey);
333
+ // Empty deps — listener is registered once and uses the stable ref
334
+ }, []);
318
335
 
319
336
  const activeFilterCount = filterModel.items?.length ?? 0;
320
337
 
@@ -330,7 +347,6 @@ function FilterPanelWrapper({
330
347
  aria-label="Advanced filters"
331
348
  style={{ top: pos.top, right: pos.right, width: 540, maxWidth: 'calc(100vw - 24px)' }}
332
349
  >
333
- { }
334
350
  <div className="ogx-toolbar__panel-header">
335
351
  <span className="ogx-toolbar__panel-title">
336
352
  <FilterIcon />
@@ -339,18 +355,27 @@ function FilterPanelWrapper({
339
355
  <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
340
356
  )}
341
357
  </span>
342
- {activeFilterCount > 0 && (
358
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
359
+ {activeFilterCount > 0 && (
360
+ <button
361
+ className="ogx-toolbar__clear-btn"
362
+ onClick={handleClearAll}
363
+ title="Clear all filters"
364
+ >
365
+ Clear all
366
+ </button>
367
+ )}
343
368
  <button
344
- className="ogx-toolbar__clear-btn"
345
- onClick={handleClearAll}
346
- title="Clear all filters"
369
+ className="ogx-toolbar__close-btn"
370
+ onClick={() => onCloseRef.current()}
371
+ title="Close filters"
372
+ aria-label="Close filters panel"
347
373
  >
348
- Clear all
374
+ Close
349
375
  </button>
350
- )}
376
+ </div>
351
377
  </div>
352
378
 
353
- { }
354
379
  <div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
355
380
  <FilterPanel
356
381
  columns={columns}
@@ -361,13 +386,14 @@ function FilterPanelWrapper({
361
386
  </div>
362
387
  );
363
388
 
364
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
389
+ return ReactDOM.createPortal(panel, portalContainer);
365
390
  }
366
391
 
367
392
  const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
368
393
 
369
394
  export function GridToolbar({
370
395
  columns = [],
396
+ baseColumns,
371
397
  aggregationModel = {},
372
398
  onAggregationModelChange,
373
399
  pivotModel,
@@ -594,7 +620,7 @@ export function GridToolbar({
594
620
  {pivotOpen && (
595
621
  <PivotPanel
596
622
  anchorRef={pivotButtonRef}
597
- columns={columns}
623
+ columns={baseColumns ?? columns}
598
624
  model={currentPivotModel}
599
625
  onChange={(m) => { onPivotModelChange?.(m); }}
600
626
  onClose={() => setPivotOpen(false)}
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef } from '../../types';
4
4
  import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
@@ -31,6 +31,11 @@ export interface PivotPanelProps {
31
31
  export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
32
32
  const panelRef = useRef<HTMLDivElement>(null);
33
33
  const [pos, setPos] = useState({ top: 0, right: 0 });
34
+ const [portalContainer, setPortalContainer] = useState<Element>(document.body);
35
+
36
+ useLayoutEffect(() => {
37
+ setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
38
+ }, [anchorRef]);
34
39
 
35
40
  const computePos = useCallback(() => {
36
41
  if (!anchorRef.current) return { top: 0, right: 0 };
@@ -235,5 +240,5 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
235
240
  </div>
236
241
  );
237
242
 
238
- return ReactDOM.createPortal(panel, anchorRef.current?.closest('.ogx-theme-provider') || document.body);
243
+ return ReactDOM.createPortal(panel, portalContainer);
239
244
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- export interface DragHandleIconProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { }
3
+ export type DragHandleIconProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
4
4
 
5
5
  export const DragHandleIcon: React.FC<DragHandleIconProps> = ({ className = '', ...props }) => {
6
6
  return (
@@ -1,5 +1,5 @@
1
1
 
2
- import { useReducer, useCallback, useMemo, useRef, useEffect } from 'react';
2
+ import { useReducer, useCallback, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
3
3
  import type {
4
4
  GridState,
5
5
  GridRowModel,
@@ -259,7 +259,7 @@ export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDa
259
259
  });
260
260
 
261
261
  const stateRef = useRef(state);
262
- stateRef.current = state;
262
+ useLayoutEffect(() => { stateRef.current = state; });
263
263
 
264
264
  // Sync internal state when the external `rows` prop changes
265
265
  const prevRowsRef = useRef(rows);