From 15ae1fe147fd914bc598617aa924bcbadec349ea Mon Sep 17 00:00:00 2001 From: Alec Koifman Date: Thu, 18 Dec 2025 16:54:19 -0500 Subject: [PATCH] feat: enhance terminal context menu with keyboard navigation - Improved context menu functionality by adding keyboard navigation support for actions (copy, paste, select all, clear). - Utilized refs to manage focus on menu items and updated platform detection for Mac users. - Ensured context menu closes on outside clicks and handles keyboard events effectively. --- .../views/terminal-view/terminal-panel.tsx | 94 ++++++++++++++++--- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/apps/app/src/components/views/terminal-view/terminal-panel.tsx b/apps/app/src/components/views/terminal-view/terminal-panel.tsx index affaa65c..333153d4 100644 --- a/apps/app/src/components/views/terminal-view/terminal-panel.tsx +++ b/apps/app/src/components/views/terminal-view/terminal-panel.tsx @@ -72,10 +72,19 @@ export function TerminalPanel({ const [shellName, setShellName] = useState("shell"); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [isMac, setIsMac] = useState(false); + const isMacRef = useRef(false); + const contextMenuRef = useRef(null); + const [focusedMenuIndex, setFocusedMenuIndex] = useState(0); // Detect platform on mount useEffect(() => { - setIsMac(navigator.platform.toUpperCase().indexOf("MAC") >= 0); + // Use modern userAgentData API with fallback to deprecated navigator.platform + const detected = (navigator as Navigator & { userAgentData?: { platform: string } }) + .userAgentData?.platform?.toLowerCase().includes("mac") + ?? navigator.platform?.toLowerCase().includes("mac") + ?? false; + setIsMac(detected); + isMacRef.current = detected; }, []); // Get effective theme from store @@ -345,9 +354,8 @@ export function TerminalPanel({ return false; } - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - const modKey = isMac ? event.metaKey : event.ctrlKey; - const otherModKey = isMac ? event.ctrlKey : event.metaKey; + const modKey = isMacRef.current ? event.metaKey : event.ctrlKey; + const otherModKey = isMacRef.current ? event.ctrlKey : event.metaKey; // Ctrl+Shift+C / Cmd+Shift+C - Always copy (Linux terminal convention) if (modKey && !otherModKey && event.shiftKey && !event.altKey && code === 'KeyC') { @@ -668,14 +676,46 @@ export function TerminalPanel({ return () => container.removeEventListener("wheel", handleWheel); }, [zoomIn, zoomOut]); - // Close context menu on click outside or scroll + // Context menu actions for keyboard navigation + const menuActions = ["copy", "paste", "selectAll", "clear"] as const; + + // Close context menu on click outside or scroll, handle keyboard navigation useEffect(() => { if (!contextMenu) return; + // Reset focus index and focus menu when opened + setFocusedMenuIndex(0); + requestAnimationFrame(() => { + const firstButton = contextMenuRef.current?.querySelector('[role="menuitem"]'); + firstButton?.focus(); + }); + const handleClick = () => closeContextMenu(); const handleScroll = () => closeContextMenu(); const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") closeContextMenu(); + switch (e.key) { + case "Escape": + e.preventDefault(); + closeContextMenu(); + break; + case "ArrowDown": + e.preventDefault(); + setFocusedMenuIndex((prev) => (prev + 1) % menuActions.length); + break; + case "ArrowUp": + e.preventDefault(); + setFocusedMenuIndex((prev) => (prev - 1 + menuActions.length) % menuActions.length); + break; + case "Enter": + case " ": + e.preventDefault(); + handleContextMenuAction(menuActions[focusedMenuIndex]); + break; + case "Tab": + e.preventDefault(); + closeContextMenu(); + break; + } }; document.addEventListener("click", handleClick); @@ -687,7 +727,14 @@ export function TerminalPanel({ document.removeEventListener("scroll", handleScroll, true); document.removeEventListener("keydown", handleKeyDown); }; - }, [contextMenu, closeContextMenu]); + }, [contextMenu, closeContextMenu, focusedMenuIndex, handleContextMenuAction]); + + // Focus the correct menu item when navigation changes + useEffect(() => { + if (!contextMenu || !contextMenuRef.current) return; + const buttons = contextMenuRef.current.querySelectorAll('[role="menuitem"]'); + buttons[focusedMenuIndex]?.focus(); + }, [focusedMenuIndex, contextMenu]); // Handle right-click context menu const handleContextMenu = useCallback((e: React.MouseEvent) => { @@ -849,12 +896,20 @@ export function TerminalPanel({ {/* Context menu */} {contextMenu && (
e.stopPropagation()} > -
+