@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -166
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +6 -7
  29. package/docs/components/datagrid.md +15 -13
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +90 -38
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +126 -39
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,127 +0,0 @@
1
- import React from 'react';
2
- import { describe, it, expect, vi } from 'vitest';
3
- import { render, screen, fireEvent } from '@testing-library/react';
4
- import { DataGrid } from './DataGrid';
5
- import type { GridColDef } from '../../types';
6
-
7
- const COLUMNS: GridColDef[] = [
8
- { field: 'id', headerName: 'ID', width: 80 },
9
- { field: 'name', headerName: 'Name', width: 150 },
10
- { field: 'dept', headerName: 'Dept', width: 120 },
11
- ];
12
-
13
- const ROWS = [
14
- { id: 1, name: 'Alice', dept: 'Eng' },
15
- { id: 2, name: 'Bob', dept: 'HR' },
16
- ];
17
-
18
- // ── density ──────────────────────────────────────────────────────────────────
19
-
20
- describe('DataGrid — density prop', () => {
21
- it('renders without throwing for compact density', () => {
22
- expect(() =>
23
- render(<DataGrid rows={ROWS} columns={COLUMNS} density="compact" />)
24
- ).not.toThrow();
25
- });
26
-
27
- it('renders without throwing for comfortable density', () => {
28
- expect(() =>
29
- render(<DataGrid rows={ROWS} columns={COLUMNS} density="comfortable" />)
30
- ).not.toThrow();
31
- });
32
-
33
- it('applies compact row height via CSS variable', () => {
34
- const { container } = render(
35
- <DataGrid rows={ROWS} columns={COLUMNS} density="compact" />
36
- );
37
- const root = container.firstElementChild as HTMLElement;
38
- expect(root.style.getPropertyValue('--ogx-row-height')).toBe('32px');
39
- });
40
-
41
- it('applies comfortable row height via CSS variable', () => {
42
- const { container } = render(
43
- <DataGrid rows={ROWS} columns={COLUMNS} density="comfortable" />
44
- );
45
- const root = container.firstElementChild as HTMLElement;
46
- expect(root.style.getPropertyValue('--ogx-row-height')).toBe('72px');
47
- });
48
-
49
- it('uses rowHeight as default when density is standard', () => {
50
- const { container } = render(
51
- <DataGrid rows={ROWS} columns={COLUMNS} density="standard" rowHeight={52} />
52
- );
53
- const root = container.firstElementChild as HTMLElement;
54
- expect(root.style.getPropertyValue('--ogx-row-height')).toBe('52px');
55
- });
56
- });
57
-
58
- // ── disableRowSelectionOnClick ────────────────────────────────────────────────
59
-
60
- describe('DataGrid — disableRowSelectionOnClick', () => {
61
- it('clicking a row changes selection when prop is false (default)', () => {
62
- const onChange = vi.fn();
63
- render(
64
- <DataGrid
65
- rows={ROWS}
66
- columns={COLUMNS}
67
- onRowSelectionModelChange={onChange}
68
- />
69
- );
70
- const rows = screen.getAllByRole('row');
71
- // rows[0] is header, rows[1] is Alice
72
- fireEvent.click(rows[1]);
73
- expect(onChange).toHaveBeenCalled();
74
- });
75
-
76
- it('clicking a row does NOT change selection when disableRowSelectionOnClick=true', () => {
77
- const onChange = vi.fn();
78
- render(
79
- <DataGrid
80
- rows={ROWS}
81
- columns={COLUMNS}
82
- disableRowSelectionOnClick
83
- onRowSelectionModelChange={onChange}
84
- />
85
- );
86
- const rows = screen.getAllByRole('row');
87
- fireEvent.click(rows[1]);
88
- expect(onChange).not.toHaveBeenCalled();
89
- });
90
- });
91
-
92
- // ── disableMultipleRowSelection ───────────────────────────────────────────────
93
-
94
- describe('DataGrid — disableMultipleRowSelection', () => {
95
- it('selecting a second row replaces the first when disableMultipleRowSelection=true', () => {
96
- const selections: unknown[][] = [];
97
- render(
98
- <DataGrid
99
- rows={ROWS}
100
- columns={COLUMNS}
101
- disableMultipleRowSelection
102
- onRowSelectionModelChange={(model) => selections.push(model)}
103
- />
104
- );
105
- const rows = screen.getAllByRole('row');
106
- fireEvent.click(rows[1]); // select Alice (id=1)
107
- fireEvent.click(rows[2]); // select Bob (id=2) — should replace
108
- expect(selections.at(-1)).toEqual([2]);
109
- expect(selections.at(-1)?.length).toBe(1);
110
- });
111
-
112
- it('clicking already-selected row deselects it when disableMultipleRowSelection=true', () => {
113
- const selections: unknown[][] = [];
114
- render(
115
- <DataGrid
116
- rows={ROWS}
117
- columns={COLUMNS}
118
- disableMultipleRowSelection
119
- onRowSelectionModelChange={(model) => selections.push(model)}
120
- />
121
- );
122
- const rows = screen.getAllByRole('row');
123
- fireEvent.click(rows[1]); // select
124
- fireEvent.click(rows[1]); // deselect
125
- expect(selections.at(-1)).toEqual([]);
126
- });
127
- });
@@ -1,183 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import { render, cleanup } from '@testing-library/react';
3
- import { useEffect } from 'react';
4
- import type { ReactNode } from 'react';
5
- import { DataGrid, useGridApiRef } from '../../index';
6
- import type { GridColDef } from '../../types';
7
-
8
- // Runs in real Chromium (vitest browser project). jsdom has no layout engine, so the
9
- // container-height behaviour these tests pin down cannot be reproduced there.
10
-
11
- type Row = { id: number; region: string; amount: number };
12
-
13
- const makeRows = (total: number, northCount: number): Row[] =>
14
- Array.from({ length: total }, (_, i) => ({ id: i + 1, region: i < northCount ? 'North' : 'South', amount: i }));
15
-
16
- const columns: GridColDef<Row>[] = [
17
- { field: 'region', width: 200 },
18
- { field: 'amount', width: 150 },
19
- ];
20
-
21
- // Bounded: a flex child with min-height: 0 cannot grow past the 600px parent.
22
- const Bounded = ({ children }: { children: ReactNode }) => (
23
- <div style={{ height: 600, display: 'flex', flexDirection: 'column' }}>
24
- <div style={{ flex: 1, minHeight: 0 }}>{children}</div>
25
- </div>
26
- );
27
-
28
- // Unbounded: the classic trap — a flex child without min-height: 0 grows to fit its content.
29
- const Unbounded = ({ children }: { children: ReactNode }) => (
30
- <div style={{ height: 600, display: 'flex', flexDirection: 'column' }}>
31
- <div style={{ flex: 1 }}>{children}</div>
32
- </div>
33
- );
34
-
35
- const domRowCount = (container: HTMLElement) => container.querySelectorAll('.ogx__rows [role="row"]').length;
36
- const firstRow = (container: HTMLElement) => container.querySelector<HTMLElement>('.ogx__rows [role="row"]')!;
37
-
38
- afterEach(() => {
39
- cleanup();
40
- vi.restoreAllMocks();
41
- });
42
-
43
- describe('DataGrid virtualization in a real browser', () => {
44
- it('virtualizes a large flat dataset in a bounded container', async () => {
45
- const { container } = render(<Bounded><DataGrid rows={makeRows(30000, 30000)} columns={columns} height="100%" /></Bounded>);
46
- await expect.poll(() => domRowCount(container)).toBeGreaterThan(0);
47
- expect(domRowCount(container)).toBeLessThan(60);
48
- });
49
-
50
- it('keeps DOM rows bounded after expanding a 25,000-row group', async () => {
51
- const { container } = render(
52
- <Bounded><DataGrid rows={makeRows(30000, 25000)} columns={columns} height="100%" rowGroupingModel={['region']} /></Bounded>
53
- );
54
- await expect.poll(() => domRowCount(container)).toBe(2);
55
-
56
- firstRow(container).click();
57
-
58
- await expect.poll(() => domRowCount(container)).toBeGreaterThan(2);
59
- expect(domRowCount(container)).toBeLessThan(60);
60
-
61
- const viewport = container.querySelector<HTMLElement>('.ogx__viewport')!;
62
- viewport.scrollTop = viewport.scrollHeight / 2;
63
- viewport.dispatchEvent(new Event('scroll'));
64
- await expect.poll(() => container.querySelector('.ogx__rows [role="row"]')?.getAttribute('data-rowindex')).not.toBe('0');
65
- expect(domRowCount(container)).toBeLessThan(80);
66
- });
67
-
68
- it('warns in development when an unbounded container defeats virtualization', async () => {
69
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
70
- const { container } = render(
71
- <Unbounded><DataGrid rows={makeRows(3000, 2500)} columns={columns} height="100%" rowGroupingModel={['region']} /></Unbounded>
72
- );
73
- await expect.poll(() => domRowCount(container)).toBe(2);
74
- firstRow(container).click();
75
-
76
- await expect.poll(() => warn.mock.calls.some(([msg]) => String(msg).includes('virtualization is effectively off'))).toBe(true);
77
- });
78
-
79
- it('warns in development that pagination is ignored under row grouping', async () => {
80
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
81
- render(<Bounded><DataGrid rows={makeRows(100, 50)} columns={columns} height="100%" pagination rowGroupingModel={['region']} /></Bounded>);
82
- await expect.poll(() => warn.mock.calls.some(([msg]) => String(msg).includes('`pagination` is ignored'))).toBe(true);
83
- });
84
-
85
- it('positions every scrolled row correctly when rows are pinned to the top', async () => {
86
- const rowHeight = 52;
87
- const rows = Array.from({ length: 2000 }, (_, i) => ({ id: i + 1, region: 'r', amount: i }));
88
- const { container } = render(
89
- <Bounded>
90
- <DataGrid rows={rows} columns={[{ field: 'id', width: 100 }, ...columns]} height="100%"
91
- rowHeight={rowHeight} pinnedRows={{ top: [1, 2] }} />
92
- </Bounded>
93
- );
94
- await expect.poll(() => domRowCount(container)).toBeGreaterThan(0);
95
-
96
- const viewport = container.querySelector<HTMLElement>('.ogx__viewport')!;
97
- viewport.scrollTop = rowHeight * 500;
98
- viewport.dispatchEvent(new Event('scroll'));
99
- await expect.poll(() => Number(firstRow(container).getAttribute('data-rowindex'))).toBeGreaterThan(100);
100
-
101
- const virtualContainer = container.querySelector<HTMLElement>('.ogx__virtual-container')!;
102
- const containerTop = virtualContainer.getBoundingClientRect().top;
103
- const centerRows = [...virtualContainer.querySelectorAll<HTMLElement>('[role="row"]')];
104
- expect(centerRows.length).toBeGreaterThan(5);
105
- for (const rowEl of centerRows) {
106
- const id = Number(rowEl.querySelector('[data-field="id"]')!.textContent);
107
- // ids 1 and 2 are pinned, so id N is unpinned index N - 3.
108
- const expectedTop = (id - 3) * rowHeight;
109
- expect(Math.round(rowEl.getBoundingClientRect().top - containerTop)).toBe(expectedTop);
110
- }
111
-
112
- // The viewport must be filled: the last rendered row reaches past the visible bottom.
113
- const lastRendered = centerRows[centerRows.length - 1].getBoundingClientRect().bottom;
114
- expect(lastRendered).toBeGreaterThanOrEqual(viewport.getBoundingClientRect().bottom - 1);
115
- });
116
-
117
- describe('keyboard navigation keeps the focused row fully visible', () => {
118
- const rowHeight = 52;
119
- const rows = Array.from({ length: 300 }, (_, i) => ({ id: i + 1, region: 'r', amount: i }));
120
-
121
- const pressArrowDown = async (times: number) => {
122
- const grid = document.querySelector<HTMLElement>('.ogx__viewport')!;
123
- for (let i = 0; i < times; i++) {
124
- grid.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
125
- await new Promise(r => requestAnimationFrame(() => r(null)));
126
- }
127
- };
128
-
129
- const assertFocusedRowVisible = (container: HTMLElement) => {
130
- const viewport = container.querySelector<HTMLElement>('.ogx__viewport')!;
131
- const focused = container.querySelector<HTMLElement>('.ogx__cell--focused')!;
132
- expect(focused).not.toBeNull();
133
- const cell = focused.getBoundingClientRect();
134
- const vp = viewport.getBoundingClientRect();
135
- const stickyTop = [...container.querySelectorAll<HTMLElement>('.ogx__header-wrap, .ogx__pinned-rows--top')]
136
- .reduce((max, el) => Math.max(max, el.getBoundingClientRect().bottom), vp.top);
137
- const stickyBottom = [...container.querySelectorAll<HTMLElement>('.ogx__pinned-rows--bottom')]
138
- .reduce((min, el) => Math.min(min, el.getBoundingClientRect().top), vp.top + viewport.clientHeight);
139
- expect(cell.top).toBeGreaterThanOrEqual(stickyTop - 1);
140
- expect(cell.bottom).toBeLessThanOrEqual(stickyBottom + 1);
141
- };
142
-
143
- for (const pinned of [false, true]) {
144
- it(`after ArrowDown past the bottom edge${pinned ? ' with pinned top rows' : ''}`, async () => {
145
- const { container } = render(
146
- <Bounded>
147
- <DataGrid rows={rows} columns={[{ field: 'id', width: 100 }, ...columns]} height="100%"
148
- rowHeight={rowHeight} pinnedRows={pinned ? { top: [1, 2] } : undefined} />
149
- </Bounded>
150
- );
151
- await expect.poll(() => domRowCount(container)).toBeGreaterThan(0);
152
- const target = container.querySelector<HTMLElement>(`.ogx__virtual-container [data-field="id"]`)!;
153
- target.click();
154
- await pressArrowDown(20);
155
- assertFocusedRowVisible(container);
156
- });
157
- }
158
- });
159
-
160
- it('apiRef.scrollToIndexes brings a row below the fold fully into view', async () => {
161
- const rows = Array.from({ length: 300 }, (_, i) => ({ id: i + 1, region: 'r', amount: i }));
162
- const onReady = vi.fn<(apiRef: ReturnType<typeof useGridApiRef>) => void>();
163
- const Harness = ({ onApi }: { onApi: typeof onReady }) => {
164
- const apiRef = useGridApiRef();
165
- useEffect(() => { onApi(apiRef); }, [apiRef, onApi]);
166
- return <DataGrid apiRef={apiRef} rows={rows} columns={[{ field: 'id', width: 100 }, ...columns]} height="100%" />;
167
- };
168
- const { container } = render(<Bounded><Harness onApi={onReady} /></Bounded>);
169
- await expect.poll(() => domRowCount(container)).toBeGreaterThan(0);
170
-
171
- onReady.mock.calls[0][0].current.scrollToIndexes({ rowIndex: 40 });
172
- const viewport = container.querySelector<HTMLElement>('.ogx__viewport')!;
173
- viewport.dispatchEvent(new Event('scroll'));
174
- const rowEl = () => [...container.querySelectorAll<HTMLElement>('.ogx__virtual-container [role="row"]')]
175
- .find(r => r.querySelector('[data-field="id"]')?.textContent === '41');
176
- await expect.poll(() => Boolean(rowEl())).toBe(true);
177
- const vp = viewport.getBoundingClientRect();
178
- const header = container.querySelector<HTMLElement>('.ogx__header-wrap')!.getBoundingClientRect();
179
- const r = rowEl()!.getBoundingClientRect();
180
- expect(r.top).toBeGreaterThanOrEqual(header.bottom - 1);
181
- expect(r.bottom).toBeLessThanOrEqual(vp.top + viewport.clientHeight + 1);
182
- });
183
- });
@@ -1,135 +0,0 @@
1
- import React from 'react';
2
- import { describe, it, expect, vi } from 'vitest';
3
- import { render, screen, fireEvent } from '@testing-library/react';
4
- import { Header } from './Header';
5
- import type { GridColDef } from '../../types';
6
-
7
- const COLUMNS: GridColDef[] = [
8
- { field: 'name', headerName: 'Name', width: 150 },
9
- { field: 'age', headerName: 'Age', width: 100 },
10
- { field: 'dept', headerName: 'Dept', width: 120 },
11
- ];
12
-
13
- describe('Header — multi-sort shift-click', () => {
14
- it('plain click calls onSort with single field (replaces model)', () => {
15
- const onSort = vi.fn();
16
- const onSortAdd = vi.fn();
17
- render(
18
- <Header
19
- columns={COLUMNS}
20
- sortModel={[]}
21
- onSort={onSort}
22
- onSortAdd={onSortAdd}
23
- />
24
- );
25
- fireEvent.click(screen.getByText('Name'));
26
- expect(onSort).toHaveBeenCalledWith('name', 'asc');
27
- expect(onSortAdd).not.toHaveBeenCalled();
28
- });
29
-
30
- it('shift-click calls onSortAdd (accumulates)', () => {
31
- const onSort = vi.fn();
32
- const onSortAdd = vi.fn();
33
- render(
34
- <Header
35
- columns={COLUMNS}
36
- sortModel={[{ field: 'name', sort: 'asc' }]}
37
- onSort={onSort}
38
- onSortAdd={onSortAdd}
39
- />
40
- );
41
- fireEvent.click(screen.getByText('Age'), { shiftKey: true });
42
- expect(onSortAdd).toHaveBeenCalledWith('age', 'asc');
43
- expect(onSort).not.toHaveBeenCalled();
44
- });
45
-
46
- it('shift-click on already-sorted field cycles direction without replacing others', () => {
47
- const onSort = vi.fn();
48
- const onSortAdd = vi.fn();
49
- render(
50
- <Header
51
- columns={COLUMNS}
52
- sortModel={[
53
- { field: 'name', sort: 'asc' },
54
- { field: 'age', sort: 'asc' },
55
- ]}
56
- onSort={onSort}
57
- onSortAdd={onSortAdd}
58
- />
59
- );
60
- fireEvent.click(screen.getByText('Age'), { shiftKey: true });
61
- expect(onSortAdd).toHaveBeenCalledWith('age', 'desc');
62
- expect(onSort).not.toHaveBeenCalled();
63
- });
64
-
65
- it('shift-click on desc-sorted field removes it from model', () => {
66
- const onSort = vi.fn();
67
- const onSortAdd = vi.fn();
68
- render(
69
- <Header
70
- columns={COLUMNS}
71
- sortModel={[
72
- { field: 'name', sort: 'asc' },
73
- { field: 'age', sort: 'desc' },
74
- ]}
75
- onSort={onSort}
76
- onSortAdd={onSortAdd}
77
- />
78
- );
79
- fireEvent.click(screen.getByText('Age'), { shiftKey: true });
80
- expect(onSortAdd).toHaveBeenCalledWith('age', null);
81
- expect(onSort).not.toHaveBeenCalled();
82
- });
83
-
84
- it('plain click on one column while another is sorted resets to single-key', () => {
85
- const onSort = vi.fn();
86
- const onSortAdd = vi.fn();
87
- render(
88
- <Header
89
- columns={COLUMNS}
90
- sortModel={[{ field: 'name', sort: 'asc' }]}
91
- onSort={onSort}
92
- onSortAdd={onSortAdd}
93
- />
94
- );
95
- fireEvent.click(screen.getByText('Age'));
96
- expect(onSort).toHaveBeenCalledWith('age', 'asc');
97
- expect(onSortAdd).not.toHaveBeenCalled();
98
- });
99
-
100
- it('does not call onSort/onSortAdd for sortable:false columns', () => {
101
- const onSort = vi.fn();
102
- const onSortAdd = vi.fn();
103
- const cols: GridColDef[] = [
104
- { field: 'id', headerName: 'ID', sortable: false },
105
- ];
106
- render(<Header columns={cols} sortModel={[]} onSort={onSort} onSortAdd={onSortAdd} />);
107
- fireEvent.click(screen.getByText('ID'));
108
- expect(onSort).not.toHaveBeenCalled();
109
- expect(onSortAdd).not.toHaveBeenCalled();
110
- });
111
- });
112
-
113
- describe('Header — description tooltip', () => {
114
- it('renders title attribute from column description', () => {
115
- const cols: GridColDef[] = [
116
- { field: 'salary', headerName: 'Salary', description: 'Annual salary in USD' },
117
- ];
118
- const { container } = render(
119
- <Header columns={cols} sortModel={[]} />
120
- );
121
- const headerCell = container.querySelector('[data-field="salary"]');
122
- expect(headerCell?.getAttribute('title')).toBe('Annual salary in USD');
123
- });
124
-
125
- it('has no title attribute when description is absent', () => {
126
- const cols: GridColDef[] = [
127
- { field: 'name', headerName: 'Name' },
128
- ];
129
- const { container } = render(
130
- <Header columns={cols} sortModel={[]} />
131
- );
132
- const headerCell = container.querySelector('[data-field="name"]');
133
- expect(headerCell?.getAttribute('title')).toBeNull();
134
- });
135
- });
@@ -1,64 +0,0 @@
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,66 +0,0 @@
1
- /**
2
- * Regression tests for bugs fixed in the useDataGrid reducer.
3
- *
4
- * Each describe block names the bug and the commit that fixed it, so a
5
- * future failure tells you exactly what regressed and where to look.
6
- */
7
-
8
- import { describe, it, expect } from 'vitest';
9
- import { renderHook, act } from '@testing-library/react';
10
- import { useDataGrid } from './useDataGrid';
11
- import type { GridColDef } from '../../types';
12
-
13
- const COLS: GridColDef[] = [
14
- { field: 'id', headerName: 'ID', width: 80 },
15
- { field: 'name', headerName: 'Name', width: 150 },
16
- { field: 'age', headerName: 'Age', width: 100 },
17
- ];
18
-
19
- const ROWS = [
20
- { id: 1, name: 'Alice', age: 30 },
21
- { id: 2, name: 'Bob', age: 25 },
22
- ];
23
-
24
- // ─────────────────────────────────────────────────────────────────────────────
25
- // Bug: SET_COLUMNS reducer was hardcoding columnVisibilityModel: {} on every
26
- // column update, silently wiping any hidden columns.
27
- // ─────────────────────────────────────────────────────────────────────────────
28
- describe('SET_COLUMNS preserves columnVisibilityModel', () => {
29
- it('keeps hidden columns hidden after setColumns is called', () => {
30
- const { result } = renderHook(() =>
31
- useDataGrid({
32
- rows: ROWS,
33
- columns: COLS,
34
- columnVisibilityModel: { age: false },
35
- })
36
- );
37
-
38
- // Confirm the initial visibility model was applied.
39
- expect(result.current.state.columns.columnVisibilityModel).toEqual({ age: false });
40
-
41
- // Simulate a column update (e.g. a width change coming from a resize handler).
42
- const updatedCols: GridColDef[] = COLS.map(c =>
43
- c.field === 'name' ? { ...c, width: 200 } : c
44
- );
45
-
46
- act(() => {
47
- result.current.setColumns(updatedCols);
48
- });
49
-
50
- // The visibility model must survive the column update — not reset to {}.
51
- expect(result.current.state.columns.columnVisibilityModel).toEqual({ age: false });
52
- });
53
-
54
- it('does not gain a visibility entry for columns that were already visible', () => {
55
- const { result } = renderHook(() =>
56
- useDataGrid({ rows: ROWS, columns: COLS })
57
- );
58
-
59
- act(() => {
60
- result.current.setColumns(COLS);
61
- });
62
-
63
- // No spurious entries: an empty model means all columns are visible.
64
- expect(result.current.state.columns.columnVisibilityModel).toEqual({});
65
- });
66
- });
@@ -1,86 +0,0 @@
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
- });