'use client' import { useEffect, useCallback, useRef, useState, RefObject } from 'react' export interface UseTableKeyboardProps { data: TData[] getRowId: (row: TData) => string selectedIds?: Set onToggleRow?: (id: string) => void onToggleAll?: () => void onRowClick?: (row: TData) => void onDelete?: (ids: Set) => void tableRef: RefObject enabled?: boolean } export interface UseTableKeyboardReturn { focusedRowIndex: number setFocusedRowIndex: (index: number) => void handleKeyDown: (event: React.KeyboardEvent) => void } /** * Hook for handling keyboard navigation in UnifiedTable * * Features: * - Arrow Up/Down: Navigate between rows * - Enter: Expand/click row (trigger onRowClick) * - Space: Toggle row selection * - Tab: Move between interactive elements (native browser behavior) * - Escape: Close menus/dialogs (handled by menu components) * - Ctrl/Cmd+A: Select all rows * - Delete: Trigger delete action if available */ export function useTableKeyboard({ data, getRowId, selectedIds, onToggleRow, onToggleAll, onRowClick, onDelete, tableRef, enabled = true, }: UseTableKeyboardProps): UseTableKeyboardReturn { // Use state for reactive updates so consumers get re-renders const [focusedRowIndex, setFocusedRowIndexState] = useState(-1) // Keep a ref in sync for use in callbacks to avoid stale closures const focusedRowIndexRef = useRef(-1) const setFocusedRowIndex = useCallback((index: number) => { focusedRowIndexRef.current = index setFocusedRowIndexState(index) }, []) const handleKeyDown = useCallback((event: React.KeyboardEvent) => { if (!enabled || data.length === 0) return const { key, ctrlKey, metaKey, shiftKey } = event const isModifierPressed = ctrlKey || metaKey // Ctrl/Cmd + A: Select all rows if (key === 'a' && isModifierPressed && onToggleAll) { event.preventDefault() onToggleAll() return } // Delete: Trigger delete action if available if (key === 'Delete' && onDelete && selectedIds && selectedIds.size > 0) { event.preventDefault() onDelete(selectedIds) return } // Arrow navigation if (key === 'ArrowDown') { event.preventDefault() const nextIndex = Math.min(focusedRowIndexRef.current + 1, data.length - 1) setFocusedRowIndex(nextIndex) // Focus the row element if (tableRef.current) { const rowElement = tableRef.current.querySelector( `[data-row-index="${nextIndex}"]` ) as HTMLElement if (rowElement && typeof rowElement.focus === 'function') { rowElement.focus() } } return } if (key === 'ArrowUp') { event.preventDefault() const prevIndex = Math.max(focusedRowIndexRef.current - 1, 0) setFocusedRowIndex(prevIndex) // Focus the row element if (tableRef.current) { const rowElement = tableRef.current.querySelector( `[data-row-index="${prevIndex}"]` ) as HTMLElement if (rowElement) { rowElement.focus() } } return } // Enter: Trigger row click if (key === 'Enter' && focusedRowIndexRef.current >= 0 && onRowClick) { event.preventDefault() const row = data[focusedRowIndexRef.current] if (row) { onRowClick(row) } return } // Space: Toggle row selection if (key === ' ' && focusedRowIndexRef.current >= 0 && onToggleRow) { event.preventDefault() const row = data[focusedRowIndexRef.current] if (row) { const rowId = getRowId(row) onToggleRow(rowId) } return } // Home: Focus first row if (key === 'Home') { event.preventDefault() setFocusedRowIndex(0) if (tableRef.current) { const rowElement = tableRef.current.querySelector( '[data-row-index="0"]' ) as HTMLElement if (rowElement) { rowElement.focus() } } return } // End: Focus last row if (key === 'End') { event.preventDefault() const lastIndex = data.length - 1 setFocusedRowIndex(lastIndex) if (tableRef.current) { const rowElement = tableRef.current.querySelector( `[data-row-index="${lastIndex}"]` ) as HTMLElement if (rowElement) { rowElement.focus() } } return } }, [ enabled, data, getRowId, selectedIds, onToggleRow, onToggleAll, onRowClick, onDelete, tableRef, setFocusedRowIndex, ]) // Set up keyboard event listener on the table useEffect(() => { if (!enabled || !tableRef.current) return const handleKeyboardEvent = (event: KeyboardEvent) => { // Convert native event to React synthetic event shape const syntheticEvent = { key: event.key, ctrlKey: event.ctrlKey, metaKey: event.metaKey, shiftKey: event.shiftKey, preventDefault: () => event.preventDefault(), stopPropagation: () => event.stopPropagation(), } as React.KeyboardEvent handleKeyDown(syntheticEvent) } const tableElement = tableRef.current tableElement.addEventListener('keydown', handleKeyboardEvent as any) return () => { tableElement.removeEventListener('keydown', handleKeyboardEvent as any) } }, [enabled, tableRef, handleKeyDown]) return { focusedRowIndex, setFocusedRowIndex, handleKeyDown, } }