Skip to content

Commit 3113ea5

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 - Add active context pattern with `onPointerDown` for tablet/touch support - Add global pointerdown listener to disable hotkeys on outside click - Add tests for active context scenarios
1 parent acf2062 commit 3113ea5

5 files changed

Lines changed: 367 additions & 165 deletions

File tree

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

Lines changed: 128 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ export const Tasks = (
116116
);
117117
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);
118118
const tableRef = useRef<HTMLDivElement>(null);
119-
const [hotkeysEnabled, setHotkeysEnabled] = useState(false);
119+
const [isMouseOver, setIsMouseOver] = useState(false);
120+
const [activeContext, setActiveContext] = useState<string | null>(null);
121+
const hotkeysEnabled = activeContext === 'TASKS' || isMouseOver;
120122
const [selectedIndex, setSelectedIndex] = useState(0);
121123
const {
122124
state: editState,
@@ -151,8 +153,27 @@ export const Tasks = (
151153
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
152154
const totalPages = Math.ceil(tempTasks.length / tasksPerPage) || 1;
153155

156+
useEffect(() => {
157+
const handleGlobalPointerDown = (e: PointerEvent) => {
158+
if (tableRef.current && !tableRef.current.contains(e.target as Node)) {
159+
setActiveContext(null);
160+
}
161+
};
162+
163+
document.addEventListener('pointerdown', handleGlobalPointerDown, true);
164+
return () => {
165+
document.removeEventListener(
166+
'pointerdown',
167+
handleGlobalPointerDown,
168+
true
169+
);
170+
};
171+
}, []);
172+
154173
useEffect(() => {
155174
const handler = (e: KeyboardEvent) => {
175+
if (!hotkeysEnabled) return;
176+
156177
const target = e.target as HTMLElement;
157178
if (
158179
target instanceof HTMLInputElement ||
@@ -1022,83 +1043,115 @@ export const Tasks = (
10221043
}
10231044
};
10241045

1025-
useHotkeys(['f'], () => {
1026-
if (!showReports) {
1027-
document.getElementById('search')?.focus();
1028-
}
1029-
});
1030-
useHotkeys(['a'], () => {
1031-
if (!showReports) {
1032-
document.getElementById('add-new-task')?.click();
1033-
}
1034-
});
1035-
useHotkeys(['r'], () => {
1036-
if (!showReports) {
1037-
document.getElementById('sync-task')?.click();
1038-
}
1039-
});
1040-
useHotkeys(['p'], () => {
1041-
if (!showReports) {
1042-
document.getElementById('projects')?.click();
1043-
}
1044-
});
1045-
useHotkeys(['s'], () => {
1046-
if (!showReports) {
1047-
document.getElementById('status')?.click();
1048-
}
1049-
});
1050-
useHotkeys(['t'], () => {
1051-
if (!showReports) {
1052-
document.getElementById('tags')?.click();
1053-
}
1054-
});
1055-
useHotkeys(['c'], () => {
1056-
if (!showReports && !_isDialogOpen) {
1057-
const task = currentTasks[selectedIndex];
1058-
if (!task) return;
1059-
const openBtn = document.getElementById(`task-row-${task.id}`);
1060-
openBtn?.click();
1061-
setTimeout(() => {
1062-
const confirmBtn = document.getElementById(
1063-
`mark-task-complete-${task.id}`
1064-
);
1065-
confirmBtn?.click();
1066-
}, 200);
1067-
} else {
1068-
if (_isDialogOpen) {
1046+
useHotkeys(
1047+
['f'],
1048+
() => {
1049+
if (!showReports) {
1050+
document.getElementById('search')?.focus();
1051+
}
1052+
},
1053+
hotkeysEnabled
1054+
);
1055+
useHotkeys(
1056+
['a'],
1057+
() => {
1058+
if (!showReports) {
1059+
document.getElementById('add-new-task')?.click();
1060+
}
1061+
},
1062+
hotkeysEnabled
1063+
);
1064+
useHotkeys(
1065+
['r'],
1066+
() => {
1067+
if (!showReports) {
1068+
document.getElementById('sync-task')?.click();
1069+
}
1070+
},
1071+
hotkeysEnabled
1072+
);
1073+
useHotkeys(
1074+
['p'],
1075+
() => {
1076+
if (!showReports) {
1077+
document.getElementById('projects')?.click();
1078+
}
1079+
},
1080+
hotkeysEnabled
1081+
);
1082+
useHotkeys(
1083+
['s'],
1084+
() => {
1085+
if (!showReports) {
1086+
document.getElementById('status')?.click();
1087+
}
1088+
},
1089+
hotkeysEnabled
1090+
);
1091+
useHotkeys(
1092+
['t'],
1093+
() => {
1094+
if (!showReports) {
1095+
document.getElementById('tags')?.click();
1096+
}
1097+
},
1098+
hotkeysEnabled
1099+
);
1100+
useHotkeys(
1101+
['c'],
1102+
() => {
1103+
if (!showReports && !_isDialogOpen) {
10691104
const task = currentTasks[selectedIndex];
10701105
if (!task) return;
1071-
const confirmBtn = document.getElementById(
1072-
`mark-task-complete-${task.id}`
1073-
);
1074-
confirmBtn?.click();
1106+
const openBtn = document.getElementById(`task-row-${task.id}`);
1107+
openBtn?.click();
1108+
setTimeout(() => {
1109+
const confirmBtn = document.getElementById(
1110+
`mark-task-complete-${task.id}`
1111+
);
1112+
confirmBtn?.click();
1113+
}, 200);
1114+
} else {
1115+
if (_isDialogOpen) {
1116+
const task = currentTasks[selectedIndex];
1117+
if (!task) return;
1118+
const confirmBtn = document.getElementById(
1119+
`mark-task-complete-${task.id}`
1120+
);
1121+
confirmBtn?.click();
1122+
}
10751123
}
1076-
}
1077-
});
1124+
},
1125+
hotkeysEnabled
1126+
);
10781127

1079-
useHotkeys(['d'], () => {
1080-
if (!showReports && !_isDialogOpen) {
1081-
const task = currentTasks[selectedIndex];
1082-
if (!task) return;
1083-
const openBtn = document.getElementById(`task-row-${task.id}`);
1084-
openBtn?.click();
1085-
setTimeout(() => {
1086-
const confirmBtn = document.getElementById(
1087-
`mark-task-as-deleted-${task.id}`
1088-
);
1089-
confirmBtn?.click();
1090-
}, 200);
1091-
} else {
1092-
if (_isDialogOpen) {
1128+
useHotkeys(
1129+
['d'],
1130+
() => {
1131+
if (!showReports && !_isDialogOpen) {
10931132
const task = currentTasks[selectedIndex];
10941133
if (!task) return;
1095-
const confirmBtn = document.getElementById(
1096-
`mark-task-as-deleted-${task.id}`
1097-
);
1098-
confirmBtn?.click();
1134+
const openBtn = document.getElementById(`task-row-${task.id}`);
1135+
openBtn?.click();
1136+
setTimeout(() => {
1137+
const confirmBtn = document.getElementById(
1138+
`mark-task-as-deleted-${task.id}`
1139+
);
1140+
confirmBtn?.click();
1141+
}, 200);
1142+
} else {
1143+
if (_isDialogOpen) {
1144+
const task = currentTasks[selectedIndex];
1145+
if (!task) return;
1146+
const confirmBtn = document.getElementById(
1147+
`mark-task-as-deleted-${task.id}`
1148+
);
1149+
confirmBtn?.click();
1150+
}
10991151
}
1100-
}
1101-
});
1152+
},
1153+
hotkeysEnabled
1154+
);
11021155

11031156
return (
11041157
<section
@@ -1159,8 +1212,10 @@ export const Tasks = (
11591212
) : (
11601213
<div
11611214
ref={tableRef}
1162-
onMouseEnter={() => setHotkeysEnabled(true)}
1163-
onMouseLeave={() => setHotkeysEnabled(false)}
1215+
data-testid="tasks-table-container"
1216+
onPointerDown={() => setActiveContext('TASKS')}
1217+
onMouseEnter={() => setIsMouseOver(true)}
1218+
onMouseLeave={() => setIsMouseOver(false)}
11641219
>
11651220
{tasks.length != 0 ? (
11661221
<>

0 commit comments

Comments
 (0)