mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 15:09:57 +02:00
fix(ui): allow clearing preloaded markdown on the first edit
Preserve the programmatic empty mount reset guard while recognizing input and Lexical deletion intent before change callbacks. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
36f7a89b37
commit
8e8cdd7aaa
2 files changed
+67
No files matched your search
@@ -164,6 +164,17 @@ vi.mock("@mdxeditor/editor", async () => {
|
||||
className={className}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Delete" || event.key === "Backspace") {
|
||||
setContent("");
|
||||
onChange?.("");
|
||||
}
|
||||
}}
|
||||
onInput={(event) => {
|
||||
const next = event.currentTarget.textContent ?? "";
|
||||
setContent(next);
|
||||
onChange?.(next);
|
||||
}}
|
||||
>
|
||||
{/* The real editor paints resolved text, never the escapes that carried it in. */}
|
||||
{content.replace(/\\</g, "<") || placeholder || ""}
|
||||
@@ -376,6 +387,43 @@ describe("MarkdownEditor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows a user to clear loaded text as the first editor change", async () => {
|
||||
const handleChange = vi.fn();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<MarkdownEditor value="Loaded draft" onChange={handleChange} />);
|
||||
});
|
||||
await flush();
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
|
||||
const editor = container.querySelector('[contenteditable="true"]')!;
|
||||
await act(async () => {
|
||||
editor.textContent = "";
|
||||
editor.dispatchEvent(new InputEvent("input", {
|
||||
bubbles: true,
|
||||
inputType: "deleteContentBackward",
|
||||
}));
|
||||
});
|
||||
expect(handleChange).toHaveBeenCalledExactlyOnceWith("");
|
||||
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
it.each(["Delete", "Backspace"])("allows first-change clearing when Lexical handles %s without an input event", async (key) => {
|
||||
const handleChange = vi.fn();
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<MarkdownEditor value="Loaded draft" onChange={handleChange} />);
|
||||
});
|
||||
await flush();
|
||||
const editor = container.querySelector('[contenteditable="true"]')!;
|
||||
await act(async () => {
|
||||
editor.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key }));
|
||||
});
|
||||
expect(handleChange).toHaveBeenCalledExactlyOnceWith("");
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
it("does not recreate the mention decoration observer when the external value changes", async () => {
|
||||
const originalMutationObserver = globalThis.MutationObserver;
|
||||
|
||||
|
||||
@@ -1366,8 +1366,27 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||
isDragOver && "ring-1 ring-primary/60 bg-accent/20",
|
||||
className,
|
||||
)}
|
||||
onInputCapture={(event) => {
|
||||
if (
|
||||
!readOnly
|
||||
&& event.target instanceof HTMLElement
|
||||
&& event.target.closest('[contenteditable="true"]')
|
||||
) {
|
||||
// Actual input may be the editor's first change. An intentional clear
|
||||
// must not be mistaken for its programmatic empty mount reset.
|
||||
initialChildOnChangeRef.current = false;
|
||||
}
|
||||
}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (readOnly) return;
|
||||
if (
|
||||
(e.key === "Backspace" || e.key === "Delete")
|
||||
&& e.target instanceof HTMLElement
|
||||
&& e.target.closest('[contenteditable="true"]')
|
||||
) {
|
||||
// Lexical handles deletion on keydown and may suppress DOM input.
|
||||
initialChildOnChangeRef.current = false;
|
||||
}
|
||||
// Cmd/Ctrl+Enter to submit
|
||||
if (onSubmit && e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in new issue
Block a user