@opencorestack/opengridx 3.9.1 → 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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.d.ts +55 -0
  3. package/dist/opengridx.es.js +6346 -6279
  4. package/dist/opengridx.es.js.map +1 -1
  5. package/dist/opengridx.umd.js +16 -16
  6. package/dist/opengridx.umd.js.map +1 -1
  7. package/docs/API_REFERENCE.md +4 -0
  8. package/docs/features/ai-toolkit.md +1 -0
  9. package/docs/performance.md +1 -1
  10. package/docs/roadmap.md +5 -5
  11. package/lib/components/Cell/Cell.tsx +2 -1
  12. package/lib/components/DataGrid/DataGrid.tsx +19 -18
  13. package/lib/components/DataGrid/GridListView.tsx +1 -1
  14. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  15. package/lib/components/DataGrid/GridViewport.tsx +2 -1
  16. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  17. package/lib/components/Header/Header.tsx +4 -3
  18. package/lib/components/Row/Row.tsx +6 -2
  19. package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
  20. package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
  21. package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
  22. package/lib/components/Toolbar/GridToolbar.tsx +7 -380
  23. package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
  24. package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
  25. package/lib/hooks/core/gridKeyDown.ts +272 -0
  26. package/lib/hooks/core/useDataGrid.ts +13 -5
  27. package/lib/hooks/core/useGridApiMethods.ts +2 -2
  28. package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
  29. package/lib/hooks/core/useGridCellSelection.ts +17 -297
  30. package/lib/hooks/core/useGridColumnLookup.ts +2 -1
  31. package/lib/hooks/core/useGridColumns.tsx +3 -2
  32. package/lib/hooks/core/useGridControlledState.ts +3 -2
  33. package/lib/hooks/core/useGridHierarchy.ts +1 -1
  34. package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
  35. package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
  36. package/lib/hooks/core/useGridRowInteractions.ts +1 -1
  37. package/lib/hooks/core/useGridRowSelection.ts +11 -2
  38. package/lib/hooks/core/useGridToolbarProps.ts +3 -2
  39. package/lib/hooks/features/useGridAiAssistant.ts +3 -2
  40. package/lib/hooks/useColumnReorder.ts +2 -1
  41. package/lib/hooks/useTreeData.ts +4 -2
  42. package/lib/types/locale.ts +22 -0
  43. package/lib/utils/aggregation/index.ts +16 -0
  44. package/lib/utils/aiAssistant.ts +46 -15
  45. package/lib/utils/export/excelCells.ts +244 -0
  46. package/lib/utils/export/exportCsv.ts +179 -0
  47. package/lib/utils/export/exportExcelHtml.ts +220 -0
  48. package/lib/utils/export/exportJson.ts +179 -0
  49. package/lib/utils/export/exportRows.ts +85 -0
  50. package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
  51. package/lib/utils/export/index.ts +5 -695
  52. package/lib/utils/export/printGrid.ts +282 -0
  53. package/lib/utils/gridLocale.ts +30 -0
  54. package/lib/utils/selectionSets.ts +36 -0
  55. package/lib/utils/untypedColumns.ts +16 -0
  56. package/llms.txt +3 -1
  57. package/package.json +4 -2
@@ -1,10 +1,15 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
2
9
  import type { RefObject } from 'react';
3
10
  import { getRowIndex } from '../../utils/rowIndex';
4
11
  import { scrollColumnIntoView, scrollRowIntoView } from '../../utils/scroll';
5
- import { isRtl } from '../../utils/direction';
6
12
  import { isSyntheticRowId } from '../../utils/syntheticRows';
7
- import { isTextEntryElement } from '../../utils/focus';
8
13
  import type { FocusedCell } from '../../utils/focus';
9
14
  import { getCellValue } from '../../utils/values';
10
15
  import { AGGREGATION_FUNCTIONS } from '../../utils/aggregation';
@@ -18,7 +23,12 @@ import {
18
23
  rectCellCount,
19
24
  resolveCellRange,
20
25
  } from '../../utils/cellSelection';
21
- import type { CellRangeContext, CellRangeRect, CellRangeSpanLookup, ResolvedCellRange } from '../../utils/cellSelection';
26
+ import type {
27
+ CellRangeContext,
28
+ CellRangeRect,
29
+ CellRangeSpanLookup,
30
+ ResolvedCellRange,
31
+ } from '../../utils/cellSelection';
22
32
  import { buildRangeTsv } from '../features/useGridClipboard';
23
33
  import type { GridSpanningResult } from '../features/useGridSpanning';
24
34
  import type { LayoutResult } from './useLayout';
@@ -34,13 +44,12 @@ import type {
34
44
  GridRowModel,
35
45
  GridSelectedCell,
36
46
  } from '../../types';
47
+ import { useGridCellRangeDrag } from './useGridCellRangeDrag';
37
48
 
38
49
  /** Root class while a pointer drag selects cells: no text selection meanwhile. Public from v3.3. */
39
50
  export const RANGE_DRAGGING_CLASS = 'ogx--range-dragging';
40
51
 
41
52
  /** Pointer distance (px) from a body edge at which a drag starts scrolling. */
42
- const AUTO_SCROLL_EDGE = 32;
43
- const AUTO_SCROLL_MAX_STEP = 40;
44
53
  const ANNOUNCEMENT_DELAY_MS = 300;
45
54
 
46
55
  /** The rectangle in the coordinates rows render with: row indices of the renderable rows, data column indices. */
@@ -127,24 +136,6 @@ export interface UseGridCellSelectionReturn {
127
136
  startTrackedDrag: (options: GridTrackedDragOptions) => void;
128
137
  }
129
138
 
130
- interface DragState {
131
- anchor: GridCellCoordinates;
132
- head: GridCellCoordinates;
133
- x: number;
134
- y: number;
135
- frame: number;
136
- moved: boolean;
137
- /**
138
- * Whether the viewport lays out right to left, read once per drag: `getComputedStyle` every frame
139
- * forces a style recalculation, which slowed WebKit's auto-scroll enough to delay the release.
140
- */
141
- rtl?: boolean;
142
- /** Ends the drag; `cancelled` unless the button was released normally (tracked drags: Escape cancels). */
143
- stop: (cancelled?: boolean, altKey?: boolean) => void;
144
- /** A tracked drag (fill handle): told the cell under the pointer each frame instead of moving the range. */
145
- onTrack?: (head: GridCellCoordinates, x: number, y: number) => void;
146
- }
147
-
148
139
  /** A drag that reuses the range drag's hit testing and auto-scroll without selecting (the fill handle, v3.5). */
149
140
  export interface GridTrackedDragOptions {
150
141
  /** The cell the drag starts on. */
@@ -159,29 +150,6 @@ export interface GridTrackedDragOptions {
159
150
  onEnd: (details: { cancelled: boolean; altKey: boolean }) => void;
160
151
  }
161
152
 
162
- /** The body cell of `viewport` holding `target`: its row index and field. Not cells of nested grids or detail panels. */
163
- function cellFromTarget(target: EventTarget | null, viewport: HTMLElement): { rowIndex: number; field: string; element: HTMLElement } | null {
164
- if (!target || (target as Node).nodeType !== 1) return null;
165
- const element = (target as Element).closest<HTMLElement>('.ogx__cell[data-field]');
166
- if (!element || element.closest('.ogx__viewport') !== viewport) return null;
167
- const panel = element.closest('.ogx__detail-panel');
168
- if (panel && viewport.contains(panel)) return null;
169
- const rowElement = element.closest('[role="row"][data-rowindex]');
170
- if (!rowElement) return null;
171
- const rowIndex = Number(rowElement.getAttribute('data-rowindex'));
172
- const field = element.dataset.field;
173
- if (!Number.isInteger(rowIndex) || !field) return null;
174
- return { rowIndex, field, element };
175
- }
176
-
177
- function clearTextSelection(doc: Document): void {
178
- const selection = doc.getSelection();
179
- if (selection && selection.rangeCount > 0) selection.removeAllRanges();
180
- }
181
-
182
- const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
183
- const autoScrollStep = (over: number) => Math.min(AUTO_SCROLL_MAX_STEP, Math.max(2, Math.round(over / 2)));
184
-
185
153
  function warnLargeCopy(cellCount: number): void {
186
154
  if (process.env.NODE_ENV === 'production') return;
187
155
  console.warn(
@@ -400,256 +368,8 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
400
368
  return cells;
401
369
  }, [currentRange]);
402
370
 
403
- // ── Pointer: click, Shift+click and drag ────────────────────────────────
404
- const dragRef = useRef<DragState | null>(null);
405
- const suppressClickRef = useRef(false);
406
-
407
- useEffect(() => () => dragRef.current?.stop(), []);
408
-
409
- const headAtPoint = useCallback((viewport: HTMLElement, x: number, y: number): GridCellCoordinates | null => {
410
- const ctx = latestRef.current.context;
411
- if (!ctx) return null;
412
- const doc = viewport.ownerDocument;
413
- // jsdom has no elementFromPoint.
414
- if (typeof doc.elementFromPoint !== 'function') return null;
415
- const hit = cellFromTarget(doc.elementFromPoint(x, y), viewport);
416
- if (!hit) return null;
417
- const id = ctx.rowIds[hit.rowIndex];
418
- if (id === undefined) return null;
419
- // Over the system or grouping column (left of the data): the first data column.
420
- const field = isRangeColumnField(hit.field) ? hit.field : ctx.fields[0];
421
- return field === undefined ? null : { id, field };
422
- }, []);
423
-
424
- /** The row whose band holds `centerY` (px from the top of the unpinned rows), with `field`. */
425
- const headRowAtOffset = useCallback((centerY: number, field: string): GridCellCoordinates | null => {
426
- const { context: ctx, layout, pinnedTopRowCount } = latestRef.current;
427
- const heights = layout.cumulativeHeights;
428
- if (!ctx || heights.length === 0 || centerY < 0) return null;
429
- let lo = 0;
430
- let hi = heights.length - 1;
431
- while (lo < hi) {
432
- const mid = (lo + hi) >> 1;
433
- if (heights[mid] > centerY) hi = mid; else lo = mid + 1;
434
- }
435
- const id = ctx.rowIds[lo + pinnedTopRowCount];
436
- return id === undefined ? null : { id, field };
437
- }, []);
438
-
439
- // The auto-scroll loop schedules the next frame through this ref: a callback cannot name itself.
440
- const runDragFrameRef = useRef<(allowScroll: boolean) => void>(() => {});
441
- const runDragFrame = useCallback((allowScroll: boolean) => {
442
- const drag = dragRef.current;
443
- const viewport = latestRef.current.viewportRef.current;
444
- if (!drag || !viewport) return;
445
- drag.frame = 0;
446
- const { layout } = latestRef.current;
447
- const box = viewport.getBoundingClientRect();
448
- const width = viewport.clientWidth;
449
- const height = viewport.clientHeight;
450
- const stickyTop = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-top');
451
- const stickyBottom = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-bottom');
452
- const headerWrap = stickyTop?.querySelector(':scope > .ogx__header-wrap');
453
- const bodyTop = stickyTop ? stickyTop.getBoundingClientRect().bottom : box.top;
454
- const bodyBottom = Math.min(box.top + height, stickyBottom ? stickyBottom.getBoundingClientRect().top : box.top + height);
455
- const headerBottom = headerWrap ? headerWrap.getBoundingClientRect().bottom : bodyTop;
456
- // Left-pinned columns (and pinned system columns) sit at the inline start: on the right in a
457
- // right-to-left grid, whose vertical scrollbar is on the left.
458
- if (drag.rtl === undefined) drag.rtl = isRtl(viewport);
459
- const rtl = drag.rtl;
460
- const contentLeft = rtl ? box.right - width : box.left;
461
- const startPinnedWidth = layout.pinnedSystemColumnsWidth + layout.leftWidth;
462
- const bodyLeft = contentLeft + (rtl ? layout.rightWidth : startPinnedWidth);
463
- const bodyRight = contentLeft + width - (rtl ? startPinnedWidth : layout.rightWidth);
464
-
465
- // The cell under the pointer; outside the viewport (or over the header) the nearest body cell.
466
- const x = clamp(drag.x, contentLeft + 1, contentLeft + width - 2);
467
- let y = drag.y;
468
- if (y < box.top || y > box.top + height || y < headerBottom) y = clamp(y, bodyTop + 1, Math.max(bodyTop + 1, bodyBottom - 2));
469
- // During fast auto-scroll the rows under the pointer may not be rendered yet (virtualization
470
- // lags a frame or more on a slow machine), so the hit test finds no cell. Work out the row from
471
- // the scroll position and the row heights instead, keeping the column the head already has.
472
- const canHitTest = typeof viewport.ownerDocument.elementFromPoint === 'function';
473
- const hit = headAtPoint(viewport, x, y);
474
- const head = hit ?? (canHitTest && drag.moved
475
- ? headRowAtOffset(y - box.top - (stickyTop ? stickyTop.getBoundingClientRect().height : 0) + viewport.scrollTop, drag.head.field)
476
- : null);
477
- if (head && !isSameCoordinates(head, drag.head)) {
478
- drag.head = head;
479
- if (!drag.moved && !isSameCoordinates(head, drag.anchor)) {
480
- drag.moved = true;
481
- latestRef.current.containerRef.current?.classList.add(RANGE_DRAGGING_CLASS);
482
- clearTextSelection(viewport.ownerDocument);
483
- }
484
- if (!drag.onTrack) setRange(drag.anchor, head, 'pointer');
485
- }
486
- // A tracked drag hears every frame: its target can change while the head stays in one cell.
487
- if (drag.onTrack) drag.onTrack(drag.head, drag.x, drag.y);
488
-
489
- if (!allowScroll) return;
490
- const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < contentLeft || drag.x > contentLeft + width;
491
- if (!drag.moved && !outside) return;
492
- let dy = 0;
493
- let dx = 0;
494
- if (drag.y < bodyTop + AUTO_SCROLL_EDGE) dy = -autoScrollStep(bodyTop + AUTO_SCROLL_EDGE - drag.y);
495
- else if (drag.y > bodyBottom - AUTO_SCROLL_EDGE) dy = autoScrollStep(drag.y - (bodyBottom - AUTO_SCROLL_EDGE));
496
- if (drag.x < bodyLeft + AUTO_SCROLL_EDGE) dx = -autoScrollStep(bodyLeft + AUTO_SCROLL_EDGE - drag.x);
497
- else if (drag.x > bodyRight - AUTO_SCROLL_EDGE) dx = autoScrollStep(drag.x - (bodyRight - AUTO_SCROLL_EDGE));
498
- const canScrollY = (dy < 0 && viewport.scrollTop > 0) || (dy > 0 && viewport.scrollTop + height < viewport.scrollHeight - 1);
499
- // scrollLeft runs from 0 to the maximum, or from minus the maximum to 0 right to left.
500
- const maxScroll = viewport.scrollWidth - width;
501
- const minLeft = rtl ? -maxScroll : 0;
502
- const canScrollX = (dx < 0 && viewport.scrollLeft > minLeft) || (dx > 0 && viewport.scrollLeft < minLeft + maxScroll - 1);
503
- if (!canScrollY && !canScrollX) return;
504
- if (canScrollY) viewport.scrollTop += dy;
505
- if (canScrollX) viewport.scrollLeft += dx;
506
- // Keep going while the pointer rests in the edge zone; the next frame picks the new head.
507
- drag.frame = requestAnimationFrame(() => runDragFrameRef.current(true));
508
- }, [headAtPoint, headRowAtOffset, setRange]);
509
- useLayoutEffect(() => {
510
- runDragFrameRef.current = runDragFrame;
511
- }, [runDragFrame]);
512
-
513
- const startDrag = useCallback((
514
- anchor: GridCellCoordinates,
515
- head: GridCellCoordinates,
516
- x: number,
517
- y: number,
518
- doc: Document,
519
- tracked?: Pick<GridTrackedDragOptions, 'pointerId' | 'onTrack' | 'onEnd'>,
520
- ) => {
521
- dragRef.current?.stop();
522
- // A range drag follows the mouse; a tracked drag (fill handle) follows its pointer, touch included.
523
- const moveType = tracked ? 'pointermove' : 'mousemove';
524
- const upType = tracked ? 'pointerup' : 'mouseup';
525
- const isOwnEvent = (event: MouseEvent) => !tracked || (event as PointerEvent).pointerId === tracked.pointerId;
526
- const onMove = (event: MouseEvent) => {
527
- const drag = dragRef.current;
528
- if (!drag || !isOwnEvent(event)) return;
529
- if (event.buttons === 0) {
530
- // Released outside the window.
531
- drag.stop();
532
- return;
533
- }
534
- drag.x = event.clientX;
535
- drag.y = event.clientY;
536
- // The native text selection started by the press would otherwise follow the pointer out
537
- // of the grid (WebKit), and a page text selection makes Ctrl/Cmd+C leave the copy to the browser.
538
- if (drag.moved) {
539
- event.preventDefault();
540
- clearTextSelection(doc);
541
- }
542
- if (!drag.frame) drag.frame = requestAnimationFrame(() => runDragFrame(true));
543
- };
544
- const onUp = (event: MouseEvent) => {
545
- const drag = dragRef.current;
546
- if (!drag || !isOwnEvent(event)) return;
547
- drag.x = event.clientX;
548
- drag.y = event.clientY;
549
- runDragFrame(false);
550
- const moved = drag.moved;
551
- drag.stop(false, event.altKey);
552
- if (moved) clearTextSelection(doc);
553
- if (moved) {
554
- // The click that follows lands on the common ancestor of both cells (often the row):
555
- // it must not select the row or move focus.
556
- suppressClickRef.current = true;
557
- setTimeout(() => { suppressClickRef.current = false; }, 0);
558
- }
559
- };
560
- const onCancel = (event: Event) => {
561
- if (isOwnEvent(event as MouseEvent)) dragRef.current?.stop();
562
- };
563
- // Escape abandons a tracked drag (the fill writes nothing).
564
- const onKeyDown = (event: KeyboardEvent) => {
565
- if (event.key !== 'Escape' || !dragRef.current) return;
566
- event.preventDefault();
567
- event.stopPropagation();
568
- dragRef.current.stop();
569
- };
570
- const view = doc.defaultView;
571
- const onBlur = () => dragRef.current?.stop();
572
- let ended = false;
573
- const drag: DragState = {
574
- anchor, head, x, y, frame: 0, moved: false,
575
- onTrack: tracked?.onTrack,
576
- stop: (cancelled = true, altKey = false) => {
577
- if (drag.frame) cancelAnimationFrame(drag.frame);
578
- drag.frame = 0;
579
- doc.removeEventListener(moveType, onMove, true);
580
- doc.removeEventListener(upType, onUp, true);
581
- if (tracked) {
582
- doc.removeEventListener('pointercancel', onCancel, true);
583
- doc.removeEventListener('keydown', onKeyDown, true);
584
- }
585
- view?.removeEventListener('blur', onBlur);
586
- latestRef.current.containerRef.current?.classList.remove(RANGE_DRAGGING_CLASS);
587
- if (dragRef.current === drag) dragRef.current = null;
588
- if (!ended) {
589
- ended = true;
590
- if (tracked) tracked.onEnd({ cancelled, altKey });
591
- }
592
- },
593
- };
594
- dragRef.current = drag;
595
- doc.addEventListener(moveType, onMove, true);
596
- doc.addEventListener(upType, onUp, true);
597
- if (tracked) {
598
- doc.addEventListener('pointercancel', onCancel, true);
599
- doc.addEventListener('keydown', onKeyDown, true);
600
- }
601
- view?.addEventListener('blur', onBlur);
602
- }, [runDragFrame]);
603
-
604
- const startTrackedDrag = useCallback((options: GridTrackedDragOptions) => {
605
- const viewport = latestRef.current.viewportRef.current;
606
- if (!viewport || !latestRef.current.enabled) return;
607
- const { start, x, y, pointerId, onTrack, onEnd } = options;
608
- startDrag(start, start, x, y, viewport.ownerDocument, { pointerId, onTrack, onEnd });
609
- }, [startDrag]);
610
-
611
- const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
612
- if (event.button !== 0 || event.defaultPrevented) return;
613
- const { context: ctx } = latestRef.current;
614
- if (!ctx) return;
615
- const viewport = event.currentTarget;
616
- const hit = cellFromTarget(event.target, viewport);
617
- if (!hit || !isRangeColumnField(hit.field)) return;
618
- // The fill handle starts its own drag (useGridFillHandle).
619
- if ((event.target as Element).closest?.('.ogx__cell-fill-handle')) return;
620
- // Typing targets and open editors keep the pointer to themselves.
621
- if (hit.element.classList.contains('ogx__cell--editing') || isTextEntryElement(event.target as Element)) return;
622
- const id = ctx.rowIds[hit.rowIndex];
623
- if (id === undefined) return;
624
- const cell = { id, field: hit.field };
625
-
626
- let anchor: GridCellCoordinates = cell;
627
- if (event.shiftKey) {
628
- const current = currentModel()[0];
629
- const focus = focusRef.current;
630
- if (current && ctx.rowIndexById.has(current.anchor.id) && ctx.colIndexByField.has(current.anchor.field)) anchor = current.anchor;
631
- else if (focus && focus.id !== null && isRangeColumnField(focus.field)) anchor = { id: focus.id, field: focus.field };
632
- // Shift+click extends the range: no text selection, no focus move and no click (row
633
- // selection, onCellClick) afterwards.
634
- event.preventDefault();
635
- suppressClickRef.current = true;
636
- const doc = viewport.ownerDocument;
637
- if (!viewport.contains(doc.activeElement)) viewport.focus({ preventScroll: true });
638
- }
639
- setRange(anchor, cell, 'pointer');
640
- startDrag(anchor, cell, event.clientX, event.clientY, viewport.ownerDocument);
641
- if (event.shiftKey) {
642
- const drag = dragRef.current;
643
- if (drag && !isSameCoordinates(anchor, cell)) drag.moved = true;
644
- }
645
- }, [currentModel, setRange, startDrag]);
646
-
647
- const handleClickCapture = useCallback((event: React.MouseEvent<HTMLElement>) => {
648
- if (!suppressClickRef.current) return;
649
- suppressClickRef.current = false;
650
- event.stopPropagation();
651
- event.preventDefault();
652
- }, []);
371
+ // ── Pointer: click, Shift+click and drag (useGridCellRangeDrag) ──────────────
372
+ const { startTrackedDrag, handleMouseDown, handleClickCapture } = useGridCellRangeDrag({ latestRef, focusRef, setRange, currentModel });
653
373
 
654
374
  // ── Live region and status bar ──────────────────────────────────────────
655
375
  const columnCount = resolved ? resolved.right - resolved.left + 1 : 0;
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { GridColDef, GridRowModel } from '../../types';
3
3
  import { buildColumnLookup, type GridColumnLookup } from '../../utils/columnLookup';
4
+ import { untypedColumns } from '../../utils/untypedColumns';
4
5
 
5
6
  const SEPARATOR = '\u0000';
6
7
 
@@ -25,6 +26,6 @@ export function useGridColumnLookup<R extends GridRowModel>(
25
26
  if (hiddenFieldsKey) {
26
27
  for (const field of hiddenFieldsKey.split(SEPARATOR)) hidden[field] = false;
27
28
  }
28
- return buildColumnLookup(columns as unknown as GridColDef[], hidden);
29
+ return buildColumnLookup(untypedColumns(columns), hidden);
29
30
  }, [columns, hiddenFieldsKey]);
30
31
  }
@@ -15,6 +15,7 @@ import type {
15
15
  GridAggregationModel,
16
16
  } from '../../types';
17
17
  import type { GridInitialState } from '../../state/types';
18
+ import { untypedColumn, untypedColumns } from '../../utils/untypedColumns';
18
19
 
19
20
  interface HierarchyHandlers {
20
21
  toggleExpansion: (id: GridRowId) => void;
@@ -334,7 +335,7 @@ export function useGridColumns<R extends GridRowModel>(
334
335
  }, []);
335
336
 
336
337
  useEffect(() => {
337
- setColumns(activeColumns as unknown as GridColDef[]);
338
+ setColumns(untypedColumns(activeColumns));
338
339
  }, [activeColumns, setColumns]);
339
340
 
340
341
  // ── Ordered / visible columns ─────────────────────────────────────────────
@@ -370,7 +371,7 @@ export function useGridColumns<R extends GridRowModel>(
370
371
  const [movedField] = newOrder.splice(oldIndex, 1);
371
372
  newOrder.splice(targetIndex, 0, movedField);
372
373
  commitColumnOrder(newOrder, {
373
- column: orderedColumns[oldIndex] as unknown as GridColDef,
374
+ column: untypedColumn(orderedColumns[oldIndex]),
374
375
  oldIndex,
375
376
  targetIndex,
376
377
  });
@@ -14,6 +14,7 @@ import type {
14
14
  } from '../../types';
15
15
  import type { GridInitialState, GridDensityState } from '../../state/types';
16
16
  import { EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
17
+ import { selectionSetOf } from '../../utils/selectionSets';
17
18
 
18
19
  export type GridDensity = GridDensityState['density'];
19
20
 
@@ -96,7 +97,7 @@ export interface UseGridControlledStateReturn {
96
97
 
97
98
  // selection
98
99
  rowSelectionModel: GridRowId[];
99
- selectedRowIds: Set<GridRowId>;
100
+ selectedRowIds: ReadonlySet<GridRowId>;
100
101
  isSelectionControlled: boolean;
101
102
  setInternalRowSelectionModel: React.Dispatch<React.SetStateAction<GridRowId[]>>;
102
103
  /** Sets the internal model when uncontrolled and fires onRowSelectionModelChange. */
@@ -267,7 +268,7 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
267
268
  const isSelectionControlled = propRowSelectionModel !== undefined;
268
269
  const [internalRowSelectionModel, setInternalRowSelectionModel] = useState<GridRowId[]>([]);
269
270
  const rowSelectionModel = isSelectionControlled ? propRowSelectionModel! : internalRowSelectionModel;
270
- const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
271
+ const selectedRowIds = useMemo(() => selectionSetOf(rowSelectionModel), [rowSelectionModel]);
271
272
 
272
273
  // Cleared on every commit: the rendered model is current again (a controlled parent may
273
274
  // also have rejected the change).
@@ -54,7 +54,7 @@ export function useGridHierarchy<R extends GridRowModel>(params: UseGridHierarch
54
54
  sortingMode,
55
55
  });
56
56
 
57
- const rowGroupingHandlers = useRowGrouping({
57
+ const rowGroupingHandlers = useRowGrouping<R>({
58
58
  rows,
59
59
  getRowId,
60
60
  columns,