@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.
Files changed (53) hide show
  1. package/dist/atomix.css +77 -0
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +77 -0
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.d.ts +2 -2
  7. package/dist/core.js.map +1 -1
  8. package/dist/forms.js.map +1 -1
  9. package/dist/heavy.js.map +1 -1
  10. package/dist/index.d.ts +578 -515
  11. package/dist/index.esm.js +3157 -2626
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/index.js +10496 -9973
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.min.js +1 -1
  16. package/dist/index.min.js.map +1 -1
  17. package/dist/theme.d.ts +237 -420
  18. package/dist/theme.js +1629 -1701
  19. package/dist/theme.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/DataTable/DataTable.stories.tsx +238 -0
  22. package/src/components/DataTable/DataTable.test.tsx +450 -0
  23. package/src/components/DataTable/DataTable.tsx +384 -61
  24. package/src/components/DatePicker/DatePicker.tsx +29 -38
  25. package/src/components/Upload/Upload.tsx +539 -40
  26. package/src/lib/composables/useDataTable.ts +355 -15
  27. package/src/lib/composables/useDatePicker.ts +19 -0
  28. package/src/lib/constants/components.ts +10 -0
  29. package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
  30. package/src/lib/theme/adapters/index.ts +1 -4
  31. package/src/lib/theme/config/configLoader.ts +53 -35
  32. package/src/lib/theme/core/composeTheme.ts +22 -30
  33. package/src/lib/theme/core/createTheme.ts +49 -26
  34. package/src/lib/theme/core/index.ts +0 -1
  35. package/src/lib/theme/generators/generateCSSNested.ts +4 -3
  36. package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
  37. package/src/lib/theme/index.ts +10 -17
  38. package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
  39. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
  40. package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
  41. package/src/lib/theme/runtime/useTheme.ts +1 -1
  42. package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
  43. package/src/lib/theme/test/testTheme.ts +2 -1
  44. package/src/lib/theme/types.ts +14 -14
  45. package/src/lib/theme/utils/componentTheming.ts +35 -27
  46. package/src/lib/theme/utils/domUtils.ts +57 -15
  47. package/src/lib/theme/utils/injectCSS.ts +0 -1
  48. package/src/lib/theme/utils/themeHelpers.ts +1 -39
  49. package/src/lib/theme/utils/themeUtils.ts +1 -170
  50. package/src/lib/types/components.ts +145 -0
  51. package/src/lib/utils/dataTableExport.ts +143 -0
  52. package/src/styles/06-components/_components.data-table.scss +95 -0
  53. package/src/lib/hooks/useThemeTokens.ts +0 -105
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shohojdhara/atomix",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Atomix Design System - A modern component library for web applications",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -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
+