@dashforge/tw 0.6.0-beta → 0.8.0-beta
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 +451 -0
- package/THEME-AUDIT.md +175 -0
- package/dist/index.esm.js +2009 -87
- package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
- package/dist/src/components/Box/box.variants.d.ts.map +1 -1
- package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts +214 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +222 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +1 -0
- package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
- package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
- package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
- package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
- package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
- package/dist/src/components/Table/Table.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +77 -5
- package/dist/src/components/Table/table.types.d.ts.map +1 -1
- package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
- package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
- package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.d.ts +1 -1
- package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useTableFilter.d.ts +36 -0
- package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSearch.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSelection.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSort.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/Accordion/accordion.variants.ts +5 -3
- package/src/components/Box/Box.test.tsx +32 -11
- package/src/components/Box/box.variants.ts +23 -4
- package/src/components/DataGrid/DataGrid.test.tsx +917 -0
- package/src/components/DataGrid/DataGrid.tsx +1199 -0
- package/src/components/DataGrid/dataGrid.types.ts +238 -0
- package/src/components/DataGrid/dataGrid.variants.ts +152 -0
- package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
- package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
- package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
- package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
- package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -0
- package/src/components/Dialog/dialog.variants.ts +10 -6
- package/src/components/Divider/Divider.test.tsx +3 -1
- package/src/components/Divider/Divider.tsx +1 -1
- package/src/components/Divider/divider.variants.ts +2 -1
- package/src/components/Pagination/Pagination.tsx +3 -1
- package/src/components/Pagination/pagination.variants.ts +19 -13
- package/src/components/Popover/popover.variants.ts +10 -4
- package/src/components/Skeleton/skeleton.variants.ts +2 -1
- package/src/components/Table/Table.tsx +22 -8
- package/src/components/Table/table.types.ts +83 -5
- package/src/components/Tabs/tabs.variants.ts +19 -8
- package/src/components/Tooltip/tooltip.variants.ts +12 -2
- package/src/components/Typography/Typography.test.tsx +3 -1
- package/src/components/Typography/typography.variants.ts +6 -2
- package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.ts +1 -1
- package/src/components/_shared/data/useTableFilter.test.ts +166 -0
- package/src/components/_shared/data/useTableFilter.ts +118 -0
- package/src/components/{Table/_internal → _shared/data}/useTableSearch.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSearch.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSelection.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSort.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSort.ts +1 -1
- package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
- package/src/components/_shared/data/useVirtualizer.ts +199 -0
- package/src/components/_shared/themeIdentity.test.ts +113 -0
- package/src/index.ts +20 -2
- package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useControllableState.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableFilter.d.ts +0 -13
- package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSort.d.ts.map +0 -1
- package/src/components/Table/_internal/themeIdentity.test.ts +0 -84
- package/src/components/Table/_internal/useTableFilter.ts +0 -37
- /package/dist/src/components/{Table/_internal → _shared/data}/getNestedValue.d.ts +0 -0
- /package/dist/src/components/{Table/_internal → _shared/data}/useControllableState.d.ts +0 -0
- /package/dist/src/components/{Table/_internal → _shared/data}/useDebouncedValue.d.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/getNestedValue.test.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/getNestedValue.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useControllableState.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useDebouncedValue.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useTableSelection.test.ts +0 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
|
|
4
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
5
|
+
import { DataGrid } from './DataGrid.js';
|
|
6
|
+
import type { TableColumn } from '../Table/table.types.js';
|
|
7
|
+
|
|
8
|
+
void React;
|
|
9
|
+
afterEach(() => cleanup());
|
|
10
|
+
|
|
11
|
+
// jsdom doesn't implement ResizeObserver / IntersectionObserver —
|
|
12
|
+
// virtualizer + Radix Popover (used via shared cells lib) need them.
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
15
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
|
|
16
|
+
observe() {}
|
|
17
|
+
unobserve() {}
|
|
18
|
+
disconnect() {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// jsdom returns 0 for clientHeight by default; the virtualizer
|
|
22
|
+
// uses initialViewportHeight=400 in the absence of a real measurement.
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
interface User {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
age: number;
|
|
30
|
+
status: 'active' | 'pending';
|
|
31
|
+
meta: { city: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const baseCols: TableColumn<User>[] = [
|
|
35
|
+
{ field: 'name', header: 'Name', sortable: true, searchable: true },
|
|
36
|
+
{ field: 'email', header: 'Email', searchable: true },
|
|
37
|
+
{ field: 'age', header: 'Age', sortable: true },
|
|
38
|
+
{ field: 'meta.city' as never, header: 'City' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function generateUsers(count: number): User[] {
|
|
42
|
+
return Array.from({ length: count }).map((_, i) => ({
|
|
43
|
+
id: `u${i}`,
|
|
44
|
+
name: `User ${i}`,
|
|
45
|
+
email: `user${i}@example.com`,
|
|
46
|
+
age: 20 + (i % 50),
|
|
47
|
+
status: i % 2 === 0 ? ('active' as const) : ('pending' as const),
|
|
48
|
+
meta: { city: ['Roma', 'Milano', 'Berlin'][i % 3] as string },
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const getRowId = (row: User) => row.id;
|
|
53
|
+
|
|
54
|
+
describe('DataGrid — rendering', () => {
|
|
55
|
+
it('renders a <table> element', () => {
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<DataGrid
|
|
58
|
+
rows={generateUsers(50)}
|
|
59
|
+
cols={baseCols}
|
|
60
|
+
getRowId={getRowId}
|
|
61
|
+
rowHeight={48}
|
|
62
|
+
height="400px"
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
expect(container.querySelector('table')).not.toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders column headers', () => {
|
|
69
|
+
render(
|
|
70
|
+
<DataGrid
|
|
71
|
+
rows={generateUsers(20)}
|
|
72
|
+
cols={baseCols}
|
|
73
|
+
getRowId={getRowId}
|
|
74
|
+
rowHeight={48}
|
|
75
|
+
height="400px"
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
expect(screen.queryByText('Name')).not.toBeNull();
|
|
79
|
+
expect(screen.queryByText('Email')).not.toBeNull();
|
|
80
|
+
expect(screen.queryByText('Age')).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders only a window of rows (virtualized)', () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<DataGrid
|
|
86
|
+
rows={generateUsers(1000)}
|
|
87
|
+
cols={baseCols}
|
|
88
|
+
getRowId={getRowId}
|
|
89
|
+
rowHeight={48}
|
|
90
|
+
height="400px"
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
// Visible window with 400px height + 48px rows = ~9 rows + overscan
|
|
94
|
+
// (5 top + 5 bottom). Initial viewport height fallback = 400px.
|
|
95
|
+
// So at scrollTop=0 we render rows 0 to ~13 (no top overscan needed).
|
|
96
|
+
const dataRows = container.querySelectorAll('tbody tr[data-selected]');
|
|
97
|
+
expect(dataRows.length).toBeLessThan(50); // certainly not all 1000
|
|
98
|
+
expect(dataRows.length).toBeGreaterThan(5); // at least the visible window
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('renders spacer rows above and below the visible window', () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<DataGrid
|
|
104
|
+
rows={generateUsers(1000)}
|
|
105
|
+
cols={baseCols}
|
|
106
|
+
getRowId={getRowId}
|
|
107
|
+
rowHeight={48}
|
|
108
|
+
height="400px"
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
// Spacer rows are <tr aria-hidden="true"> with inline height style.
|
|
112
|
+
const spacers = container.querySelectorAll('tbody tr[aria-hidden="true"]');
|
|
113
|
+
expect(spacers.length).toBeGreaterThanOrEqual(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('renders empty state when rows is empty', () => {
|
|
117
|
+
render(
|
|
118
|
+
<DataGrid
|
|
119
|
+
rows={[]}
|
|
120
|
+
cols={baseCols}
|
|
121
|
+
getRowId={getRowId}
|
|
122
|
+
rowHeight={48}
|
|
123
|
+
height="400px"
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
expect(screen.queryByText('No data')).not.toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('renders loading skeleton rows', () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<DataGrid
|
|
132
|
+
rows={[]}
|
|
133
|
+
cols={baseCols}
|
|
134
|
+
getRowId={getRowId}
|
|
135
|
+
rowHeight={48}
|
|
136
|
+
height="400px"
|
|
137
|
+
loading
|
|
138
|
+
loadingRowCount={6}
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
const busyRows = container.querySelectorAll('tr[aria-busy="true"]');
|
|
142
|
+
expect(busyRows.length).toBe(6);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('DataGrid — sort', () => {
|
|
147
|
+
it('sortable header has aria-sort=none initially', () => {
|
|
148
|
+
render(
|
|
149
|
+
<DataGrid
|
|
150
|
+
rows={generateUsers(20)}
|
|
151
|
+
cols={baseCols}
|
|
152
|
+
getRowId={getRowId}
|
|
153
|
+
rowHeight={48}
|
|
154
|
+
height="400px"
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
const header = screen.getByText('Name').closest('th');
|
|
158
|
+
expect(header?.getAttribute('aria-sort')).toBe('none');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('click sortable header → asc', () => {
|
|
162
|
+
render(
|
|
163
|
+
<DataGrid
|
|
164
|
+
rows={generateUsers(20)}
|
|
165
|
+
cols={baseCols}
|
|
166
|
+
getRowId={getRowId}
|
|
167
|
+
rowHeight={48}
|
|
168
|
+
height="400px"
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
172
|
+
expect(screen.getByText('Name').closest('th')?.getAttribute('aria-sort')).toBe(
|
|
173
|
+
'ascending',
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('serverSideSort prevents local sort, emits onSortChange', () => {
|
|
178
|
+
let lastModel: unknown = null;
|
|
179
|
+
const rows = generateUsers(20);
|
|
180
|
+
const { container } = render(
|
|
181
|
+
<DataGrid
|
|
182
|
+
rows={rows}
|
|
183
|
+
cols={baseCols}
|
|
184
|
+
getRowId={getRowId}
|
|
185
|
+
rowHeight={48}
|
|
186
|
+
height="400px"
|
|
187
|
+
serverSideSort
|
|
188
|
+
onSortChange={(m) => (lastModel = m)}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
const firstNameBeforeSort = container.querySelector('tbody tr[data-selected] td')?.textContent;
|
|
192
|
+
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
193
|
+
expect(lastModel).toEqual([{ field: 'name', direction: 'asc' }]);
|
|
194
|
+
// Row order didn't change since server-side: still original order.
|
|
195
|
+
const firstNameAfterSort = container.querySelector('tbody tr[data-selected] td')?.textContent;
|
|
196
|
+
expect(firstNameAfterSort).toBe(firstNameBeforeSort);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('DataGrid — search', () => {
|
|
201
|
+
it('renders search input when enableSearch', () => {
|
|
202
|
+
render(
|
|
203
|
+
<DataGrid
|
|
204
|
+
rows={generateUsers(20)}
|
|
205
|
+
cols={baseCols}
|
|
206
|
+
getRowId={getRowId}
|
|
207
|
+
rowHeight={48}
|
|
208
|
+
height="400px"
|
|
209
|
+
enableSearch
|
|
210
|
+
/>,
|
|
211
|
+
);
|
|
212
|
+
expect(screen.getByPlaceholderText('Search...')).not.toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('filters rows by search query (client-side)', () => {
|
|
216
|
+
const { container } = render(
|
|
217
|
+
<DataGrid
|
|
218
|
+
rows={generateUsers(20)}
|
|
219
|
+
cols={baseCols}
|
|
220
|
+
getRowId={getRowId}
|
|
221
|
+
rowHeight={48}
|
|
222
|
+
height="400px"
|
|
223
|
+
enableSearch
|
|
224
|
+
searchDebounceMs={0}
|
|
225
|
+
/>,
|
|
226
|
+
);
|
|
227
|
+
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
|
|
228
|
+
fireEvent.change(input, { target: { value: 'User 5' } });
|
|
229
|
+
// Only rows with 'User 5' in name/email survive (User 5, User 15, ...)
|
|
230
|
+
const rows = container.querySelectorAll('tbody tr[data-selected]');
|
|
231
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
232
|
+
expect(rows.length).toBeLessThan(20);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('serverSideSearch prevents local filter, emits onSearchQueryChange', () => {
|
|
236
|
+
let lastQuery = '';
|
|
237
|
+
render(
|
|
238
|
+
<DataGrid
|
|
239
|
+
rows={generateUsers(20)}
|
|
240
|
+
cols={baseCols}
|
|
241
|
+
getRowId={getRowId}
|
|
242
|
+
rowHeight={48}
|
|
243
|
+
height="400px"
|
|
244
|
+
enableSearch
|
|
245
|
+
searchDebounceMs={0}
|
|
246
|
+
serverSideSearch
|
|
247
|
+
onSearchQueryChange={(q) => (lastQuery = q)}
|
|
248
|
+
/>,
|
|
249
|
+
);
|
|
250
|
+
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
|
|
251
|
+
fireEvent.change(input, { target: { value: 'alice' } });
|
|
252
|
+
expect(lastQuery).toBe('alice');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('DataGrid — selection', () => {
|
|
257
|
+
it('renders checkbox for each visible row when rowSelection=multiple', () => {
|
|
258
|
+
const { container } = render(
|
|
259
|
+
<DataGrid
|
|
260
|
+
rows={generateUsers(20)}
|
|
261
|
+
cols={baseCols}
|
|
262
|
+
getRowId={getRowId}
|
|
263
|
+
rowHeight={48}
|
|
264
|
+
height="400px"
|
|
265
|
+
rowSelection="multiple"
|
|
266
|
+
/>,
|
|
267
|
+
);
|
|
268
|
+
// 1 header + N row checkboxes (N = visible window).
|
|
269
|
+
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
|
270
|
+
expect(checkboxes.length).toBeGreaterThan(2);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('toggleRow updates selectedRowIds (controlled)', () => {
|
|
274
|
+
let lastIds: string[] = [];
|
|
275
|
+
const { container } = render(
|
|
276
|
+
<DataGrid
|
|
277
|
+
rows={generateUsers(10)}
|
|
278
|
+
cols={baseCols}
|
|
279
|
+
getRowId={getRowId}
|
|
280
|
+
rowHeight={48}
|
|
281
|
+
height="400px"
|
|
282
|
+
rowSelection="multiple"
|
|
283
|
+
selectedRowIds={[]}
|
|
284
|
+
onSelectionChange={(ids) => (lastIds = ids)}
|
|
285
|
+
/>,
|
|
286
|
+
);
|
|
287
|
+
const firstRowCheckbox = container.querySelector(
|
|
288
|
+
'tbody tr[data-selected] input[type="checkbox"]',
|
|
289
|
+
) as HTMLInputElement;
|
|
290
|
+
fireEvent.click(firstRowCheckbox);
|
|
291
|
+
expect(lastIds.length).toBe(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('selectAllScope=allLoaded toggles every row in the sorted set', () => {
|
|
295
|
+
let lastIds: string[] = [];
|
|
296
|
+
const { container } = render(
|
|
297
|
+
<DataGrid
|
|
298
|
+
rows={generateUsers(50)}
|
|
299
|
+
cols={baseCols}
|
|
300
|
+
getRowId={getRowId}
|
|
301
|
+
rowHeight={48}
|
|
302
|
+
height="400px"
|
|
303
|
+
rowSelection="multiple"
|
|
304
|
+
selectAllScope="allLoaded"
|
|
305
|
+
selectedRowIds={[]}
|
|
306
|
+
onSelectionChange={(ids) => (lastIds = ids)}
|
|
307
|
+
/>,
|
|
308
|
+
);
|
|
309
|
+
const headerCheckbox = container.querySelector(
|
|
310
|
+
'thead input[type="checkbox"]',
|
|
311
|
+
) as HTMLInputElement;
|
|
312
|
+
fireEvent.click(headerCheckbox);
|
|
313
|
+
expect(lastIds.length).toBe(50); // all 50 rows toggled
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('selectAllScope=visible toggles only rendered rows', () => {
|
|
317
|
+
let lastIds: string[] = [];
|
|
318
|
+
const { container } = render(
|
|
319
|
+
<DataGrid
|
|
320
|
+
rows={generateUsers(1000)}
|
|
321
|
+
cols={baseCols}
|
|
322
|
+
getRowId={getRowId}
|
|
323
|
+
rowHeight={48}
|
|
324
|
+
height="400px"
|
|
325
|
+
rowSelection="multiple"
|
|
326
|
+
selectAllScope="visible"
|
|
327
|
+
selectedRowIds={[]}
|
|
328
|
+
onSelectionChange={(ids) => (lastIds = ids)}
|
|
329
|
+
/>,
|
|
330
|
+
);
|
|
331
|
+
const headerCheckbox = container.querySelector(
|
|
332
|
+
'thead input[type="checkbox"]',
|
|
333
|
+
) as HTMLInputElement;
|
|
334
|
+
fireEvent.click(headerCheckbox);
|
|
335
|
+
// Only visible window — way less than 1000
|
|
336
|
+
expect(lastIds.length).toBeLessThan(50);
|
|
337
|
+
expect(lastIds.length).toBeGreaterThan(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('renders bulk action footer when selection > 0', () => {
|
|
341
|
+
const { rerender } = render(
|
|
342
|
+
<DataGrid
|
|
343
|
+
rows={generateUsers(10)}
|
|
344
|
+
cols={baseCols}
|
|
345
|
+
getRowId={getRowId}
|
|
346
|
+
rowHeight={48}
|
|
347
|
+
height="400px"
|
|
348
|
+
rowSelection="multiple"
|
|
349
|
+
bulkActions={(rows) => <span>Bulk × {rows.length}</span>}
|
|
350
|
+
/>,
|
|
351
|
+
);
|
|
352
|
+
expect(screen.queryByText(/Bulk ×/)).toBeNull();
|
|
353
|
+
rerender(
|
|
354
|
+
<DataGrid
|
|
355
|
+
rows={generateUsers(10)}
|
|
356
|
+
cols={baseCols}
|
|
357
|
+
getRowId={getRowId}
|
|
358
|
+
rowHeight={48}
|
|
359
|
+
height="400px"
|
|
360
|
+
rowSelection="multiple"
|
|
361
|
+
selectedRowIds={['u0', 'u1']}
|
|
362
|
+
bulkActions={(rows) => <span>Bulk × {rows.length}</span>}
|
|
363
|
+
/>,
|
|
364
|
+
);
|
|
365
|
+
expect(screen.getByText('Bulk × 2')).not.toBeNull();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('DataGrid — RBAC', () => {
|
|
370
|
+
it('per-column hide removes the column entirely', () => {
|
|
371
|
+
const colsWithHide: TableColumn<User>[] = [
|
|
372
|
+
baseCols[0]!,
|
|
373
|
+
{
|
|
374
|
+
...baseCols[1]!,
|
|
375
|
+
access: { resource: 'email', action: 'read', onUnauthorized: 'hide' },
|
|
376
|
+
},
|
|
377
|
+
baseCols[2]!,
|
|
378
|
+
];
|
|
379
|
+
render(
|
|
380
|
+
<DataGrid
|
|
381
|
+
rows={generateUsers(20)}
|
|
382
|
+
cols={colsWithHide}
|
|
383
|
+
getRowId={getRowId}
|
|
384
|
+
rowHeight={48}
|
|
385
|
+
height="400px"
|
|
386
|
+
/>,
|
|
387
|
+
);
|
|
388
|
+
expect(screen.queryByText('Email')).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('DataGrid — sticky left column', () => {
|
|
393
|
+
it('applies sticky CSS class to sticky="left" columns', () => {
|
|
394
|
+
const colsSticky: TableColumn<User>[] = [
|
|
395
|
+
{ ...baseCols[0]!, sticky: 'left' },
|
|
396
|
+
...baseCols.slice(1),
|
|
397
|
+
];
|
|
398
|
+
render(
|
|
399
|
+
<DataGrid
|
|
400
|
+
rows={generateUsers(20)}
|
|
401
|
+
cols={colsSticky}
|
|
402
|
+
getRowId={getRowId}
|
|
403
|
+
rowHeight={48}
|
|
404
|
+
height="400px"
|
|
405
|
+
/>,
|
|
406
|
+
);
|
|
407
|
+
const nameHeader = screen.getByText('Name').closest('th');
|
|
408
|
+
expect(nameHeader?.className).toContain('sticky');
|
|
409
|
+
expect(nameHeader?.className).toContain('left-0');
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('DataGrid — internal pagination', () => {
|
|
414
|
+
it('renders Pagination footer when pagination prop is set', () => {
|
|
415
|
+
render(
|
|
416
|
+
<DataGrid
|
|
417
|
+
rows={generateUsers(100)}
|
|
418
|
+
cols={baseCols}
|
|
419
|
+
getRowId={getRowId}
|
|
420
|
+
rowHeight={48}
|
|
421
|
+
height="400px"
|
|
422
|
+
pagination={{
|
|
423
|
+
page: 1,
|
|
424
|
+
pageSize: 20,
|
|
425
|
+
onPageChange: () => {},
|
|
426
|
+
}}
|
|
427
|
+
/>,
|
|
428
|
+
);
|
|
429
|
+
// Pagination renders a <nav aria-label="Pagination">
|
|
430
|
+
expect(screen.queryByRole('navigation', { name: 'Pagination' })).not.toBeNull();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('DataGrid — smart defaults', () => {
|
|
435
|
+
it('right-aligns number columns (Age)', () => {
|
|
436
|
+
render(
|
|
437
|
+
<DataGrid
|
|
438
|
+
rows={generateUsers(20)}
|
|
439
|
+
cols={baseCols}
|
|
440
|
+
getRowId={getRowId}
|
|
441
|
+
rowHeight={48}
|
|
442
|
+
height="400px"
|
|
443
|
+
/>,
|
|
444
|
+
);
|
|
445
|
+
const ageHeader = screen.getByText('Age').closest('th');
|
|
446
|
+
expect(ageHeader?.className).toContain('text-right');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('auto-applies tabular-nums to number cells (no font-mono)', () => {
|
|
450
|
+
const { container } = render(
|
|
451
|
+
<DataGrid
|
|
452
|
+
rows={generateUsers(5)}
|
|
453
|
+
cols={baseCols}
|
|
454
|
+
getRowId={getRowId}
|
|
455
|
+
rowHeight={48}
|
|
456
|
+
height="400px"
|
|
457
|
+
/>,
|
|
458
|
+
);
|
|
459
|
+
const ageCell = container.querySelector('tbody tr[data-selected] td:nth-child(3)');
|
|
460
|
+
expect(ageCell?.className).toContain('tabular-nums');
|
|
461
|
+
expect(ageCell?.className).not.toContain('font-mono');
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('DataGrid — i18n', () => {
|
|
466
|
+
it('column headers accept translated strings', () => {
|
|
467
|
+
const colsTranslated: TableColumn<User>[] = [
|
|
468
|
+
{ field: 'name', header: /* t('users.name') */ 'Nome', sortable: true },
|
|
469
|
+
];
|
|
470
|
+
render(
|
|
471
|
+
<DataGrid
|
|
472
|
+
rows={generateUsers(5)}
|
|
473
|
+
cols={colsTranslated}
|
|
474
|
+
getRowId={getRowId}
|
|
475
|
+
rowHeight={48}
|
|
476
|
+
height="400px"
|
|
477
|
+
/>,
|
|
478
|
+
);
|
|
479
|
+
expect(screen.getByText('Nome')).not.toBeNull();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('labels prop translates internal a11y strings', () => {
|
|
483
|
+
render(
|
|
484
|
+
<DataGrid
|
|
485
|
+
rows={generateUsers(5)}
|
|
486
|
+
cols={baseCols}
|
|
487
|
+
getRowId={getRowId}
|
|
488
|
+
rowHeight={48}
|
|
489
|
+
height="400px"
|
|
490
|
+
rowSelection="multiple"
|
|
491
|
+
labels={{ ariaSelectRow: 'Seleziona riga', ariaSelectAllRows: 'Seleziona tutto' }}
|
|
492
|
+
/>,
|
|
493
|
+
);
|
|
494
|
+
expect(screen.queryByLabelText('Seleziona tutto')).not.toBeNull();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe('DataGrid — per-column filter UI', () => {
|
|
499
|
+
it('renders a filter trigger button only for filterable columns', () => {
|
|
500
|
+
const cols: TableColumn<User>[] = [
|
|
501
|
+
{ field: 'name', header: 'Name', filterable: true },
|
|
502
|
+
{ field: 'email', header: 'Email' },
|
|
503
|
+
{ field: 'age', header: 'Age', filterable: true },
|
|
504
|
+
];
|
|
505
|
+
render(
|
|
506
|
+
<DataGrid
|
|
507
|
+
rows={generateUsers(5)}
|
|
508
|
+
cols={cols}
|
|
509
|
+
getRowId={getRowId}
|
|
510
|
+
rowHeight={48}
|
|
511
|
+
height="400px"
|
|
512
|
+
/>,
|
|
513
|
+
);
|
|
514
|
+
// 2 filterable cols → 2 filter buttons
|
|
515
|
+
const triggers = screen.getAllByRole('button', { name: 'Filter' });
|
|
516
|
+
expect(triggers).toHaveLength(2);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('shows active state (aria-pressed=true) when a filter is applied', () => {
|
|
520
|
+
const cols: TableColumn<User>[] = [
|
|
521
|
+
{ field: 'name', header: 'Name', filterable: true },
|
|
522
|
+
];
|
|
523
|
+
render(
|
|
524
|
+
<DataGrid
|
|
525
|
+
rows={generateUsers(5)}
|
|
526
|
+
cols={cols}
|
|
527
|
+
getRowId={getRowId}
|
|
528
|
+
rowHeight={48}
|
|
529
|
+
height="400px"
|
|
530
|
+
filterModel={[{ field: 'name', op: 'contains', value: 'User 1' }]}
|
|
531
|
+
/>,
|
|
532
|
+
);
|
|
533
|
+
const trigger = screen.getByRole('button', { name: 'Filter' });
|
|
534
|
+
expect(trigger.getAttribute('aria-pressed')).toBe('true');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('opens a text input when filterable string column is clicked', () => {
|
|
538
|
+
const cols: TableColumn<User>[] = [
|
|
539
|
+
{ field: 'name', header: 'Name', filterable: true },
|
|
540
|
+
];
|
|
541
|
+
render(
|
|
542
|
+
<DataGrid
|
|
543
|
+
rows={generateUsers(5)}
|
|
544
|
+
cols={cols}
|
|
545
|
+
getRowId={getRowId}
|
|
546
|
+
rowHeight={48}
|
|
547
|
+
height="400px"
|
|
548
|
+
/>,
|
|
549
|
+
);
|
|
550
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
551
|
+
// Apply button means the Popover opened
|
|
552
|
+
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeNull();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('opens Min/Max inputs for filterable number column', () => {
|
|
556
|
+
const cols: TableColumn<User>[] = [
|
|
557
|
+
{ field: 'age', header: 'Age', filterable: true },
|
|
558
|
+
];
|
|
559
|
+
render(
|
|
560
|
+
<DataGrid
|
|
561
|
+
rows={generateUsers(5)}
|
|
562
|
+
cols={cols}
|
|
563
|
+
getRowId={getRowId}
|
|
564
|
+
rowHeight={48}
|
|
565
|
+
height="400px"
|
|
566
|
+
/>,
|
|
567
|
+
);
|
|
568
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
569
|
+
expect(screen.queryByText('Min')).not.toBeNull();
|
|
570
|
+
expect(screen.queryByText('Max')).not.toBeNull();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('explicit col.filterType="text" overrides number autodetect', () => {
|
|
574
|
+
const cols: TableColumn<User>[] = [
|
|
575
|
+
{ field: 'age', header: 'Age', filterable: true, filterType: 'text' },
|
|
576
|
+
];
|
|
577
|
+
render(
|
|
578
|
+
<DataGrid
|
|
579
|
+
rows={generateUsers(5)}
|
|
580
|
+
cols={cols}
|
|
581
|
+
getRowId={getRowId}
|
|
582
|
+
rowHeight={48}
|
|
583
|
+
height="400px"
|
|
584
|
+
/>,
|
|
585
|
+
);
|
|
586
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
587
|
+
// Text filter has Apply but no Min/Max
|
|
588
|
+
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeNull();
|
|
589
|
+
expect(screen.queryByText('Min')).toBeNull();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('emits onFilterChange when Apply is clicked', () => {
|
|
593
|
+
const onFilterChange = vi.fn();
|
|
594
|
+
const cols: TableColumn<User>[] = [
|
|
595
|
+
{ field: 'name', header: 'Name', filterable: true },
|
|
596
|
+
];
|
|
597
|
+
render(
|
|
598
|
+
<DataGrid
|
|
599
|
+
rows={generateUsers(5)}
|
|
600
|
+
cols={cols}
|
|
601
|
+
getRowId={getRowId}
|
|
602
|
+
rowHeight={48}
|
|
603
|
+
height="400px"
|
|
604
|
+
onFilterChange={onFilterChange}
|
|
605
|
+
/>,
|
|
606
|
+
);
|
|
607
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filter' }));
|
|
608
|
+
const input = screen.getByPlaceholderText('…') as HTMLInputElement;
|
|
609
|
+
fireEvent.change(input, { target: { value: 'User 2' } });
|
|
610
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
|
611
|
+
expect(onFilterChange).toHaveBeenCalledWith([
|
|
612
|
+
{ field: 'name', op: 'contains', value: 'User 2' },
|
|
613
|
+
]);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('translates filter labels via the labels prop', () => {
|
|
617
|
+
const cols: TableColumn<User>[] = [
|
|
618
|
+
{ field: 'age', header: 'Età', filterable: true },
|
|
619
|
+
];
|
|
620
|
+
render(
|
|
621
|
+
<DataGrid
|
|
622
|
+
rows={generateUsers(5)}
|
|
623
|
+
cols={cols}
|
|
624
|
+
getRowId={getRowId}
|
|
625
|
+
rowHeight={48}
|
|
626
|
+
height="400px"
|
|
627
|
+
labels={{
|
|
628
|
+
filterColumn: 'Filtra',
|
|
629
|
+
filterApply: 'Applica',
|
|
630
|
+
filterClear: 'Pulisci',
|
|
631
|
+
filterMin: 'Min',
|
|
632
|
+
filterMax: 'Max',
|
|
633
|
+
}}
|
|
634
|
+
/>,
|
|
635
|
+
);
|
|
636
|
+
fireEvent.click(screen.getByRole('button', { name: 'Filtra' }));
|
|
637
|
+
expect(screen.queryByRole('button', { name: 'Applica' })).not.toBeNull();
|
|
638
|
+
expect(screen.queryByRole('button', { name: 'Pulisci' })).not.toBeNull();
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe('DataGrid — column visibility', () => {
|
|
643
|
+
it('renders the Columns toolbar button by default', () => {
|
|
644
|
+
render(
|
|
645
|
+
<DataGrid
|
|
646
|
+
rows={generateUsers(5)}
|
|
647
|
+
cols={baseCols}
|
|
648
|
+
getRowId={getRowId}
|
|
649
|
+
rowHeight={48}
|
|
650
|
+
height="400px"
|
|
651
|
+
/>,
|
|
652
|
+
);
|
|
653
|
+
expect(screen.queryByRole('button', { name: 'Columns' })).not.toBeNull();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('does NOT render the Columns button when enableColumnVisibility=false', () => {
|
|
657
|
+
render(
|
|
658
|
+
<DataGrid
|
|
659
|
+
rows={generateUsers(5)}
|
|
660
|
+
cols={baseCols}
|
|
661
|
+
getRowId={getRowId}
|
|
662
|
+
rowHeight={48}
|
|
663
|
+
height="400px"
|
|
664
|
+
enableColumnVisibility={false}
|
|
665
|
+
/>,
|
|
666
|
+
);
|
|
667
|
+
expect(screen.queryByRole('button', { name: 'Columns' })).toBeNull();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('opens dialog with one checkbox per hideable column', () => {
|
|
671
|
+
const cols: TableColumn<User>[] = [
|
|
672
|
+
{ field: 'name', header: 'Name' },
|
|
673
|
+
{ field: 'email', header: 'Email' },
|
|
674
|
+
// hideable: false → not shown in the dialog (structurally required)
|
|
675
|
+
{ field: 'age', header: 'Age', hideable: false },
|
|
676
|
+
];
|
|
677
|
+
render(
|
|
678
|
+
<DataGrid
|
|
679
|
+
rows={generateUsers(5)}
|
|
680
|
+
cols={cols}
|
|
681
|
+
getRowId={getRowId}
|
|
682
|
+
rowHeight={48}
|
|
683
|
+
height="400px"
|
|
684
|
+
/>,
|
|
685
|
+
);
|
|
686
|
+
fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
|
|
687
|
+
expect(screen.queryByRole('dialog')).not.toBeNull();
|
|
688
|
+
// The dialog should expose checkboxes for Name + Email (not Age).
|
|
689
|
+
const labels = screen.getAllByText(/Name|Email|Age/);
|
|
690
|
+
// Name + Email appear in BOTH the table header and the dialog; Age
|
|
691
|
+
// appears only in the header (hideable=false → excluded from dialog).
|
|
692
|
+
const dialog = screen.getByRole('dialog');
|
|
693
|
+
expect(dialog.textContent).toContain('Name');
|
|
694
|
+
expect(dialog.textContent).toContain('Email');
|
|
695
|
+
expect(dialog.textContent).not.toContain('Age');
|
|
696
|
+
void labels;
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('respects col.defaultHidden on first render', () => {
|
|
700
|
+
const cols: TableColumn<User>[] = [
|
|
701
|
+
{ field: 'name', header: 'Name' },
|
|
702
|
+
{ field: 'email', header: 'Email', defaultHidden: true },
|
|
703
|
+
];
|
|
704
|
+
render(
|
|
705
|
+
<DataGrid
|
|
706
|
+
rows={generateUsers(5)}
|
|
707
|
+
cols={cols}
|
|
708
|
+
getRowId={getRowId}
|
|
709
|
+
rowHeight={48}
|
|
710
|
+
height="400px"
|
|
711
|
+
/>,
|
|
712
|
+
);
|
|
713
|
+
// Email column header should NOT be rendered initially.
|
|
714
|
+
expect(screen.queryByText('Email')).toBeNull();
|
|
715
|
+
// Name column header SHOULD be rendered.
|
|
716
|
+
expect(screen.queryByText('Name')).not.toBeNull();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('toggling a column hides it from the table after Done', () => {
|
|
720
|
+
const onHiddenColumnsChange = vi.fn();
|
|
721
|
+
render(
|
|
722
|
+
<DataGrid
|
|
723
|
+
rows={generateUsers(5)}
|
|
724
|
+
cols={baseCols}
|
|
725
|
+
getRowId={getRowId}
|
|
726
|
+
rowHeight={48}
|
|
727
|
+
height="400px"
|
|
728
|
+
onHiddenColumnsChange={onHiddenColumnsChange}
|
|
729
|
+
/>,
|
|
730
|
+
);
|
|
731
|
+
fireEvent.click(screen.getByRole('button', { name: 'Columns' }));
|
|
732
|
+
// Uncheck "Email" inside the dialog
|
|
733
|
+
const dialog = screen.getByRole('dialog');
|
|
734
|
+
const emailCheckbox = Array.from(
|
|
735
|
+
dialog.querySelectorAll('input[type="checkbox"]'),
|
|
736
|
+
).find((el) => {
|
|
737
|
+
const li = el.closest('label');
|
|
738
|
+
return li?.textContent?.includes('Email');
|
|
739
|
+
}) as HTMLInputElement | undefined;
|
|
740
|
+
expect(emailCheckbox).toBeDefined();
|
|
741
|
+
fireEvent.click(emailCheckbox!);
|
|
742
|
+
// Commit via Done
|
|
743
|
+
fireEvent.click(screen.getByRole('button', { name: 'Done' }));
|
|
744
|
+
expect(onHiddenColumnsChange).toHaveBeenCalledWith(['email']);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('controlled hiddenColumns hides the matching columns', () => {
|
|
748
|
+
render(
|
|
749
|
+
<DataGrid
|
|
750
|
+
rows={generateUsers(5)}
|
|
751
|
+
cols={baseCols}
|
|
752
|
+
getRowId={getRowId}
|
|
753
|
+
rowHeight={48}
|
|
754
|
+
height="400px"
|
|
755
|
+
hiddenColumns={['email']}
|
|
756
|
+
/>,
|
|
757
|
+
);
|
|
758
|
+
expect(screen.queryByText('Email')).toBeNull();
|
|
759
|
+
expect(screen.queryByText('Name')).not.toBeNull();
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
describe('DataGrid — column resize', () => {
|
|
764
|
+
it('renders a resize handle (separator) for each column by default', () => {
|
|
765
|
+
render(
|
|
766
|
+
<DataGrid
|
|
767
|
+
rows={generateUsers(5)}
|
|
768
|
+
cols={baseCols}
|
|
769
|
+
getRowId={getRowId}
|
|
770
|
+
rowHeight={48}
|
|
771
|
+
height="400px"
|
|
772
|
+
/>,
|
|
773
|
+
);
|
|
774
|
+
// One separator per column (4 columns).
|
|
775
|
+
const handles = screen.getAllByRole('separator');
|
|
776
|
+
expect(handles.length).toBe(baseCols.length);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('omits resize handles when enableColumnResize=false', () => {
|
|
780
|
+
render(
|
|
781
|
+
<DataGrid
|
|
782
|
+
rows={generateUsers(5)}
|
|
783
|
+
cols={baseCols}
|
|
784
|
+
getRowId={getRowId}
|
|
785
|
+
rowHeight={48}
|
|
786
|
+
height="400px"
|
|
787
|
+
enableColumnResize={false}
|
|
788
|
+
/>,
|
|
789
|
+
);
|
|
790
|
+
expect(screen.queryAllByRole('separator')).toHaveLength(0);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('omits resize handle for col.resizable=false', () => {
|
|
794
|
+
const cols: TableColumn<User>[] = [
|
|
795
|
+
{ field: 'name', header: 'Name' },
|
|
796
|
+
{ field: 'email', header: 'Email', resizable: false },
|
|
797
|
+
];
|
|
798
|
+
render(
|
|
799
|
+
<DataGrid
|
|
800
|
+
rows={generateUsers(5)}
|
|
801
|
+
cols={cols}
|
|
802
|
+
getRowId={getRowId}
|
|
803
|
+
rowHeight={48}
|
|
804
|
+
height="400px"
|
|
805
|
+
/>,
|
|
806
|
+
);
|
|
807
|
+
// Only 1 separator (Name); Email opted out via resizable=false.
|
|
808
|
+
expect(screen.getAllByRole('separator')).toHaveLength(1);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('applies columnWidths to <th> style.width', () => {
|
|
812
|
+
const cols: TableColumn<User>[] = [
|
|
813
|
+
{ field: 'name', header: 'Name', width: 100 },
|
|
814
|
+
];
|
|
815
|
+
render(
|
|
816
|
+
<DataGrid
|
|
817
|
+
rows={generateUsers(5)}
|
|
818
|
+
cols={cols}
|
|
819
|
+
getRowId={getRowId}
|
|
820
|
+
rowHeight={48}
|
|
821
|
+
height="400px"
|
|
822
|
+
columnWidths={{ name: 250 }}
|
|
823
|
+
/>,
|
|
824
|
+
);
|
|
825
|
+
const th = screen.getByText('Name').closest('th');
|
|
826
|
+
expect(th?.style.width).toBe('250px');
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('falls back to col.width when no resize state for the column', () => {
|
|
830
|
+
const cols: TableColumn<User>[] = [
|
|
831
|
+
{ field: 'name', header: 'Name', width: 100 },
|
|
832
|
+
];
|
|
833
|
+
render(
|
|
834
|
+
<DataGrid
|
|
835
|
+
rows={generateUsers(5)}
|
|
836
|
+
cols={cols}
|
|
837
|
+
getRowId={getRowId}
|
|
838
|
+
rowHeight={48}
|
|
839
|
+
height="400px"
|
|
840
|
+
/>,
|
|
841
|
+
);
|
|
842
|
+
const th = screen.getByText('Name').closest('th');
|
|
843
|
+
expect(th?.style.width).toBe('100px');
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
describe('DataGrid — column reorder', () => {
|
|
848
|
+
it('applies the columnOrder prop to header order', () => {
|
|
849
|
+
const { container } = render(
|
|
850
|
+
<DataGrid
|
|
851
|
+
rows={generateUsers(3)}
|
|
852
|
+
cols={baseCols}
|
|
853
|
+
getRowId={getRowId}
|
|
854
|
+
rowHeight={48}
|
|
855
|
+
height="400px"
|
|
856
|
+
columnOrder={['age', 'name', 'email', 'meta.city']}
|
|
857
|
+
/>,
|
|
858
|
+
);
|
|
859
|
+
const headerTexts = Array.from(
|
|
860
|
+
container.querySelectorAll('thead th'),
|
|
861
|
+
).map((th) => th.textContent?.trim() ?? '');
|
|
862
|
+
// First three column headers should match the order
|
|
863
|
+
expect(headerTexts[0]).toContain('Age');
|
|
864
|
+
expect(headerTexts[1]).toContain('Name');
|
|
865
|
+
expect(headerTexts[2]).toContain('Email');
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('headers are draggable by default', () => {
|
|
869
|
+
const { container } = render(
|
|
870
|
+
<DataGrid
|
|
871
|
+
rows={generateUsers(3)}
|
|
872
|
+
cols={baseCols}
|
|
873
|
+
getRowId={getRowId}
|
|
874
|
+
rowHeight={48}
|
|
875
|
+
height="400px"
|
|
876
|
+
/>,
|
|
877
|
+
);
|
|
878
|
+
const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
|
|
879
|
+
expect(draggableThs.length).toBe(baseCols.length);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('does NOT make headers draggable when enableColumnReorder=false', () => {
|
|
883
|
+
const { container } = render(
|
|
884
|
+
<DataGrid
|
|
885
|
+
rows={generateUsers(3)}
|
|
886
|
+
cols={baseCols}
|
|
887
|
+
getRowId={getRowId}
|
|
888
|
+
rowHeight={48}
|
|
889
|
+
height="400px"
|
|
890
|
+
enableColumnReorder={false}
|
|
891
|
+
/>,
|
|
892
|
+
);
|
|
893
|
+
expect(
|
|
894
|
+
container.querySelectorAll('thead th[draggable="true"]'),
|
|
895
|
+
).toHaveLength(0);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('respects col.reorderable=false per-column', () => {
|
|
899
|
+
const cols: TableColumn<User>[] = [
|
|
900
|
+
{ field: 'name', header: 'Name', reorderable: false },
|
|
901
|
+
{ field: 'email', header: 'Email' },
|
|
902
|
+
];
|
|
903
|
+
const { container } = render(
|
|
904
|
+
<DataGrid
|
|
905
|
+
rows={generateUsers(3)}
|
|
906
|
+
cols={cols}
|
|
907
|
+
getRowId={getRowId}
|
|
908
|
+
rowHeight={48}
|
|
909
|
+
height="400px"
|
|
910
|
+
/>,
|
|
911
|
+
);
|
|
912
|
+
// Only the Email th should be draggable.
|
|
913
|
+
const draggableThs = container.querySelectorAll('thead th[draggable="true"]');
|
|
914
|
+
expect(draggableThs.length).toBe(1);
|
|
915
|
+
expect(draggableThs[0]?.textContent).toContain('Email');
|
|
916
|
+
});
|
|
917
|
+
});
|