From a31f99cb76ada74c6835c122f3fff131f1b1ff5c Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 14 Jul 2026 16:06:59 +0530 Subject: [PATCH] fix keyboard fixed --- src-tauri/Cargo.lock | 2 +- src/components/AddPageMenu.tsx | 22 ++++++ src/components/ConfirmDialog.tsx | 19 ++++- src/components/EditorView.tsx | 130 ++++++++++++++++++++----------- src/components/FindBar.tsx | 4 +- src/components/Library.tsx | 15 +++- src/components/RowMenu.tsx | 21 ++++- src/components/Settings.tsx | 13 +++- src/components/Sidebar.tsx | 32 ++++++++ src/editor/Editor.tsx | 120 ++++++++++++++++++++-------- src/store/useBook.ts | 10 +++ src/styles/app.css | 16 ++++ 12 files changed, 321 insertions(+), 83 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c2b38d9..4e46e71 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4713,7 +4713,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "margin-app" -version = "0.1.9" +version = "0.1.11" dependencies = [ "base64 0.22.1", "fontdb", diff --git a/src/components/AddPageMenu.tsx b/src/components/AddPageMenu.tsx index 4a62985..f66555c 100644 --- a/src/components/AddPageMenu.tsx +++ b/src/components/AddPageMenu.tsx @@ -16,6 +16,7 @@ export function AddPageMenu({ onAdd, onAddPart }: AddPageMenuProps) { const [open, setOpen] = useState(false); const [coords, setCoords] = useState({ top: 0, left: 0 }); const btnRef = useRef(null); + const popRef = useRef(null); const toggle = () => { if (!open && btnRef.current) { @@ -38,6 +39,25 @@ export function AddPageMenu({ onAdd, onAddPart }: AddPageMenuProps) { }; }, [open]); + useEffect(() => { + if (!open) return; + popRef.current?.querySelector(".add-page-item")?.focus(); + }, [open]); + + const onMenuKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + setOpen(false); + btnRef.current?.focus(); + } else if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + const items = Array.from(popRef.current?.querySelectorAll(".add-page-item") ?? []); + const idx = items.indexOf(document.activeElement as HTMLElement); + const nextIdx = e.key === "ArrowDown" ? (idx + 1) % items.length : (idx - 1 + items.length) % items.length; + items[nextIdx]?.focus(); + } + }; + const pick = (group: "front" | "back", label: string) => { setOpen(false); onAdd(group, label); @@ -56,9 +76,11 @@ export function AddPageMenu({ onAdd, onAddPart }: AddPageMenuProps) { {open && (
e.stopPropagation()} + onKeyDown={onMenuKeyDown} >
Body
diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 4d90d6c..dff4f25 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { useEffect, useRef, type ReactNode } from "react"; import { Icon } from "./Icon"; interface ConfirmDialogProps { @@ -10,6 +10,21 @@ interface ConfirmDialogProps { } export function ConfirmDialog({ title, message, confirmLabel = "Delete", onConfirm, onClose }: ConfirmDialogProps) { + const confirmRef = useRef(null); + + useEffect(() => { + confirmRef.current?.focus(); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
e.stopPropagation()}> @@ -26,7 +41,7 @@ export function ConfirmDialog({ title, message, confirmLabel = "Delete", onConfi -
diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index 530947b..edf0589 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -44,6 +44,9 @@ export function EditorView() { const isCompact = useCompact(); const [editor, setEditor] = useState(null); + const editorRef = useRef(null); + editorRef.current = editor; + const lastChapterRef = useRef(""); const [sidebarOpen, setSidebarOpen] = useState(!isCompact); const [dock, setDock] = useState(!isCompact); const [settingsOpen, setSettingsOpen] = useState(false); @@ -83,6 +86,18 @@ export function EditorView() { setFindOpen(true); }, []); + const focusEditorSoon = useCallback(() => { + requestAnimationFrame(() => editorRef.current?.commands.focus(undefined, { scrollIntoView: false })); + }, []); + + const switchChapter = useCallback( + (dir: 1 | -1) => { + useBook.getState().goToAdjacentChapter(dir); + focusEditorSoon(); + }, + [focusEditorSoon] + ); + const reproof = useCallback(() => { if (!editor) return; const state = useProofing.getState(); @@ -120,11 +135,36 @@ export function EditorView() { } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "f") { e.preventDefault(); openFind(e.altKey); + } else if (e.metaKey && e.altKey && (e.key === "ArrowDown" || e.key === "ArrowRight")) { + e.preventDefault(); + e.stopPropagation(); + switchChapter(1); + } else if (e.metaKey && e.altKey && (e.key === "ArrowUp" || e.key === "ArrowLeft")) { + e.preventDefault(); + e.stopPropagation(); + switchChapter(-1); + } else if (e.ctrlKey && !e.metaKey && e.key === "Tab") { + e.preventDefault(); + e.stopPropagation(); + switchChapter(e.shiftKey ? -1 : 1); + } + }; + window.addEventListener("keydown", onKey, true); + return () => window.removeEventListener("keydown", onKey, true); + }, [saveNow, openFind, switchChapter]); + + useEffect(() => { + if (!widthOpen && !exportOpen && !moreOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setWidthOpen(false); + setExportOpen(false); + setMoreOpen(false); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [saveNow, openFind]); + }, [widthOpen, exportOpen, moreOpen]); useEffect(() => { if (!notice) return; @@ -177,8 +217,11 @@ export function EditorView() { }; if (!book) return null; - const idx = book.chapters.findIndex((c) => c.id === activeChapterId); - const chapter = book.chapters[idx] ?? book.chapters[0]; + const activeIdx = book.chapters.findIndex((c) => c.id === activeChapterId); + const activeChapter = book.chapters[activeIdx]; + if (activeChapter) lastChapterRef.current = activeChapter.id; + const editorIdx = activeChapter ? activeIdx : book.chapters.findIndex((c) => c.id === lastChapterRef.current); + const chapter = book.chapters[editorIdx] ?? book.chapters[0]; const realIdx = book.chapters.findIndex((c) => c.id === chapter?.id); const kind = chapter ? chapterKind(chapter) : "body"; const eyebrow = @@ -342,50 +385,49 @@ export function EditorView() { />
- isCompact && setSidebarOpen(false)} /> + { + if (isCompact) setSidebarOpen(false); + focusEditorSoon(); + }} + /> {sidebarOpen && !isCompact && }
- {coverActive ? ( - - ) : ( - <> -
-
-
{eyebrow}
- setChapterTitle(chapter.id, value)} + {coverActive && } + - - - )} + No title + + )} +
+ setChapterContent(chapter.id, content)} + onReady={setEditor} + onContentError={() => + setNotice("This chapter contains content Margin couldn't fully read; editing may drop the unrecognized parts.") + } + /> +
+ {!coverActive && }
{dock && !isCompact && } {dock && } diff --git a/src/components/FindBar.tsx b/src/components/FindBar.tsx index 4c930e6..e6ed10d 100644 --- a/src/components/FindBar.tsx +++ b/src/components/FindBar.tsx @@ -58,7 +58,7 @@ export function FindBar({ editor, open, initialExpanded, onClose }: FindBarProps return () => { editor.off("transaction", update); }; - }, [editor]); + }, [editor, activeChapterId]); useEffect(() => { if (!editor) return; @@ -67,7 +67,7 @@ export function FindBar({ editor, open, initialExpanded, onClose }: FindBarProps } else { editor.commands.clearSearch(); } - }, [editor, open, query, caseSensitive, wholeWord]); + }, [editor, open, query, caseSensitive, wholeWord, activeChapterId]); useEffect(() => { if (open) { diff --git a/src/components/Library.tsx b/src/components/Library.tsx index 66b5f8d..abe8177 100644 --- a/src/components/Library.tsx +++ b/src/components/Library.tsx @@ -51,6 +51,11 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) { onOpen(copy); }; + const openById = (id: string) => + loadBook(id) + .then(onOpen) + .catch((e) => setNotice(`Could not open book: ${e}`)); + const removeBook = async () => { if (!pendingDelete) return; await deleteBook(pendingDelete.id); @@ -108,7 +113,15 @@ export function Library({ onOpen }: { onOpen: (book: Book) => void }) {
loadBook(b.id).then(onOpen).catch((e) => setNotice(`Could not open book: ${e}`))} + role="button" + tabIndex={0} + onClick={() => openById(b.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openById(b.id); + } + }} > {b.title || "Untitled"} {b.author && {b.author}} diff --git a/src/components/RowMenu.tsx b/src/components/RowMenu.tsx index e95e659..f93c56e 100644 --- a/src/components/RowMenu.tsx +++ b/src/components/RowMenu.tsx @@ -33,6 +33,25 @@ export function RowMenu({ label, onDuplicate, onDelete, onToggleTitle, titleHidd setOpen((v) => !v); }; + useEffect(() => { + if (!open) return; + popRef.current?.querySelector(".row-menu-item")?.focus(); + }, [open]); + + const onMenuKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + setOpen(false); + btnRef.current?.focus(); + } else if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + const items = Array.from(popRef.current?.querySelectorAll(".row-menu-item") ?? []); + const idx = items.indexOf(document.activeElement as HTMLElement); + const nextIdx = e.key === "ArrowDown" ? (idx + 1) % items.length : (idx - 1 + items.length) % items.length; + items[nextIdx]?.focus(); + } + }; + useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { @@ -88,7 +107,7 @@ export function RowMenu({ label, onDuplicate, onDelete, onToggleTitle, titleHidd {open && createPortal( -
+
{onToggleTitle && (
- set({ title: e.target.value })} /> + set({ title: e.target.value })} /> set({ subtitle: e.target.value })} /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7d8e5ba..65b4677 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -155,6 +155,33 @@ export function Sidebar({ onNavigate }: { onNavigate?: () => void }) { onNavigate?.(); }; + const focusRow = (id: string) => + requestAnimationFrame(() => { + document.querySelector(`.chapter[data-id="${CSS.escape(id)}"]`)?.focus(); + }); + + const onRowKeyDown = (e: React.KeyboardEvent, index: number, id: string) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setActiveChapter(id); + onNavigate?.(); + } else if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") { + e.preventDefault(); + const target = + e.key === "Home" + ? chapters[0] + : e.key === "End" + ? chapters[chapters.length - 1] + : chapters[index + (e.key === "ArrowDown" ? 1 : -1)]; + if (target) { + setActiveChapter(target.id); + focusRow(target.id); + } + } + }; + + const tabStopId = chapters.some((c) => c.id === activeChapterId) ? activeChapterId : chapters[0]?.id; + return (