fa54ef4cbd
- Arrow left/right scrolls the timeline by 1 day (Shift: 1 week) - Delete/Backspace deletes selected allocations - ? toggles a keyboard shortcut overlay - Floating ? button in bottom-right corner provides persistent access - (Del) hint added to the FloatingActionBar delete button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import React, { useEffect, useRef, useState } from "react";
|
|
import type { RefObject } from "react";
|
|
|
|
interface UseTimelineKeyboardOptions {
|
|
scrollContainerRef: RefObject<HTMLElement | null>;
|
|
cellWidth: number;
|
|
selectedAllocationIds: string[];
|
|
onDeleteSelected: () => void;
|
|
}
|
|
|
|
export interface UseTimelineKeyboardResult {
|
|
showShortcuts: boolean;
|
|
setShowShortcuts: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
function isTypingTarget(el: Element | null): boolean {
|
|
if (!el) return false;
|
|
const tag = el.tagName.toLowerCase();
|
|
if (tag === "input" || tag === "textarea" || tag === "select") return true;
|
|
if ((el as HTMLElement).isContentEditable) return true;
|
|
return false;
|
|
}
|
|
|
|
export function useTimelineKeyboard({
|
|
scrollContainerRef,
|
|
cellWidth,
|
|
selectedAllocationIds,
|
|
onDeleteSelected,
|
|
}: UseTimelineKeyboardOptions): UseTimelineKeyboardResult {
|
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
|
|
// Keep stable refs so the handler closure doesn't need to be re-registered on every render
|
|
const onDeleteRef = useRef(onDeleteSelected);
|
|
onDeleteRef.current = onDeleteSelected;
|
|
const selectedCountRef = useRef(selectedAllocationIds.length);
|
|
selectedCountRef.current = selectedAllocationIds.length;
|
|
const cellWidthRef = useRef(cellWidth);
|
|
cellWidthRef.current = cellWidth;
|
|
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (isTypingTarget(document.activeElement)) return;
|
|
|
|
const isMac = navigator.platform.toUpperCase().includes("MAC");
|
|
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
const dayPx = cellWidthRef.current;
|
|
const el = scrollContainerRef.current;
|
|
|
|
switch (e.key) {
|
|
case "?":
|
|
e.preventDefault();
|
|
setShowShortcuts((prev) => !prev);
|
|
break;
|
|
|
|
case "ArrowLeft":
|
|
if (el) {
|
|
e.preventDefault();
|
|
el.scrollLeft -= (e.shiftKey ? 7 : 1) * dayPx;
|
|
}
|
|
break;
|
|
|
|
case "ArrowRight":
|
|
if (el) {
|
|
e.preventDefault();
|
|
el.scrollLeft += (e.shiftKey ? 7 : 1) * dayPx;
|
|
}
|
|
break;
|
|
|
|
case "Delete":
|
|
case "Backspace":
|
|
if (modKey) break; // let browser handle Cmd+Backspace etc.
|
|
if (selectedCountRef.current > 0) {
|
|
e.preventDefault();
|
|
onDeleteRef.current();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
}, [scrollContainerRef]); // stable — uses refs for everything that changes
|
|
|
|
return { showShortcuts, setShowShortcuts };
|
|
}
|