@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.
Files changed (35) hide show
  1. package/dist/index.d.mts +2 -2
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.global.js +9 -9
  4. package/dist/index.global.js.map +1 -1
  5. package/dist/index.js +21 -15
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +21 -15
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/lib/theme.global.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/use-intersection-observer.test.tsx +18 -4
  12. package/src/__tests__/use-local-storage.test.tsx +34 -20
  13. package/src/components/ui/__tests__/avatar.test.tsx +5 -7
  14. package/src/components/ui/__tests__/badge.test.tsx +24 -14
  15. package/src/components/ui/__tests__/breadcrumb.test.tsx +4 -7
  16. package/src/components/ui/__tests__/button.test.tsx +6 -3
  17. package/src/components/ui/__tests__/card.test.tsx +4 -1
  18. package/src/components/ui/__tests__/checkbox.test.tsx +4 -1
  19. package/src/components/ui/__tests__/command.test.tsx +268 -124
  20. package/src/components/ui/__tests__/data-table-basic.test.tsx +271 -0
  21. package/src/components/ui/__tests__/date-picker.test.tsx +184 -197
  22. package/src/components/ui/__tests__/progress.test.tsx +0 -1
  23. package/src/components/ui/__tests__/table.test.tsx +8 -2
  24. package/src/components/ui/__tests__/tabs.test.tsx +5 -3
  25. package/src/components/ui/__tests__/toast.test.tsx +461 -159
  26. package/src/components/ui/__tests__/tooltip.test.tsx +387 -264
  27. package/src/components/ui/badge.tsx +4 -4
  28. package/src/components/ui/command.tsx +11 -7
  29. package/src/components/ui/date-picker.tsx +12 -6
  30. package/src/components/ui/toast.tsx +3 -0
  31. package/src/styles/tokens.css +21 -6
  32. package/src/use-intersection-observer.tsx +9 -0
  33. package/src/use-local-storage.tsx +35 -1
  34. package/src/components/ui/__tests__/data-table.test.tsx +0 -256
  35. 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-white",
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-white dark:shadow-inner dark:shadow-success/10",
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-white",
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-gray-900 dark:shadow-inner dark:shadow-warning/10",
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
- Command.displayName = CommandPrimitive.displayName
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 = CommandPrimitive.Input.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 = CommandPrimitive.List.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 = CommandPrimitive.Empty.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 = CommandPrimitive.Group.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 = CommandPrimitive.Separator.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 = CommandPrimitive.Item.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: Date | undefined };
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: Date | undefined } | undefined) => void;
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
- const handleSelect = (range: { from: Date | undefined; to: Date | undefined } | undefined) => {
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 as any}
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
- formatString = "PPP p",
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="PPP"
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;
@@ -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% 59.8%;
130
- --primary-foreground: 222.2 47.4% 11.2%;
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% 59.8%;
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% 59.8%;
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% 59.8%;
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: 142 76% 36%;
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
- return () => window.removeEventListener('storage', handleStorageChange)
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
- });