import { useEffect, useState } from 'react'; import { BulkApi } from '../api/client.js'; /// /// Keyboard navigation for an email list. Shortcuts: /// j / ArrowDown move focus down /// k / ArrowUp move focus up /// e archive the focused email /// u mark the focused email unread /// # (shift+3) trash the focused email /// All shortcuts are ignored while an input/textarea/select (or any /// contenteditable) has focus, and modifier chords (Ctrl/Cmd/Alt) are left /// alone, so typing and browser/OS shortcuts are never hijacked. /// `onRemoved(id)` lets the caller drop the row from local state after a /// successful archive/trash. /// export default function useListKeyboardNav(emails, onRemoved) { const [focusedId, setFocusedId] = useState(null); useEffect(() => { const isEditable = (el) => { if (!el) return false; const tag = el.tagName; return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable; }; const handler = async (e) => { // Never hijack typing or modifier chords (Ctrl+C, Cmd+K, Alt+…). if (isEditable(document.activeElement)) return; if (e.ctrlKey || e.metaKey || e.altKey) return; if (!emails.length) return; const idx = emails.findIndex((x) => x.id === focusedId); switch (e.key) { case 'j': case 'ArrowDown': { e.preventDefault(); const next = idx < 0 ? 0 : Math.min(idx + 1, emails.length - 1); setFocusedId(emails[next].id); break; } case 'k': case 'ArrowUp': { e.preventDefault(); const prev = idx < 0 ? 0 : Math.max(idx - 1, 0); setFocusedId(emails[prev].id); break; } case 'e': { if (idx < 0) break; e.preventDefault(); const id = emails[idx].id; await BulkApi.archive([id]); onRemoved(id); break; } case 'u': { if (idx < 0) break; e.preventDefault(); await BulkApi.markUnread([emails[idx].id]); break; } case '#': { if (idx < 0) break; e.preventDefault(); const id = emails[idx].id; await BulkApi.trash([id]); onRemoved(id); break; } default: break; } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [emails, focusedId, onRemoved]); return focusedId; }