diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css
index e14ddd6cfa..63b03a8fcb 100644
--- a/packages/core/src/editor/Block.css
+++ b/packages/core/src/editor/Block.css
@@ -673,6 +673,16 @@ NESTED BLOCKS
width: 0;
}
+/* A block with a line box, so native inserts into an empty inline source land inside it, and native deletions that empty it don't remove it (#3234). */
+.bn-inline-content-section
+ .bn-code-block-source-popup-body
+ > pre
+ > code
+ > [data-node-view-content-react] {
+ display: block;
+ min-height: 1.5em;
+}
+
.bn-code-block-source-popup-input-empty::before {
color: currentColor;
content: attr(data-placeholder);
diff --git a/packages/math-block/src/inlineContent/emptySourceInput.browser.test.tsx b/packages/math-block/src/inlineContent/emptySourceInput.browser.test.tsx
new file mode 100644
index 0000000000..c149bd0be5
--- /dev/null
+++ b/packages/math-block/src/inlineContent/emptySourceInput.browser.test.tsx
@@ -0,0 +1,114 @@
+import "@blocknote/core/style.css";
+
+import { BlockNoteEditor, BlockNoteSchema } from "@blocknote/core";
+import { BlockNoteViewRaw } from "@blocknote/react";
+import { flushSync } from "react-dom";
+import { createRoot } from "react-dom/client";
+import { expect, it, vi } from "vite-plus/test";
+import { userEvent } from "vite-plus/test/browser";
+
+import { getMathSlashMenuItems } from "../getMathSlashMenuItems.js";
+import { en } from "../i18n/locales/en.js";
+import { createReactInlineMathSpec } from "./createReactMathInlineContentSpec.js";
+
+const schema = BlockNoteSchema.create().extend({
+ inlineContentSpecs: { math: createReactInlineMathSpec() },
+});
+
+// Dead keys (`^` on AZERTY) and IME commits insert text natively, like this.
+function insertTextNatively(text: string) {
+ document.execCommand("insertText", false, text);
+}
+
+it.each([
+ // Without the CSS line box, Chromium & WebKit insert outside the source.
+ { name: "an empty source", typedFirst: "", deleteNatively: false },
+ // `MathInlinePreviewWithPopup` trims the source, so " " used to render the
+ // React `
` next to non-empty DOM, which Firefox removed while inserting.
+ { name: "a whitespace-only source", typedFirst: " ", deleteNatively: false },
+ // Like Option+Backspace. Chromium used to also remove the source's inline
+ // ancestors, which tiptap ignores as outside the content DOM.
+ {
+ name: "a source emptied by a native deletion",
+ typedFirst: "hello",
+ deleteNatively: true,
+ },
+])(
+ "natively inserts text into $name (#3234)",
+ async ({ typedFirst, deleteNatively }) => {
+ const editor = BlockNoteEditor.create({
+ schema,
+ trailingBlock: false,
+ initialContent: [{ id: "paragraph", type: "paragraph" }],
+ });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+
+ function getMathSource() {
+ let source: string | undefined;
+ editor.prosemirrorState.doc.descendants((node) => {
+ if (node.type.name === "math") {
+ source = node.textContent;
+ }
+ });
+ return source;
+ }
+
+ try {
+ flushSync(() => {
+ root.render(