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);
|
void remove(tag.id);
|
||||||
assignedFocusIdx = Math.min(assignedFocusIdx, assignedTags.length - 2);
|
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>
|
</script>
|
||||||
|
|||||||
@@ -184,9 +184,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Keyboard ----
|
// ---- Keyboard ----
|
||||||
|
let viewerPage = $state<HTMLElement>();
|
||||||
let tagsSection = $state<HTMLElement>();
|
let tagsSection = $state<HTMLElement>();
|
||||||
let pendingTagFocus = false;
|
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) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||||
@@ -242,7 +250,7 @@
|
|||||||
|
|
||||||
<svelte:window onkeydown={handleKeydown} />
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<div class="viewer-page">
|
<div class="viewer-page" bind:this={viewerPage}>
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<button class="back-btn" onclick={onClose} aria-label="Back to files">
|
<button class="back-btn" onclick={onClose} aria-label="Back to files">
|
||||||
@@ -377,7 +385,7 @@
|
|||||||
<section class="section" use:tagsSentinel bind:this={tagsSection}>
|
<section class="section" use:tagsSentinel bind:this={tagsSection}>
|
||||||
<div class="field-label">Tags</div>
|
<div class="field-label">Tags</div>
|
||||||
{#if tagsLoaded}
|
{#if tagsLoaded}
|
||||||
<TagPicker {fileTags} onAdd={addTag} onRemove={removeTag} />
|
<TagPicker {fileTags} onAdd={addTag} onRemove={removeTag} onExit={revealPreview} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="tags-loading">Loading tags…</p>
|
<p class="tags-loading">Loading tags…</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
fileTags: Tag[];
|
fileTags: Tag[];
|
||||||
onAdd: (tagId: string) => Promise<void>;
|
onAdd: (tagId: string) => Promise<void>;
|
||||||
onRemove: (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 allTags = $state<Tag[]>([]);
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
@@ -110,11 +113,18 @@
|
|||||||
assignedFocusIdx = Math.min(assignedFocusIdx, filteredAssigned.length - 2);
|
assignedFocusIdx = Math.min(assignedFocusIdx, filteredAssigned.length - 2);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} 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();
|
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;
|
assignedFocusIdx = -1;
|
||||||
(e.currentTarget as HTMLInputElement).blur();
|
(e.currentTarget as HTMLInputElement).blur();
|
||||||
|
onExit?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user