Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export class BlockNoteEditor<

public readonly _tiptapEditor: TiptapEditor & {
contentComponent: any;
isEditorContentInitialized?: boolean;
};

/**
Expand Down Expand Up @@ -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();
};

Expand Down
88 changes: 88 additions & 0 deletions packages/react/src/schema/ReactStyleSpec.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <span ref={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(
<>
<button
onClick={() => {
editor.focus();
editor.insertInlineContent([
{
type: "text",
text: "Styled text",
styles: { highlight: true },
},
]);
}}
>
Insert styled text
</button>
<BlockNoteViewRaw
editor={editor}
formattingToolbar={false}
linkToolbar={false}
sideMenu={false}
slashMenu={false}
/>
</>,
);
});
}

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();
}
},
);
4 changes: 3 additions & 1 deletion packages/react/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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
Expand Down
Loading