@opencorestack/opengridx 2.0.4 → 3.0.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 (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  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 -161
  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 +45 -22
  29. package/docs/components/datagrid.md +14 -12
  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 +100 -36
  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 +147 -41
  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 +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -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 +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -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 +11 -0
  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 +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  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/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. 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,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
- });
@@ -1,176 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { renderHook, act } from '@testing-library/react';
3
- import React from 'react';
4
- import { useGridKeyboardNavigation } from './useGridKeyboardNavigation';
5
-
6
- // Helper: build a minimal React.KeyboardEvent mock
7
- function keyEvent(key: string): React.KeyboardEvent {
8
- return {
9
- key,
10
- preventDefault: vi.fn(),
11
- stopPropagation: vi.fn(),
12
- nativeEvent: new KeyboardEvent('keydown', { key }),
13
- isDefaultPrevented: () => false,
14
- isPropagationStopped: () => false,
15
- persist: vi.fn(),
16
- bubbles: true,
17
- cancelable: true,
18
- defaultPrevented: false,
19
- eventPhase: 0,
20
- isTrusted: false,
21
- timeStamp: 0,
22
- type: 'keydown',
23
- currentTarget: null,
24
- target: null,
25
- shiftKey: false,
26
- ctrlKey: false,
27
- metaKey: false,
28
- altKey: false,
29
- } as unknown as React.KeyboardEvent;
30
- }
31
-
32
- const ROWS = [
33
- { id: 1, name: 'Alice' },
34
- { id: 2, name: 'Bob' },
35
- { id: 3, name: 'Carol' },
36
- ];
37
-
38
- const NAV_COLS = [
39
- { field: 'name' },
40
- { field: 'age' },
41
- ];
42
-
43
- const MOCK_EDITING = {
44
- editingCell: null,
45
- startCellEdit: vi.fn(),
46
- stopCellEdit: vi.fn(),
47
- };
48
-
49
- const MOCK_VIRTUALIZATION = {
50
- cumulativeHeights: [52, 104, 156],
51
- pinnedTopHeight: 0,
52
- columnMetrics: null,
53
- };
54
-
55
- const BASE_PARAMS = {
56
- allRenderableRows: ROWS,
57
- navigationColumns: NAV_COLS,
58
- checkboxSelection: false,
59
- selectedRowIds: new Set<number | string>(),
60
- handleSelectionChange: vi.fn(),
61
- handleDetailPanelToggle: vi.fn(),
62
- editingHandlers: MOCK_EDITING,
63
- setKeyboardMode: vi.fn(),
64
- sortModel: [],
65
- handleSort: vi.fn(),
66
- isCellEditable: undefined,
67
- pagination: false,
68
- pageSize: 100,
69
- virtualization: MOCK_VIRTUALIZATION,
70
- viewportRef: { current: null } as React.RefObject<HTMLDivElement | null>,
71
- };
72
-
73
- describe('useGridKeyboardNavigation — arrow keys', () => {
74
- it('ArrowDown moves focusedCell to the next row', () => {
75
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
76
- act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
77
- act(() => { result.current.handleKeyDown(keyEvent('ArrowDown')); });
78
- expect(result.current.focusedCell).toEqual({ id: 2, field: 'name' });
79
- });
80
-
81
- it('ArrowUp at header row does not move above it', () => {
82
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
83
- // HEADER is the topmost position; ArrowUp from there should not move
84
- act(() => { result.current.setFocusedCell({ id: 'HEADER', field: 'name' }); });
85
- act(() => { result.current.handleKeyDown(keyEvent('ArrowUp')); });
86
- expect(result.current.focusedCell?.id).toBe('HEADER');
87
- });
88
-
89
- it('ArrowDown at last row — no change', () => {
90
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
91
- // ROWS has 3 entries; last row id is 3
92
- const lastRow = ROWS[ROWS.length - 1];
93
- act(() => { result.current.setFocusedCell({ id: lastRow.id, field: 'name' }); });
94
- act(() => { result.current.handleKeyDown(keyEvent('ArrowDown')); });
95
- expect(result.current.focusedCell?.id).toBe(lastRow.id);
96
- });
97
-
98
- it('ArrowRight moves focusedCell to next column', () => {
99
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
100
- act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
101
- act(() => { result.current.handleKeyDown(keyEvent('ArrowRight')); });
102
- expect(result.current.focusedCell?.field).toBe('age');
103
- });
104
-
105
- it('ArrowLeft at header first column does not move left', () => {
106
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
107
- // HEADER + first col is the leftmost position; wrap-left would require r=-2 which is out of bounds
108
- act(() => { result.current.setFocusedCell({ id: 'HEADER', field: 'name' }); });
109
- act(() => { result.current.handleKeyDown(keyEvent('ArrowLeft')); });
110
- expect(result.current.focusedCell?.field).toBe('name');
111
- });
112
- });
113
-
114
- describe('useGridKeyboardNavigation — Home / End', () => {
115
- it('Home moves to first column', () => {
116
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
117
- act(() => { result.current.setFocusedCell({ id: 1, field: 'age' }); });
118
- act(() => { result.current.handleKeyDown(keyEvent('Home')); });
119
- expect(result.current.focusedCell?.field).toBe('name');
120
- });
121
-
122
- it('End moves to last column', () => {
123
- const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
124
- act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
125
- act(() => { result.current.handleKeyDown(keyEvent('End')); });
126
- expect(result.current.focusedCell?.field).toBe('age');
127
- });
128
- });
129
-
130
- describe('useGridKeyboardNavigation — editing', () => {
131
- it('Enter calls startCellEdit on an editable cell', () => {
132
- const startCellEdit = vi.fn();
133
- const params = {
134
- ...BASE_PARAMS,
135
- editingHandlers: { ...MOCK_EDITING, startCellEdit },
136
- navigationColumns: [{ field: 'name', editable: true }],
137
- };
138
- const { result } = renderHook(() => useGridKeyboardNavigation(params));
139
- act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
140
- act(() => { result.current.handleKeyDown(keyEvent('Enter')); });
141
- expect(startCellEdit).toHaveBeenCalledWith(
142
- expect.objectContaining({ id: 1, field: 'name' })
143
- );
144
- });
145
-
146
- it('Escape calls stopCellEdit with cancel=true when editing', () => {
147
- const stopCellEdit = vi.fn();
148
- const params = {
149
- ...BASE_PARAMS,
150
- editingHandlers: {
151
- editingCell: { id: 1, field: 'name', value: 'Alice' },
152
- startCellEdit: vi.fn(),
153
- stopCellEdit,
154
- },
155
- };
156
- const { result } = renderHook(() => useGridKeyboardNavigation(params));
157
- act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
158
- act(() => { result.current.handleKeyDown(keyEvent('Escape')); });
159
- expect(stopCellEdit).toHaveBeenCalledWith({ cancel: true });
160
- });
161
- });
162
-
163
- describe('useGridKeyboardNavigation — selection', () => {
164
- it('Space toggles checkbox selection on focused row', () => {
165
- const handleSelectionChange = vi.fn();
166
- const { result } = renderHook(() => useGridKeyboardNavigation({
167
- ...BASE_PARAMS,
168
- checkboxSelection: true,
169
- handleSelectionChange,
170
- }));
171
- // Space only fires handleSelectionChange when field === '__checkbox_col__'
172
- act(() => { result.current.setFocusedCell({ id: 2, field: '__checkbox_col__' }); });
173
- act(() => { result.current.handleKeyDown(keyEvent(' ')); });
174
- expect(handleSelectionChange).toHaveBeenCalledWith(2, true);
175
- });
176
- });