From 0f7dad13e3b759ff5bd135dfc86c234983c4b6ba Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Sat, 24 Oct 2026 08:40:54 +0200 Subject: [PATCH] fix: preserve native input in empty React inline content --- packages/react/src/editor/styles.css | 10 +++ .../ReactInlineContentSpec.browser.test.tsx | 77 +++++++++++++++++++ 2 files changed, 87 insertions(+) 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(); + } + }, +);