Skip to content

Commit 7d0605a

Browse files
committed
fix(Tasks): implement hotkeysEnabled state to control keyboard shortcuts
- Add `enabled` parameter to useHotkeys hook with default true value - Pass hotkeysEnabled state to all useHotkeys calls in Tasks component - Add hotkeysEnabled check in manual keyboard handler (ArrowUp/Down/Enter) - Add data-testid to tasks table container for testing - Add tests for useHotkeys enabled parameter - Add tests for hotkeys enable/disable on mouse hover
1 parent 39e595c commit 7d0605a

4 files changed

Lines changed: 192 additions & 72 deletions

File tree

frontend/src/components/HomeComponents/Tasks/Tasks.tsx

Lines changed: 105 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ export const Tasks = (
152152

153153
useEffect(() => {
154154
const handler = (e: KeyboardEvent) => {
155+
if (!hotkeysEnabled) return;
156+
155157
const target = e.target as HTMLElement;
156158
if (
157159
target instanceof HTMLInputElement ||
@@ -991,83 +993,115 @@ export const Tasks = (
991993
}
992994
};
993995

994-
useHotkeys(['f'], () => {
995-
if (!showReports) {
996-
document.getElementById('search')?.focus();
997-
}
998-
});
999-
useHotkeys(['a'], () => {
1000-
if (!showReports) {
1001-
document.getElementById('add-new-task')?.click();
1002-
}
1003-
});
1004-
useHotkeys(['r'], () => {
1005-
if (!showReports) {
1006-
document.getElementById('sync-task')?.click();
1007-
}
1008-
});
1009-
useHotkeys(['p'], () => {
1010-
if (!showReports) {
1011-
document.getElementById('projects')?.click();
1012-
}
1013-
});
1014-
useHotkeys(['s'], () => {
1015-
if (!showReports) {
1016-
document.getElementById('status')?.click();
1017-
}
1018-
});
1019-
useHotkeys(['t'], () => {
1020-
if (!showReports) {
1021-
document.getElementById('tags')?.click();
1022-
}
1023-
});
1024-
useHotkeys(['c'], () => {
1025-
if (!showReports && !_isDialogOpen) {
1026-
const task = currentTasks[selectedIndex];
1027-
if (!task) return;
1028-
const openBtn = document.getElementById(`task-row-${task.id}`);
1029-
openBtn?.click();
1030-
setTimeout(() => {
1031-
const confirmBtn = document.getElementById(
1032-
`mark-task-complete-${task.id}`
1033-
);
1034-
confirmBtn?.click();
1035-
}, 200);
1036-
} else {
1037-
if (_isDialogOpen) {
996+
useHotkeys(
997+
['f'],
998+
() => {
999+
if (!showReports) {
1000+
document.getElementById('search')?.focus();
1001+
}
1002+
},
1003+
hotkeysEnabled
1004+
);
1005+
useHotkeys(
1006+
['a'],
1007+
() => {
1008+
if (!showReports) {
1009+
document.getElementById('add-new-task')?.click();
1010+
}
1011+
},
1012+
hotkeysEnabled
1013+
);
1014+
useHotkeys(
1015+
['r'],
1016+
() => {
1017+
if (!showReports) {
1018+
document.getElementById('sync-task')?.click();
1019+
}
1020+
},
1021+
hotkeysEnabled
1022+
);
1023+
useHotkeys(
1024+
['p'],
1025+
() => {
1026+
if (!showReports) {
1027+
document.getElementById('projects')?.click();
1028+
}
1029+
},
1030+
hotkeysEnabled
1031+
);
1032+
useHotkeys(
1033+
['s'],
1034+
() => {
1035+
if (!showReports) {
1036+
document.getElementById('status')?.click();
1037+
}
1038+
},
1039+
hotkeysEnabled
1040+
);
1041+
useHotkeys(
1042+
['t'],
1043+
() => {
1044+
if (!showReports) {
1045+
document.getElementById('tags')?.click();
1046+
}
1047+
},
1048+
hotkeysEnabled
1049+
);
1050+
useHotkeys(
1051+
['c'],
1052+
() => {
1053+
if (!showReports && !_isDialogOpen) {
10381054
const task = currentTasks[selectedIndex];
10391055
if (!task) return;
1040-
const confirmBtn = document.getElementById(
1041-
`mark-task-complete-${task.id}`
1042-
);
1043-
confirmBtn?.click();
1056+
const openBtn = document.getElementById(`task-row-${task.id}`);
1057+
openBtn?.click();
1058+
setTimeout(() => {
1059+
const confirmBtn = document.getElementById(
1060+
`mark-task-complete-${task.id}`
1061+
);
1062+
confirmBtn?.click();
1063+
}, 200);
1064+
} else {
1065+
if (_isDialogOpen) {
1066+
const task = currentTasks[selectedIndex];
1067+
if (!task) return;
1068+
const confirmBtn = document.getElementById(
1069+
`mark-task-complete-${task.id}`
1070+
);
1071+
confirmBtn?.click();
1072+
}
10441073
}
1045-
}
1046-
});
1074+
},
1075+
hotkeysEnabled
1076+
);
10471077

1048-
useHotkeys(['d'], () => {
1049-
if (!showReports && !_isDialogOpen) {
1050-
const task = currentTasks[selectedIndex];
1051-
if (!task) return;
1052-
const openBtn = document.getElementById(`task-row-${task.id}`);
1053-
openBtn?.click();
1054-
setTimeout(() => {
1055-
const confirmBtn = document.getElementById(
1056-
`mark-task-as-deleted-${task.id}`
1057-
);
1058-
confirmBtn?.click();
1059-
}, 200);
1060-
} else {
1061-
if (_isDialogOpen) {
1078+
useHotkeys(
1079+
['d'],
1080+
() => {
1081+
if (!showReports && !_isDialogOpen) {
10621082
const task = currentTasks[selectedIndex];
10631083
if (!task) return;
1064-
const confirmBtn = document.getElementById(
1065-
`mark-task-as-deleted-${task.id}`
1066-
);
1067-
confirmBtn?.click();
1084+
const openBtn = document.getElementById(`task-row-${task.id}`);
1085+
openBtn?.click();
1086+
setTimeout(() => {
1087+
const confirmBtn = document.getElementById(
1088+
`mark-task-as-deleted-${task.id}`
1089+
);
1090+
confirmBtn?.click();
1091+
}, 200);
1092+
} else {
1093+
if (_isDialogOpen) {
1094+
const task = currentTasks[selectedIndex];
1095+
if (!task) return;
1096+
const confirmBtn = document.getElementById(
1097+
`mark-task-as-deleted-${task.id}`
1098+
);
1099+
confirmBtn?.click();
1100+
}
10681101
}
1069-
}
1070-
});
1102+
},
1103+
hotkeysEnabled
1104+
);
10711105

10721106
return (
10731107
<section
@@ -1128,6 +1162,7 @@ export const Tasks = (
11281162
) : (
11291163
<div
11301164
ref={tableRef}
1165+
data-testid="tasks-table-container"
11311166
onMouseEnter={() => setHotkeysEnabled(true)}
11321167
onMouseLeave={() => setHotkeysEnabled(false)}
11331168
>

frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,4 +1737,44 @@ describe('Tasks Component', () => {
17371737
expect(task1Row).toBeInTheDocument();
17381738
});
17391739
});
1740+
1741+
describe('Hotkeys Enable/Disable on Hover', () => {
1742+
test('hotkeys are disabled by default (mouse not over task table)', async () => {
1743+
render(<Tasks {...mockProps} />);
1744+
await screen.findByText('Task 1');
1745+
1746+
fireEvent.keyDown(window, { key: 'f' });
1747+
1748+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1749+
expect(document.activeElement).not.toBe(searchInput);
1750+
});
1751+
1752+
test('hotkeys are enabled when mouse enters task table', async () => {
1753+
render(<Tasks {...mockProps} />);
1754+
await screen.findByText('Task 1');
1755+
1756+
const taskContainer = screen.getByTestId('tasks-table-container');
1757+
fireEvent.mouseEnter(taskContainer);
1758+
1759+
fireEvent.keyDown(window, { key: 'f' });
1760+
1761+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1762+
expect(document.activeElement).toBe(searchInput);
1763+
});
1764+
1765+
test('hotkeys are disabled when mouse leaves task table', async () => {
1766+
render(<Tasks {...mockProps} />);
1767+
await screen.findByText('Task 1');
1768+
1769+
const taskContainer = screen.getByTestId('tasks-table-container');
1770+
1771+
fireEvent.mouseEnter(taskContainer);
1772+
fireEvent.mouseLeave(taskContainer);
1773+
1774+
fireEvent.keyDown(window, { key: 'f' });
1775+
1776+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1777+
expect(document.activeElement).not.toBe(searchInput);
1778+
});
1779+
});
17401780
});

frontend/src/components/utils/__tests__/use-hotkeys.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,43 @@ describe('useHotkeys', () => {
216216

217217
removeEventListenerSpy.mockRestore();
218218
});
219+
220+
it('should call callback when enabled is true', () => {
221+
renderHook(() => useHotkeys(['s'], callback, true));
222+
223+
const event = new KeyboardEvent('keydown', {
224+
key: 's',
225+
bubbles: true,
226+
});
227+
228+
window.dispatchEvent(event);
229+
230+
expect(callback).toHaveBeenCalledTimes(1);
231+
});
232+
233+
it('should not call callback when enabled is false', () => {
234+
renderHook(() => useHotkeys(['s'], callback, false));
235+
236+
const event = new KeyboardEvent('keydown', {
237+
key: 's',
238+
bubbles: true,
239+
});
240+
241+
window.dispatchEvent(event);
242+
243+
expect(callback).not.toHaveBeenCalled();
244+
});
245+
246+
it('should default enabled to true when not provided', () => {
247+
renderHook(() => useHotkeys(['s'], callback));
248+
249+
const event = new KeyboardEvent('keydown', {
250+
key: 's',
251+
bubbles: true,
252+
});
253+
254+
window.dispatchEvent(event);
255+
256+
expect(callback).toHaveBeenCalledTimes(1);
257+
});
219258
});

frontend/src/components/utils/use-hotkeys.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useEffect } from 'react';
22

3-
export function useHotkeys(keys: string[], callback: () => void) {
3+
export function useHotkeys(
4+
keys: string[],
5+
callback: () => void,
6+
enabled: boolean = true
7+
) {
48
useEffect(() => {
9+
if (!enabled) return;
10+
511
const handler = (e: KeyboardEvent) => {
612
const target = e.target as HTMLElement;
713
if (
@@ -29,5 +35,5 @@ export function useHotkeys(keys: string[], callback: () => void) {
2935

3036
window.addEventListener('keydown', handler);
3137
return () => window.removeEventListener('keydown', handler);
32-
}, [keys, callback]);
38+
}, [keys, callback, enabled]);
3339
}

0 commit comments

Comments
 (0)