diff --git a/frontend/src/lib/shared/discussion/MentionDropdown.svelte b/frontend/src/lib/shared/discussion/MentionDropdown.svelte
index c647f434..48ee6db7 100644
--- a/frontend/src/lib/shared/discussion/MentionDropdown.svelte
+++ b/frontend/src/lib/shared/discussion/MentionDropdown.svelte
@@ -2,7 +2,7 @@
import type { components } from '$lib/generated/api';
// eslint-disable-next-line boundaries/dependencies -- mention dropdown needs person date formatting; extract to shared if it becomes reusable
import { formatLifeDateRange } from '$lib/person/personLifeDates';
-import { untrack } from 'svelte';
+import { onMount, untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
// Layered defence cap on the @mention search query length (CWE-400
// amplification). The attribute below caps direct
@@ -31,17 +31,42 @@ type DropdownState = {
let {
model,
editorQuery = '',
- onSearch = () => {}
+ onSearch = () => {},
+ ondismiss = () => {},
+ focusOnMount = false
}: {
model: DropdownState;
/** Text typed after `@` in the host editor. Mirrors into the search input
* until the user takes manual ownership by typing into the input itself. */
editorQuery?: string;
onSearch?: (query: string) => void;
+ /** Closes the dropdown without touching the document — invoked by the visible
+ * × dismiss control and by Escape on the re-edit path (#628 AC-4). */
+ ondismiss?: () => void;
+ /** Re-edit (#628) opens with the search field focused; the fresh-@ path keeps
+ * focus in the editor so typing flows to the contenteditable. */
+ focusOnMount?: boolean;
} = $props();
let searchQuery = $state(untrack(() => editorQuery.slice(0, MAX_QUERY_LENGTH)));
let userHasEdited = $state(false);
+let searchInput: HTMLInputElement;
+
+onMount(() => {
+ if (focusOnMount) searchInput?.focus();
+});
+
+// Re-edit has no Tiptap suggestion plugin to forward keys, so the search input
+// handles its own navigation: Escape dismisses (and is prevented from clearing
+// the native search field), Arrow/Enter reuse the exported selection logic.
+function handleSearchKeydown(event: KeyboardEvent) {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ ondismiss();
+ return;
+ }
+ if (onKeyDown(event)) event.preventDefault();
+}
// Intent-revealing alias used by both the persistent aria-live announcer and
// the visible empty-state copy. Folding the duplicated rule into one $derived
@@ -184,6 +209,7 @@ function selectItem(item: Person) {
{
userHasEdited = true;
}}
+ onkeydown={handleSearchKeydown}
onmousedown={(e) => e.stopPropagation()}
/>
+
+