diff --git a/src/App.tsx b/src/App.tsx index 63df4af..6a741a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -280,6 +280,7 @@ function App() { const menuItemRefs = useRef<{ [key: string]: HTMLButtonElement | null }>({}); const kineticDragActiveRef = useRef(false); const contextMenuRef = useRef(null); + const contextMenuTriggerRef = useRef(null); const tabScrollRef = useRef(null); const tabRefs = useRef<(HTMLDivElement | null)[]>([]); const sidebarItemRefs = useRef<(HTMLButtonElement | null)[]>([]); @@ -332,6 +333,7 @@ function App() { })); } + contextMenuTriggerRef.current = document.activeElement; setContextMenu({ x, y, selectedText }); setSelectionRects(rects); } @@ -365,6 +367,19 @@ function App() { if (!contextMenu) setSelectionRects(prev => prev.length > 0 ? [] : prev); }, [contextMenu]); + // Auto-focus first menuitem when context menu opens; restore focus on close + useEffect(() => { + if (contextMenu) { + requestAnimationFrame(() => { + const firstItem = contextMenuRef.current?.querySelector('[role="menuitem"]'); + firstItem?.focus(); + }); + } else { + (contextMenuTriggerRef.current as HTMLElement)?.focus(); + contextMenuTriggerRef.current = null; + } + }, [contextMenu]); + // Clamp context menu within window bounds after render useLayoutEffect(() => { const menu = contextMenuRef.current; @@ -639,10 +654,27 @@ function App() { else if (e.shiftKey && e.key === 'S') { e.preventDefault(); setShowSidebar(s => !s); } } else if (e.key === 'F11') { e.preventDefault(); setFocusMode(f => !f); } else if (e.key === 'Escape') { setShowSearch(false); setShowSidebar(false); } + else if (e.key === 'F10' && e.shiftKey) { + e.preventDefault(); + contextMenuTriggerRef.current = document.activeElement; + const zf = uiZoom / 100; + const focused = document.activeElement as HTMLElement; + let x: number, y: number; + if (focused && focused !== document.body) { + const rect = focused.getBoundingClientRect(); + x = (rect.left + rect.width / 2) / zf; + y = (rect.top + rect.height / 2) / zf; + } else { + x = window.innerWidth / 2 / zf; + y = window.innerHeight / 2 / zf; + } + const selectedText = window.getSelection()?.toString() || ''; + setContextMenu({ x, y, selectedText }); + } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [handleOpenDialog, closeTab, activeTabId, closeWindow]); + }, [handleOpenDialog, closeTab, activeTabId, closeWindow, uiZoom]); useEffect(() => { const handleWheel = (e: WheelEvent) => { @@ -874,6 +906,35 @@ function App() { } }; + // Context menu keyboard navigation (WAI-ARIA menu pattern) + const handleContextMenuKeyDown = (e: React.KeyboardEvent) => { + const menu = contextMenuRef.current; + if (!menu) return; + const items = menu.querySelectorAll('[role="menuitem"]'); + if (!items.length) return; + + const currentIndex = Array.from(items).indexOf(document.activeElement as HTMLElement); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0; + items[next]?.focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1; + items[prev]?.focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + items[0]?.focus(); + } else if (e.key === 'End') { + e.preventDefault(); + items[items.length - 1]?.focus(); + } else if (e.key === 'Escape') { + e.preventDefault(); + setContextMenu(null); + } + }; + // Auto-focus first menuitem when a dropdown opens useEffect(() => { if (menuOpen) { @@ -1152,7 +1213,7 @@ function App() { {contextMenu && ( - + {contextMenu.selectedText && ( <>