diff --git a/ui/src/components/MarkdownEditor.test.tsx b/ui/src/components/MarkdownEditor.test.tsx
index 73c96c94b1b..20bd75cd321 100644
--- a/ui/src/components/MarkdownEditor.test.tsx
+++ b/ui/src/components/MarkdownEditor.test.tsx
@@ -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(/\\ {
});
});
+ 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();
+ });
+ 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();
+ });
+ 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;
diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx
index 1b1190939ec..0fdee647e5b 100644
--- a/ui/src/components/MarkdownEditor.tsx
+++ b/ui/src/components/MarkdownEditor.tsx
@@ -1366,8 +1366,27 @@ export const MarkdownEditor = forwardRef
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();