From 6a3bb9ff51ff5c6b78ebb4334439e42d75fdaf4d Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Thu, 11 Jun 2026 21:27:06 +0300 Subject: [PATCH] feat(frontend): stage Escape in the tag editors to clear, then exit In the single-file viewer's tag filter, Escape now clears a non-empty filter first; on an empty filter it blurs and scrolls the preview back to the top. In the bulk (multi-file) editor it clears, then releases focus, so only the next Escape reaches the page handler and closes the popup. Co-Authored-By: Claude Opus 4.8 --- .../src/lib/components/file/BulkTagEditor.svelte | 14 ++++++++++++++ .../src/lib/components/file/FileViewer.svelte | 12 ++++++++++-- .../src/lib/components/file/TagPicker.svelte | 16 +++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/file/BulkTagEditor.svelte b/frontend/src/lib/components/file/BulkTagEditor.svelte index 444422c..5695d35 100644 --- a/frontend/src/lib/components/file/BulkTagEditor.svelte +++ b/frontend/src/lib/components/file/BulkTagEditor.svelte @@ -159,6 +159,20 @@ void remove(tag.id); assignedFocusIdx = Math.min(assignedFocusIdx, assignedTags.length - 2); } + } else if (e.key === 'Escape') { + // Staged exit: a non-empty filter clears first; once empty, Escape + // releases focus. Stop propagation so neither step reaches the page's + // window handler — only the *next* Escape (with focus already gone) does, + // and that one closes the popup. + e.preventDefault(); + e.stopPropagation(); + if (search) { + search = ''; + assignedFocusIdx = -1; + } else { + assignedFocusIdx = -1; + (e.currentTarget as HTMLInputElement).blur(); + } } } diff --git a/frontend/src/lib/components/file/FileViewer.svelte b/frontend/src/lib/components/file/FileViewer.svelte index 1ef95c0..a936447 100644 --- a/frontend/src/lib/components/file/FileViewer.svelte +++ b/frontend/src/lib/components/file/FileViewer.svelte @@ -184,9 +184,17 @@ } // ---- Keyboard ---- + let viewerPage = $state(); let tagsSection = $state(); let pendingTagFocus = false; + // Bring the preview back to the top of the scroll container (the overlay, or + // the page in the standalone route). scrollIntoView resolves the right + // scroller in either case. Called when Escape leaves the tag filter. + function revealPreview() { + viewerPage?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + function handleKeydown(e: KeyboardEvent) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.ctrlKey || e.metaKey || e.altKey) return; @@ -242,7 +250,7 @@ -
+