From d8f87ca520a9ce15ba7be98344e265acf9a8f12e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 23 Sep 2026 10:42:56 +0200 Subject: [PATCH 1/3] Made inline content get inserted before cursor instead of after --- packages/react/src/editor/BlockNoteView.tsx | 8 +- .../schema/ReactStyleSpec.browser.test.tsx | 82 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/schema/ReactStyleSpec.browser.test.tsx diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 7bfd60c4f1..d5c7046404 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -5,6 +5,7 @@ import { StyleSchema, mergeCSSClasses, } from "@blocknote/core"; +import type { ReactRenderer } from "@tiptap/react"; import React, { HTMLAttributes, ReactNode, @@ -364,10 +365,15 @@ export const BlockNoteViewEditor = (props: { children?: ReactNode }) => { editor.isEditable = ctx.editorProps.editable !== false; // Since we are not using TipTap's React Components, we need to set up the contentComponent it expects // This is a simple replacement for the state management that Tiptap does internally - editor._tiptapEditor.contentComponent = portalManager; + const tiptapEditor: ReactRenderer["editor"] = editor._tiptapEditor; + tiptapEditor.contentComponent = portalManager; if (element) { editor.mount(element); + // Render subsequent React node/mark views synchronously so their + // contentDOM is attached when ProseMirror places the selection. + tiptapEditor.isEditorContentInitialized = true; } else { + tiptapEditor.isEditorContentInitialized = false; editor.unmount(); } }, diff --git a/packages/react/src/schema/ReactStyleSpec.browser.test.tsx b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx new file mode 100644 index 0000000000..22bfd4ce06 --- /dev/null +++ b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx @@ -0,0 +1,82 @@ +import { + BlockNoteEditor, + BlockNoteSchema, + defaultStyleSpecs, +} 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 { createReactStyleSpec } from "./ReactStyleSpec.js"; + +const highlight = createReactStyleSpec( + { type: "highlight", propSchema: "boolean" }, + { render: ({ contentRef }) => }, +); +const schema = BlockNoteSchema.create({ + styleSpecs: { ...defaultStyleSpecs, highlight }, +}); + +it.each(["initial mount", "remount"])( + "keeps typing after text inserted with a React style (%s)", + async (mountMode) => { + const editor = BlockNoteEditor.create({ + schema, + initialContent: [{ type: "paragraph", content: [] }], + }); + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + + function mount() { + flushSync(() => { + root.render( + <> + + + , + ); + }); + } + + try { + mount(); + if (mountMode === "remount") { + flushSync(() => root.render(null)); + mount(); + } + + await userEvent.click(host.querySelector("button")!); + await userEvent.keyboard(" continued"); + + expect(editor.prosemirrorState.doc.textContent).toBe( + "Styled text continued", + ); + } finally { + root.unmount(); + editor._tiptapEditor.destroy(); + host.remove(); + } + }, +); From 908f0b5c7e8f5e66ff37bd03226d2d83b6f448af Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 23 Sep 2026 11:06:30 +0200 Subject: [PATCH 2/3] Updated test config --- packages/react/vite.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index d4e59a60b4..0d0bd6893c 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -1,7 +1,7 @@ import react from "@vitejs/plugin-react"; import * as path from "path"; import { webpackStats } from "rollup-plugin-webpack-stats"; -import { defineConfig, type UserConfig } from "vite-plus"; +import { configDefaults, defineConfig, type UserConfig } from "vite-plus"; import pkg from "./package.json"; // import eslintPlugin from "vite-plugin-eslint"; @@ -26,6 +26,8 @@ export default defineConfig( test: { environment: "jsdom", setupFiles: ["./vitestSetup.ts"], + // Browser tests run in the tests package's Docker browser suite. + exclude: [...configDefaults.exclude, "**/*.browser.test.*"], }, plugins: [react(), webpackStats()], // used so that vitest resolves the core package from the sources instead of the built version From 0a37b4df87ceeef12a8c6640d4cc51a068177b68 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 25 Sep 2026 18:35:22 +0200 Subject: [PATCH 3/3] Moved fix to `mount` --- packages/core/src/editor/BlockNoteEditor.ts | 6 ++++++ packages/react/src/editor/BlockNoteView.tsx | 8 +------- packages/react/src/schema/ReactStyleSpec.browser.test.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index bc30d58092..519adfa033 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -359,6 +359,7 @@ export class BlockNoteEditor< public readonly _tiptapEditor: TiptapEditor & { contentComponent: any; + isEditorContentInitialized?: boolean; }; /** @@ -729,12 +730,17 @@ export class BlockNoteEditor< */ public mount = (element: HTMLElement) => { this._tiptapEditor.mount({ mount: element }); + // TipTap's ReactRenderer uses this flag to render new node/mark views + // synchronously. Otherwise, inserting React-styled text can place the + // caret before it because its contentDOM is not attached yet. + this._tiptapEditor.isEditorContentInitialized = true; }; /** * Unmount the editor from the DOM element it is bound to */ public unmount = () => { + this._tiptapEditor.isEditorContentInitialized = false; this._tiptapEditor.unmount(); }; diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index d5c7046404..7bfd60c4f1 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -5,7 +5,6 @@ import { StyleSchema, mergeCSSClasses, } from "@blocknote/core"; -import type { ReactRenderer } from "@tiptap/react"; import React, { HTMLAttributes, ReactNode, @@ -365,15 +364,10 @@ export const BlockNoteViewEditor = (props: { children?: ReactNode }) => { editor.isEditable = ctx.editorProps.editable !== false; // Since we are not using TipTap's React Components, we need to set up the contentComponent it expects // This is a simple replacement for the state management that Tiptap does internally - const tiptapEditor: ReactRenderer["editor"] = editor._tiptapEditor; - tiptapEditor.contentComponent = portalManager; + editor._tiptapEditor.contentComponent = portalManager; if (element) { editor.mount(element); - // Render subsequent React node/mark views synchronously so their - // contentDOM is attached when ProseMirror places the selection. - tiptapEditor.isEditorContentInitialized = true; } else { - tiptapEditor.isEditorContentInitialized = false; editor.unmount(); } }, diff --git a/packages/react/src/schema/ReactStyleSpec.browser.test.tsx b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx index 22bfd4ce06..e57f9b3abb 100644 --- a/packages/react/src/schema/ReactStyleSpec.browser.test.tsx +++ b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx @@ -19,7 +19,7 @@ const schema = BlockNoteSchema.create({ styleSpecs: { ...defaultStyleSpecs, highlight }, }); -it.each(["initial mount", "remount"])( +it.each(["initial mount", "remount", "API remount"])( "keeps typing after text inserted with a React style (%s)", async (mountMode) => { const editor = BlockNoteEditor.create({ @@ -66,6 +66,12 @@ it.each(["initial mount", "remount"])( flushSync(() => root.render(null)); mount(); } + if (mountMode === "API remount") { + const element = editor.prosemirrorView.dom.parentElement!; + editor.unmount(); + expect(editor._tiptapEditor.isEditorContentInitialized).toBe(false); + editor.mount(element); + } await userEvent.click(host.querySelector("button")!); await userEvent.keyboard(" continued");