fix(editor): preserve focus during vault refresh

This commit is contained in:
lucaronin
2026-05-05 02:00:28 +02:00
parent 75a8444b41
commit 92c0b895dc
4 changed files with 47 additions and 2 deletions

View File

@@ -130,6 +130,14 @@ import {
import { requestPlainTextPaste } from './utils/plainTextPaste'
import './App.css'
const ACTIVE_EDITOR_SURFACE_SELECTOR = '.editor__blocknote-container, .raw-editor-codemirror'
function isActiveElementInsideEditorSurface(): boolean {
const activeElement = document.activeElement
if (!(activeElement instanceof HTMLElement)) return false
return Boolean(activeElement.closest(ACTIVE_EDITOR_SURFACE_SELECTOR))
}
// Type declarations for mock content storage and test overrides
declare global {
interface Window {
@@ -619,12 +627,18 @@ function App() {
useEffect(() => {
noteWindowActionsRef.current = { handleSelectNote, openTabWithContent }
}, [handleSelectNote, openTabWithContent])
const handlePulledVaultUpdate = useCallback(async (updatedFiles: string[]) => {
const handleVaultUpdate = useCallback(async (
updatedFiles: string[],
options: { preserveFocusedEditor?: boolean } = {},
) => {
await refreshPulledVaultState({
activeTabPath: notes.activeTabPath,
closeAllTabs,
getActiveTabPath: () => notes.activeTabPathRef.current,
hasUnsavedChanges: (path) => vault.unsavedPaths.has(path),
shouldKeepActiveEditorMounted: options.preserveFocusedEditor
? isActiveElementInsideEditorSurface
: undefined,
reloadFolders: vault.reloadFolders,
reloadVault: vault.reloadVault,
reloadViews: vault.reloadViews,
@@ -643,9 +657,17 @@ function App() {
vault.reloadViews,
vault.unsavedPaths,
])
const handlePulledVaultUpdate = useCallback(
(updatedFiles: string[]) => handleVaultUpdate(updatedFiles),
[handleVaultUpdate],
)
const handleFocusedVaultUpdate = useCallback(
(updatedFiles: string[]) => handleVaultUpdate(updatedFiles, { preserveFocusedEditor: true }),
[handleVaultUpdate],
)
useVaultWatcher({
vaultPath: noteWindowParams ? '' : resolvedPath,
onVaultChanged: handlePulledVaultUpdate,
onVaultChanged: handleFocusedVaultUpdate,
filterChangedPaths: filterExternalVaultPaths,
})
const autoSync = useAutoSync({
@@ -774,6 +796,7 @@ function App() {
closeAllTabs,
replaceActiveTab: handleReplaceActiveTab,
hasUnsavedChanges: (path) => vault.unsavedPaths.has(path),
shouldKeepActiveEditorMounted: isActiveElementInsideEditorSurface,
onSelectNote: notes.handleSelectNote,
activeTabPath: notes.activeTabPath,
getActiveTabPath: () => notes.activeTabPathRef.current,

View File

@@ -11,6 +11,7 @@ interface VaultBridgeDeps {
closeAllTabs: () => void
replaceActiveTab: (entry: VaultEntry) => Promise<void>
hasUnsavedChanges: (path: string) => boolean
shouldKeepActiveEditorMounted?: () => boolean
onSelectNote: (entry: VaultEntry) => void
activeTabPath: string | null
getActiveTabPath?: () => string | null
@@ -33,6 +34,7 @@ export function useVaultBridge({
closeAllTabs,
replaceActiveTab,
hasUnsavedChanges,
shouldKeepActiveEditorMounted,
onSelectNote,
activeTabPath,
getActiveTabPath,
@@ -50,6 +52,7 @@ export function useVaultBridge({
closeAllTabs,
getActiveTabPath,
hasUnsavedChanges,
shouldKeepActiveEditorMounted,
reloadFolders,
reloadVault,
reloadViews,
@@ -62,6 +65,7 @@ export function useVaultBridge({
closeAllTabs,
getActiveTabPath,
hasUnsavedChanges,
shouldKeepActiveEditorMounted,
reloadFolders,
reloadVault,
reloadViews,

View File

@@ -88,6 +88,21 @@ describe('refreshPulledVaultState', () => {
expect(options.closeAllTabs).not.toHaveBeenCalled()
})
it('keeps the active tab mounted while the editor is focused', async () => {
const options = makeOptions({
shouldKeepActiveEditorMounted: vi.fn(() => true),
})
await refreshPulledVaultState(options)
expect(options.shouldKeepActiveEditorMounted).toHaveBeenCalledOnce()
expect(options.reloadVault).toHaveBeenCalledOnce()
expect(options.reloadFolders).toHaveBeenCalledOnce()
expect(options.reloadViews).toHaveBeenCalledOnce()
expect(options.replaceActiveTab).not.toHaveBeenCalled()
expect(options.closeAllTabs).not.toHaveBeenCalled()
})
it('skips stale tab replacement when the active note changes during reload', async () => {
let resolveReload!: (entries: VaultEntry[]) => void
let currentActivePath: string | null = '/vault/active.md'

View File

@@ -6,6 +6,7 @@ interface PulledVaultRefreshOptions {
getActiveTabPath?: () => string | null
closeAllTabs: () => void
hasUnsavedChanges: (path: string) => boolean
shouldKeepActiveEditorMounted?: () => boolean
reloadFolders: () => Promise<unknown> | unknown
reloadVault: () => Promise<VaultEntry[]>
reloadViews: () => Promise<unknown> | unknown
@@ -33,6 +34,7 @@ export async function refreshPulledVaultState(options: PulledVaultRefreshOptions
closeAllTabs,
getActiveTabPath,
hasUnsavedChanges,
shouldKeepActiveEditorMounted,
reloadFolders,
reloadVault,
reloadViews,
@@ -51,6 +53,7 @@ export async function refreshPulledVaultState(options: PulledVaultRefreshOptions
if (!activeTabPath || !latestActiveTabPath) return entries
if (didActivePathChange(activeTabPath, latestActiveTabPath)) return entries
if (hasUnsavedChanges(latestActiveTabPath)) return entries
if (shouldKeepActiveEditorMounted?.()) return entries
const refreshedEntry = findByNotePath(entries, latestActiveTabPath)
if (!refreshedEntry) {