diff --git a/src/App.tsx b/src/App.tsx index bc713b5..e15f33e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -271,6 +271,7 @@ function App() { const [resizeStartState, setResizeStartState] = useState<{ startX: number; startWidth: number } | null>(null); 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 contentRef = useRef(null); const sidebarRef = useRef(null); @@ -279,6 +280,7 @@ function App() { const kineticDragActiveRef = useRef(false); const contextMenuRef = useRef(null); const tabScrollRef = useRef(null); + const tabRefs = useRef<(HTMLDivElement | null)[]>([]); const [tabScrollState, setTabScrollState] = useState({ canLeft: false, canRight: false }); const tabsRef = useRef(tabs); tabsRef.current = tabs; @@ -806,6 +808,40 @@ function App() { } }; + // Tab bar keyboard navigation (WAI-ARIA tabs pattern with roving tabindex) + const handleTabListKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault(); + const next = (tabFocusIndex + 1) % tabs.length; + setTabFocusIndex(next); + tabRefs.current[next]?.focus(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prev = (tabFocusIndex - 1 + tabs.length) % tabs.length; + setTabFocusIndex(prev); + tabRefs.current[prev]?.focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + setTabFocusIndex(0); + tabRefs.current[0]?.focus(); + } else if (e.key === 'End') { + e.preventDefault(); + const last = tabs.length - 1; + setTabFocusIndex(last); + tabRefs.current[last]?.focus(); + } else if (e.key === 'Delete') { + e.preventDefault(); + if (tabs[tabFocusIndex]) closeTab(tabs[tabFocusIndex].id); + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const tab = tabs[tabFocusIndex]; + if (tab) { + setActiveTabId(tab.id); + parseHeadings(tab.content); + } + } + }; + // Auto-focus first menuitem when a dropdown opens useEffect(() => { if (menuOpen) { @@ -817,6 +853,20 @@ function App() { } }, [menuOpen]); + // Clamp tabFocusIndex when tabs are closed + useEffect(() => { + if (tabFocusIndex >= tabs.length && tabs.length > 0) { + setTabFocusIndex(tabs.length - 1); + } + }, [tabs.length, tabFocusIndex]); + + // Scroll the focused tab into view when tabFocusIndex changes + useEffect(() => { + if (tabs.length > 1) { + scrollToTabIndex(tabFocusIndex); + } + }, [tabFocusIndex, tabs.length, scrollToTabIndex]); + const osScrollbarOptions = { scrollbars: { autoHide: 'scroll' as const, autoHideDelay: 800, theme: 'os-theme-dark' as const } }; @@ -901,10 +951,10 @@ function App() { {tabs.length > 1 && ( -
+
- {tabs.map(tab => ( - { setActiveTabId(tab.id); parseHeadings(tab.content); }}> + {tabs.map((tab, index) => ( + { tabRefs.current[index] = el; }} className={`tab ${tab.id === activeTabId ? 'active' : ''}`} initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} transition={smoothTransition} onClick={() => { setTabFocusIndex(index); setActiveTabId(tab.id); parseHeadings(tab.content); }}> {tab.title}