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:
@@ -957,4 +957,93 @@ describe('PersonMentionEditor — #628 re-edit pencil', () => {
|
||||
expect(host.snapshot.value).not.toContain('@Anna');
|
||||
});
|
||||
});
|
||||
|
||||
// AC-4 — dismiss leaves the node byte-identical on BOTH close paths ---------
|
||||
|
||||
function dismissButton() {
|
||||
return page.getByRole('button', { name: m.person_mention_dismiss_label() });
|
||||
}
|
||||
|
||||
it('AC-4: Escape closes the re-edit dropdown leaving the node byte-identical', async () => {
|
||||
mockFetchWithPersons();
|
||||
const host = await renderSavedMention();
|
||||
clickPencil(await pencilElement());
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('searchbox')).toBeVisible();
|
||||
});
|
||||
|
||||
const search = (await page.getByRole('searchbox').element()) as HTMLElement;
|
||||
search.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('listbox')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(host.snapshot.value).toBe('@Aug ');
|
||||
expect(host.snapshot.mentionedPersons).toEqual([{ personId: 'p-aug', displayName: 'Aug' }]);
|
||||
});
|
||||
|
||||
it('AC-4: the visible dismiss control closes the re-edit dropdown leaving the node byte-identical', async () => {
|
||||
mockFetchWithPersons();
|
||||
const host = await renderSavedMention();
|
||||
clickPencil(await pencilElement());
|
||||
await expect.element(dismissButton()).toBeVisible();
|
||||
|
||||
const dismissEl = (await dismissButton().element()) as HTMLElement;
|
||||
dismissEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('listbox')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(host.snapshot.value).toBe('@Aug ');
|
||||
expect(host.snapshot.mentionedPersons).toEqual([{ personId: 'p-aug', displayName: 'Aug' }]);
|
||||
});
|
||||
|
||||
// AC-9 parity — the re-edit dropdown is keyboard-operable on its own --------
|
||||
|
||||
it('re-edit open focuses the search input so keyboard users land in the field', async () => {
|
||||
mockFetchEmpty();
|
||||
await renderSavedMention();
|
||||
clickPencil(await pencilElement());
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('searchbox')).toBeVisible();
|
||||
});
|
||||
|
||||
const search = (await page.getByRole('searchbox').element()) as HTMLElement;
|
||||
await vi.waitFor(() => {
|
||||
expect(document.activeElement).toBe(search);
|
||||
});
|
||||
});
|
||||
|
||||
it('re-edit dropdown is keyboard-navigable: ArrowDown then Enter relinks', async () => {
|
||||
mockFetchWithPersons(); // [AUGUSTE (p-aug, highlighted), ANNA (p-anna)]
|
||||
const host = await renderSavedMention();
|
||||
clickPencil(await pencilElement());
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(page.getByRole('option', { name: /Anna Schmidt/ })).toBeVisible();
|
||||
});
|
||||
|
||||
const search = (await page.getByRole('searchbox').element()) as HTMLElement;
|
||||
search.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })
|
||||
);
|
||||
search.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(host.snapshot.mentionedPersons[0].personId).toBe('p-anna');
|
||||
expect(host.snapshot.mentionedPersons[0].displayName).toBe('Aug');
|
||||
});
|
||||
});
|
||||
|
||||
// The × control is shared with the fresh-@ dropdown ------------------------
|
||||
|
||||
it('the fresh-@ dropdown also exposes the shared dismiss control', async () => {
|
||||
mockFetchWithPersons();
|
||||
renderHost();
|
||||
await userEvent.type(page.getByRole('textbox'), '@Aug');
|
||||
await expect.element(dismissButton()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user