diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 7291385e..8dc30f42 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -115,7 +115,9 @@ export const Tasks = ( new Set() ); const tableRef = useRef(null); - const [hotkeysEnabled, setHotkeysEnabled] = useState(false); + const [isMouseOver, setIsMouseOver] = useState(false); + const [activeContext, setActiveContext] = useState(null); + const hotkeysEnabled = activeContext === 'TASKS' || isMouseOver; const [selectedIndex, setSelectedIndex] = useState(0); const { state: editState, @@ -150,8 +152,27 @@ export const Tasks = ( const paginate = (pageNumber: number) => setCurrentPage(pageNumber); const totalPages = Math.ceil(tempTasks.length / tasksPerPage) || 1; + useEffect(() => { + const handleGlobalPointerDown = (e: PointerEvent) => { + if (tableRef.current && !tableRef.current.contains(e.target as Node)) { + setActiveContext(null); + } + }; + + document.addEventListener('pointerdown', handleGlobalPointerDown, true); + return () => { + document.removeEventListener( + 'pointerdown', + handleGlobalPointerDown, + true + ); + }; + }, []); + useEffect(() => { const handler = (e: KeyboardEvent) => { + if (!hotkeysEnabled) return; + const target = e.target as HTMLElement; if ( target instanceof HTMLInputElement || @@ -991,83 +1012,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)} + data-testid="tasks-table-container" + onPointerDown={() => setActiveContext('TASKS')} + onMouseEnter={() => setIsMouseOver(true)} + onMouseLeave={() => setIsMouseOver(false)} > {tasks.length != 0 ? ( <> diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index d650a64b..9f1c68c6 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -1737,4 +1737,88 @@ describe('Tasks Component', () => { expect(task1Row).toBeInTheDocument(); }); }); + + describe('Hotkeys Enable/Disable on Hover and Active Context', () => { + test('hotkeys are disabled by default (mouse not over task table)', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + expect(document.activeElement).not.toBe(searchInput); + }); + + test('hotkeys are enabled when mouse enters task table', async () => { + render(); + await screen.findByText('Task 1'); + + const taskContainer = screen.getByTestId('tasks-table-container'); + fireEvent.mouseEnter(taskContainer); + + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + expect(document.activeElement).toBe(searchInput); + }); + + test('hotkeys are disabled when mouse leaves task table', async () => { + render(); + await screen.findByText('Task 1'); + + const taskContainer = screen.getByTestId('tasks-table-container'); + + fireEvent.mouseEnter(taskContainer); + fireEvent.mouseLeave(taskContainer); + + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + expect(document.activeElement).not.toBe(searchInput); + }); + + test('hotkeys are enabled when user clicks/taps on task table', async () => { + render(); + await screen.findByText('Task 1'); + + const taskContainer = screen.getByTestId('tasks-table-container'); + + fireEvent.pointerDown(taskContainer); + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + + expect(document.activeElement).toBe(searchInput); + }); + + test('hotkeys remain enabled after mouse leaves if user clicked on task table', async () => { + render(); + await screen.findByText('Task 1'); + + const taskContainer = screen.getByTestId('tasks-table-container'); + + fireEvent.pointerDown(taskContainer); + fireEvent.mouseLeave(taskContainer); + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + + expect(document.activeElement).toBe(searchInput); + }); + + test('hotkeys are disabled when user clicks outside task table', async () => { + render(); + await screen.findByText('Task 1'); + + const taskContainer = screen.getByTestId('tasks-table-container'); + + fireEvent.pointerDown(taskContainer); + fireEvent.pointerDown(document.body); + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + + expect(document.activeElement).not.toBe(searchInput); + }); + }); }); diff --git a/frontend/src/components/utils/__tests__/use-hotkeys.test.ts b/frontend/src/components/utils/__tests__/use-hotkeys.test.ts index 25b55a09..5c5fcea4 100644 --- a/frontend/src/components/utils/__tests__/use-hotkeys.test.ts +++ b/frontend/src/components/utils/__tests__/use-hotkeys.test.ts @@ -216,4 +216,43 @@ describe('useHotkeys', () => { removeEventListenerSpy.mockRestore(); }); + + it('should call callback when enabled is true', () => { + renderHook(() => useHotkeys(['s'], callback, true)); + + const event = new KeyboardEvent('keydown', { + key: 's', + bubbles: true, + }); + + window.dispatchEvent(event); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should not call callback when enabled is false', () => { + renderHook(() => useHotkeys(['s'], callback, false)); + + const event = new KeyboardEvent('keydown', { + key: 's', + bubbles: true, + }); + + window.dispatchEvent(event); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should default enabled to true when not provided', () => { + renderHook(() => useHotkeys(['s'], callback)); + + const event = new KeyboardEvent('keydown', { + key: 's', + bubbles: true, + }); + + window.dispatchEvent(event); + + expect(callback).toHaveBeenCalledTimes(1); + }); }); diff --git a/frontend/src/components/utils/use-hotkeys.ts b/frontend/src/components/utils/use-hotkeys.ts index e10612b7..6aedaf3c 100644 --- a/frontend/src/components/utils/use-hotkeys.ts +++ b/frontend/src/components/utils/use-hotkeys.ts @@ -1,7 +1,13 @@ import { useEffect } from 'react'; -export function useHotkeys(keys: string[], callback: () => void) { +export function useHotkeys( + keys: string[], + callback: () => void, + enabled: boolean = true +) { useEffect(() => { + if (!enabled) return; + const handler = (e: KeyboardEvent) => { const target = e.target as HTMLElement; if ( @@ -29,5 +35,5 @@ export function useHotkeys(keys: string[], callback: () => void) { window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [keys, callback]); + }, [keys, callback, enabled]); }