diff --git a/src/App.tsx b/src/App.tsx index e15f33e..63df4af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -272,6 +272,7 @@ function App() { const [contextMenu, setContextMenu] = useState<{ x: number; y: number; selectedText: string } | null>(null); const [selectionRects, setSelectionRects] = useState<{ left: number; top: number; width: number; height: number }[]>([]); const [tabFocusIndex, setTabFocusIndex] = useState(0); + const [sidebarFocusIndex, setSidebarFocusIndex] = useState(0); const contentRef = useRef(null); const sidebarRef = useRef(null); @@ -281,6 +282,7 @@ function App() { const contextMenuRef = useRef(null); const tabScrollRef = useRef(null); const tabRefs = useRef<(HTMLDivElement | null)[]>([]); + const sidebarItemRefs = useRef<(HTMLButtonElement | null)[]>([]); const [tabScrollState, setTabScrollState] = useState({ canLeft: false, canRight: false }); const tabsRef = useRef(tabs); tabsRef.current = tabs; @@ -842,6 +844,36 @@ function App() { } }; + // Sidebar keyboard navigation (roving tabindex) + const handleSidebarKeyDown = (e: React.KeyboardEvent) => { + if (headings.length === 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + const next = Math.min(sidebarFocusIndex + 1, headings.length - 1); + setSidebarFocusIndex(next); + sidebarItemRefs.current[next]?.focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const prev = Math.max(sidebarFocusIndex - 1, 0); + setSidebarFocusIndex(prev); + sidebarItemRefs.current[prev]?.focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + setSidebarFocusIndex(0); + sidebarItemRefs.current[0]?.focus(); + } else if (e.key === 'End') { + e.preventDefault(); + const last = headings.length - 1; + setSidebarFocusIndex(last); + sidebarItemRefs.current[last]?.focus(); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const heading = headings[sidebarFocusIndex]; + if (heading) scrollToHeading(heading.text); + } + }; + // Auto-focus first menuitem when a dropdown opens useEffect(() => { if (menuOpen) { @@ -860,6 +892,11 @@ function App() { } }, [tabs.length, tabFocusIndex]); + // Reset sidebar focus index when headings change + useEffect(() => { + setSidebarFocusIndex(0); + }, [headings]); + // Scroll the focused tab into view when tabFocusIndex changes useEffect(() => { if (tabs.length > 1) { @@ -971,15 +1008,41 @@ function App() { {showSidebar && ( -
+

Contents

{headings.length > 0 - ? headings.map(h => ) + ? headings.map((h, index) => ( + + )) :
No headings
}
-
+
{ + if (e.key === 'ArrowRight') { + e.preventDefault(); + setSidebarWidth(w => Math.min(400, w + (e.shiftKey ? 50 : 10))); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + setSidebarWidth(w => Math.max(150, w - (e.shiftKey ? 50 : 10))); + } + }} + >
)}