@moontra/moonui 5.0.0 → 5.0.1
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/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.global.js +9 -9
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +17 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -11
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/use-intersection-observer.test.tsx +18 -4
- package/src/__tests__/use-local-storage.test.tsx +34 -20
- package/src/components/ui/__tests__/avatar.test.tsx +5 -7
- package/src/components/ui/__tests__/badge.test.tsx +19 -12
- package/src/components/ui/__tests__/breadcrumb.test.tsx +4 -7
- package/src/components/ui/__tests__/button.test.tsx +6 -3
- package/src/components/ui/__tests__/card.test.tsx +4 -1
- package/src/components/ui/__tests__/checkbox.test.tsx +4 -1
- package/src/components/ui/__tests__/command.test.tsx +268 -124
- package/src/components/ui/__tests__/data-table-basic.test.tsx +271 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +184 -197
- package/src/components/ui/__tests__/progress.test.tsx +0 -1
- package/src/components/ui/__tests__/table.test.tsx +8 -2
- package/src/components/ui/__tests__/tabs.test.tsx +5 -3
- package/src/components/ui/__tests__/toast.test.tsx +461 -159
- package/src/components/ui/__tests__/tooltip.test.tsx +387 -264
- package/src/components/ui/command.tsx +11 -7
- package/src/components/ui/date-picker.tsx +12 -6
- package/src/components/ui/toast.tsx +3 -0
- package/src/use-intersection-observer.tsx +9 -0
- package/src/use-local-storage.tsx +35 -1
- package/src/components/ui/__tests__/data-table.test.tsx +0 -256
- package/src/components/ui/data-table.stories.tsx +0 -511
|
@@ -52,7 +52,11 @@ const Command = React.forwardRef<
|
|
|
52
52
|
{...props}
|
|
53
53
|
/>
|
|
54
54
|
))
|
|
55
|
-
|
|
55
|
+
// NOT: cmdk primitive'leri displayName tanımlamaz (Radix'ten farklı olarak). Bu yüzden
|
|
56
|
+
// `CommandPrimitive.*.displayName` okumak `undefined` üretiyordu ve React DevTools ile hata
|
|
57
|
+
// mesajlarında bileşenler isimsiz görünüyordu. Kütüphane genelindeki konvansiyona uyarak
|
|
58
|
+
// displayName'ler açık string olarak verilir.
|
|
59
|
+
Command.displayName = "Command"
|
|
56
60
|
|
|
57
61
|
// CommandDialog bileşeni
|
|
58
62
|
interface CommandDialogProps extends React.ComponentProps<typeof Dialog> {
|
|
@@ -100,7 +104,7 @@ const CommandInput = React.forwardRef<
|
|
|
100
104
|
/>
|
|
101
105
|
</div>
|
|
102
106
|
))
|
|
103
|
-
CommandInput.displayName =
|
|
107
|
+
CommandInput.displayName = "CommandInput"
|
|
104
108
|
|
|
105
109
|
// CommandList bileşeni
|
|
106
110
|
interface CommandListProps
|
|
@@ -116,7 +120,7 @@ const CommandList = React.forwardRef<
|
|
|
116
120
|
{...props}
|
|
117
121
|
/>
|
|
118
122
|
))
|
|
119
|
-
CommandList.displayName =
|
|
123
|
+
CommandList.displayName = "CommandList"
|
|
120
124
|
|
|
121
125
|
// CommandEmpty bileşeni
|
|
122
126
|
interface CommandEmptyProps
|
|
@@ -132,7 +136,7 @@ const CommandEmpty = React.forwardRef<
|
|
|
132
136
|
{...props}
|
|
133
137
|
/>
|
|
134
138
|
))
|
|
135
|
-
CommandEmpty.displayName =
|
|
139
|
+
CommandEmpty.displayName = "CommandEmpty"
|
|
136
140
|
|
|
137
141
|
// CommandGroup bileşeni
|
|
138
142
|
interface CommandGroupProps
|
|
@@ -151,7 +155,7 @@ const CommandGroup = React.forwardRef<
|
|
|
151
155
|
{...props}
|
|
152
156
|
/>
|
|
153
157
|
))
|
|
154
|
-
CommandGroup.displayName =
|
|
158
|
+
CommandGroup.displayName = "CommandGroup"
|
|
155
159
|
|
|
156
160
|
// CommandSeparator bileşeni
|
|
157
161
|
interface CommandSeparatorProps
|
|
@@ -167,7 +171,7 @@ const CommandSeparator = React.forwardRef<
|
|
|
167
171
|
{...props}
|
|
168
172
|
/>
|
|
169
173
|
))
|
|
170
|
-
CommandSeparator.displayName =
|
|
174
|
+
CommandSeparator.displayName = "CommandSeparator"
|
|
171
175
|
|
|
172
176
|
// CommandItem bileşeni
|
|
173
177
|
interface CommandItemProps
|
|
@@ -186,7 +190,7 @@ const CommandItem = React.forwardRef<
|
|
|
186
190
|
{...props}
|
|
187
191
|
/>
|
|
188
192
|
))
|
|
189
|
-
CommandItem.displayName =
|
|
193
|
+
CommandItem.displayName = "CommandItem"
|
|
190
194
|
|
|
191
195
|
// CommandShortcut bileşeni - utility component
|
|
192
196
|
const CommandShortcut = ({
|
|
@@ -312,11 +312,11 @@ export interface DateRangePickerProps
|
|
|
312
312
|
/**
|
|
313
313
|
* Selected date range
|
|
314
314
|
*/
|
|
315
|
-
value?: { from: Date | undefined; to
|
|
315
|
+
value?: { from: Date | undefined; to?: Date | undefined };
|
|
316
316
|
/**
|
|
317
317
|
* Callback when date range changes
|
|
318
318
|
*/
|
|
319
|
-
onChange?: (range: { from: Date | undefined; to
|
|
319
|
+
onChange?: (range: { from: Date | undefined; to?: Date | undefined } | undefined) => void;
|
|
320
320
|
/**
|
|
321
321
|
* Separator between dates
|
|
322
322
|
*/
|
|
@@ -339,7 +339,10 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
339
339
|
setInternalRange(value);
|
|
340
340
|
}, [value]);
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
// NOT: `to` OPSİYONEL. react-day-picker'ın `DateRange` tipi de öyle
|
|
343
|
+
// (`to?: Date | undefined`). Önce burada zorunlu-ama-undefined yazılmıştı ve
|
|
344
|
+
// uyuşmadığı için `onSelect={handleSelect}` cast'i gerekiyordu.
|
|
345
|
+
const handleSelect = (range: { from: Date | undefined; to?: Date | undefined } | undefined) => {
|
|
343
346
|
setInternalRange(range);
|
|
344
347
|
onChange?.(range);
|
|
345
348
|
if (range?.from && range?.to) {
|
|
@@ -386,7 +389,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
386
389
|
mode="range"
|
|
387
390
|
defaultMonth={internalRange?.from}
|
|
388
391
|
selected={internalRange}
|
|
389
|
-
onSelect={handleSelect
|
|
392
|
+
onSelect={handleSelect}
|
|
390
393
|
showOutsideDays={false}
|
|
391
394
|
{...props.calendarProps}
|
|
392
395
|
className="rounded-2xl"
|
|
@@ -421,7 +424,10 @@ export interface DateTimePickerProps extends DatePickerProps {
|
|
|
421
424
|
export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(function DateTimePicker({
|
|
422
425
|
value,
|
|
423
426
|
onChange,
|
|
424
|
-
|
|
427
|
+
// Saat ayrı bir <select> ile gösterildiği için buton yalnızca TARİHİ yazar;
|
|
428
|
+
// varsayılan bu yüzden "PPP p" değil "PPP". (Eski hâlde "PPP p" alınıp hiç
|
|
429
|
+
// kullanılmıyordu — çağıranın verdiği formatString sessizce yok sayılıyordu.)
|
|
430
|
+
formatString = "PPP",
|
|
425
431
|
showTimePicker = true,
|
|
426
432
|
timeFormat = "24",
|
|
427
433
|
timeInterval = 15,
|
|
@@ -486,7 +492,7 @@ export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerPro
|
|
|
486
492
|
{...props}
|
|
487
493
|
value={date}
|
|
488
494
|
onChange={handleDateChange}
|
|
489
|
-
formatString=
|
|
495
|
+
formatString={formatString}
|
|
490
496
|
showTodayButton={false}
|
|
491
497
|
/>
|
|
492
498
|
{showTimePicker && date && (
|
|
@@ -93,6 +93,9 @@ const ToastClose = React.forwardRef<
|
|
|
93
93
|
{...props}
|
|
94
94
|
>
|
|
95
95
|
<X className="h-4 w-4" />
|
|
96
|
+
{/* İkon-only kapatma düğmesinin erişilebilir bir ismi olmalı (WCAG 4.1.2);
|
|
97
|
+
aynı desen dialog.tsx'teki kapatma düğmesinde de kullanılıyor. */}
|
|
98
|
+
<span className="sr-only">Close</span>
|
|
96
99
|
</ToastPrimitives.Close>
|
|
97
100
|
));
|
|
98
101
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
@@ -27,6 +27,15 @@ export function useIntersectionObserver({
|
|
|
27
27
|
|
|
28
28
|
const observer = new IntersectionObserver(
|
|
29
29
|
([entry]) => {
|
|
30
|
+
// freezeOnceVisible: bir kez görünür olduktan sonra state DONMALI.
|
|
31
|
+
// frozen guard'ı yalnızca yukarıdaki effect girişinde vardı; effect'in
|
|
32
|
+
// deps'i değişmediği için yeniden koşmuyor, dolayısıyla guard hiç
|
|
33
|
+
// devreye girmiyordu. Canlı observer bir sonraki entry'de
|
|
34
|
+
// setIsIntersecting(false) çağırıp donmayı bozuyordu.
|
|
35
|
+
if (frozen.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
const isElementIntersecting = entry.isIntersecting;
|
|
31
40
|
setIsIntersecting(isElementIntersecting);
|
|
32
41
|
onChange?.(isElementIntersecting, entry);
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Aynı sekmedeki diğer hook instance'larını haberdar eden özel olay.
|
|
5
|
+
*
|
|
6
|
+
* Native `storage` olayı YALNIZCA diğer sekme/pencerelerde tetiklenir — değeri
|
|
7
|
+
* yazan dokümanda hiç tetiklenmez. Bu yüzden aynı sayfada `useLocalStorage("k")`
|
|
8
|
+
* kullanan iki component birbirinden habersiz kalıyor, biri güncellenince
|
|
9
|
+
* diğeri bayat değeri göstermeye devam ediyordu.
|
|
10
|
+
*/
|
|
11
|
+
const LOCAL_SYNC_EVENT = 'moonui:local-storage'
|
|
12
|
+
|
|
13
|
+
interface LocalSyncDetail {
|
|
14
|
+
key: string
|
|
15
|
+
value: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
// Local storage hook with error handling and SSR support
|
|
4
19
|
export function useLocalStorage<T>(
|
|
5
20
|
key: string,
|
|
@@ -40,6 +55,13 @@ export function useLocalStorage<T>(
|
|
|
40
55
|
} else {
|
|
41
56
|
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
|
42
57
|
}
|
|
58
|
+
|
|
59
|
+
// Aynı dokümandaki diğer instance'ları bilgilendir (bkz. LOCAL_SYNC_EVENT)
|
|
60
|
+
window.dispatchEvent(
|
|
61
|
+
new CustomEvent<LocalSyncDetail>(LOCAL_SYNC_EVENT, {
|
|
62
|
+
detail: { key, value: valueToStore },
|
|
63
|
+
})
|
|
64
|
+
)
|
|
43
65
|
}
|
|
44
66
|
} catch (error) {
|
|
45
67
|
// A more advanced implementation would handle the error case
|
|
@@ -63,8 +85,20 @@ export function useLocalStorage<T>(
|
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
87
|
|
|
88
|
+
// Aynı sekmedeki kardeş instance'lar
|
|
89
|
+
const handleLocalSync = (e: Event) => {
|
|
90
|
+
const detail = (e as CustomEvent<LocalSyncDetail>).detail
|
|
91
|
+
if (detail && detail.key === key) {
|
|
92
|
+
setStoredValue(detail.value as T)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
window.addEventListener('storage', handleStorageChange)
|
|
67
|
-
|
|
97
|
+
window.addEventListener(LOCAL_SYNC_EVENT, handleLocalSync)
|
|
98
|
+
return () => {
|
|
99
|
+
window.removeEventListener('storage', handleStorageChange)
|
|
100
|
+
window.removeEventListener(LOCAL_SYNC_EVENT, handleLocalSync)
|
|
101
|
+
}
|
|
68
102
|
}, [key])
|
|
69
103
|
|
|
70
104
|
return [storedValue, setValue]
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
-
import { DataTable } from '../data-table';
|
|
3
|
-
import { ColumnDef } from '@tanstack/react-table';
|
|
4
|
-
import '@testing-library/jest-dom';
|
|
5
|
-
|
|
6
|
-
type TestData = {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
email: string;
|
|
10
|
-
age: number;
|
|
11
|
-
status: 'active' | 'inactive';
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const mockData: TestData[] = [
|
|
15
|
-
{ id: '1', name: 'John Doe', email: 'john@example.com', age: 30, status: 'active' },
|
|
16
|
-
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', age: 25, status: 'inactive' },
|
|
17
|
-
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', age: 35, status: 'active' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const mockColumns: ColumnDef<TestData>[] = [
|
|
21
|
-
{
|
|
22
|
-
accessorKey: 'name',
|
|
23
|
-
header: 'Name',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
accessorKey: 'email',
|
|
27
|
-
header: 'Email',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
accessorKey: 'age',
|
|
31
|
-
header: 'Age',
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
accessorKey: 'status',
|
|
35
|
-
header: 'Status',
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
describe('DataTable', () => {
|
|
40
|
-
it('renders correctly with basic data', () => {
|
|
41
|
-
render(<DataTable columns={mockColumns} data={mockData} />);
|
|
42
|
-
|
|
43
|
-
// Check headers
|
|
44
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
45
|
-
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
46
|
-
expect(screen.getByText('Age')).toBeInTheDocument();
|
|
47
|
-
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
48
|
-
|
|
49
|
-
// Check data rows
|
|
50
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
51
|
-
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
52
|
-
expect(screen.getByText('30')).toBeInTheDocument();
|
|
53
|
-
expect(screen.getByText('active')).toBeInTheDocument();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('renders empty state when no data', () => {
|
|
57
|
-
render(<DataTable columns={mockColumns} data={[]} />);
|
|
58
|
-
expect(screen.getByText('No results.')).toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('shows search input when searchable is true', () => {
|
|
62
|
-
render(<DataTable columns={mockColumns} data={mockData} searchable />);
|
|
63
|
-
expect(screen.getByPlaceholderText('Search all columns...')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('filters data based on search input', async () => {
|
|
67
|
-
render(<DataTable columns={mockColumns} data={mockData} searchable />);
|
|
68
|
-
|
|
69
|
-
const searchInput = screen.getByPlaceholderText('Search all columns...');
|
|
70
|
-
fireEvent.change(searchInput, { target: { value: 'john' } });
|
|
71
|
-
|
|
72
|
-
await waitFor(() => {
|
|
73
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
74
|
-
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('shows filter button when filterable is true', () => {
|
|
79
|
-
render(<DataTable columns={mockColumns} data={mockData} filterable />);
|
|
80
|
-
expect(screen.getByText('Filter')).toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('shows export button when exportable is true', () => {
|
|
84
|
-
render(<DataTable columns={mockColumns} data={mockData} exportable />);
|
|
85
|
-
expect(screen.getByText('Export')).toBeInTheDocument();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('handles pagination correctly', () => {
|
|
89
|
-
const largeData = Array.from({ length: 25 }, (_, i) => ({
|
|
90
|
-
id: `${i + 1}`,
|
|
91
|
-
name: `User ${i + 1}`,
|
|
92
|
-
email: `user${i + 1}@example.com`,
|
|
93
|
-
age: 20 + i,
|
|
94
|
-
status: i % 2 === 0 ? 'active' as const : 'inactive' as const,
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
|
-
render(<DataTable columns={mockColumns} data={largeData} />);
|
|
98
|
-
|
|
99
|
-
// Check pagination controls
|
|
100
|
-
expect(screen.getByText('Previous')).toBeInTheDocument();
|
|
101
|
-
expect(screen.getByText('Next')).toBeInTheDocument();
|
|
102
|
-
expect(screen.getByText('1 of 3')).toBeInTheDocument(); // 25 items, 10 per page = 3 pages
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('handles sorting correctly', async () => {
|
|
106
|
-
const sortableColumns: ColumnDef<TestData>[] = [
|
|
107
|
-
{
|
|
108
|
-
accessorKey: 'name',
|
|
109
|
-
header: ({ column }) => (
|
|
110
|
-
<button onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
|
|
111
|
-
Name
|
|
112
|
-
</button>
|
|
113
|
-
),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
accessorKey: 'age',
|
|
117
|
-
header: ({ column }) => (
|
|
118
|
-
<button onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
|
|
119
|
-
Age
|
|
120
|
-
</button>
|
|
121
|
-
),
|
|
122
|
-
},
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
render(<DataTable columns={sortableColumns} data={mockData} />);
|
|
126
|
-
|
|
127
|
-
const nameHeader = screen.getByText('Name');
|
|
128
|
-
fireEvent.click(nameHeader);
|
|
129
|
-
|
|
130
|
-
await waitFor(() => {
|
|
131
|
-
const rows = screen.getAllByRole('row');
|
|
132
|
-
// First row should be header, second should be Bob Johnson (alphabetically first)
|
|
133
|
-
expect(rows[1]).toHaveTextContent('Bob Johnson');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('handles row selection', () => {
|
|
138
|
-
const selectableColumns: ColumnDef<TestData>[] = [
|
|
139
|
-
{
|
|
140
|
-
id: 'select',
|
|
141
|
-
header: ({ table }) => (
|
|
142
|
-
<input
|
|
143
|
-
type="checkbox"
|
|
144
|
-
checked={table.getIsAllPageRowsSelected()}
|
|
145
|
-
onChange={(e) => table.toggleAllPageRowsSelected(e.target.checked)}
|
|
146
|
-
/>
|
|
147
|
-
),
|
|
148
|
-
cell: ({ row }) => (
|
|
149
|
-
<input
|
|
150
|
-
type="checkbox"
|
|
151
|
-
checked={row.getIsSelected()}
|
|
152
|
-
onChange={(e) => row.toggleSelected(e.target.checked)}
|
|
153
|
-
/>
|
|
154
|
-
),
|
|
155
|
-
},
|
|
156
|
-
...mockColumns,
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
render(<DataTable columns={selectableColumns} data={mockData} />);
|
|
160
|
-
|
|
161
|
-
const checkboxes = screen.getAllByRole('checkbox');
|
|
162
|
-
expect(checkboxes).toHaveLength(4); // 1 header + 3 rows
|
|
163
|
-
|
|
164
|
-
// Select first row
|
|
165
|
-
fireEvent.click(checkboxes[1]);
|
|
166
|
-
expect(checkboxes[1]).toBeChecked();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('handles column visibility', () => {
|
|
170
|
-
render(<DataTable columns={mockColumns} data={mockData} />);
|
|
171
|
-
|
|
172
|
-
// All columns should be visible by default
|
|
173
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
174
|
-
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
175
|
-
expect(screen.getByText('Age')).toBeInTheDocument();
|
|
176
|
-
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('shows loading state', () => {
|
|
180
|
-
render(<DataTable columns={mockColumns} data={mockData} loading />);
|
|
181
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('handles custom cell rendering', () => {
|
|
185
|
-
const customColumns: ColumnDef<TestData>[] = [
|
|
186
|
-
{
|
|
187
|
-
accessorKey: 'name',
|
|
188
|
-
header: 'Name',
|
|
189
|
-
cell: ({ row }) => (
|
|
190
|
-
<div className="font-bold">{row.getValue('name')}</div>
|
|
191
|
-
),
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
accessorKey: 'status',
|
|
195
|
-
header: 'Status',
|
|
196
|
-
cell: ({ row }) => {
|
|
197
|
-
const status = row.getValue('status') as string;
|
|
198
|
-
return (
|
|
199
|
-
<span className={status === 'active' ? 'text-green-600' : 'text-red-600'}>
|
|
200
|
-
{status}
|
|
201
|
-
</span>
|
|
202
|
-
);
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
render(<DataTable columns={customColumns} data={mockData} />);
|
|
208
|
-
|
|
209
|
-
// Check if custom rendering is applied
|
|
210
|
-
const johnDoeCell = screen.getByText('John Doe');
|
|
211
|
-
expect(johnDoeCell).toHaveClass('font-bold');
|
|
212
|
-
|
|
213
|
-
const activeStatus = screen.getAllByText('active')[0];
|
|
214
|
-
expect(activeStatus).toHaveClass('text-green-600');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('handles empty columns gracefully', () => {
|
|
218
|
-
render(<DataTable columns={[]} data={mockData} />);
|
|
219
|
-
expect(screen.getByText('No results.')).toBeInTheDocument();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('handles keyboard navigation', () => {
|
|
223
|
-
render(<DataTable columns={mockColumns} data={mockData} />);
|
|
224
|
-
|
|
225
|
-
const table = screen.getByRole('table');
|
|
226
|
-
expect(table).toBeInTheDocument();
|
|
227
|
-
|
|
228
|
-
// Table should be keyboard accessible
|
|
229
|
-
fireEvent.keyDown(table, { key: 'Tab' });
|
|
230
|
-
expect(table).toBeInTheDocument();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('exports data when export button is clicked', async () => {
|
|
234
|
-
// Mock the download functionality
|
|
235
|
-
const mockCreateObjectURL = jest.fn();
|
|
236
|
-
const mockRevokeObjectURL = jest.fn();
|
|
237
|
-
global.URL.createObjectURL = mockCreateObjectURL;
|
|
238
|
-
global.URL.revokeObjectURL = mockRevokeObjectURL;
|
|
239
|
-
|
|
240
|
-
render(<DataTable columns={mockColumns} data={mockData} exportable />);
|
|
241
|
-
|
|
242
|
-
const exportButton = screen.getByText('Export');
|
|
243
|
-
fireEvent.click(exportButton);
|
|
244
|
-
|
|
245
|
-
await waitFor(() => {
|
|
246
|
-
expect(mockCreateObjectURL).toHaveBeenCalled();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('handles responsive design', () => {
|
|
251
|
-
render(<DataTable columns={mockColumns} data={mockData} />);
|
|
252
|
-
|
|
253
|
-
const tableContainer = screen.getByTestId('data-table-container');
|
|
254
|
-
expect(tableContainer).toHaveClass('overflow-auto');
|
|
255
|
-
});
|
|
256
|
-
});
|