fix(editor): preserve focus during vault refresh
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user