@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.
- package/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- 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
|
-
});
|