@moontra/moonui 4.1.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 +21 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +21 -15
- 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 +24 -14
- 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/badge.tsx +4 -4
- 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/styles/tokens.css +21 -6
- 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
|
@@ -51,16 +51,16 @@ const badgeVariants = cva(
|
|
|
51
51
|
"dark:bg-error/90 dark:text-white dark:shadow-inner dark:shadow-error/10",
|
|
52
52
|
],
|
|
53
53
|
success: [
|
|
54
|
-
"border-transparent bg-success text-
|
|
54
|
+
"border-transparent bg-success text-success-foreground",
|
|
55
55
|
"hover:bg-success/90 dark:hover:bg-success/80",
|
|
56
56
|
"focus-visible:ring-success/30 dark:focus-visible:ring-success/25",
|
|
57
|
-
"dark:bg-success/90 dark:text-
|
|
57
|
+
"dark:bg-success/90 dark:text-success-foreground dark:shadow-inner dark:shadow-success/10",
|
|
58
58
|
],
|
|
59
59
|
warning: [
|
|
60
|
-
"border-transparent bg-warning text-
|
|
60
|
+
"border-transparent bg-warning text-warning-foreground",
|
|
61
61
|
"hover:bg-warning/90 dark:hover:bg-warning/80",
|
|
62
62
|
"focus-visible:ring-warning/30 dark:focus-visible:ring-warning/25",
|
|
63
|
-
"dark:bg-warning/90 dark:text-
|
|
63
|
+
"dark:bg-warning/90 dark:text-warning-foreground dark:shadow-inner dark:shadow-warning/10",
|
|
64
64
|
],
|
|
65
65
|
ghost: [
|
|
66
66
|
"border border-gray-200 bg-gray-50/50 text-gray-700",
|
|
@@ -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;
|
package/src/styles/tokens.css
CHANGED
|
@@ -126,24 +126,34 @@
|
|
|
126
126
|
* surum yukseltmesinde sessizce maviden mora cevirirdi — kirici bir gorsel
|
|
127
127
|
* degisiklik ve tum renk akislarinin bozulmasi demekti. Marka rengi isteyen
|
|
128
128
|
* tuketici bilerek `bg-brand-600` yazar ya da "brand" preset'ini secer. */
|
|
129
|
-
--primary: 217.2 91.2%
|
|
130
|
-
--primary
|
|
129
|
+
--primary: 217.2 91.2% 51%;
|
|
130
|
+
/* Açık temada --primary MAVİ (L=59.8%); üzerine gelen metin/ikon de AÇIK
|
|
131
|
+
olmalı. Bu değer kardeşleriyle uyumsuz tek çiftti (L=11.2%, koyu) ve
|
|
132
|
+
`bg-primary text-primary-foreground` kullanan her yüzeyde — buton, rozet,
|
|
133
|
+
ilerleme noktası — mavi zemin üstünde KOYU metin üretiyordu.
|
|
134
|
+
Kardeş referanslar: --destructive-foreground ve --success-foreground
|
|
135
|
+
ikisi de 210 40% 98%. (Karanlık temada --primary zaten beyaz olduğu için
|
|
136
|
+
oradaki koyu foreground DOĞRU ve değiştirilmedi.) */
|
|
137
|
+
--primary-foreground: 210 40% 98%;
|
|
131
138
|
--secondary: 217.2 32.6% 94%;
|
|
132
|
-
--secondary-foreground: 217.2 91.2%
|
|
139
|
+
--secondary-foreground: 217.2 91.2% 51%;
|
|
133
140
|
--muted: 217.2 32.6% 96%;
|
|
134
141
|
--muted-foreground: 215.4 16.3% 46.9%;
|
|
135
142
|
--accent: 217.2 32.6% 94%;
|
|
136
|
-
--accent-foreground: 217.2 91.2%
|
|
143
|
+
--accent-foreground: 217.2 91.2% 51%;
|
|
137
144
|
--destructive: 0 84.2% 60.2%;
|
|
138
145
|
--destructive-foreground: 210 40% 98%;
|
|
139
146
|
--border: 214.3 31.8% 91.4%;
|
|
140
147
|
--input: 214.3 31.8% 91.4%;
|
|
141
|
-
--ring: 217.2 91.2%
|
|
148
|
+
--ring: 217.2 91.2% 51%;
|
|
142
149
|
|
|
143
150
|
/* Durum renkleri. Tailwind preset'i bg-success/bg-warning/bg-error/bg-info/
|
|
144
151
|
* bg-caution siniflarini bu degiskenlere bagliyor; tanimsiz kalirlarsa o
|
|
145
152
|
* siniflar renksiz uretilir. */
|
|
146
|
-
--success
|
|
153
|
+
/* Açık temada zemin, üzerine gelen açık metin (--success-foreground) için
|
|
154
|
+
fazla açıktı: beyaz metinle kontrast 3.18 (WCAG AA 4.5:1 altı). L=36% -> 29%
|
|
155
|
+
ile 4.69'a çıkar. --primary'de uygulanan düzeltmenin aynısı. */
|
|
156
|
+
--success: 142 76% 29%;
|
|
147
157
|
--success-foreground: 210 40% 98%;
|
|
148
158
|
--warning: 38 92% 50%;
|
|
149
159
|
--warning-foreground: 24 10% 10%;
|
|
@@ -409,6 +419,11 @@
|
|
|
409
419
|
--accent: 216 34% 17%;
|
|
410
420
|
--accent-foreground: 210 40% 98%;
|
|
411
421
|
--destructive: 0 63% 31%;
|
|
422
|
+
/* `--success` iki rolde birden kullanılıyor: açık temada `bg-success` ZEMİN
|
|
423
|
+
(üstünde açık metin), karanlık temada `text-success` METİN (koyu zemin
|
|
424
|
+
üstünde). Açık tema için 29%'e koyulaştırıldı; karanlık temada o değer
|
|
425
|
+
metin olarak okunmuyordu (3.30) — burada 36% korunur (4.86). */
|
|
426
|
+
--success: 142 76% 36%;
|
|
412
427
|
--destructive-foreground: 210 40% 98%;
|
|
413
428
|
--border: 216 34% 17%;
|
|
414
429
|
--input: 216 34% 17%;
|
|
@@ -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
|
-
});
|