@opencorestack/opengridx 1.0.6 → 1.1.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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.ts +40 -12
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +3212 -3130
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +12 -12
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +35 -0
  9. package/docs/components/cell.md +14 -0
  10. package/docs/components/filter-panel.md +13 -0
  11. package/docs/components/pagination.md +26 -0
  12. package/docs/components/row.md +26 -0
  13. package/docs/roadmap.md +8 -0
  14. package/lib/components/Cell/Cell.tsx +44 -44
  15. package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
  16. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +0 -2
  17. package/lib/components/DataGrid/DataGrid.css +47 -0
  18. package/lib/components/DataGrid/DataGrid.tsx +34 -8
  19. package/lib/components/DataGrid/GridAggregationFooter.tsx +47 -9
  20. package/lib/components/DataGrid/GridListView.tsx +4 -0
  21. package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
  22. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  23. package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
  24. package/lib/components/FilterPanel/FilterPanel.tsx +5 -4
  25. package/lib/components/Header/Header.tsx +4 -3
  26. package/lib/components/Pagination/Pagination.test.tsx +64 -0
  27. package/lib/components/Pagination/Pagination.tsx +19 -7
  28. package/lib/components/Row/Row.tsx +9 -5
  29. package/lib/components/Toolbar/GlobalSearch.tsx +0 -2
  30. package/lib/components/Toolbar/GridToolbar.tsx +0 -4
  31. package/lib/hooks/core/useGridColumns.tsx +11 -11
  32. package/lib/hooks/core/useGridControlledState.test.ts +86 -0
  33. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
  34. package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
  35. package/lib/hooks/useRowGrouping.test.ts +84 -0
  36. package/lib/hooks/useRowGrouping.ts +38 -13
  37. package/lib/hooks/useTreeData.ts +22 -2
  38. package/lib/index.ts +2 -0
  39. package/lib/types/index.ts +38 -12
  40. package/package.json +1 -1
@@ -8,6 +8,7 @@ import type {
8
8
  GridListViewColDef,
9
9
  GridPaginationModel,
10
10
  GridDataSource,
11
+ GridLocaleText,
11
12
  } from '../../types';
12
13
 
13
14
  export interface GridListViewProps<R extends GridRowModel> {
@@ -27,6 +28,7 @@ export interface GridListViewProps<R extends GridRowModel> {
27
28
  serverRowCount: number;
28
29
  paginationSlot?: React.ComponentType<Record<string, unknown>>;
29
30
  paginationSlotProps?: Record<string, unknown>;
31
+ localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
30
32
  onRowClick: (params: GridRowParams<R>) => void;
31
33
  onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
32
34
  onPaginationModelChange: (model: GridPaginationModel) => void;
@@ -49,6 +51,7 @@ export function GridListView<R extends GridRowModel>({
49
51
  serverRowCount,
50
52
  paginationSlot,
51
53
  paginationSlotProps,
54
+ localeText,
52
55
  onRowClick,
53
56
  onSelectionChange,
54
57
  onPaginationModelChange,
@@ -108,6 +111,7 @@ export function GridListView<R extends GridRowModel>({
108
111
  pageSizeOptions={pageSizeOptions}
109
112
  onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
110
113
  onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
114
+ localeText={localeText}
111
115
  {...paginationSlotProps}
112
116
  />
113
117
  )}
@@ -10,6 +10,7 @@ import type {
10
10
  GridRowPinning,
11
11
  GridDetailPanelParams,
12
12
  GridDetailPanelHeight,
13
+ GridRowMeta,
13
14
  } from '../../types';
14
15
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
15
16
 
@@ -36,6 +37,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
36
37
  colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
37
38
  rowSpanningCaches?: RowSpanningCaches;
38
39
  rowHeight: number;
40
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
39
41
  }
40
42
 
41
43
  export function GridPinnedRows<R extends GridRowModel>({
@@ -61,6 +63,7 @@ export function GridPinnedRows<R extends GridRowModel>({
61
63
  colspanMap,
62
64
  rowSpanningCaches,
63
65
  rowHeight,
66
+ rowMetaMap,
64
67
  }: GridPinnedRowsProps<R>) {
65
68
  if (rows.length === 0) return null;
66
69
 
@@ -91,6 +94,7 @@ export function GridPinnedRows<R extends GridRowModel>({
91
94
  colspanMap={colspanMap}
92
95
  rowSpanningCaches={rowSpanningCaches}
93
96
  rowHeight={rowHeight}
97
+ rowMeta={rowMetaMap.get(row.id)}
94
98
  />
95
99
  ))}
96
100
  </div>
@@ -6,6 +6,7 @@ import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../.
6
6
  export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
7
7
  isOpen: boolean;
8
8
  containerRef: React.RefObject<HTMLDivElement | null>;
9
+ panelRef?: React.RefObject<HTMLDivElement | null>;
9
10
  effectiveColumns: GridColDef<R>[];
10
11
  columnVisibilityModel: Record<string, boolean>;
11
12
  effectiveColumnOrder: string[];
@@ -20,6 +21,7 @@ export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
20
21
  export function GridStandaloneColumnPanel<R extends GridRowModel>({
21
22
  isOpen,
22
23
  containerRef,
24
+ panelRef,
23
25
  effectiveColumns,
24
26
  columnVisibilityModel,
25
27
  effectiveColumnOrder,
@@ -64,7 +66,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
64
66
 
65
67
  return ReactDOM.createPortal(
66
68
  <div
67
- id="ogx-standalone-col-panel"
69
+ ref={panelRef}
68
70
  style={{
69
71
  position: 'fixed',
70
72
  top: panelTop,
@@ -12,6 +12,7 @@ import type {
12
12
  GridRowPinning,
13
13
  GridDetailPanelParams,
14
14
  GridDetailPanelHeight,
15
+ GridRowMeta,
15
16
  } from '../../types';
16
17
  import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
17
18
  import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
@@ -60,6 +61,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
60
61
  sortedUnpinnedRowCount: number;
61
62
  infiniteScrollSkeletonCount: number;
62
63
  unpinnedRowsLength: number;
64
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
63
65
  }
64
66
 
65
67
  export function GridVirtualRows<R extends GridRowModel>({
@@ -97,6 +99,7 @@ export function GridVirtualRows<R extends GridRowModel>({
97
99
  sortedUnpinnedRowCount,
98
100
  infiniteScrollSkeletonCount,
99
101
  unpinnedRowsLength,
102
+ rowMetaMap,
100
103
  }: GridVirtualRowsProps<R>) {
101
104
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
102
105
  ? baseColumns
@@ -179,6 +182,7 @@ export function GridVirtualRows<R extends GridRowModel>({
179
182
  colspanMap={colspanMap}
180
183
  rowSpanningCaches={rowSpanningCaches}
181
184
  rowHeight={rowHeight}
185
+ rowMeta={rowMetaMap.get(row.id)}
182
186
  />
183
187
  ))
184
188
  )}
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import type {
3
3
  GridFilterModel,
4
4
  GridFilterItem,
@@ -25,6 +25,7 @@ const FilterRow: React.FC<{
25
25
  item: GridFilterItem | undefined;
26
26
  onChange: (item: GridFilterItem | null) => void;
27
27
  }> = ({ col, item, onChange }) => {
28
+ const filterId = useId();
28
29
  const operators = getOperatorsForType(col.type);
29
30
  const isActive = !!item;
30
31
  const currentOperator = item?.operator ?? operators[0];
@@ -99,7 +100,7 @@ const FilterRow: React.FC<{
99
100
  <div className="ogx-filter__controls">
100
101
  { }
101
102
  <select
102
- id={`ogx-filter-op-${col.field}`}
103
+ id={`${filterId}-op`}
103
104
  name={`filter-op-${col.field}`}
104
105
  className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
105
106
  value={String(currentOperator)}
@@ -114,7 +115,7 @@ const FilterRow: React.FC<{
114
115
  { }
115
116
  {showValue && isBoolean && (
116
117
  <select
117
- id={`ogx-filter-val-${col.field}`}
118
+ id={`${filterId}-val`}
118
119
  name={`filter-val-${col.field}`}
119
120
  className={`ogx-filter__value-input ogx-filter__value-select${isActive ? ' ogx-filter__value-input--active' : ''}`}
120
121
  value={String(item?.value ?? 'true')}
@@ -136,7 +137,7 @@ const FilterRow: React.FC<{
136
137
 
137
138
  {showValue && !isBoolean && (
138
139
  <input
139
- id={`ogx-filter-val-${col.field}`}
140
+ id={`${filterId}-val`}
140
141
  name={`filter-val-${col.field}`}
141
142
  className={`ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`}
142
143
  type="text"
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
4
  import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroup, GridColumnGroupingModel } from '../../types';
@@ -171,6 +171,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
171
171
  onManageColumns,
172
172
  } = props;
173
173
 
174
+ const selectAllId = useId();
174
175
  const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
175
176
  const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
176
177
 
@@ -340,8 +341,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
340
341
  }}
341
342
  >
342
343
  <Checkbox
343
- id="ogx-select-all"
344
- name="ogx-select-all"
344
+ id={selectAllId}
345
+ name={selectAllId}
345
346
  checked={allSelected}
346
347
  indeterminate={someSelected}
347
348
  onChange={(e) => onSelectAll?.(e.target.checked)}
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { Pagination } from './Pagination';
4
+
5
+ const NOOP = () => {};
6
+
7
+ describe('Pagination', () => {
8
+ it('renders "0 of 0" when rowCount is 0', () => {
9
+ render(
10
+ <Pagination
11
+ page={0}
12
+ pageSize={10}
13
+ rowCount={0}
14
+ onPageChange={NOOP}
15
+ onPageSizeChange={NOOP}
16
+ />
17
+ );
18
+ expect(screen.getByText('0 of 0')).toBeTruthy();
19
+ });
20
+
21
+ it('calls localeText.paginationOf with (0, 0, 0) when rowCount is 0', () => {
22
+ const paginationOf = vi.fn().mockReturnValue('custom empty');
23
+ render(
24
+ <Pagination
25
+ page={0}
26
+ pageSize={10}
27
+ rowCount={0}
28
+ onPageChange={NOOP}
29
+ onPageSizeChange={NOOP}
30
+ localeText={{ paginationOf }}
31
+ />
32
+ );
33
+ expect(paginationOf).toHaveBeenCalledWith(0, 0, 0);
34
+ expect(screen.getByText('custom empty')).toBeTruthy();
35
+ });
36
+
37
+ it('renders correct row range label for non-empty data', () => {
38
+ render(
39
+ <Pagination
40
+ page={0}
41
+ pageSize={10}
42
+ rowCount={25}
43
+ onPageChange={NOOP}
44
+ onPageSizeChange={NOOP}
45
+ />
46
+ );
47
+ expect(screen.getByText('1–10 of 25')).toBeTruthy();
48
+ });
49
+
50
+ it('calls localeText.paginationOf with correct args for non-empty data', () => {
51
+ const paginationOf = vi.fn().mockReturnValue('custom range');
52
+ render(
53
+ <Pagination
54
+ page={1}
55
+ pageSize={10}
56
+ rowCount={25}
57
+ onPageChange={NOOP}
58
+ onPageSizeChange={NOOP}
59
+ localeText={{ paginationOf }}
60
+ />
61
+ );
62
+ expect(paginationOf).toHaveBeenCalledWith(11, 20, 25);
63
+ });
64
+ });
@@ -1,6 +1,7 @@
1
1
 
2
- import React from 'react';
2
+ import React, { useId } from 'react';
3
3
  import { Button } from '../ui/Button';
4
+ import type { GridLocaleText } from '../../types';
4
5
 
5
6
  export interface PaginationProps {
6
7
  page: number;
@@ -11,9 +12,11 @@ export interface PaginationProps {
11
12
  pageSizeOptions?: number[];
12
13
  showFirstButton?: boolean;
13
14
  showLastButton?: boolean;
15
+ localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
14
16
  }
15
17
 
16
18
  export function Pagination(props: PaginationProps) {
19
+ const pageSizeId = useId();
17
20
  const {
18
21
  page,
19
22
  pageSize,
@@ -22,7 +25,8 @@ export function Pagination(props: PaginationProps) {
22
25
  onPageSizeChange,
23
26
  pageSizeOptions = [10, 25, 50, 100],
24
27
  showFirstButton = true,
25
- showLastButton = true
28
+ showLastButton = true,
29
+ localeText,
26
30
  } = props;
27
31
 
28
32
  const pageCount = Math.max(1, Math.ceil(rowCount / pageSize));
@@ -32,6 +36,14 @@ export function Pagination(props: PaginationProps) {
32
36
  const lastRowIndex = Math.min(firstRowIndex + pageSize, rowCount);
33
37
  const displayedRows = rowCount === 0 ? 0 : `${firstRowIndex + 1}–${lastRowIndex}`;
34
38
 
39
+ const rowsPerPageLabel = localeText?.paginationRowsPerPage ?? 'Rows per page:';
40
+ const ofLabel = localeText?.paginationOf
41
+ ? localeText.paginationOf(rowCount === 0 ? 0 : firstRowIndex + 1, lastRowIndex, rowCount)
42
+ : `${displayedRows} of ${rowCount}`;
43
+ const pageLabel = localeText?.paginationPage
44
+ ? localeText.paginationPage(currentPage + 1, pageCount)
45
+ : `Page ${currentPage + 1} of ${pageCount}`;
46
+
35
47
  const handleFirstPage = () => {
36
48
  onPageChange(0);
37
49
  };
@@ -61,11 +73,11 @@ export function Pagination(props: PaginationProps) {
61
73
  <div className="ogx-pagination" role="navigation" aria-label="Pagination">
62
74
  { }
63
75
  <div className="ogx-pagination__page-size">
64
- <label htmlFor="page-size-select" className="ogx-pagination__label">
65
- Rows per page:
76
+ <label htmlFor={pageSizeId} className="ogx-pagination__label">
77
+ {rowsPerPageLabel}
66
78
  </label>
67
79
  <select
68
- id="page-size-select"
80
+ id={pageSizeId}
69
81
  className="ogx-pagination__select"
70
82
  value={pageSize}
71
83
  onChange={handlePageSizeChange}
@@ -81,7 +93,7 @@ export function Pagination(props: PaginationProps) {
81
93
 
82
94
  { }
83
95
  <div className="ogx-pagination__info" aria-live="polite" aria-atomic="true">
84
- {displayedRows} of {rowCount}
96
+ {ofLabel}
85
97
  </div>
86
98
 
87
99
  { }
@@ -115,7 +127,7 @@ export function Pagination(props: PaginationProps) {
115
127
  </Button>
116
128
 
117
129
  <div className="ogx-pagination__page-info" aria-current="page">
118
- Page {currentPage + 1} of {pageCount}
130
+ {pageLabel}
119
131
  </div>
120
132
 
121
133
  <Button
@@ -1,11 +1,11 @@
1
1
 
2
- import React from 'react';
2
+ import React, { useId } from 'react';
3
3
  import { Cell } from '../Cell/Cell';
4
4
  import { Checkbox } from '../ui/Checkbox';
5
5
  import { ExpandIcon } from '../ui/ExpandIcon';
6
6
  import { DragHandleIcon } from '../ui/DragHandleIcon';
7
7
  import { DetailPanel } from '../DetailPanel/DetailPanel';
8
- import type { GridRowModel, GridColDef, GridRowId, GridColumnPinning, GridRowPinning, GridRowParams, GridCellParams, GridDetailPanelHeight } from '../../types';
8
+ import type { GridRowModel, GridColDef, GridRowId, GridColumnPinning, GridRowPinning, GridRowParams, GridCellParams, GridDetailPanelHeight, GridRowMeta } from '../../types';
9
9
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
10
10
  import { isColumnPinned, calculatePinnedPositions, isRowPinned } from '../../utils/pinning';
11
11
 
@@ -51,6 +51,7 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
51
51
  colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
52
52
  rowSpanningCaches?: RowSpanningCaches;
53
53
  rowHeight?: number;
54
+ rowMeta?: GridRowMeta;
54
55
  }
55
56
 
56
57
  export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
@@ -91,9 +92,11 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
91
92
 
92
93
  colspanMap,
93
94
  rowSpanningCaches,
94
- rowHeight = 52
95
+ rowHeight = 52,
96
+ rowMeta
95
97
  } = props;
96
98
 
99
+ const checkboxId = useId();
97
100
  const expandCellRef = React.useRef<HTMLDivElement>(null);
98
101
  const checkboxCellRef = React.useRef<HTMLDivElement>(null);
99
102
 
@@ -120,7 +123,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
120
123
  );
121
124
  }, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
122
125
 
123
- const isGroupRow = row._hasChildren === true;
126
+ const isGroupRow = rowMeta?.hasChildren === true;
124
127
 
125
128
  const handleCellEditStart = React.useCallback((field: string, value: unknown) => {
126
129
  if (!isGroupRow) {
@@ -291,7 +294,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
291
294
  }}
292
295
  >
293
296
  <Checkbox
294
- id={`ogx-select-row-${row.id}`}
297
+ id={checkboxId}
295
298
  name={`ogx-select-row-${row.id}`}
296
299
  checked={isSelected}
297
300
  onChange={handleCheckboxChange}
@@ -357,6 +360,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
357
360
  colSpanInfo={colSpanInfo}
358
361
  rowSpan={rowSpan}
359
362
  isHiddenByRowSpan={isHiddenByRowSpan}
363
+ rowMeta={rowMeta}
360
364
  />
361
365
  );
362
366
  })}
@@ -146,8 +146,6 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
146
146
  </div>
147
147
  <input
148
148
  ref={inputRef}
149
- id="ogx-global-search-input"
150
- name="ogx-global-search"
151
149
  className="ogx-global-search__input"
152
150
  value={localValue}
153
151
  onChange={(e) => setLocalValue(e.target.value)}
@@ -684,11 +684,9 @@ export function GridToolbar({
684
684
  <GridTooltip title="Pivot">
685
685
  <button
686
686
  ref={pivotButtonRef}
687
- id="ogx-pivot-btn"
688
687
  className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
689
688
  aria-label="Configure pivot"
690
689
  aria-expanded={pivotOpen}
691
- aria-controls="ogx-pivot-panel"
692
690
  onClick={() => {
693
691
  setPivotOpen(!pivotOpen);
694
692
  setAggOpen(false);
@@ -727,11 +725,9 @@ export function GridToolbar({
727
725
  <GridTooltip title="Summaries">
728
726
  <button
729
727
  ref={aggButtonRef}
730
- id="ogx-aggregation-btn"
731
728
  className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
732
729
  aria-label="Configure summaries"
733
730
  aria-expanded={aggOpen}
734
- aria-controls="ogx-aggregation-panel"
735
731
  onClick={() => {
736
732
  setAggOpen(!aggOpen);
737
733
  setPivotOpen(false);
@@ -74,14 +74,14 @@ export function useGridColumns<R extends GridRowModel>(
74
74
  return {
75
75
  ...col,
76
76
  renderCell: (cellParams: GridRenderCellParams<R>) => {
77
- const r = cellParams.row as Record<string, unknown>;
78
- const depth = typeof r._treeDepth === 'number' ? r._treeDepth : 0;
79
- const hasChildren = Boolean(r._hasChildren);
80
- const isExpanded = Boolean(r._isExpanded);
81
- const groupingField = typeof r._groupingField === 'string' ? r._groupingField : undefined;
82
- const groupingValue = r._groupingValue;
83
- const descendantCount = typeof r._descendantCount === 'number' ? r._descendantCount : undefined;
84
- const isGroupRow = Boolean(r._isGroupRow);
77
+ const meta = cellParams.rowMeta;
78
+ const depth = meta?.treeDepth ?? 0;
79
+ const hasChildren = Boolean(meta?.hasChildren);
80
+ const isExpanded = Boolean(meta?.isExpanded);
81
+ const groupingField = meta?.groupingField;
82
+ const groupingValue = meta?.groupingValue;
83
+ const descendantCount = meta?.descendantCount;
84
+ const isGroupRow = Boolean(meta?.isGroupRow);
85
85
 
86
86
  let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : cellParams.value as React.ReactNode;
87
87
 
@@ -130,9 +130,9 @@ export function useGridColumns<R extends GridRowModel>(
130
130
  return {
131
131
  ...col,
132
132
  renderCell: (cellParams: GridRenderCellParams<R>) => {
133
- const r = cellParams.row as Record<string, unknown>;
134
- const hasChildren = Boolean(r._hasChildren);
135
- const groupingField = typeof r._groupingField === 'string' ? r._groupingField : undefined;
133
+ const meta = cellParams.rowMeta;
134
+ const hasChildren = Boolean(meta?.hasChildren);
135
+ const groupingField = meta?.groupingField;
136
136
 
137
137
  if (isRowGrouping && hasChildren) {
138
138
  if (col.field === groupingField) return null;
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useGridControlledState } from './useGridControlledState';
4
+
5
+ const BASE_PARAMS = {
6
+ sortModel: undefined,
7
+ paginationModel: undefined,
8
+ rowSelectionModel: undefined,
9
+ };
10
+
11
+ describe('useGridControlledState — sort', () => {
12
+ it('uncontrolled: setInternalSortModel updates sortModel', () => {
13
+ const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
14
+ expect(result.current.sortModel).toEqual([]);
15
+ act(() => {
16
+ result.current.setInternalSortModel([{ field: 'name', sort: 'asc' }]);
17
+ });
18
+ expect(result.current.sortModel).toEqual([{ field: 'name', sort: 'asc' }]);
19
+ });
20
+
21
+ it('controlled: external sortModel prop always wins', () => {
22
+ const externalSort = [{ field: 'age', sort: 'desc' as const }];
23
+ const { result } = renderHook(() =>
24
+ useGridControlledState({ ...BASE_PARAMS, sortModel: externalSort })
25
+ );
26
+ expect(result.current.isSortControlled).toBe(true);
27
+ expect(result.current.sortModel).toEqual(externalSort);
28
+ act(() => {
29
+ result.current.setInternalSortModel([{ field: 'name', sort: 'asc' }]);
30
+ });
31
+ // External prop still wins
32
+ expect(result.current.sortModel).toEqual(externalSort);
33
+ });
34
+
35
+ it('initialState.sorting seeds the internal sort model', () => {
36
+ const { result } = renderHook(() =>
37
+ useGridControlledState({
38
+ ...BASE_PARAMS,
39
+ initialState: { sorting: { sortModel: [{ field: 'name', sort: 'asc' }] } },
40
+ })
41
+ );
42
+ expect(result.current.sortModel).toEqual([{ field: 'name', sort: 'asc' }]);
43
+ });
44
+ });
45
+
46
+ describe('useGridControlledState — pagination', () => {
47
+ it('uncontrolled: handlePaginationModelChange updates effectivePaginationModel', () => {
48
+ const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
49
+ act(() => {
50
+ result.current.handlePaginationModelChange({ page: 2, pageSize: 25 });
51
+ });
52
+ expect(result.current.effectivePaginationModel).toEqual({ page: 2, pageSize: 25 });
53
+ });
54
+
55
+ it('controlled: external paginationModel always wins', () => {
56
+ const externalPagination = { page: 3, pageSize: 50 };
57
+ const { result } = renderHook(() =>
58
+ useGridControlledState({ ...BASE_PARAMS, paginationModel: externalPagination })
59
+ );
60
+ expect(result.current.effectivePaginationModel).toEqual(externalPagination);
61
+ act(() => {
62
+ result.current.handlePaginationModelChange({ page: 0, pageSize: 10 });
63
+ });
64
+ expect(result.current.effectivePaginationModel).toEqual(externalPagination);
65
+ });
66
+
67
+ it('initialState.pagination seeds page and pageSize', () => {
68
+ const { result } = renderHook(() =>
69
+ useGridControlledState({
70
+ ...BASE_PARAMS,
71
+ initialState: { pagination: { paginationModel: { page: 1, pageSize: 25 } } },
72
+ })
73
+ );
74
+ expect(result.current.effectivePaginationModel).toEqual({ page: 1, pageSize: 25 });
75
+ });
76
+ });
77
+
78
+ describe('useGridControlledState — selection', () => {
79
+ it('uncontrolled: setInternalRowSelectionModel updates selectedRowIds', () => {
80
+ const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
81
+ act(() => {
82
+ result.current.setInternalRowSelectionModel([1, 2, 3]);
83
+ });
84
+ expect(result.current.selectedRowIds).toEqual(new Set([1, 2, 3]));
85
+ });
86
+ });