From c158cbccff72c706cbc1f23d2d70866a70e5ac41 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 11 Mar 2026 23:05:11 +0100 Subject: [PATCH] fix: make empty space below editor content clickable to focus editor Clicking anywhere in the editor container (including empty space below the last block) now focuses the editor and places the cursor at the end of the last block. This matches the behavior of Notion, Bear, and Obsidian. Co-Authored-By: Claude Opus 4.6 --- src/components/Editor.css | 1 + src/components/Editor.test.tsx | 64 +++++++++++++++++++++++++++++ src/components/SingleEditorView.tsx | 13 +++++- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/components/Editor.css b/src/components/Editor.css index 0fea8e0b..049c1aa9 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -31,6 +31,7 @@ display: flex; justify-content: center; position: relative; + cursor: text; } /* Drag-over state: subtle border highlight */ diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index 587038bf..2ebaa1b0 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -13,6 +13,8 @@ const mockEditor = vi.hoisted(() => ({ prosemirrorView: {} as Record, blocksToHTMLLossy: vi.fn(() => ''), _tiptapEditor: { commands: { setContent: vi.fn() } }, + focus: vi.fn(), + setTextCursorPosition: vi.fn(), })) // Mock BlockNote components @@ -358,6 +360,68 @@ describe('Editor', () => { }) }) +describe('click empty editor space', () => { + it('focuses editor at end of last block when clicking empty space below content', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + render( + + ) + + const container = document.querySelector('.editor__blocknote-container') + expect(container).toBeTruthy() + + // Click directly on the container (simulates clicking empty space below content) + fireEvent.click(container!) + + expect(mockEditor.setTextCursorPosition).toHaveBeenCalledWith('1', 'end') + expect(mockEditor.focus).toHaveBeenCalled() + }) + + it('does not interfere with clicks on contenteditable elements', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + render( + + ) + + // Simulate clicking on a contenteditable child (which ProseMirror would handle) + const container = document.querySelector('.editor__blocknote-container')! + const editableDiv = document.createElement('div') + editableDiv.setAttribute('contenteditable', 'true') + container.appendChild(editableDiv) + + fireEvent.click(editableDiv) + + expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled() + // Clean up + container.removeChild(editableDiv) + }) + + it('does not focus editor when note is not editable (trashed)', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + const trashedEntry: VaultEntry = { ...mockEntry, trashed: true, trashedAt: Date.now() / 1000 } + render( + + ) + + const container = document.querySelector('.editor__blocknote-container') + expect(container).toBeTruthy() + fireEvent.click(container!) + + expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled() + expect(mockEditor.focus).not.toHaveBeenCalled() + }) +}) + describe('archived note behavior', () => { it('shows archive banner immediately when entry changes to archived (reactive)', () => { const { rerender } = render( diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx index c0ca86e7..27f0751b 100644 --- a/src/components/SingleEditorView.tsx +++ b/src/components/SingleEditorView.tsx @@ -39,6 +39,17 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange const onImageUrl = useInsertImageCallback(editor) const { isDragOver } = useImageDrop({ containerRef, onImageUrl, vaultPath }) + const handleContainerClick = useCallback((e: React.MouseEvent) => { + if (!editable) return + const target = e.target as HTMLElement + if (target.closest('[contenteditable="true"]')) return + const blocks = editor.document + if (blocks.length > 0) { + editor.setTextCursorPosition(blocks[blocks.length - 1].id, 'end') + } + editor.focus() + }, [editor, editable]) + useEffect(() => { _wikilinkEntriesRef.current = entries }, [entries]) @@ -94,7 +105,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }, [baseItems, insertWikilink, typeEntryMap]) return ( -
+
{isDragOver && (
Drop image here