@shohojdhara/atomix 0.3.7 → 0.3.8
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/dist/atomix.css +77 -0
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +77 -0
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js.map +1 -1
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +578 -515
- package/dist/index.esm.js +3157 -2626
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +10496 -9973
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +237 -420
- package/dist/theme.js +1629 -1701
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.stories.tsx +238 -0
- package/src/components/DataTable/DataTable.test.tsx +450 -0
- package/src/components/DataTable/DataTable.tsx +384 -61
- package/src/components/DatePicker/DatePicker.tsx +29 -38
- package/src/components/Upload/Upload.tsx +539 -40
- package/src/lib/composables/useDataTable.ts +355 -15
- package/src/lib/composables/useDatePicker.ts +19 -0
- package/src/lib/constants/components.ts +10 -0
- package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
- package/src/lib/theme/adapters/index.ts +1 -4
- package/src/lib/theme/config/configLoader.ts +53 -35
- package/src/lib/theme/core/composeTheme.ts +22 -30
- package/src/lib/theme/core/createTheme.ts +49 -26
- package/src/lib/theme/core/index.ts +0 -1
- package/src/lib/theme/generators/generateCSSNested.ts +4 -3
- package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
- package/src/lib/theme/index.ts +10 -17
- package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
- package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
- package/src/lib/theme/runtime/useTheme.ts +1 -1
- package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
- package/src/lib/theme/test/testTheme.ts +2 -1
- package/src/lib/theme/types.ts +14 -14
- package/src/lib/theme/utils/componentTheming.ts +35 -27
- package/src/lib/theme/utils/domUtils.ts +57 -15
- package/src/lib/theme/utils/injectCSS.ts +0 -1
- package/src/lib/theme/utils/themeHelpers.ts +1 -39
- package/src/lib/theme/utils/themeUtils.ts +1 -170
- package/src/lib/types/components.ts +145 -0
- package/src/lib/utils/dataTableExport.ts +143 -0
- package/src/styles/06-components/_components.data-table.scss +95 -0
- package/src/lib/hooks/useThemeTokens.ts +0 -105
package/package.json
CHANGED
|
@@ -253,3 +253,241 @@ export const Interactive: Story = {
|
|
|
253
253
|
sortable: true,
|
|
254
254
|
},
|
|
255
255
|
};
|
|
256
|
+
|
|
257
|
+
// Row selection - multiple
|
|
258
|
+
export const RowSelectionMultiple: Story = {
|
|
259
|
+
render: args => {
|
|
260
|
+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div>
|
|
264
|
+
<DataTable
|
|
265
|
+
{...args}
|
|
266
|
+
selectionMode="multiple"
|
|
267
|
+
onSelectionChange={(rows, ids) => setSelectedRows(rows)}
|
|
268
|
+
/>
|
|
269
|
+
{selectedRows.length > 0 && (
|
|
270
|
+
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
271
|
+
<strong>Selected: {selectedRows.length} row(s)</strong>
|
|
272
|
+
<pre style={{ marginTop: '0.5rem', fontSize: '0.875rem' }}>
|
|
273
|
+
{JSON.stringify(selectedRows.map(r => r.name), null, 2)}
|
|
274
|
+
</pre>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
args: {
|
|
281
|
+
data: users,
|
|
282
|
+
columns,
|
|
283
|
+
sortable: true,
|
|
284
|
+
},
|
|
285
|
+
parameters: {
|
|
286
|
+
docs: {
|
|
287
|
+
description: {
|
|
288
|
+
story: 'DataTable with multiple row selection enabled. Select rows using checkboxes.',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Row selection - single
|
|
295
|
+
export const RowSelectionSingle: Story = {
|
|
296
|
+
render: args => {
|
|
297
|
+
const [selectedRow, setSelectedRow] = useState<any>(null);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div>
|
|
301
|
+
<DataTable
|
|
302
|
+
{...args}
|
|
303
|
+
selectionMode="single"
|
|
304
|
+
onSelectionChange={(rows) => setSelectedRow(rows[0] || null)}
|
|
305
|
+
/>
|
|
306
|
+
{selectedRow && (
|
|
307
|
+
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
308
|
+
<strong>Selected:</strong>
|
|
309
|
+
<pre style={{ marginTop: '0.5rem', fontSize: '0.875rem' }}>
|
|
310
|
+
{JSON.stringify(selectedRow, null, 2)}
|
|
311
|
+
</pre>
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
args: {
|
|
318
|
+
data: users,
|
|
319
|
+
columns,
|
|
320
|
+
sortable: true,
|
|
321
|
+
},
|
|
322
|
+
parameters: {
|
|
323
|
+
docs: {
|
|
324
|
+
description: {
|
|
325
|
+
story: 'DataTable with single row selection enabled. Select a row using radio buttons.',
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Column-specific filtering
|
|
332
|
+
export const ColumnFilters: Story = {
|
|
333
|
+
args: {
|
|
334
|
+
data: users,
|
|
335
|
+
columns: columns.map(col => ({
|
|
336
|
+
...col,
|
|
337
|
+
filterable: ['name', 'role', 'email'].includes(col.key),
|
|
338
|
+
})),
|
|
339
|
+
columnFilters: true,
|
|
340
|
+
sortable: true,
|
|
341
|
+
},
|
|
342
|
+
parameters: {
|
|
343
|
+
docs: {
|
|
344
|
+
description: {
|
|
345
|
+
story: 'DataTable with column-specific filters. Each filterable column has its own filter input.',
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Column resizing
|
|
352
|
+
export const ResizableColumns: Story = {
|
|
353
|
+
args: {
|
|
354
|
+
data: users,
|
|
355
|
+
columns: columns.map(col => ({
|
|
356
|
+
...col,
|
|
357
|
+
resizable: true,
|
|
358
|
+
minWidth: '100px',
|
|
359
|
+
})),
|
|
360
|
+
resizable: true,
|
|
361
|
+
sortable: true,
|
|
362
|
+
},
|
|
363
|
+
parameters: {
|
|
364
|
+
docs: {
|
|
365
|
+
description: {
|
|
366
|
+
story: 'DataTable with resizable columns. Drag the right edge of column headers to resize.',
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Column reordering
|
|
373
|
+
export const ReorderableColumns: Story = {
|
|
374
|
+
args: {
|
|
375
|
+
data: users,
|
|
376
|
+
columns,
|
|
377
|
+
reorderable: true,
|
|
378
|
+
sortable: true,
|
|
379
|
+
},
|
|
380
|
+
parameters: {
|
|
381
|
+
docs: {
|
|
382
|
+
description: {
|
|
383
|
+
story: 'DataTable with reorderable columns. Drag column headers to reorder them.',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Column visibility toggle
|
|
390
|
+
export const ColumnVisibility: Story = {
|
|
391
|
+
args: {
|
|
392
|
+
data: users,
|
|
393
|
+
columns,
|
|
394
|
+
showColumnVisibility: true,
|
|
395
|
+
sortable: true,
|
|
396
|
+
},
|
|
397
|
+
parameters: {
|
|
398
|
+
docs: {
|
|
399
|
+
description: {
|
|
400
|
+
story: 'DataTable with column visibility toggle. Use the Columns button to show/hide columns.',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Export functionality
|
|
407
|
+
export const Exportable: Story = {
|
|
408
|
+
args: {
|
|
409
|
+
data: users,
|
|
410
|
+
columns,
|
|
411
|
+
exportable: true,
|
|
412
|
+
exportFormats: ['csv', 'excel', 'json'],
|
|
413
|
+
exportFilename: 'users',
|
|
414
|
+
sortable: true,
|
|
415
|
+
},
|
|
416
|
+
parameters: {
|
|
417
|
+
docs: {
|
|
418
|
+
description: {
|
|
419
|
+
story: 'DataTable with export functionality. Export data as CSV, Excel, or JSON.',
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Sticky headers
|
|
426
|
+
export const StickyHeaders: Story = {
|
|
427
|
+
args: {
|
|
428
|
+
data: largeDataSet,
|
|
429
|
+
columns,
|
|
430
|
+
stickyHeader: true,
|
|
431
|
+
stickyHeaderOffset: '0px',
|
|
432
|
+
sortable: true,
|
|
433
|
+
paginated: true,
|
|
434
|
+
pageSize: 20,
|
|
435
|
+
},
|
|
436
|
+
parameters: {
|
|
437
|
+
docs: {
|
|
438
|
+
description: {
|
|
439
|
+
story: 'DataTable with sticky headers. Headers remain visible when scrolling.',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// All advanced features
|
|
446
|
+
export const AllAdvancedFeatures: Story = {
|
|
447
|
+
render: args => {
|
|
448
|
+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
<DataTable
|
|
453
|
+
{...args}
|
|
454
|
+
selectionMode="multiple"
|
|
455
|
+
onSelectionChange={(rows) => setSelectedRows(rows)}
|
|
456
|
+
/>
|
|
457
|
+
{selectedRows.length > 0 && (
|
|
458
|
+
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
459
|
+
<strong>Selected: {selectedRows.length} row(s)</strong>
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
},
|
|
465
|
+
args: {
|
|
466
|
+
data: largeDataSet,
|
|
467
|
+
columns: columns.map(col => ({
|
|
468
|
+
...col,
|
|
469
|
+
filterable: ['name', 'role'].includes(col.key),
|
|
470
|
+
resizable: true,
|
|
471
|
+
})),
|
|
472
|
+
sortable: true,
|
|
473
|
+
filterable: true,
|
|
474
|
+
columnFilters: true,
|
|
475
|
+
paginated: true,
|
|
476
|
+
pageSize: 10,
|
|
477
|
+
striped: true,
|
|
478
|
+
bordered: true,
|
|
479
|
+
resizable: true,
|
|
480
|
+
reorderable: true,
|
|
481
|
+
showColumnVisibility: true,
|
|
482
|
+
exportable: true,
|
|
483
|
+
exportFormats: ['csv', 'excel', 'json'],
|
|
484
|
+
stickyHeader: true,
|
|
485
|
+
},
|
|
486
|
+
parameters: {
|
|
487
|
+
docs: {
|
|
488
|
+
description: {
|
|
489
|
+
story: 'DataTable with all advanced features enabled: selection, filtering, resizing, reordering, visibility toggle, export, and sticky headers.',
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
};
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { DataTable } from './DataTable';
|
|
4
|
+
import { DataTableColumn } from '../../lib/types/components';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('../AtomixGlass/AtomixGlass', () => ({
|
|
8
|
+
AtomixGlass: ({ children }: any) => <div>{children}</div>,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('../Spinner/Spinner', () => ({
|
|
12
|
+
Spinner: () => <div data-testid="spinner">Loading...</div>,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../Icon/Icon', () => ({
|
|
16
|
+
Icon: ({ name }: any) => <span data-testid={`icon-${name}`}>{name}</span>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../Pagination/Pagination', () => ({
|
|
20
|
+
Pagination: ({ currentPage, totalPages, onPageChange }: any) => (
|
|
21
|
+
<div data-testid="pagination">
|
|
22
|
+
<button onClick={() => onPageChange(currentPage - 1)}>Prev</button>
|
|
23
|
+
<span>{currentPage} / {totalPages}</span>
|
|
24
|
+
<button onClick={() => onPageChange(currentPage + 1)}>Next</button>
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('../Form/Checkbox', () => ({
|
|
30
|
+
Checkbox: ({ checked, onChange, label, ...props }: any) => (
|
|
31
|
+
<label>
|
|
32
|
+
<input
|
|
33
|
+
type="checkbox"
|
|
34
|
+
checked={checked}
|
|
35
|
+
onChange={onChange}
|
|
36
|
+
data-testid={props['data-testid'] || 'checkbox'}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
{label}
|
|
40
|
+
</label>
|
|
41
|
+
),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('../Dropdown/Dropdown', () => ({
|
|
45
|
+
Dropdown: ({ children, menu, trigger }: any) => (
|
|
46
|
+
<div data-testid="dropdown">
|
|
47
|
+
{children}
|
|
48
|
+
{trigger === 'click' && <div data-testid="dropdown-menu">{menu}</div>}
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
DropdownItem: ({ children, onClick }: any) => (
|
|
52
|
+
<div data-testid="dropdown-item" onClick={onClick}>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
DropdownDivider: () => <hr data-testid="dropdown-divider" />,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.mock('../Button/Button', () => ({
|
|
60
|
+
Button: ({ children, onClick, ...props }: any) => (
|
|
61
|
+
<button onClick={onClick} data-testid="button" {...props}>
|
|
62
|
+
{children}
|
|
63
|
+
</button>
|
|
64
|
+
),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Sample data and columns
|
|
68
|
+
const sampleData = [
|
|
69
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'Active' },
|
|
70
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'Inactive' },
|
|
71
|
+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User', status: 'Active' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const sampleColumns: DataTableColumn[] = [
|
|
75
|
+
{ key: 'id', title: 'ID', sortable: true },
|
|
76
|
+
{ key: 'name', title: 'Name', sortable: true, filterable: true },
|
|
77
|
+
{ key: 'email', title: 'Email', sortable: true },
|
|
78
|
+
{ key: 'role', title: 'Role', sortable: true, filterable: true },
|
|
79
|
+
{ key: 'status', title: 'Status', sortable: true },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
describe('DataTable Component', () => {
|
|
83
|
+
describe('Basic Rendering', () => {
|
|
84
|
+
it('renders table with data', () => {
|
|
85
|
+
render(<DataTable data={sampleData} columns={sampleColumns} />);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders column headers', () => {
|
|
93
|
+
render(<DataTable data={sampleData} columns={sampleColumns} />);
|
|
94
|
+
|
|
95
|
+
expect(screen.getByText('ID')).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText('Role')).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('displays empty message when no data', () => {
|
|
103
|
+
render(
|
|
104
|
+
<DataTable
|
|
105
|
+
data={[]}
|
|
106
|
+
columns={sampleColumns}
|
|
107
|
+
emptyMessage="No records found"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('No records found')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('displays loading state', () => {
|
|
115
|
+
render(
|
|
116
|
+
<DataTable
|
|
117
|
+
data={sampleData}
|
|
118
|
+
columns={sampleColumns}
|
|
119
|
+
loading={true}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByTestId('spinner')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Sorting', () => {
|
|
128
|
+
it('renders sortable columns when sortable is enabled', () => {
|
|
129
|
+
render(
|
|
130
|
+
<DataTable
|
|
131
|
+
data={sampleData}
|
|
132
|
+
columns={sampleColumns}
|
|
133
|
+
sortable={true}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const nameHeader = screen.getByText('Name').closest('th');
|
|
138
|
+
expect(nameHeader).toHaveClass('c-data-table__header-cell--sortable');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('calls onSort when column header is clicked', () => {
|
|
142
|
+
const handleSort = vi.fn();
|
|
143
|
+
render(
|
|
144
|
+
<DataTable
|
|
145
|
+
data={sampleData}
|
|
146
|
+
columns={sampleColumns}
|
|
147
|
+
sortable={true}
|
|
148
|
+
onSort={handleSort}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const nameHeader = screen.getByText('Name').closest('th');
|
|
153
|
+
fireEvent.click(nameHeader!);
|
|
154
|
+
|
|
155
|
+
expect(handleSort).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
key: 'name',
|
|
158
|
+
direction: 'asc',
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Filtering', () => {
|
|
165
|
+
it('renders search input when filterable is enabled', () => {
|
|
166
|
+
render(
|
|
167
|
+
<DataTable
|
|
168
|
+
data={sampleData}
|
|
169
|
+
columns={sampleColumns}
|
|
170
|
+
filterable={true}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const searchInput = screen.getByPlaceholderText('Search...');
|
|
175
|
+
expect(searchInput).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('filters data when search query is entered', async () => {
|
|
179
|
+
render(
|
|
180
|
+
<DataTable
|
|
181
|
+
data={sampleData}
|
|
182
|
+
columns={sampleColumns}
|
|
183
|
+
filterable={true}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const searchInput = screen.getByPlaceholderText('Search...');
|
|
188
|
+
fireEvent.change(searchInput, { target: { value: 'John' } });
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
192
|
+
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('renders column filters when columnFilters is enabled', () => {
|
|
197
|
+
render(
|
|
198
|
+
<DataTable
|
|
199
|
+
data={sampleData}
|
|
200
|
+
columns={sampleColumns}
|
|
201
|
+
columnFilters={true}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const columnFilters = screen.getAllByPlaceholderText('Filter...');
|
|
206
|
+
expect(columnFilters.length).toBeGreaterThan(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Pagination', () => {
|
|
211
|
+
it('renders pagination when paginated is enabled', () => {
|
|
212
|
+
render(
|
|
213
|
+
<DataTable
|
|
214
|
+
data={sampleData}
|
|
215
|
+
columns={sampleColumns}
|
|
216
|
+
paginated={true}
|
|
217
|
+
pageSize={2}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(screen.getByTestId('pagination')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('does not render pagination when paginated is false', () => {
|
|
225
|
+
render(
|
|
226
|
+
<DataTable
|
|
227
|
+
data={sampleData}
|
|
228
|
+
columns={sampleColumns}
|
|
229
|
+
paginated={false}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(screen.queryByTestId('pagination')).not.toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('Row Selection', () => {
|
|
238
|
+
it('renders selection checkboxes when selectionMode is multiple', () => {
|
|
239
|
+
render(
|
|
240
|
+
<DataTable
|
|
241
|
+
data={sampleData}
|
|
242
|
+
columns={sampleColumns}
|
|
243
|
+
selectionMode="multiple"
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const checkboxes = screen.getAllByTestId('checkbox');
|
|
248
|
+
expect(checkboxes.length).toBeGreaterThan(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('renders select all checkbox in header for multiple selection', () => {
|
|
252
|
+
render(
|
|
253
|
+
<DataTable
|
|
254
|
+
data={sampleData}
|
|
255
|
+
columns={sampleColumns}
|
|
256
|
+
selectionMode="multiple"
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const checkboxes = screen.getAllByTestId('checkbox');
|
|
261
|
+
// Should have at least one checkbox (select all)
|
|
262
|
+
expect(checkboxes.length).toBeGreaterThanOrEqual(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('calls onSelectionChange when row is selected', () => {
|
|
266
|
+
const handleSelectionChange = vi.fn();
|
|
267
|
+
render(
|
|
268
|
+
<DataTable
|
|
269
|
+
data={sampleData}
|
|
270
|
+
columns={sampleColumns}
|
|
271
|
+
selectionMode="multiple"
|
|
272
|
+
onSelectionChange={handleSelectionChange}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const checkboxes = screen.getAllByTestId('checkbox');
|
|
277
|
+
// Click the first row checkbox (skip select all if present)
|
|
278
|
+
const rowCheckbox = checkboxes[checkboxes.length > sampleData.length ? 1 : 0];
|
|
279
|
+
fireEvent.change(rowCheckbox, { target: { checked: true } });
|
|
280
|
+
|
|
281
|
+
expect(handleSelectionChange).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('Row Click', () => {
|
|
286
|
+
it('calls onRowClick when row is clicked', () => {
|
|
287
|
+
const handleRowClick = vi.fn();
|
|
288
|
+
render(
|
|
289
|
+
<DataTable
|
|
290
|
+
data={sampleData}
|
|
291
|
+
columns={sampleColumns}
|
|
292
|
+
onRowClick={handleRowClick}
|
|
293
|
+
/>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const row = screen.getByText('John Doe').closest('tr');
|
|
297
|
+
fireEvent.click(row!);
|
|
298
|
+
|
|
299
|
+
expect(handleRowClick).toHaveBeenCalledWith(sampleData[0]);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Export Functionality', () => {
|
|
304
|
+
it('renders export dropdown when exportable is enabled', () => {
|
|
305
|
+
render(
|
|
306
|
+
<DataTable
|
|
307
|
+
data={sampleData}
|
|
308
|
+
columns={sampleColumns}
|
|
309
|
+
exportable={true}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const exportButton = screen.getByText('Export');
|
|
314
|
+
expect(exportButton).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('does not render export when exportable is false', () => {
|
|
318
|
+
render(
|
|
319
|
+
<DataTable
|
|
320
|
+
data={sampleData}
|
|
321
|
+
columns={sampleColumns}
|
|
322
|
+
exportable={false}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(screen.queryByText('Export')).not.toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Column Visibility', () => {
|
|
331
|
+
it('renders column visibility dropdown when showColumnVisibility is enabled', () => {
|
|
332
|
+
render(
|
|
333
|
+
<DataTable
|
|
334
|
+
data={sampleData}
|
|
335
|
+
columns={sampleColumns}
|
|
336
|
+
showColumnVisibility={true}
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const columnsButton = screen.getByText('Columns');
|
|
341
|
+
expect(columnsButton).toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Styling Variants', () => {
|
|
346
|
+
it('applies striped class when striped is enabled', () => {
|
|
347
|
+
const { container } = render(
|
|
348
|
+
<DataTable
|
|
349
|
+
data={sampleData}
|
|
350
|
+
columns={sampleColumns}
|
|
351
|
+
striped={true}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const table = container.querySelector('.c-data-table');
|
|
356
|
+
expect(table).toHaveClass('c-data-table--striped');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('applies bordered class when bordered is enabled', () => {
|
|
360
|
+
const { container } = render(
|
|
361
|
+
<DataTable
|
|
362
|
+
data={sampleData}
|
|
363
|
+
columns={sampleColumns}
|
|
364
|
+
bordered={true}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const table = container.querySelector('.c-data-table');
|
|
369
|
+
expect(table).toHaveClass('c-data-table--bordered');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('applies dense class when dense is enabled', () => {
|
|
373
|
+
const { container } = render(
|
|
374
|
+
<DataTable
|
|
375
|
+
data={sampleData}
|
|
376
|
+
columns={sampleColumns}
|
|
377
|
+
dense={true}
|
|
378
|
+
/>
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const table = container.querySelector('.c-data-table');
|
|
382
|
+
expect(table).toHaveClass('c-data-table--dense');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('applies sticky header class when stickyHeader is enabled', () => {
|
|
386
|
+
const { container } = render(
|
|
387
|
+
<DataTable
|
|
388
|
+
data={sampleData}
|
|
389
|
+
columns={sampleColumns}
|
|
390
|
+
stickyHeader={true}
|
|
391
|
+
/>
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const table = container.querySelector('.c-data-table');
|
|
395
|
+
expect(table).toHaveClass('c-data-table--sticky-header');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('Custom Rendering', () => {
|
|
400
|
+
it('uses custom render function for cells', () => {
|
|
401
|
+
const columnsWithRender: DataTableColumn[] = [
|
|
402
|
+
{
|
|
403
|
+
key: 'name',
|
|
404
|
+
title: 'Name',
|
|
405
|
+
render: (value) => <strong>{value}</strong>,
|
|
406
|
+
},
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
render(
|
|
410
|
+
<DataTable
|
|
411
|
+
data={[{ name: 'John Doe' }]}
|
|
412
|
+
columns={columnsWithRender}
|
|
413
|
+
/>
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const strongElement = screen.getByText('John Doe').closest('strong');
|
|
417
|
+
expect(strongElement).toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('Accessibility', () => {
|
|
422
|
+
it('applies correct ARIA attributes for sortable columns', () => {
|
|
423
|
+
render(
|
|
424
|
+
<DataTable
|
|
425
|
+
data={sampleData}
|
|
426
|
+
columns={sampleColumns}
|
|
427
|
+
sortable={true}
|
|
428
|
+
/>
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const nameHeader = screen.getByText('Name').closest('th');
|
|
432
|
+
expect(nameHeader).toHaveAttribute('aria-sort');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('applies role="button" to clickable rows', () => {
|
|
436
|
+
const handleRowClick = vi.fn();
|
|
437
|
+
render(
|
|
438
|
+
<DataTable
|
|
439
|
+
data={sampleData}
|
|
440
|
+
columns={sampleColumns}
|
|
441
|
+
onRowClick={handleRowClick}
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const row = screen.getByText('John Doe').closest('tr');
|
|
446
|
+
expect(row).toHaveAttribute('role', 'button');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|