From f55a24cb06678305e9f1bfedc99e5bb6ee0525ef Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 09:56:13 +0000 Subject: [PATCH] feat: add keyboard navigation for session list in sidebar Arrow Up/Down to navigate sessions, Enter to select, Escape to close. Sessions use role='option' with aria-selected for screen reader support. Mouse hover syncs with keyboard focus index for smooth interaction. --- src/components/Sidebar.tsx | 43 +++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1ebf68c..a1a418f 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -15,7 +15,9 @@ interface Props { export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Props) { const t = useT(); const [filter, setFilter] = useState(''); + const [focusIdx, setFocusIdx] = useState(-1); const searchRef = useRef(null); + const listRef = useRef(null); // Keyboard shortcut: Ctrl+K or Cmd+K to focus search when sidebar is open useEffect(() => { @@ -29,6 +31,9 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr return () => window.removeEventListener('keydown', handler); }, []); + // Reset focus index when filter changes + useEffect(() => { setFocusIdx(-1); }, [filter]); + const filtered = useMemo(() => { if (!filter.trim()) return sessions; const q = filter.toLowerCase(); @@ -82,26 +87,58 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr )} -
+
{ + const len = filtered.length; + if (!len) return; + if (e.key === 'ArrowDown') { + e.preventDefault(); + const next = focusIdx < len - 1 ? focusIdx + 1 : 0; + setFocusIdx(next); + listRef.current?.querySelectorAll('[role="option"]')[next]?.scrollIntoView({ block: 'nearest' }); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const prev = focusIdx > 0 ? focusIdx - 1 : len - 1; + setFocusIdx(prev); + listRef.current?.querySelectorAll('[role="option"]')[prev]?.scrollIntoView({ block: 'nearest' }); + } else if (e.key === 'Enter' && focusIdx >= 0 && focusIdx < len) { + e.preventDefault(); + onSwitch(filtered[focusIdx].key); + onClose(); + } else if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }} + > {sessions.length === 0 && (
{t('sidebar.empty')}
)} {sessions.length > 0 && filtered.length === 0 && (
{t('sidebar.noResults')}
)} - {filtered.map(s => { + {filtered.map((s, idx) => { const isActive = s.key === activeSession; + const isFocused = idx === focusIdx; return (