@startsimpli/ui 0.1.0

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 (86) hide show
  1. package/README.md +537 -0
  2. package/package.json +80 -0
  3. package/src/components/index.ts +50 -0
  4. package/src/components/navigation/sidebar.tsx +178 -0
  5. package/src/components/ui/accordion.tsx +58 -0
  6. package/src/components/ui/alert.tsx +59 -0
  7. package/src/components/ui/badge.tsx +36 -0
  8. package/src/components/ui/button.tsx +57 -0
  9. package/src/components/ui/calendar.tsx +70 -0
  10. package/src/components/ui/card.tsx +68 -0
  11. package/src/components/ui/checkbox.tsx +30 -0
  12. package/src/components/ui/collapsible.tsx +12 -0
  13. package/src/components/ui/dialog.tsx +122 -0
  14. package/src/components/ui/dropdown-menu.tsx +200 -0
  15. package/src/components/ui/index.ts +24 -0
  16. package/src/components/ui/input.tsx +25 -0
  17. package/src/components/ui/label.tsx +26 -0
  18. package/src/components/ui/popover.tsx +31 -0
  19. package/src/components/ui/progress.tsx +28 -0
  20. package/src/components/ui/scroll-area.tsx +48 -0
  21. package/src/components/ui/select.tsx +160 -0
  22. package/src/components/ui/separator.tsx +31 -0
  23. package/src/components/ui/skeleton.tsx +15 -0
  24. package/src/components/ui/table.tsx +117 -0
  25. package/src/components/ui/tabs.tsx +55 -0
  26. package/src/components/ui/textarea.tsx +24 -0
  27. package/src/components/ui/tooltip.tsx +30 -0
  28. package/src/components/unified-table/UnifiedTable.tsx +553 -0
  29. package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
  30. package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
  31. package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
  32. package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
  33. package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
  34. package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
  35. package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
  36. package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
  37. package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
  38. package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
  39. package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
  40. package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
  41. package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
  42. package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
  43. package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
  44. package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
  45. package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
  46. package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
  47. package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
  48. package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
  49. package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
  50. package/src/components/unified-table/components/MobileView/README.md +411 -0
  51. package/src/components/unified-table/components/MobileView/index.tsx +77 -0
  52. package/src/components/unified-table/components/MobileView/types.ts +77 -0
  53. package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
  54. package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
  55. package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
  56. package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
  57. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
  58. package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
  59. package/src/components/unified-table/hooks/index.ts +21 -0
  60. package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
  61. package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
  62. package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
  63. package/src/components/unified-table/hooks/useFilters.ts +53 -0
  64. package/src/components/unified-table/hooks/usePagination.ts +120 -0
  65. package/src/components/unified-table/hooks/useResponsive.ts +50 -0
  66. package/src/components/unified-table/hooks/useSelection.ts +152 -0
  67. package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
  68. package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
  69. package/src/components/unified-table/hooks/useTableState.ts +103 -0
  70. package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
  71. package/src/components/unified-table/hooks/useTableURL.ts +301 -0
  72. package/src/components/unified-table/index.ts +16 -0
  73. package/src/components/unified-table/types.ts +393 -0
  74. package/src/components/unified-table/utils/export.ts +236 -0
  75. package/src/components/unified-table/utils/index.ts +4 -0
  76. package/src/components/unified-table/utils/renderers.ts +105 -0
  77. package/src/components/unified-table/utils/themes.ts +87 -0
  78. package/src/components/unified-table/utils/validation.ts +122 -0
  79. package/src/index.ts +6 -0
  80. package/src/lib/utils.ts +1 -0
  81. package/src/theme/contract.ts +46 -0
  82. package/src/theme/index.ts +9 -0
  83. package/src/theme/tailwind.config.js +70 -0
  84. package/src/theme/tailwind.preset.ts +93 -0
  85. package/src/utils/cn.ts +6 -0
  86. package/src/utils/index.ts +91 -0
@@ -0,0 +1,803 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useTableKeyboard } from '../../hooks/useTableKeyboard'
3
+ import { MutableRefObject } from 'react'
4
+
5
+ interface TestData {
6
+ id: string
7
+ name: string
8
+ }
9
+
10
+ describe('useTableKeyboard', () => {
11
+ const mockData: TestData[] = [
12
+ { id: '1', name: 'Item 1' },
13
+ { id: '2', name: 'Item 2' },
14
+ { id: '3', name: 'Item 3' },
15
+ ]
16
+
17
+ const getRowId = (row: TestData) => row.id
18
+
19
+ let mockTableRef: MutableRefObject<HTMLDivElement | null> = { current: null }
20
+ let mockTableElement: HTMLDivElement
21
+ let mockRowElements: HTMLTableRowElement[]
22
+
23
+ beforeEach(() => {
24
+ // Create mock table structure
25
+ mockTableElement = document.createElement('div')
26
+
27
+ // Create a proper table structure
28
+ const table = document.createElement('table')
29
+ const tbody = document.createElement('tbody')
30
+
31
+ mockRowElements = mockData.map((_, index) => {
32
+ const row = document.createElement('tr') as HTMLTableRowElement
33
+ row.setAttribute('data-row-index', String(index))
34
+ row.tabIndex = 0
35
+ // Create a proper jest mock function
36
+ const focusMock = jest.fn()
37
+ Object.defineProperty(row, 'focus', {
38
+ value: focusMock,
39
+ writable: true,
40
+ configurable: true,
41
+ })
42
+ tbody.appendChild(row)
43
+ return row
44
+ })
45
+
46
+ table.appendChild(tbody)
47
+ mockTableElement.appendChild(table)
48
+ // Update the ref's current property instead of creating a new ref object
49
+ mockTableRef.current = mockTableElement
50
+ document.body.appendChild(mockTableElement)
51
+ })
52
+
53
+ afterEach(() => {
54
+ document.body.removeChild(mockTableElement)
55
+ mockTableRef.current = null
56
+ })
57
+
58
+ const defaultProps = {
59
+ data: mockData,
60
+ getRowId,
61
+ tableRef: mockTableRef,
62
+ enabled: true,
63
+ }
64
+
65
+ describe('Initialization', () => {
66
+ it('should initialize with focusedRowIndex of -1', () => {
67
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
68
+
69
+ expect(result.current.focusedRowIndex).toBe(-1)
70
+ })
71
+
72
+ it('should provide setFocusedRowIndex function', () => {
73
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
74
+
75
+ expect(typeof result.current.setFocusedRowIndex).toBe('function')
76
+ })
77
+
78
+ it('should provide handleKeyDown function', () => {
79
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
80
+
81
+ expect(typeof result.current.handleKeyDown).toBe('function')
82
+ })
83
+ })
84
+
85
+ describe('Arrow Down Navigation', () => {
86
+ it('should move focus to next row on ArrowDown', () => {
87
+ const { result, rerender } = renderHook(() => useTableKeyboard(defaultProps))
88
+
89
+ // Set initial focused row
90
+ act(() => {
91
+ result.current.setFocusedRowIndex(0)
92
+ })
93
+
94
+ // Force a rerender to ensure the ref update is captured
95
+ rerender()
96
+
97
+ const mockEvent = {
98
+ key: 'ArrowDown',
99
+ ctrlKey: false,
100
+ metaKey: false,
101
+ shiftKey: false,
102
+ preventDefault: jest.fn(),
103
+ stopPropagation: jest.fn(),
104
+ } as any
105
+
106
+ act(() => {
107
+ result.current.handleKeyDown(mockEvent)
108
+ })
109
+
110
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
111
+
112
+ // Verify the element was found and focus was called
113
+ const focusedElement = mockTableElement.querySelector('[data-row-index="1"]')
114
+ expect(focusedElement).toBeTruthy()
115
+ expect((mockRowElements[1].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
116
+ })
117
+
118
+ it('should not move past last row on ArrowDown', () => {
119
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
120
+
121
+ act(() => {
122
+ result.current.setFocusedRowIndex(2) // Last row
123
+ })
124
+
125
+ const mockEvent = {
126
+ key: 'ArrowDown',
127
+ ctrlKey: false,
128
+ metaKey: false,
129
+ shiftKey: false,
130
+ preventDefault: jest.fn(),
131
+ stopPropagation: jest.fn(),
132
+ } as any
133
+
134
+ act(() => {
135
+ result.current.handleKeyDown(mockEvent)
136
+ })
137
+
138
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
139
+ // Should stay on last row
140
+ expect((mockRowElements[2].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
141
+ })
142
+ })
143
+
144
+ describe('Arrow Up Navigation', () => {
145
+ it('should move focus to previous row on ArrowUp', () => {
146
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
147
+
148
+ act(() => {
149
+ result.current.setFocusedRowIndex(2)
150
+ })
151
+
152
+ const mockEvent = {
153
+ key: 'ArrowUp',
154
+ ctrlKey: false,
155
+ metaKey: false,
156
+ shiftKey: false,
157
+ preventDefault: jest.fn(),
158
+ stopPropagation: jest.fn(),
159
+ } as any
160
+
161
+ act(() => {
162
+ result.current.handleKeyDown(mockEvent)
163
+ })
164
+
165
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
166
+ expect((mockRowElements[1].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
167
+ })
168
+
169
+ it('should not move past first row on ArrowUp', () => {
170
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
171
+
172
+ act(() => {
173
+ result.current.setFocusedRowIndex(0)
174
+ })
175
+
176
+ const mockEvent = {
177
+ key: 'ArrowUp',
178
+ ctrlKey: false,
179
+ metaKey: false,
180
+ shiftKey: false,
181
+ preventDefault: jest.fn(),
182
+ stopPropagation: jest.fn(),
183
+ } as any
184
+
185
+ act(() => {
186
+ result.current.handleKeyDown(mockEvent)
187
+ })
188
+
189
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
190
+ // Should stay on first row
191
+ expect((mockRowElements[0].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
192
+ })
193
+ })
194
+
195
+ describe('Home/End Navigation', () => {
196
+ it('should move to first row on Home key', () => {
197
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
198
+
199
+ act(() => {
200
+ result.current.setFocusedRowIndex(2)
201
+ })
202
+
203
+ const mockEvent = {
204
+ key: 'Home',
205
+ ctrlKey: false,
206
+ metaKey: false,
207
+ shiftKey: false,
208
+ preventDefault: jest.fn(),
209
+ stopPropagation: jest.fn(),
210
+ } as any
211
+
212
+ act(() => {
213
+ result.current.handleKeyDown(mockEvent)
214
+ })
215
+
216
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
217
+ expect((mockRowElements[0].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
218
+ })
219
+
220
+ it('should move to last row on End key', () => {
221
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
222
+
223
+ act(() => {
224
+ result.current.setFocusedRowIndex(0)
225
+ })
226
+
227
+ const mockEvent = {
228
+ key: 'End',
229
+ ctrlKey: false,
230
+ metaKey: false,
231
+ shiftKey: false,
232
+ preventDefault: jest.fn(),
233
+ stopPropagation: jest.fn(),
234
+ } as any
235
+
236
+ act(() => {
237
+ result.current.handleKeyDown(mockEvent)
238
+ })
239
+
240
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
241
+ expect((mockRowElements[2].focus as jest.Mock).mock.calls.length).toBeGreaterThan(0)
242
+ })
243
+ })
244
+
245
+ describe('Enter Key - Row Click', () => {
246
+ it('should trigger onRowClick when Enter is pressed', () => {
247
+ const onRowClick = jest.fn()
248
+ const { result } = renderHook(() =>
249
+ useTableKeyboard({ ...defaultProps, onRowClick })
250
+ )
251
+
252
+ act(() => {
253
+ result.current.setFocusedRowIndex(1)
254
+ })
255
+
256
+ const mockEvent = {
257
+ key: 'Enter',
258
+ ctrlKey: false,
259
+ metaKey: false,
260
+ shiftKey: false,
261
+ preventDefault: jest.fn(),
262
+ stopPropagation: jest.fn(),
263
+ } as any
264
+
265
+ act(() => {
266
+ result.current.handleKeyDown(mockEvent)
267
+ })
268
+
269
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
270
+ expect(onRowClick).toHaveBeenCalledWith(mockData[1])
271
+ })
272
+
273
+ it('should not trigger onRowClick when no row is focused', () => {
274
+ const onRowClick = jest.fn()
275
+ const { result } = renderHook(() =>
276
+ useTableKeyboard({ ...defaultProps, onRowClick })
277
+ )
278
+
279
+ const mockEvent = {
280
+ key: 'Enter',
281
+ ctrlKey: false,
282
+ metaKey: false,
283
+ shiftKey: false,
284
+ preventDefault: jest.fn(),
285
+ stopPropagation: jest.fn(),
286
+ } as any
287
+
288
+ act(() => {
289
+ result.current.handleKeyDown(mockEvent)
290
+ })
291
+
292
+ expect(onRowClick).not.toHaveBeenCalled()
293
+ })
294
+
295
+ it('should not trigger onRowClick when onRowClick is not provided', () => {
296
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
297
+
298
+ act(() => {
299
+ result.current.setFocusedRowIndex(1)
300
+ })
301
+
302
+ const mockEvent = {
303
+ key: 'Enter',
304
+ ctrlKey: false,
305
+ metaKey: false,
306
+ shiftKey: false,
307
+ preventDefault: jest.fn(),
308
+ stopPropagation: jest.fn(),
309
+ } as any
310
+
311
+ act(() => {
312
+ result.current.handleKeyDown(mockEvent)
313
+ })
314
+
315
+ // Should not throw error
316
+ expect(mockEvent.preventDefault).not.toHaveBeenCalled()
317
+ })
318
+ })
319
+
320
+ describe('Space Key - Row Selection', () => {
321
+ it('should toggle row selection when Space is pressed', () => {
322
+ const onToggleRow = jest.fn()
323
+ const { result } = renderHook(() =>
324
+ useTableKeyboard({ ...defaultProps, onToggleRow })
325
+ )
326
+
327
+ act(() => {
328
+ result.current.setFocusedRowIndex(1)
329
+ })
330
+
331
+ const mockEvent = {
332
+ key: ' ',
333
+ ctrlKey: false,
334
+ metaKey: false,
335
+ shiftKey: false,
336
+ preventDefault: jest.fn(),
337
+ stopPropagation: jest.fn(),
338
+ } as any
339
+
340
+ act(() => {
341
+ result.current.handleKeyDown(mockEvent)
342
+ })
343
+
344
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
345
+ expect(onToggleRow).toHaveBeenCalledWith('2')
346
+ })
347
+
348
+ it('should not toggle selection when no row is focused', () => {
349
+ const onToggleRow = jest.fn()
350
+ const { result } = renderHook(() =>
351
+ useTableKeyboard({ ...defaultProps, onToggleRow })
352
+ )
353
+
354
+ const mockEvent = {
355
+ key: ' ',
356
+ ctrlKey: false,
357
+ metaKey: false,
358
+ shiftKey: false,
359
+ preventDefault: jest.fn(),
360
+ stopPropagation: jest.fn(),
361
+ } as any
362
+
363
+ act(() => {
364
+ result.current.handleKeyDown(mockEvent)
365
+ })
366
+
367
+ expect(onToggleRow).not.toHaveBeenCalled()
368
+ })
369
+ })
370
+
371
+ describe('Ctrl/Cmd + A - Select All', () => {
372
+ it('should select all rows on Ctrl+A', () => {
373
+ const onToggleAll = jest.fn()
374
+ const { result } = renderHook(() =>
375
+ useTableKeyboard({ ...defaultProps, onToggleAll })
376
+ )
377
+
378
+ const mockEvent = {
379
+ key: 'a',
380
+ ctrlKey: true,
381
+ metaKey: false,
382
+ shiftKey: false,
383
+ preventDefault: jest.fn(),
384
+ stopPropagation: jest.fn(),
385
+ } as any
386
+
387
+ act(() => {
388
+ result.current.handleKeyDown(mockEvent)
389
+ })
390
+
391
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
392
+ expect(onToggleAll).toHaveBeenCalled()
393
+ })
394
+
395
+ it('should select all rows on Cmd+A (Mac)', () => {
396
+ const onToggleAll = jest.fn()
397
+ const { result } = renderHook(() =>
398
+ useTableKeyboard({ ...defaultProps, onToggleAll })
399
+ )
400
+
401
+ const mockEvent = {
402
+ key: 'a',
403
+ ctrlKey: false,
404
+ metaKey: true,
405
+ shiftKey: false,
406
+ preventDefault: jest.fn(),
407
+ stopPropagation: jest.fn(),
408
+ } as any
409
+
410
+ act(() => {
411
+ result.current.handleKeyDown(mockEvent)
412
+ })
413
+
414
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
415
+ expect(onToggleAll).toHaveBeenCalled()
416
+ })
417
+
418
+ it('should not select all when onToggleAll is not provided', () => {
419
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
420
+
421
+ const mockEvent = {
422
+ key: 'a',
423
+ ctrlKey: true,
424
+ metaKey: false,
425
+ shiftKey: false,
426
+ preventDefault: jest.fn(),
427
+ stopPropagation: jest.fn(),
428
+ } as any
429
+
430
+ act(() => {
431
+ result.current.handleKeyDown(mockEvent)
432
+ })
433
+
434
+ // Should not throw error
435
+ expect(mockEvent.preventDefault).not.toHaveBeenCalled()
436
+ })
437
+ })
438
+
439
+ describe('Delete Key', () => {
440
+ it('should trigger delete action when Delete is pressed with selection', () => {
441
+ const onDelete = jest.fn()
442
+ const selectedIds = new Set(['1', '2'])
443
+
444
+ const { result } = renderHook(() =>
445
+ useTableKeyboard({ ...defaultProps, onDelete, selectedIds })
446
+ )
447
+
448
+ const mockEvent = {
449
+ key: 'Delete',
450
+ ctrlKey: false,
451
+ metaKey: false,
452
+ shiftKey: false,
453
+ preventDefault: jest.fn(),
454
+ stopPropagation: jest.fn(),
455
+ } as any
456
+
457
+ act(() => {
458
+ result.current.handleKeyDown(mockEvent)
459
+ })
460
+
461
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
462
+ expect(onDelete).toHaveBeenCalledWith(selectedIds)
463
+ })
464
+
465
+ it('should not trigger delete when no items are selected', () => {
466
+ const onDelete = jest.fn()
467
+ const selectedIds = new Set<string>()
468
+
469
+ const { result } = renderHook(() =>
470
+ useTableKeyboard({ ...defaultProps, onDelete, selectedIds })
471
+ )
472
+
473
+ const mockEvent = {
474
+ key: 'Delete',
475
+ ctrlKey: false,
476
+ metaKey: false,
477
+ shiftKey: false,
478
+ preventDefault: jest.fn(),
479
+ stopPropagation: jest.fn(),
480
+ } as any
481
+
482
+ act(() => {
483
+ result.current.handleKeyDown(mockEvent)
484
+ })
485
+
486
+ expect(onDelete).not.toHaveBeenCalled()
487
+ })
488
+
489
+ it('should not trigger delete when onDelete is not provided', () => {
490
+ const selectedIds = new Set(['1', '2'])
491
+
492
+ const { result } = renderHook(() =>
493
+ useTableKeyboard({ ...defaultProps, selectedIds })
494
+ )
495
+
496
+ const mockEvent = {
497
+ key: 'Delete',
498
+ ctrlKey: false,
499
+ metaKey: false,
500
+ shiftKey: false,
501
+ preventDefault: jest.fn(),
502
+ stopPropagation: jest.fn(),
503
+ } as any
504
+
505
+ act(() => {
506
+ result.current.handleKeyDown(mockEvent)
507
+ })
508
+
509
+ // Should not throw error
510
+ expect(mockEvent.preventDefault).not.toHaveBeenCalled()
511
+ })
512
+ })
513
+
514
+ describe('Disabled State', () => {
515
+ it('should not handle keyboard events when disabled', () => {
516
+ const onRowClick = jest.fn()
517
+ const onToggleRow = jest.fn()
518
+
519
+ const { result } = renderHook(() =>
520
+ useTableKeyboard({
521
+ ...defaultProps,
522
+ onRowClick,
523
+ onToggleRow,
524
+ enabled: false,
525
+ })
526
+ )
527
+
528
+ act(() => {
529
+ result.current.setFocusedRowIndex(1)
530
+ })
531
+
532
+ const mockEvent = {
533
+ key: 'Enter',
534
+ ctrlKey: false,
535
+ metaKey: false,
536
+ shiftKey: false,
537
+ preventDefault: jest.fn(),
538
+ stopPropagation: jest.fn(),
539
+ } as any
540
+
541
+ act(() => {
542
+ result.current.handleKeyDown(mockEvent)
543
+ })
544
+
545
+ expect(onRowClick).not.toHaveBeenCalled()
546
+ expect(mockEvent.preventDefault).not.toHaveBeenCalled()
547
+ })
548
+ })
549
+
550
+ describe('Empty Data', () => {
551
+ it('should not handle keyboard events when data is empty', () => {
552
+ const onRowClick = jest.fn()
553
+
554
+ const { result } = renderHook(() =>
555
+ useTableKeyboard({
556
+ ...defaultProps,
557
+ data: [],
558
+ onRowClick,
559
+ })
560
+ )
561
+
562
+ const mockEvent = {
563
+ key: 'ArrowDown',
564
+ ctrlKey: false,
565
+ metaKey: false,
566
+ shiftKey: false,
567
+ preventDefault: jest.fn(),
568
+ stopPropagation: jest.fn(),
569
+ } as any
570
+
571
+ act(() => {
572
+ result.current.handleKeyDown(mockEvent)
573
+ })
574
+
575
+ expect(mockEvent.preventDefault).not.toHaveBeenCalled()
576
+ })
577
+ })
578
+
579
+ describe('Edge Cases', () => {
580
+ it('should handle missing table ref gracefully', () => {
581
+ const nullRef: MutableRefObject<HTMLDivElement | null> = { current: null }
582
+
583
+ const { result } = renderHook(() =>
584
+ useTableKeyboard({
585
+ ...defaultProps,
586
+ tableRef: nullRef,
587
+ })
588
+ )
589
+
590
+ const mockEvent = {
591
+ key: 'ArrowDown',
592
+ ctrlKey: false,
593
+ metaKey: false,
594
+ shiftKey: false,
595
+ preventDefault: jest.fn(),
596
+ stopPropagation: jest.fn(),
597
+ } as any
598
+
599
+ act(() => {
600
+ result.current.handleKeyDown(mockEvent)
601
+ })
602
+
603
+ // Should not throw error
604
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
605
+ })
606
+
607
+ it('should handle missing row element gracefully', () => {
608
+ // Create empty table
609
+ const emptyTableElement = document.createElement('div')
610
+ const emptyTableRef = { current: emptyTableElement }
611
+
612
+ const { result } = renderHook(() =>
613
+ useTableKeyboard({
614
+ ...defaultProps,
615
+ tableRef: emptyTableRef,
616
+ })
617
+ )
618
+
619
+ const mockEvent = {
620
+ key: 'ArrowDown',
621
+ ctrlKey: false,
622
+ metaKey: false,
623
+ shiftKey: false,
624
+ preventDefault: jest.fn(),
625
+ stopPropagation: jest.fn(),
626
+ } as any
627
+
628
+ act(() => {
629
+ result.current.handleKeyDown(mockEvent)
630
+ })
631
+
632
+ // Should not throw error
633
+ expect(mockEvent.preventDefault).toHaveBeenCalled()
634
+ })
635
+
636
+ it('should handle invalid focusedRowIndex gracefully', () => {
637
+ const onRowClick = jest.fn()
638
+
639
+ const { result } = renderHook(() =>
640
+ useTableKeyboard({
641
+ ...defaultProps,
642
+ onRowClick,
643
+ })
644
+ )
645
+
646
+ act(() => {
647
+ result.current.setFocusedRowIndex(999) // Invalid index
648
+ })
649
+
650
+ const mockEvent = {
651
+ key: 'Enter',
652
+ ctrlKey: false,
653
+ metaKey: false,
654
+ shiftKey: false,
655
+ preventDefault: jest.fn(),
656
+ stopPropagation: jest.fn(),
657
+ } as any
658
+
659
+ act(() => {
660
+ result.current.handleKeyDown(mockEvent)
661
+ })
662
+
663
+ // Should not call onRowClick for invalid index
664
+ expect(onRowClick).not.toHaveBeenCalled()
665
+ })
666
+ })
667
+
668
+ describe('Integration Scenarios', () => {
669
+ it('should handle sequential arrow key navigation', () => {
670
+ const { result } = renderHook(() => useTableKeyboard(defaultProps))
671
+
672
+ act(() => {
673
+ result.current.setFocusedRowIndex(0)
674
+ })
675
+
676
+ // Press ArrowDown multiple times
677
+ const mockEventDown = {
678
+ key: 'ArrowDown',
679
+ ctrlKey: false,
680
+ metaKey: false,
681
+ shiftKey: false,
682
+ preventDefault: jest.fn(),
683
+ stopPropagation: jest.fn(),
684
+ } as any
685
+
686
+ act(() => {
687
+ result.current.handleKeyDown(mockEventDown)
688
+ })
689
+ const firstFocused = mockTableElement.querySelector('[data-row-index="1"]')
690
+ expect((firstFocused as any).focus).toHaveBeenCalled()
691
+
692
+ act(() => {
693
+ result.current.handleKeyDown(mockEventDown)
694
+ })
695
+ const secondFocused = mockTableElement.querySelector('[data-row-index="2"]')
696
+ expect((secondFocused as any).focus).toHaveBeenCalled()
697
+ })
698
+
699
+ it('should handle selection and navigation together', () => {
700
+ const onToggleRow = jest.fn()
701
+ const { result } = renderHook(() =>
702
+ useTableKeyboard({ ...defaultProps, onToggleRow })
703
+ )
704
+
705
+ act(() => {
706
+ result.current.setFocusedRowIndex(0)
707
+ })
708
+
709
+ // Select row
710
+ const spaceEvent = {
711
+ key: ' ',
712
+ ctrlKey: false,
713
+ metaKey: false,
714
+ shiftKey: false,
715
+ preventDefault: jest.fn(),
716
+ stopPropagation: jest.fn(),
717
+ } as any
718
+
719
+ act(() => {
720
+ result.current.handleKeyDown(spaceEvent)
721
+ })
722
+ expect(onToggleRow).toHaveBeenCalledWith('1')
723
+
724
+ // Navigate down
725
+ const downEvent = {
726
+ key: 'ArrowDown',
727
+ ctrlKey: false,
728
+ metaKey: false,
729
+ shiftKey: false,
730
+ preventDefault: jest.fn(),
731
+ stopPropagation: jest.fn(),
732
+ } as any
733
+
734
+ act(() => {
735
+ result.current.handleKeyDown(downEvent)
736
+ })
737
+ const focusedElement = mockTableElement.querySelector('[data-row-index="1"]')
738
+ expect((focusedElement as any).focus).toHaveBeenCalled()
739
+
740
+ // Select another row
741
+ act(() => {
742
+ result.current.handleKeyDown(spaceEvent)
743
+ })
744
+ expect(onToggleRow).toHaveBeenCalledWith('2')
745
+ })
746
+
747
+ it('should handle navigation to end and then click', () => {
748
+ const onRowClick = jest.fn()
749
+ const { result } = renderHook(() =>
750
+ useTableKeyboard({ ...defaultProps, onRowClick })
751
+ )
752
+
753
+ // Press End
754
+ const endEvent = {
755
+ key: 'End',
756
+ ctrlKey: false,
757
+ metaKey: false,
758
+ shiftKey: false,
759
+ preventDefault: jest.fn(),
760
+ stopPropagation: jest.fn(),
761
+ } as any
762
+
763
+ act(() => {
764
+ result.current.handleKeyDown(endEvent)
765
+ })
766
+ const focusedElement = mockTableElement.querySelector('[data-row-index="2"]')
767
+ expect((focusedElement as any).focus).toHaveBeenCalled()
768
+
769
+ // Press Enter
770
+ const enterEvent = {
771
+ key: 'Enter',
772
+ ctrlKey: false,
773
+ metaKey: false,
774
+ shiftKey: false,
775
+ preventDefault: jest.fn(),
776
+ stopPropagation: jest.fn(),
777
+ } as any
778
+
779
+ act(() => {
780
+ result.current.handleKeyDown(enterEvent)
781
+ })
782
+ expect(onRowClick).toHaveBeenCalledWith(mockData[2])
783
+ })
784
+ })
785
+
786
+ describe('Accessibility', () => {
787
+ it('should set tabIndex on row elements for keyboard navigation', () => {
788
+ renderHook(() => useTableKeyboard(defaultProps))
789
+
790
+ mockRowElements.forEach((row) => {
791
+ expect(row.tabIndex).toBe(0)
792
+ })
793
+ })
794
+
795
+ it('should provide data-row-index attributes for screen readers', () => {
796
+ renderHook(() => useTableKeyboard(defaultProps))
797
+
798
+ mockRowElements.forEach((row, index) => {
799
+ expect(row.getAttribute('data-row-index')).toBe(String(index))
800
+ })
801
+ })
802
+ })
803
+ })