feat(transcription): dismiss + keyboard-operate the re-edit dropdown (#628 AC-4/AC-9)

Adds a visible × dismiss control to MentionDropdown (shared by the fresh-@ and
re-edit paths) and, for the re-edit path which has no Tiptap suggestion plugin
to forward keys, focuses the search input on open and handles its own keyboard:
Escape dismisses (AC-4), Arrow/Enter reuse the exported selection logic so the
dropdown is navigable on its own (AC-9 parity with the fresh-@ dropdown).

Both close paths (Escape + ×) leave the mention node attrs + text byte-identical
(AC-4) — close() never touches the document. Controller wires ondismiss=close
(+refocus editor) and focusOnMount only for the re-edit open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-02 19:40:37 +02:00
committed by marcel
parent 4a93543645
commit 2430092e43
3 changed files with 162 additions and 5 deletions

View File

@@ -82,8 +82,10 @@ type LooseRenderProps = {
// and (#628) the pencil re-edit affordance — so at most one dropdown is ever
// mounted (the AC-6 single-dropdown invariant). open() closes any prior
// dropdown first; render() is a thin adapter over open()/update()/close().
type OpenOptions = { focusOnMount?: boolean };
type MentionController = {
open: (clientRect: RectGetter, query: string, commit: CommitFn) => void;
open: (clientRect: RectGetter, query: string, commit: CommitFn, opts?: OpenOptions) => void;
update: (clientRect: RectGetter, query: string, commit: CommitFn) => void;
close: () => void;
onKeyDown: (event: KeyboardEvent) => boolean;
@@ -155,7 +157,19 @@ function createMentionController(): MentionController {
dropdownState.editorQuery = query.slice(0, MAX_QUERY_LENGTH);
};
const open = (clientRect: RectGetter, query: string, commit: CommitFn) => {
// Close the dropdown without touching the document and return focus to the
// editor — wired to the dropdown's × control and the re-edit Escape path.
const dismiss = () => {
close();
editor?.commands.focus();
};
const open = (
clientRect: RectGetter,
query: string,
commit: CommitFn,
opts: OpenOptions = {}
) => {
// Single-dropdown invariant: tear down any open dropdown before mounting a
// new one, and bump the request token so a previous open's in-flight fetch
// cannot repopulate this dropdown.
@@ -167,6 +181,8 @@ function createMentionController(): MentionController {
target: document.body,
props: {
model: dropdownState,
ondismiss: dismiss,
focusOnMount: opts.focusOnMount ?? false,
// MentionDropdown reads `editorQuery` off the shared state proxy via
// this getter — Svelte 5's mount() does not expose settable prop
// accessors, so we route through the proxy (same pattern as items).
@@ -219,7 +235,7 @@ function commitRelink(pos: number): CommitFn {
// the stored displayName.
function requestRelink(getRect: () => DOMRect | null, displayName: string, pos: number) {
if (!editor || !editor.isEditable) return;
controller.open(getRect, displayName, commitRelink(pos));
controller.open(getRect, displayName, commitRelink(pos), { focusOnMount: true });
}
onMount(() => {