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/schema/ReactStyleSpec.browser.test.tsx b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx
new file mode 100644
index 0000000000..e57f9b3abb
--- /dev/null
+++ b/packages/react/src/schema/ReactStyleSpec.browser.test.tsx
@@ -0,0 +1,88 @@
+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", "API 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();
+ }
+ 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");
+
+ expect(editor.prosemirrorState.doc.textContent).toBe(
+ "Styled text continued",
+ );
+ } finally {
+ root.unmount();
+ editor._tiptapEditor.destroy();
+ host.remove();
+ }
+ },
+);
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