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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -184,9 +184,17 @@
|
||||
}
|
||||
|
||||
// ---- Keyboard ----
|
||||
let viewerPage = $state<HTMLElement>();
|
||||
let tagsSection = $state<HTMLElement>();
|
||||
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 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="viewer-page">
|
||||
<div class="viewer-page" bind:this={viewerPage}>
|
||||
<!-- Top bar -->
|
||||
<div class="top-bar">
|
||||
<button class="back-btn" onclick={onClose} aria-label="Back to files">
|
||||
@@ -377,7 +385,7 @@
|
||||
<section class="section" use:tagsSentinel bind:this={tagsSection}>
|
||||
<div class="field-label">Tags</div>
|
||||
{#if tagsLoaded}
|
||||
<TagPicker {fileTags} onAdd={addTag} onRemove={removeTag} />
|
||||
<TagPicker {fileTags} onAdd={addTag} onRemove={removeTag} onExit={revealPreview} />
|
||||
{:else}
|
||||
<p class="tags-loading">Loading tags…</p>
|
||||
{/if}
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
fileTags: Tag[];
|
||||
onAdd: (tagId: string) => Promise<void>;
|
||||
onRemove: (tagId: string) => Promise<void>;
|
||||
/** Called when Escape leaves an already-empty filter, so the viewer can
|
||||
* scroll the preview back into view. */
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
let { fileTags, onAdd, onRemove }: Props = $props();
|
||||
let { fileTags, onAdd, onRemove, onExit }: Props = $props();
|
||||
|
||||
let allTags = $state<Tag[]>([]);
|
||||
let search = $state('');
|
||||
@@ -110,11 +113,18 @@
|
||||
assignedFocusIdx = Math.min(assignedFocusIdx, filteredAssigned.length - 2);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
// Let the keyboard leave the field: blur back to the page so arrow keys
|
||||
// and Escape reach the viewer again (e.g. a second Esc closes it).
|
||||
e.preventDefault();
|
||||
if (search) {
|
||||
// Non-empty filter: just clear it, keeping focus for more editing.
|
||||
search = '';
|
||||
assignedFocusIdx = -1;
|
||||
return;
|
||||
}
|
||||
// Empty: blur back to the page (so arrow keys and a further Escape reach
|
||||
// the viewer) and let it scroll the preview back into view.
|
||||
assignedFocusIdx = -1;
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
onExit?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user