diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css
index 8ce0056370..8b406bd720 100644
--- a/packages/react/src/editor/styles.css
+++ b/packages/react/src/editor/styles.css
@@ -1,5 +1,15 @@
@import url("@blocknote/core/style.css");
+/* Give TipTap's empty content element the contentRef's minimum dimensions so
+ native input lands inside it, not beside it (#2142). Leave populated content
+ inline so text and formatting can wrap normally. */
+.bn-ic-react-node-view-renderer
+ :where([data-editable] > [data-node-view-content-react]:empty) {
+ display: inline-block;
+ min-height: inherit;
+ min-width: inherit;
+}
+
/* Default theme params */
.bn-root {
--bn-colors-editor-text: #3f3f3f;
diff --git a/packages/react/src/schema/ReactInlineContentSpec.browser.test.tsx b/packages/react/src/schema/ReactInlineContentSpec.browser.test.tsx
index 63ea726f60..47258d9c3a 100644
--- a/packages/react/src/schema/ReactInlineContentSpec.browser.test.tsx
+++ b/packages/react/src/schema/ReactInlineContentSpec.browser.test.tsx
@@ -2,6 +2,7 @@ import { BlockNoteEditor, BlockNoteSchema } from "@blocknote/core";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { expect, it } from "vite-plus/test";
+import { userEvent } from "vite-plus/test/browser";
import { BlockNoteViewRaw } from "../editor/BlockNoteView.js";
import { createReactInlineContentSpec } from "./ReactInlineContentSpec.js";
@@ -100,3 +101,79 @@ it.each([
}
},
);
+
+it.each([
+ ["plain", "inline-block"],
+ ["styled", "inline-block"],
+ ["plain", "inline-flex"],
+ ["styled", "inline-flex"],
+] as const)(
+ "records native input after clicking empty %s inline content with %s layout (#2142)",
+ async (content, display) => {
+ const schema = BlockNoteSchema.create().extend({
+ inlineContentSpecs: {
+ tag: createReactInlineContentSpec(
+ { type: "tag", propSchema: {}, content },
+ {
+ render: ({ contentRef }) => (
+
+ ),
+ },
+ ),
+ },
+ });
+ const editor = BlockNoteEditor.create({
+ schema,
+ trailingBlock: false,
+ initialContent: [{ id: "paragraph", type: "paragraph", content: [] }],
+ });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ try {
+ flushSync(() => root.render());
+ editor.setTextCursorPosition("paragraph", "start");
+ editor.insertInlineContent([{ type: "tag" }]);
+ await expect.poll(() => host.querySelector(".tag")).not.toBeNull();
+ for (const text of ["a", "b"]) {
+ // Click unused space again after the first character shrinks contentDOM.
+ await userEvent.click(host.querySelector(".tag")!);
+ // Native input bypasses ProseMirror's keypress insertion handler.
+ document.execCommand("insertText", false, text);
+ const expected = text === "a" ? "a" : "ab";
+ await expect
+ .poll(() => host.querySelector(".tag")?.textContent)
+ .toBe(expected);
+ expect(
+ getComputedStyle(
+ host.querySelector("[data-node-view-content-react]")!,
+ ).display,
+ ).toBe(display === "inline-flex" ? "block" : "inline");
+ await expect
+ .poll(() => editor.document[0].content)
+ .toEqual([
+ {
+ type: "tag",
+ props: {},
+ content:
+ content === "plain"
+ ? expected
+ : [{ type: "text", text: expected, styles: {} }],
+ },
+ ]);
+ }
+ } finally {
+ root.unmount();
+ editor.unmount();
+ host.remove();
+ }
+ },
+);