From 21efa311dad1b9699f528e22e8edbff2755dbaae Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 1 Aug 2026 12:58:46 +0000 Subject: [PATCH] fix: prevent NoteEditor re-render stealing focus - use key remount, onBlur save, stable ref --- apps/web/src/routes/_app/notes.tsx | 45 +++++------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/apps/web/src/routes/_app/notes.tsx b/apps/web/src/routes/_app/notes.tsx index fab2471..89dce5f 100644 --- a/apps/web/src/routes/_app/notes.tsx +++ b/apps/web/src/routes/_app/notes.tsx @@ -17,16 +17,17 @@ import { cn } from "@/lib/utils"; import type { Note, PaginatedResponse } from "@/lib/types"; // Simple TipTap-like editor using contentEditable +// Uses a key to force remount on note change instead of setting innerHTML const NoteEditor = memo(function NoteEditor({ initialContent, onSave, placeholder = "Start writing..." }: { initialContent: string; onSave: (html: string) => void; placeholder?: string }) { const editorRef = useRef(null); const [isPlaceholder, setIsPlaceholder] = useState(!initialContent); useEffect(() => { - if (editorRef.current) { + if (editorRef.current && !editorRef.current.innerHTML) { editorRef.current.innerHTML = initialContent || ""; } setIsPlaceholder(!initialContent); - }, [initialContent]); + }, []); // Only set initial content once on mount const handleBlur = () => { const html = editorRef.current?.innerHTML || ""; @@ -49,40 +50,13 @@ const NoteEditor = memo(function NoteEditor({ initialContent, onSave, placeholde ); }); -// Separate component for title input to prevent focus stealing on parent re-render -const NoteTitleInput = memo(function NoteTitleInput({ noteId, initialTitle }: { noteId: string; initialTitle: string }) { - const [localTitle, setLocalTitle] = useState(initialTitle); - const queryClient = useQueryClient(); - - useEffect(() => { - setLocalTitle(initialTitle); - }, [initialTitle, noteId]); - - const handleBlur = () => { - if (localTitle !== initialTitle) { - api.patch(/notes/ + noteId, { title: localTitle }).then(() => { - queryClient.invalidateQueries({ queryKey: [notes] }); - }); - } - }; - - return ( - setLocalTitle(e.target.value)} - onBlur={handleBlur} - className="flex h-9 w-full rounded-md border-0 bg-transparent px-0 py-1 text-lg font-semibold text-base shadow-none transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50" - /> - ); -}); - function NotesPage() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); const [selectedNote, setSelectedNote] = useState(null); - const [titleDraft, setTitleDraft] = useState(""); const [editorContent, setEditorContent] = useState(""); const saveTimerRef = useRef | null>(null); + const selectedNoteRef = useRef(null); const [showBacklinks, setShowBacklinks] = useState(false); const [showVersions, setShowVersions] = useState(false); @@ -121,20 +95,17 @@ function NotesPage() { try { const detail = await api.get("/notes/" + note.id); setSelectedNote(detail); - setTitleDraft(detail.title); + selectedNoteRef.current = detail; setEditorContent(detail.content || ""); } catch { setSelectedNote(note); - setTitleDraft(note.title); + selectedNoteRef.current = note; setEditorContent(note.content || ""); } setShowBacklinks(false); setShowVersions(false); }; - const selectedNoteRef = useRef(selectedNote); - selectedNoteRef.current = selectedNote; - const handleContentChange = useCallback((html: string) => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { @@ -145,7 +116,6 @@ function NotesPage() { }, 500); }, [updateMutation]); - return (
{/* Left pane - note list */} @@ -199,7 +169,6 @@ function NotesPage() { e.stopPropagation()} onBlur={(e) => { if (selectedNote && e.target.value !== selectedNote.title) { updateMutation.mutate({ id: selectedNote.id, data: { title: e.target.value } }); @@ -235,7 +204,7 @@ function NotesPage() {
- +
{/* Backlinks section */} {showBacklinks && selectedNote.backlinks && selectedNote.backlinks.length > 0 && (