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 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-11 23:05:11 +01:00
parent 12416b99bc
commit c158cbccff
3 changed files with 77 additions and 1 deletions

View File

@@ -31,6 +31,7 @@
display: flex;
justify-content: center;
position: relative;
cursor: text;
}
/* Drag-over state: subtle border highlight */

View File

@@ -13,6 +13,8 @@ const mockEditor = vi.hoisted(() => ({
prosemirrorView: {} as Record<string, unknown>,
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(
<Editor {...defaultProps} tabs={[mockTab]} activeTabPath={mockEntry.path} />
)
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(
<Editor {...defaultProps} tabs={[mockTab]} activeTabPath={mockEntry.path} />
)
// 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(
<Editor
{...defaultProps}
tabs={[{ entry: trashedEntry, content: mockContent }]}
activeTabPath={trashedEntry.path}
/>
)
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(

View File

@@ -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<HTMLDivElement>) => {
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 (
<div ref={containerRef} className={`editor__blocknote-container${isDragOver ? ' editor__blocknote-container--drag-over' : ''}`} style={cssVars as React.CSSProperties}>
<div ref={containerRef} className={`editor__blocknote-container${isDragOver ? ' editor__blocknote-container--drag-over' : ''}`} style={cssVars as React.CSSProperties} onClick={handleContainerClick}>
{isDragOver && (
<div className="editor__drop-overlay">
<div className="editor__drop-overlay-label">Drop image here</div>