@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.
- package/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- 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
|
+
})
|