@dashforge/tw 0.6.0-beta → 0.7.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 +258 -0
- package/THEME-AUDIT.md +175 -0
- package/dist/index.esm.js +973 -69
- 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 +171 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
- package/dist/src/components/DataGrid/dataGrid.variants.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.types.d.ts +8 -0
- 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/{Table/_internal → _shared/data}/useTableFilter.d.ts +1 -1
- 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 +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- 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 +496 -0
- package/src/components/DataGrid/DataGrid.tsx +831 -0
- package/src/components/DataGrid/dataGrid.types.ts +189 -0
- package/src/components/DataGrid/dataGrid.variants.ts +144 -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 +8 -8
- package/src/components/Table/table.types.ts +9 -0
- 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/{Table/_internal → _shared/data}/useTableFilter.ts +1 -1
- 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 +18 -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.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/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,496 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { describe, it, expect, afterEach, beforeAll } 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
|
+
});
|