fix(markdown-editor): key the link popover on the link start, not its full range

The link edit popover was keyed on `${range.from}-${range.to}`. Typing inside a
link grows range.to on every keystroke (the character inherits the link mark), so
<LinkEditForm key={key}> unmounted and remounted each keystroke and re-seeded its
URL field from initialUrl — silently discarding an in-progress URL edit and
churning the popover DOM.

Key on range.from alone: it is stable while the caret stays in the same link, and
still changes when the selection moves onto a different link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-07 19:27:50 +07:00
co-authored by Claude Opus 4.8
parent bfc6b7a0d1
commit b214bafe60
2 changed files with 36 additions and 1 deletions
@@ -0,0 +1,32 @@
import { Editor, getMarkRange } from '@tiptap/core';
import { beforeAll, describe, expect, it } from 'vitest';
import { createMarkdownExtensions } from './markdown-editor-extensions';
import { setupEditorJsdom } from './markdown-editor-test-setup';
beforeAll(setupEditorJsdom);
describe('link handle popover key is stable while typing inside a link (LINK-REMOUNT)', () => {
it('keeps range.from fixed as range.to grows, so a from-based key does not remount', () => {
const editor = new Editor({
content: '[label](https://example.com)',
contentType: 'markdown',
extensions: createMarkdownExtensions(),
});
const linkType = editor.schema.marks.link!;
editor.commands.setTextSelection(3);
const before = getMarkRange(editor.state.selection.$from, linkType);
editor.commands.insertContent('X');
const after = getMarkRange(editor.state.selection.$from, linkType);
editor.destroy();
expect(before && after).toBeTruthy();
// The fix keys the popover on range.from alone: stable here, so the edit form is not remounted.
expect(after!.from).toBe(before!.from);
// range.to grew by the inserted char — the old `${from}-${to}` key would have remounted every keystroke.
expect(after!.to).toBe(before!.to + 1);
});
});
@@ -97,7 +97,10 @@ function useLinkHandle(editor: Editor) {
const rect = posToDOMRect(editor.view, range.from, range.to);
const href = (editor.getAttributes('link').href as string | undefined) ?? '';
setTarget({ href, key: `${range.from}-${range.to}`, rect });
// Key on the link's START only. Typing inside the link grows range.to every keystroke, and
// <LinkEditForm key={key}> would then unmount/remount and re-seed its URL field from initialUrl,
// silently discarding an in-progress edit. from is stable while the caret stays in the same link.
setTarget({ href, key: `${range.from}`, rect });
};
const clear = () => setTarget(null);