diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 43b53077..67cb6e65 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -153,6 +153,10 @@ export const Tasks = ( useEffect(() => { const handler = (e: KeyboardEvent) => { + if (!hotkeysEnabled) { + return; + } + const target = e.target as HTMLElement; if ( target instanceof HTMLInputElement || @@ -1022,83 +1026,115 @@ export const Tasks = ( } }; - useHotkeys(['f'], () => { - if (!showReports) { - document.getElementById('search')?.focus(); - } - }); - useHotkeys(['a'], () => { - if (!showReports) { - document.getElementById('add-new-task')?.click(); - } - }); - useHotkeys(['r'], () => { - if (!showReports) { - document.getElementById('sync-task')?.click(); - } - }); - useHotkeys(['p'], () => { - if (!showReports) { - document.getElementById('projects')?.click(); - } - }); - useHotkeys(['s'], () => { - if (!showReports) { - document.getElementById('status')?.click(); - } - }); - useHotkeys(['t'], () => { - if (!showReports) { - document.getElementById('tags')?.click(); - } - }); - useHotkeys(['c'], () => { - if (!showReports && !_isDialogOpen) { - const task = currentTasks[selectedIndex]; - if (!task) return; - const openBtn = document.getElementById(`task-row-${task.id}`); - openBtn?.click(); - setTimeout(() => { - const confirmBtn = document.getElementById( - `mark-task-complete-${task.id}` - ); - confirmBtn?.click(); - }, 200); - } else { - if (_isDialogOpen) { + useHotkeys( + ['f'], + () => { + if (!showReports) { + document.getElementById('search')?.focus(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['a'], + () => { + if (!showReports) { + document.getElementById('add-new-task')?.click(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['r'], + () => { + if (!showReports) { + document.getElementById('sync-task')?.click(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['p'], + () => { + if (!showReports) { + document.getElementById('projects')?.click(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['s'], + () => { + if (!showReports) { + document.getElementById('status')?.click(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['t'], + () => { + if (!showReports) { + document.getElementById('tags')?.click(); + } + }, + hotkeysEnabled + ); + useHotkeys( + ['c'], + () => { + if (!showReports && !_isDialogOpen) { const task = currentTasks[selectedIndex]; if (!task) return; - const confirmBtn = document.getElementById( - `mark-task-complete-${task.id}` - ); - confirmBtn?.click(); + const openBtn = document.getElementById(`task-row-${task.id}`); + openBtn?.click(); + setTimeout(() => { + const confirmBtn = document.getElementById( + `mark-task-complete-${task.id}` + ); + confirmBtn?.click(); + }, 200); + } else { + if (_isDialogOpen) { + const task = currentTasks[selectedIndex]; + if (!task) return; + const confirmBtn = document.getElementById( + `mark-task-complete-${task.id}` + ); + confirmBtn?.click(); + } } - } - }); + }, + hotkeysEnabled + ); - useHotkeys(['d'], () => { - if (!showReports && !_isDialogOpen) { - const task = currentTasks[selectedIndex]; - if (!task) return; - const openBtn = document.getElementById(`task-row-${task.id}`); - openBtn?.click(); - setTimeout(() => { - const confirmBtn = document.getElementById( - `mark-task-as-deleted-${task.id}` - ); - confirmBtn?.click(); - }, 200); - } else { - if (_isDialogOpen) { + useHotkeys( + ['d'], + () => { + if (!showReports && !_isDialogOpen) { const task = currentTasks[selectedIndex]; if (!task) return; - const confirmBtn = document.getElementById( - `mark-task-as-deleted-${task.id}` - ); - confirmBtn?.click(); + const openBtn = document.getElementById(`task-row-${task.id}`); + openBtn?.click(); + setTimeout(() => { + const confirmBtn = document.getElementById( + `mark-task-as-deleted-${task.id}` + ); + confirmBtn?.click(); + }, 200); + } else { + if (_isDialogOpen) { + const task = currentTasks[selectedIndex]; + if (!task) return; + const confirmBtn = document.getElementById( + `mark-task-as-deleted-${task.id}` + ); + confirmBtn?.click(); + } } - } - }); + }, + hotkeysEnabled + ); return (
setHotkeysEnabled(true)} onMouseLeave={() => setHotkeysEnabled(false)} > diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index f1dd1ed0..7a77cfb5 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -1750,10 +1750,15 @@ describe('Tasks Component', () => { }); describe('Keyboard Navigation', () => { + const hoverTasksTable = () => { + fireEvent.mouseEnter(screen.getByTestId('tasks-table-container')); + }; + describe('Arrow Key Navigation', () => { test('ArrowDown key moves selection to next task', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const taskRows = screen.getAllByTestId(/task-row-/); expect(taskRows[0]).toHaveAttribute('data-selected', 'true'); @@ -1768,6 +1773,7 @@ describe('Tasks Component', () => { test('ArrowUp moves selection back to previous task', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const taskRows = screen.getAllByTestId(/task-row-/); fireEvent.keyDown(window, { key: 'ArrowDown' }); @@ -1785,6 +1791,7 @@ describe('Tasks Component', () => { test('ArrowDown stops at last task on page', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const taskRows = screen.getAllByTestId(/task-row-/); @@ -1801,6 +1808,7 @@ describe('Tasks Component', () => { test('ArrowUp stops at first task', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const taskRows = screen.getAllByTestId(/task-row-/); const middleIndex = Math.floor(taskRows.length / 2); @@ -1819,6 +1827,7 @@ describe('Tasks Component', () => { test('pressing "a" opens the Add Task dialog', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); fireEvent.keyDown(window, { key: 'a' }); @@ -1834,6 +1843,7 @@ describe('Tasks Component', () => { async (key, _action, fn) => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); fireEvent.keyDown(window, { key }); @@ -1849,6 +1859,7 @@ describe('Tasks Component', () => { test('pressing "Enter" key opens the selected task dialog', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const taskRows = screen.getAllByTestId(/task-row-/); const selectedRow = taskRows.find( @@ -1868,6 +1879,7 @@ describe('Tasks Component', () => { test('pressing "f" focuses the search input', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); fireEvent.keyDown(window, { key: 'f' }); @@ -1878,6 +1890,7 @@ describe('Tasks Component', () => { test('pressing "r" triggers sync', async () => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); fireEvent.keyDown(window, { key: 'r' }); @@ -1894,6 +1907,7 @@ describe('Tasks Component', () => { ])('pressing "%s" opens the %s filter', async (key, filterName) => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); const filterButton = screen.getByTestId(`multi-select-${filterName}`); expect(filterButton).toHaveAttribute('aria-expanded', 'false'); @@ -1925,6 +1939,7 @@ describe('Tasks Component', () => { async (key, _action, fn) => { render(); await screen.findByText('Task 1'); + hoverTasksTable(); fireEvent.click(screen.getByText('Task 1')); await screen.findByRole('dialog'); diff --git a/frontend/src/components/utils/__tests__/use-hotkeys.test.ts b/frontend/src/components/utils/__tests__/use-hotkeys.test.ts index 25b55a09..239790dc 100644 --- a/frontend/src/components/utils/__tests__/use-hotkeys.test.ts +++ b/frontend/src/components/utils/__tests__/use-hotkeys.test.ts @@ -26,6 +26,18 @@ describe('useHotkeys', () => { expect(callback).toHaveBeenCalledTimes(1); }); + it('should not call callback when hotkeys are disabled', () => { + renderHook(() => useHotkeys(['s'], callback, false)); + + const event = new KeyboardEvent('keydown', { + key: 's', + bubbles: true, + }); + window.dispatchEvent(event); + + expect(callback).not.toHaveBeenCalled(); + }); + it('should handle multiple modifier keys', () => { renderHook(() => useHotkeys(['ctrl', 'shift', 'a'], callback)); diff --git a/frontend/src/components/utils/use-hotkeys.ts b/frontend/src/components/utils/use-hotkeys.ts index e10612b7..e26bde56 100644 --- a/frontend/src/components/utils/use-hotkeys.ts +++ b/frontend/src/components/utils/use-hotkeys.ts @@ -1,8 +1,16 @@ import { useEffect } from 'react'; -export function useHotkeys(keys: string[], callback: () => void) { +export function useHotkeys( + keys: string[], + callback: () => void, + enabled = true +) { useEffect(() => { const handler = (e: KeyboardEvent) => { + if (!enabled) { + return; + } + const target = e.target as HTMLElement; if ( target instanceof HTMLInputElement || @@ -29,5 +37,5 @@ export function useHotkeys(keys: string[], callback: () => void) { window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [keys, callback]); + }, [keys, callback, enabled]); }