From 50ebffbf23474749a24f547aba486d334a3c0fc5 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 24 Mar 2026 18:56:25 +0100 Subject: [PATCH 01/11] fix: portal floating UI elements to document.body to prevent overflow clipping Floating UI elements (menus, toolbars, emoji picker) are now portaled to a dedicated container at document.body, preventing them from being clipped by overflow:hidden ancestors. Co-Authored-By: Claude Opus 4.6 --- .../docs/react/styling-theming/themes.mdx | 2 +- examples/01-basic/12-multi-editor/src/App.tsx | 11 ++++- .../04-rendering-static-documents/src/App.tsx | 2 +- .../02-changing-font/src/styles.css | 2 +- .../04-theming/03-theming-css/src/styles.css | 7 ++- .../04-theming-css-variables/src/styles.css | 4 +- .../src/App.tsx | 2 +- packages/ariakit/src/comments/Comment.tsx | 5 ++- packages/ariakit/src/popover/Popover.tsx | 15 +++++-- packages/core/src/editor/editor.css | 20 --------- .../core/src/extensions/SideMenu/SideMenu.ts | 8 +--- packages/mantine/src/popover/Popover.tsx | 7 +-- .../src/components/Comments/EmojiPicker.tsx | 45 ++++++++----------- .../components/Popovers/GenericPopover.tsx | 39 ++++++++++------ packages/react/src/editor/BlockNoteContext.ts | 6 +++ packages/react/src/editor/BlockNoteView.tsx | 24 +++++++++- .../react/src/editor/ComponentsContext.tsx | 1 + packages/react/src/editor/styles.css | 13 +++--- packages/shadcn/src/comments/Comment.tsx | 5 ++- packages/shadcn/src/popover/popover.tsx | 23 ++++++++-- playground/src/style.css | 3 ++ 21 files changed, 143 insertions(+), 101 deletions(-) diff --git a/docs/content/docs/react/styling-theming/themes.mdx b/docs/content/docs/react/styling-theming/themes.mdx index a0b0631e3e..5965818ca0 100644 --- a/docs/content/docs/react/styling-theming/themes.mdx +++ b/docs/content/docs/react/styling-theming/themes.mdx @@ -67,7 +67,7 @@ Here are each of the theme CSS variables you can set, with values from the defau --bn-border-radius: 6px; ``` -Setting these variables on the `.bn-container[data-color-scheme]` selector will overwrite them for both default light & dark themes. To overwrite variables separately for light & dark themes, use the `.bn-container[data-color-scheme="light"]` and `.bn-container[data-color-scheme="dark"]` selectors. +Setting these variables on the `.bn-root[data-color-scheme]` selector will overwrite them for both default light & dark themes. To overwrite variables separately for light & dark themes, use the `.bn-root[data-color-scheme="light"]` and `.bn-root[data-color-scheme="dark"]` selectors. ## Programmatic Configuration diff --git a/examples/01-basic/12-multi-editor/src/App.tsx b/examples/01-basic/12-multi-editor/src/App.tsx index bf891fecbf..0aa2b11810 100644 --- a/examples/01-basic/12-multi-editor/src/App.tsx +++ b/examples/01-basic/12-multi-editor/src/App.tsx @@ -5,14 +5,19 @@ import "@blocknote/mantine/style.css"; import { useCreateBlockNote } from "@blocknote/react"; // Component that creates & renders a BlockNote editor. -function Editor(props: { initialContent?: PartialBlock[] }) { +function Editor(props: { + initialContent?: PartialBlock[]; + theme: "dark" | "light"; +}) { // Creates a new editor instance. const editor = useCreateBlockNote({ initialContent: props.initialContent, }); // Renders the editor instance using a React component. - return ; + return ( + + ); } export default function App() { @@ -20,6 +25,7 @@ export default function App() { return (
+
diff --git a/packages/ariakit/src/comments/Comment.tsx b/packages/ariakit/src/comments/Comment.tsx index efc1746f20..55ebfe2dba 100644 --- a/packages/ariakit/src/comments/Comment.tsx +++ b/packages/ariakit/src/comments/Comment.tsx @@ -58,7 +58,7 @@ export const Comment = forwardRef< actions, children, edited, - emojiPickerOpen, // Unused + emojiPickerOpen, ...rest } = props; @@ -72,7 +72,8 @@ export const Comment = forwardRef< (showActions === true || showActions === undefined || (showActions === "hover" && hovered) || - focused); + focused || + emojiPickerOpen); return ( ( + undefined, +); export const PopoverTrigger = forwardRef< HTMLButtonElement, @@ -27,6 +31,8 @@ export const PopoverContent = forwardRef< assertEmpty(rest); + const portalRoot = useContext(PortalRootContext); + return ( {children} @@ -44,7 +51,7 @@ export const PopoverContent = forwardRef< export const Popover = ( props: ComponentProps["Generic"]["Popover"]["Root"], ) => { - const { children, open, onOpenChange, position, ...rest } = props; + const { children, open, onOpenChange, position, portalRoot, ...rest } = props; assertEmpty(rest); @@ -54,7 +61,9 @@ export const Popover = ( setOpen={onOpenChange} placement={position} > - {children} + + {children} + ); }; diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index e85a7165d3..11b0e66841 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -20,26 +20,6 @@ padding: 0; } -/* -bn-root should be applied to all top-level elements - -This includes the Prosemirror editor, but also
element such as -Tippy popups that are appended to document.body directly -*/ -.bn-root { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.bn-root *, -.bn-root *::before, -.bn-root *::after { - -webkit-box-sizing: inherit; - -moz-box-sizing: inherit; - box-sizing: inherit; -} - /* reset styles, they will be set on blockContent */ .bn-default-styles p, .bn-default-styles h1, diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index e65d4b1d99..d222b63c9d 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -612,8 +612,7 @@ export class SideMenuView< this.mousePos.y > editorOuterBoundingBox.top && this.mousePos.y < editorOuterBoundingBox.bottom; - // TODO: remove parentElement, but then we need to remove padding from boundingbox or find a different solution - const editorWrapper = this.pmView.dom!.parentElement!; + const closestBNRoot = (event.target as HTMLElement).closest(".bn-root"); // Doesn't update if the mouse hovers an element that's over the editor but // isn't a part of it or the side menu. @@ -624,10 +623,7 @@ export class SideMenuView< event && event.target && // Element is outside the editor - !( - editorWrapper === event.target || - editorWrapper.contains(event.target as HTMLElement) - ) + !closestBNRoot ) { if (this.state?.show) { this.state.show = false; diff --git a/packages/mantine/src/popover/Popover.tsx b/packages/mantine/src/popover/Popover.tsx index 29564585ce..844daab5df 100644 --- a/packages/mantine/src/popover/Popover.tsx +++ b/packages/mantine/src/popover/Popover.tsx @@ -11,18 +11,19 @@ import { forwardRef } from "react"; export const Popover = ( props: ComponentProps["Generic"]["Popover"]["Root"], ) => { - const { open, onOpenChange, position, children, ...rest } = props; + const { open, onOpenChange, position, portalRoot, children, ...rest } = props; assertEmpty(rest); return ( {children} diff --git a/packages/react/src/components/Comments/EmojiPicker.tsx b/packages/react/src/components/Comments/EmojiPicker.tsx index 9b685e71e4..959e91e923 100644 --- a/packages/react/src/components/Comments/EmojiPicker.tsx +++ b/packages/react/src/components/Comments/EmojiPicker.tsx @@ -3,8 +3,6 @@ import { ReactNode, useState } from "react"; import { useComponentsContext } from "../../editor/ComponentsContext.js"; import { useBlockNoteContext } from "../../editor/BlockNoteContext.js"; import Picker from "./EmojiMartPicker.js"; -import { createPortal } from "react-dom"; -import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; export const EmojiPicker = (props: { onEmojiSelect: (emoji: { native: string }) => void; @@ -14,11 +12,10 @@ export const EmojiPicker = (props: { const [open, setOpen] = useState(false); const Components = useComponentsContext()!; - const editor = useBlockNoteEditor(); const blockNoteContext = useBlockNoteContext(); return ( - +
{ @@ -39,28 +36,24 @@ export const EmojiPicker = (props: { {props.children}
- {editor.domElement?.parentElement && - createPortal( - - { - setOpen(false); - props.onOpenChange?.(false); - }} - onEmojiSelect={(emoji: { native: string }) => { - props.onEmojiSelect(emoji); - setOpen(false); - props.onOpenChange?.(false); - }} - theme={blockNoteContext?.colorSchemePreference} - /> - , - editor.domElement.parentElement, - )} + + { + setOpen(false); + props.onOpenChange?.(false); + }} + onEmojiSelect={(emoji: { native: string }) => { + props.onEmojiSelect(emoji); + setOpen(false); + props.onOpenChange?.(false); + }} + theme={blockNoteContext?.colorSchemePreference} + /> +
); }; diff --git a/packages/react/src/components/Popovers/GenericPopover.tsx b/packages/react/src/components/Popovers/GenericPopover.tsx index c5e40c88d4..459c765e14 100644 --- a/packages/react/src/components/Popovers/GenericPopover.tsx +++ b/packages/react/src/components/Popovers/GenericPopover.tsx @@ -1,6 +1,7 @@ import { autoUpdate, FloatingFocusManager, + FloatingPortal, useDismiss, useFloating, useHover, @@ -11,6 +12,7 @@ import { } from "@floating-ui/react"; import { HTMLAttributes, ReactNode, useEffect, useRef } from "react"; +import { useBlockNoteContext } from "../../editor/BlockNoteContext.js"; import { FloatingUIOptions } from "./FloatingUIOptions.js"; export type GenericPopoverReference = @@ -83,6 +85,9 @@ export const GenericPopover = ( children: ReactNode; }, ) => { + const blockNoteContext = useBlockNoteContext(); + const portalRoot = blockNoteContext?.portalRoot ?? undefined; + const { refs, floatingStyles, context } = useFloating({ whileElementsMounted: autoUpdate, ...props.useFloatingOptions, @@ -152,7 +157,7 @@ export const GenericPopover = ( style: { display: "flex", ...props.elementProps?.style, - zIndex: `calc(var(--bn-ui-base-z-index) + ${props.elementProps?.style?.zIndex || 0})`, + zIndex: `calc(var(--bn-ui-base-z-index, 0) + ${props.elementProps?.style?.zIndex || 0})`, ...floatingStyles, ...styles, }, @@ -169,27 +174,33 @@ export const GenericPopover = ( // should be open. So without this fix, the popover just won't transition // out and will instead appear to hide instantly. return ( -
+ +
+ ); } if (!props.focusManagerProps?.disabled) { return ( - -
- {props.children} -
-
+ + +
+ {props.children} +
+
+
); } return ( -
- {props.children} -
+ +
+ {props.children} +
+
); }; diff --git a/packages/react/src/editor/BlockNoteContext.ts b/packages/react/src/editor/BlockNoteContext.ts index 5ee613e5dc..5e3384d3ea 100644 --- a/packages/react/src/editor/BlockNoteContext.ts +++ b/packages/react/src/editor/BlockNoteContext.ts @@ -18,6 +18,12 @@ export type BlockNoteContextValue< setContentEditableProps?: ReturnType>>[1]; // copy type of setXXX from useState editor?: BlockNoteEditor; colorSchemePreference?: "light" | "dark"; + /** + * A portal container element rendered at `document.body` level, used by + * floating UI elements (menus, toolbars) to escape `overflow: hidden` + * ancestors. Has the same theming classes as the editor container. + */ + portalRoot?: HTMLDivElement | null; }; export const BlockNoteContext = createContext< diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index d810fafcbe..325a0739b5 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -13,6 +13,7 @@ import React, { useMemo, useState, } from "react"; +import { createPortal } from "react-dom"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor.js"; import { useEditorChange } from "../hooks/useEditorChange.js"; import { useEditorSelectionChange } from "../hooks/useEditorSelectionChange.js"; @@ -147,6 +148,12 @@ function BlockNoteViewComponent< [editor], ); + // Portal container at document.body level for floating UI elements (menus, + // toolbars) to render into, escaping any overflow:hidden ancestors. Gets the + // same theming classes as the editor container (bn-root + color scheme), but + // not bn-container (which is for layout targeting only). + const [portalRoot, setPortalRoot] = useState(null); + // The BlockNoteContext makes sure the editor and some helper methods // are always available to nesteed compoenents const blockNoteContext: BlockNoteContextValue = useMemo(() => { @@ -155,8 +162,9 @@ function BlockNoteViewComponent< editor, setContentEditableProps, colorSchemePreference: editorColorScheme, + portalRoot, }; - }, [existingContext, editor, editorColorScheme]); + }, [existingContext, editor, editorColorScheme, portalRoot]); // We set defaultUIProps and editorProps on a different context, the BlockNoteViewContext. // This BlockNoteViewContext is used to render the editor and the default UI. @@ -205,6 +213,18 @@ function BlockNoteViewComponent< > {children} + {createPortal( +
, + document.body, + )} ); @@ -226,7 +246,7 @@ const BlockNoteViewContainer = React.forwardRef< > >(({ className, renderEditor, editorColorScheme, children, ...rest }, ref) => (
( + undefined, +); + export const Popover = ( props: ComponentProps["Generic"]["Popover"]["Root"], ) => { @@ -13,6 +18,7 @@ export const Popover = ( open, onOpenChange, position, // unused + portalRoot, ...rest } = props; @@ -22,7 +28,9 @@ export const Popover = ( return ( - {children} + + {children} + ); }; @@ -52,13 +60,14 @@ export const PopoverContent = forwardRef< assertEmpty(rest); const ShadCNComponents = useShadCNComponentsContext()!; + const portalRoot = useContext(PortalRootContext); - return ( + const content = ( ); + + if (portalRoot) { + return createPortal(content, portalRoot); + } + + return content; }); diff --git a/playground/src/style.css b/playground/src/style.css index f69602d9f3..7ce5324f7c 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -37,6 +37,9 @@ body { padding-top: 8px; margin: 0 auto; max-width: 731px; +} + +.bn-root { --bn-ui-base-z-index: 100; } From 11bdffdd5bcf5aeb61258cd4c82055f8cb69d89d Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 25 Mar 2026 14:01:28 +0100 Subject: [PATCH 02/11] fix playwright --- packages/core/src/editor/BlockNoteEditor.ts | 19 +++++++++++++++ .../managers/ExtensionManager/extensions.ts | 1 + .../core/src/extensions/SideMenu/SideMenu.ts | 6 ++--- .../tiptap-extensions/UniqueID/UniqueID.ts | 17 ++++++------- packages/react/src/editor/BlockNoteView.tsx | 24 ++++++++++++++----- 5 files changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 2a6648f04b..8e21ddf7d3 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -714,6 +714,25 @@ export class BlockNoteEditor< return this.prosemirrorView?.dom as HTMLDivElement | undefined; } + /** + * The portal container element at `document.body` used by floating UI + * elements (menus, toolbars) to escape overflow:hidden ancestors. + * Set by BlockNoteView; undefined in headless mode. + */ + public portalElement: HTMLElement | undefined; + + /** + * Checks whether a DOM element belongs to this editor — either inside the + * editor's DOM tree or inside its portal container (used for floating UI + * elements like menus and toolbars). + */ + public isWithinEditor = (element: Element): boolean => { + return !!( + this.domElement?.parentElement?.contains(element) || + this.portalElement?.contains(element) + ); + }; + public isFocused() { if (this.headless) { return false; diff --git a/packages/core/src/editor/managers/ExtensionManager/extensions.ts b/packages/core/src/editor/managers/ExtensionManager/extensions.ts index 45f0acf8e5..e61069c2bb 100644 --- a/packages/core/src/editor/managers/ExtensionManager/extensions.ts +++ b/packages/core/src/editor/managers/ExtensionManager/extensions.ts @@ -71,6 +71,7 @@ export function getDefaultTiptapExtensions( // everything from bnBlock group (nodes that represent a BlockNote block should have an id) types: ["blockContainer", "columnList", "column"], setIdAttribute: options.setIdAttribute, + isWithinEditor: editor.isWithinEditor, }), HardBreak, Text, diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index d222b63c9d..4186939d2c 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -612,8 +612,6 @@ export class SideMenuView< this.mousePos.y > editorOuterBoundingBox.top && this.mousePos.y < editorOuterBoundingBox.bottom; - const closestBNRoot = (event.target as HTMLElement).closest(".bn-root"); - // Doesn't update if the mouse hovers an element that's over the editor but // isn't a part of it or the side menu. if ( @@ -622,8 +620,8 @@ export class SideMenuView< // An element is hovered event && event.target && - // Element is outside the editor - !closestBNRoot + // Element is outside this editor and its portaled UI + !this.editor.isWithinEditor(event.target as HTMLElement) ) { if (this.state?.show) { this.state.show = false; diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts index 23f6591256..2f19981f89 100644 --- a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts +++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts @@ -51,6 +51,9 @@ const UniqueID = Extension.create({ attributeName: "id", types: [], setIdAttribute: false, + isWithinEditor: undefined as + | ((element: Element) => boolean) + | undefined, generateID: () => { // Use mock ID if tests are running. if (typeof window !== "undefined" && (window as any).__TEST_OPTIONS) { @@ -128,6 +131,7 @@ const UniqueID = Extension.create({ // view.dispatch(tr); // }, addProseMirrorPlugins() { + const { isWithinEditor } = this.options; let dragSourceElement: any = null; let transformPasted = false; return [ @@ -228,14 +232,11 @@ const UniqueID = Extension.create({ // we register a global drag handler to track the current drag source element view(view) { const handleDragstart = (event: any) => { - let _a; - dragSourceElement = ( - (_a = view.dom.parentElement) === null || _a === void 0 - ? void 0 - : _a.contains(event.target) - ) - ? view.dom.parentElement - : null; + const editorParent = view.dom.parentElement; + const isFromEditor = + editorParent?.contains(event.target) || + isWithinEditor?.(event.target); + dragSourceElement = isFromEditor ? editorParent : null; }; window.addEventListener("dragstart", handleDragstart); return { diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 325a0739b5..2962612da1 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -10,6 +10,7 @@ import React, { ReactNode, Ref, useCallback, + useEffect, useMemo, useState, } from "react"; @@ -154,6 +155,16 @@ function BlockNoteViewComponent< // not bn-container (which is for layout targeting only). const [portalRoot, setPortalRoot] = useState(null); + // Register the portal element on the editor so core extensions (SideMenu, + // UniqueID) can identify portaled elements as belonging to this editor. + // (through editor.isWithinEditor) + useEffect(() => { + editor.portalElement = portalRoot ?? undefined; + return () => { + editor.portalElement = undefined; + }; + }, [portalRoot, editor]); + // The BlockNoteContext makes sure the editor and some helper methods // are always available to nesteed compoenents const blockNoteContext: BlockNoteContextValue = useMemo(() => { @@ -216,11 +227,7 @@ function BlockNoteViewComponent< {createPortal(
, document.body, @@ -246,7 +253,12 @@ const BlockNoteViewContainer = React.forwardRef< > >(({ className, renderEditor, editorColorScheme, children, ...rest }, ref) => (
Date: Wed, 25 Mar 2026 14:14:01 +0100 Subject: [PATCH 03/11] fix static html example --- .claude/worktrees/emoji-fixes | 1 + .claude/worktrees/fix-pr-2553-port | 1 + .claude/worktrees/toggle-block-bugs-blo-1018 | 1 + examples/05-interoperability/10-static-html-render/src/App.tsx | 3 +-- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 160000 .claude/worktrees/emoji-fixes create mode 160000 .claude/worktrees/fix-pr-2553-port create mode 160000 .claude/worktrees/toggle-block-bugs-blo-1018 diff --git a/.claude/worktrees/emoji-fixes b/.claude/worktrees/emoji-fixes new file mode 160000 index 0000000000..c9234debd6 --- /dev/null +++ b/.claude/worktrees/emoji-fixes @@ -0,0 +1 @@ +Subproject commit c9234debd656a48163bc309ca102c2a05599aa88 diff --git a/.claude/worktrees/fix-pr-2553-port b/.claude/worktrees/fix-pr-2553-port new file mode 160000 index 0000000000..da22c7eb7e --- /dev/null +++ b/.claude/worktrees/fix-pr-2553-port @@ -0,0 +1 @@ +Subproject commit da22c7eb7ed1266f9ac9353c0e2a116533d70580 diff --git a/.claude/worktrees/toggle-block-bugs-blo-1018 b/.claude/worktrees/toggle-block-bugs-blo-1018 new file mode 160000 index 0000000000..7b7762322f --- /dev/null +++ b/.claude/worktrees/toggle-block-bugs-blo-1018 @@ -0,0 +1 @@ +Subproject commit 7b7762322fea934ecfd879c0acf6723233edb5fa diff --git a/examples/05-interoperability/10-static-html-render/src/App.tsx b/examples/05-interoperability/10-static-html-render/src/App.tsx index 9d04f5d9a5..0094c677cb 100644 --- a/examples/05-interoperability/10-static-html-render/src/App.tsx +++ b/examples/05-interoperability/10-static-html-render/src/App.tsx @@ -1,7 +1,6 @@ import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; import { useCreateBlockNote, usePrefersColorScheme } from "@blocknote/react"; -import { useRef, useEffect } from "react"; export default function App() { // Creates a new editor instance. @@ -158,7 +157,7 @@ export default function App() { // Renders the exported static HTML from the editor. return (
From 5756910d9d12e11211f6f41088325f0a4c9da0e8 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 13:16:05 +0200 Subject: [PATCH 04/11] refactor: add portalRef prop to BlockNoteView to allow external portal element access and customization --- packages/mantine/src/BlockNoteView.tsx | 31 ++++++++++++++------- packages/react/src/editor/BlockNoteView.tsx | 30 ++++++++++++-------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/mantine/src/BlockNoteView.tsx b/packages/mantine/src/BlockNoteView.tsx index 3b92da582a..54707734f1 100644 --- a/packages/mantine/src/BlockNoteView.tsx +++ b/packages/mantine/src/BlockNoteView.tsx @@ -45,10 +45,16 @@ export const BlockNoteView = < const defaultColorScheme = existingContext?.colorSchemePreference || systemColorScheme; - const ref = useCallback( + const finalTheme = + typeof theme === "string" + ? theme + : defaultColorScheme !== "no-preference" + ? defaultColorScheme + : "light"; + + const applyThemeVariables = useCallback( (node: HTMLDivElement | null) => { if (!node) { - // todo: clean variables? return; } @@ -70,14 +76,18 @@ export const BlockNoteView = < [defaultColorScheme, theme], ); - const mantineContext = useContext(MantineContext); + const portalRef = useCallback( + (node: HTMLDivElement | null) => { + if (!node) { + return; + } + node.setAttribute("data-mantine-color-scheme", finalTheme); + applyThemeVariables(node); + }, + [applyThemeVariables, finalTheme], + ); - const finalTheme = - typeof theme === "string" - ? theme - : defaultColorScheme !== "no-preference" - ? defaultColorScheme - : "light"; + const mantineContext = useContext(MantineContext); const view = ( @@ -86,7 +96,8 @@ export const BlockNoteView = < className={mergeCSSClasses("bn-mantine", className || "")} theme={typeof theme === "object" ? undefined : theme} {...rest} - ref={ref} + ref={applyThemeVariables} + portalRef={portalRef} /> ); diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 2962612da1..57fd9031fc 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -10,7 +10,6 @@ import React, { ReactNode, Ref, useCallback, - useEffect, useMemo, useState, } from "react"; @@ -35,6 +34,7 @@ import { import { Portals, getContentComponent } from "./EditorContent.js"; import { ElementRenderer } from "./ElementRenderer.js"; +import { mergeRefs } from "../util/mergeRefs.js"; import "./styles.css"; const emptyFn = () => { @@ -91,6 +91,8 @@ export type BlockNoteViewProps< children?: ReactNode; ref?: Ref | undefined; // only here to get types working with the generics. Regular form doesn't work + + portalRef?: Ref; } & BlockNoteDefaultUIProps; function BlockNoteViewComponent< @@ -123,6 +125,7 @@ function BlockNoteViewComponent< comments, autoFocus, renderEditor = true, + portalRef, ...rest } = props; @@ -155,15 +158,20 @@ function BlockNoteViewComponent< // not bn-container (which is for layout targeting only). const [portalRoot, setPortalRoot] = useState(null); - // Register the portal element on the editor so core extensions (SideMenu, - // UniqueID) can identify portaled elements as belonging to this editor. - // (through editor.isWithinEditor) - useEffect(() => { - editor.portalElement = portalRoot ?? undefined; - return () => { - editor.portalElement = undefined; - }; - }, [portalRoot, editor]); + // Ref callback that both updates state (for context consumers) and syncs + // the portal element to the editor (for core extensions like SideMenu/UniqueID). + const internalPortalRef = useCallback( + (node: HTMLDivElement | null) => { + editor.portalElement = node ?? undefined; + setPortalRoot(node); + }, + [editor], + ); + + const mergedPortalRef = useMemo( + () => mergeRefs([internalPortalRef, portalRef]), + [internalPortalRef, portalRef], + ); // The BlockNoteContext makes sure the editor and some helper methods // are always available to nesteed compoenents @@ -226,7 +234,7 @@ function BlockNoteViewComponent< {createPortal(
, From eaae88718a92a6c91c8e7598753ef8cf9ff21282 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 14:20:43 +0200 Subject: [PATCH 05/11] chore: remove .claude worktree files from PR Co-Authored-By: Claude Opus 4.6 --- .claude/worktrees/emoji-fixes | 1 - .claude/worktrees/fix-pr-2553-port | 1 - .claude/worktrees/toggle-block-bugs-blo-1018 | 1 - 3 files changed, 3 deletions(-) delete mode 160000 .claude/worktrees/emoji-fixes delete mode 160000 .claude/worktrees/fix-pr-2553-port delete mode 160000 .claude/worktrees/toggle-block-bugs-blo-1018 diff --git a/.claude/worktrees/emoji-fixes b/.claude/worktrees/emoji-fixes deleted file mode 160000 index c9234debd6..0000000000 --- a/.claude/worktrees/emoji-fixes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c9234debd656a48163bc309ca102c2a05599aa88 diff --git a/.claude/worktrees/fix-pr-2553-port b/.claude/worktrees/fix-pr-2553-port deleted file mode 160000 index da22c7eb7e..0000000000 --- a/.claude/worktrees/fix-pr-2553-port +++ /dev/null @@ -1 +0,0 @@ -Subproject commit da22c7eb7ed1266f9ac9353c0e2a116533d70580 diff --git a/.claude/worktrees/toggle-block-bugs-blo-1018 b/.claude/worktrees/toggle-block-bugs-blo-1018 deleted file mode 160000 index 7b7762322f..0000000000 --- a/.claude/worktrees/toggle-block-bugs-blo-1018 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7b7762322fea934ecfd879c0acf6723233edb5fa From 0d114d615dce1e5865221ff58230ecf441d468be Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 14:36:33 +0200 Subject: [PATCH 06/11] docs --- docs/content/docs/react/styling-theming/overriding-css.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/react/styling-theming/overriding-css.mdx b/docs/content/docs/react/styling-theming/overriding-css.mdx index fe7c2047c1..bece286f4d 100644 --- a/docs/content/docs/react/styling-theming/overriding-css.mdx +++ b/docs/content/docs/react/styling-theming/overriding-css.mdx @@ -21,8 +21,9 @@ BlockNote uses classes with the `bn-` prefix to style editor elements. Here are #### Editor Structure -- `.bn-container`: Container for editor and all menus/toolbars. -- `.bn-editor`: Main editor element. +- `.bn-root`: Container class both the floating menus / toolbars and the editor +- `.bn-container`: Container around `.bn-editor` +- `.bn-editor`: Main editor element (the "contenteditable"). - `.bn-block`: Individual block element (including nested). - `.bn-block-group`: Container for nested blocks. - `.bn-block-content`: Block content wrapper. From 670dffc2817ae884404efc170650d989086a0f26 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 19:52:04 +0200 Subject: [PATCH 07/11] remove hardcoded zIndex values --- packages/ariakit/src/style.css | 4 ---- packages/mantine/src/popover/Popover.tsx | 1 - packages/shadcn/src/popover/popover.tsx | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index 1fde174b11..46917be46b 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -38,10 +38,6 @@ inset 0 1px 1px 1px var(--shadow); } -.bn-ak-popover { - z-index: 10000; -} - .bn-toolbar .bn-ak-popover { gap: 0.5rem; } diff --git a/packages/mantine/src/popover/Popover.tsx b/packages/mantine/src/popover/Popover.tsx index 844daab5df..882a715cc0 100644 --- a/packages/mantine/src/popover/Popover.tsx +++ b/packages/mantine/src/popover/Popover.tsx @@ -23,7 +23,6 @@ export const Popover = ( opened={open} onChange={onOpenChange} position={position} - zIndex={portalRoot ? undefined : 10000} > {children} diff --git a/packages/shadcn/src/popover/popover.tsx b/packages/shadcn/src/popover/popover.tsx index 337ecf9255..737908ce60 100644 --- a/packages/shadcn/src/popover/popover.tsx +++ b/packages/shadcn/src/popover/popover.tsx @@ -67,7 +67,7 @@ export const PopoverContent = forwardRef< sideOffset={8} className={cn( className, - portalRoot ? "flex flex-col gap-2" : "z-[10000] flex flex-col gap-2", + "flex flex-col gap-2", variant === "panel-popover" ? "w-fit max-w-none border-none p-0 shadow-none" : "", From c6fbfec00fbe2cec741270b1bb030962e828a14e Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 21:14:52 +0200 Subject: [PATCH 08/11] refactor: replace portal element state with eager creation and external portal root support --- packages/mantine/src/BlockNoteView.tsx | 2 +- packages/react/src/editor/BlockNoteContext.ts | 2 +- packages/react/src/editor/BlockNoteView.tsx | 98 +++++++++++++------ 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/packages/mantine/src/BlockNoteView.tsx b/packages/mantine/src/BlockNoteView.tsx index 54707734f1..f84e9db8c0 100644 --- a/packages/mantine/src/BlockNoteView.tsx +++ b/packages/mantine/src/BlockNoteView.tsx @@ -97,7 +97,7 @@ export const BlockNoteView = < theme={typeof theme === "object" ? undefined : theme} {...rest} ref={applyThemeVariables} - portalRef={portalRef} + portalRootRef={portalRef} /> ); diff --git a/packages/react/src/editor/BlockNoteContext.ts b/packages/react/src/editor/BlockNoteContext.ts index 5e3384d3ea..59bbdd9554 100644 --- a/packages/react/src/editor/BlockNoteContext.ts +++ b/packages/react/src/editor/BlockNoteContext.ts @@ -23,7 +23,7 @@ export type BlockNoteContextValue< * floating UI elements (menus, toolbars) to escape `overflow: hidden` * ancestors. Has the same theming classes as the editor container. */ - portalRoot?: HTMLDivElement | null; + portalRoot?: HTMLElement | null; }; export const BlockNoteContext = createContext< diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 57fd9031fc..1f20c42360 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -10,10 +10,10 @@ import React, { ReactNode, Ref, useCallback, + useEffect, useMemo, useState, } from "react"; -import { createPortal } from "react-dom"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor.js"; import { useEditorChange } from "../hooks/useEditorChange.js"; import { useEditorSelectionChange } from "../hooks/useEditorSelectionChange.js"; @@ -92,7 +92,19 @@ export type BlockNoteViewProps< ref?: Ref | undefined; // only here to get types working with the generics. Regular form doesn't work - portalRef?: Ref; + /** + * An element to portal floating UI elements (menus, toolbars) into, + * escaping any `overflow: hidden` ancestors. The caller is responsible + * for theming the element (adding `bn-root`, color scheme classes, etc.). + * + * When omitted, a default portal container is created at `document.body` or the shadowRoot of the editor. + */ + portalRoot?: HTMLElement; + + /** + * A ref to the portal root element. + */ + portalRootRef?: Ref; } & BlockNoteDefaultUIProps; function BlockNoteViewComponent< @@ -125,10 +137,12 @@ function BlockNoteViewComponent< comments, autoFocus, renderEditor = true, - portalRef, + portalRoot, + portalRootRef, ...rest } = props; + console.log("render"); // Used so other components (suggestion menu) can set // aria related props to the contenteditable div const [contentEditableProps, setContentEditableProps] = @@ -152,25 +166,57 @@ function BlockNoteViewComponent< [editor], ); - // Portal container at document.body level for floating UI elements (menus, - // toolbars) to render into, escaping any overflow:hidden ancestors. Gets the - // same theming classes as the editor container (bn-root + color scheme), but - // not bn-container (which is for layout targeting only). - const [portalRoot, setPortalRoot] = useState(null); - - // Ref callback that both updates state (for context consumers) and syncs - // the portal element to the editor (for core extensions like SideMenu/UniqueID). - const internalPortalRef = useCallback( - (node: HTMLDivElement | null) => { - editor.portalElement = node ?? undefined; - setPortalRoot(node); + // Portal container for floating UI elements (menus, toolbars) to render + // into, escaping any overflow:hidden ancestors. + // When portalContainer is provided externally, use it directly. + // Otherwise, create an internal element eagerly (no state, no rerender). + const internalPortalRoot = useMemo( + () => portalRoot ?? document.createElement("div"), + [portalRoot], + ); + + useEffect(() => { + if (typeof portalRootRef === "function") { + portalRootRef(internalPortalRoot); + } else if (portalRootRef) { + portalRootRef.current = internalPortalRoot; + } + }, [portalRootRef, internalPortalRoot]); + + // const portalRoot = portalContainer ?? internalPortalEl; + + useEffect(() => { + editor.portalElement = internalPortalRoot; + internalPortalRoot.className = mergeCSSClasses( + "bn-root", + editorColorScheme, + className || "", + ); + internalPortalRoot.setAttribute("data-color-scheme", editorColorScheme); + }, [internalPortalRoot, editorColorScheme, className]); + + const internalRef = useCallback( + (element: HTMLDivElement | null) => { + if (portalRoot) { + // container was passed externally, + // we're not responsible for attaching it to a parent + return; + } + // container was created internally, attach it to the rootNode of the editor + const root = element?.getRootNode(); + const container = root instanceof ShadowRoot ? root : document.body; + if (container) { + container.appendChild(internalPortalRoot); + } else { + internalPortalRoot.remove(); + } }, - [editor], + [portalRoot, internalPortalRoot], ); - const mergedPortalRef = useMemo( - () => mergeRefs([internalPortalRef, portalRef]), - [internalPortalRef, portalRef], + const mergedRef = useMemo( + () => mergeRefs([ref, internalRef]), + [ref, internalRef], ); // The BlockNoteContext makes sure the editor and some helper methods @@ -181,9 +227,9 @@ function BlockNoteViewComponent< editor, setContentEditableProps, colorSchemePreference: editorColorScheme, - portalRoot, + portalRoot: internalPortalRoot, }; - }, [existingContext, editor, editorColorScheme, portalRoot]); + }, [existingContext, editor, editorColorScheme, internalPortalRoot]); // We set defaultUIProps and editorProps on a different context, the BlockNoteViewContext. // This BlockNoteViewContext is used to render the editor and the default UI. @@ -227,19 +273,11 @@ function BlockNoteViewComponent< className={className} renderEditor={renderEditor} editorColorScheme={editorColorScheme} - ref={ref} + ref={mergedRef} {...rest} > {children} - {createPortal( -
, - document.body, - )} ); From 73d3e51796dce788beba79563216bb180a7ff611 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 7 Apr 2026 21:20:32 +0200 Subject: [PATCH 09/11] chore: remove debug console log from BlockNoteView --- packages/react/src/editor/BlockNoteView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 1f20c42360..9af87ea621 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -142,7 +142,6 @@ function BlockNoteViewComponent< ...rest } = props; - console.log("render"); // Used so other components (suggestion menu) can set // aria related props to the contenteditable div const [contentEditableProps, setContentEditableProps] = From 3e13fa9fb84701c52b72b94fb7d9e9faf5e21da5 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 8 Apr 2026 13:03:07 +0200 Subject: [PATCH 10/11] refactor: centralize portal element management within the BlockNoteEditor class and remove redundant portal props from BlockNoteView. --- packages/core/src/editor/BlockNoteEditor.ts | 21 +++++- packages/mantine/src/BlockNoteView.tsx | 25 +++---- .../src/components/Comments/EmojiPicker.tsx | 11 ++- .../components/Popovers/GenericPopover.tsx | 9 ++- packages/react/src/editor/BlockNoteContext.ts | 6 -- packages/react/src/editor/BlockNoteView.tsx | 73 ++----------------- 6 files changed, 52 insertions(+), 93 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 8e21ddf7d3..7defaa4e87 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -680,6 +680,12 @@ export class BlockNoteEditor< * @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting */ public mount = (element: HTMLElement) => { + const root = element.getRootNode(); + if (root instanceof ShadowRoot) { + root.appendChild(this.portalElement); + } else { + document.body.appendChild(this.portalElement); + } this._tiptapEditor.mount({ mount: element }); }; @@ -687,6 +693,7 @@ export class BlockNoteEditor< * Unmount the editor from the DOM element it is bound to */ public unmount = () => { + this.portalElement?.remove(); this._tiptapEditor.unmount(); }; @@ -714,12 +721,24 @@ export class BlockNoteEditor< return this.prosemirrorView?.dom as HTMLDivElement | undefined; } + private _portalElement: HTMLElement | undefined; + /** * The portal container element at `document.body` used by floating UI * elements (menus, toolbars) to escape overflow:hidden ancestors. * Set by BlockNoteView; undefined in headless mode. */ - public portalElement: HTMLElement | undefined; + public get portalElement() { + if (typeof document === "undefined") { + throw new Error( + "Portal element accessed, but not available in headless mode", + ); + } + if (!this._portalElement) { + this._portalElement = document.createElement("div"); + } + return this._portalElement; + } /** * Checks whether a DOM element belongs to this editor — either inside the diff --git a/packages/mantine/src/BlockNoteView.tsx b/packages/mantine/src/BlockNoteView.tsx index f84e9db8c0..2b714326ce 100644 --- a/packages/mantine/src/BlockNoteView.tsx +++ b/packages/mantine/src/BlockNoteView.tsx @@ -11,7 +11,7 @@ import { usePrefersColorScheme, } from "@blocknote/react"; import { MantineContext, MantineProvider } from "@mantine/core"; -import React, { useCallback, useContext } from "react"; +import React, { useCallback, useContext, useEffect } from "react"; import { applyBlockNoteCSSVariablesFromTheme, removeBlockNoteCSSVariables, @@ -38,7 +38,7 @@ export const BlockNoteView = < }; }, ) => { - const { className, theme, ...rest } = props; + const { className, theme, editor, ...rest } = props; const existingContext = useBlockNoteContext(); const systemColorScheme = usePrefersColorScheme(); @@ -53,7 +53,7 @@ export const BlockNoteView = < : "light"; const applyThemeVariables = useCallback( - (node: HTMLDivElement | null) => { + (node: HTMLElement | null) => { if (!node) { return; } @@ -76,16 +76,13 @@ export const BlockNoteView = < [defaultColorScheme, theme], ); - const portalRef = useCallback( - (node: HTMLDivElement | null) => { - if (!node) { - return; - } - node.setAttribute("data-mantine-color-scheme", finalTheme); - applyThemeVariables(node); - }, - [applyThemeVariables, finalTheme], - ); + useEffect(() => { + if (!editor.portalElement) { + throw new Error("Portal element not found"); + } + editor.portalElement.setAttribute("data-mantine-color-scheme", finalTheme); + applyThemeVariables(editor.portalElement); + }, [editor, applyThemeVariables, finalTheme]); const mantineContext = useContext(MantineContext); @@ -95,9 +92,9 @@ export const BlockNoteView = < data-mantine-color-scheme={finalTheme} className={mergeCSSClasses("bn-mantine", className || "")} theme={typeof theme === "object" ? undefined : theme} + editor={editor} {...rest} ref={applyThemeVariables} - portalRootRef={portalRef} /> ); diff --git a/packages/react/src/components/Comments/EmojiPicker.tsx b/packages/react/src/components/Comments/EmojiPicker.tsx index 959e91e923..db078703f2 100644 --- a/packages/react/src/components/Comments/EmojiPicker.tsx +++ b/packages/react/src/components/Comments/EmojiPicker.tsx @@ -1,7 +1,7 @@ import { ReactNode, useState } from "react"; -import { useComponentsContext } from "../../editor/ComponentsContext.js"; import { useBlockNoteContext } from "../../editor/BlockNoteContext.js"; +import { useComponentsContext } from "../../editor/ComponentsContext.js"; import Picker from "./EmojiMartPicker.js"; export const EmojiPicker = (props: { @@ -12,10 +12,15 @@ export const EmojiPicker = (props: { const [open, setOpen] = useState(false); const Components = useComponentsContext()!; - const blockNoteContext = useBlockNoteContext(); + const blockNoteContext = useBlockNoteContext()!; + const portalRoot = blockNoteContext.editor?.portalElement; + + if (!portalRoot) { + throw new Error("Portal root not found"); + } return ( - +
{ diff --git a/packages/react/src/components/Popovers/GenericPopover.tsx b/packages/react/src/components/Popovers/GenericPopover.tsx index 021bf941bb..88defa0607 100644 --- a/packages/react/src/components/Popovers/GenericPopover.tsx +++ b/packages/react/src/components/Popovers/GenericPopover.tsx @@ -13,7 +13,7 @@ import { } from "@floating-ui/react"; import { HTMLAttributes, ReactNode, useEffect, useRef } from "react"; -import { useBlockNoteContext } from "../../editor/BlockNoteContext.js"; +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; import { FloatingUIOptions } from "./FloatingUIOptions.js"; export type GenericPopoverReference = @@ -111,8 +111,11 @@ export const GenericPopover = ( children: ReactNode; }, ) => { - const blockNoteContext = useBlockNoteContext(); - const portalRoot = blockNoteContext?.portalRoot ?? undefined; + const editor = useBlockNoteEditor(); + const portalRoot = editor?.portalElement; + if (!portalRoot) { + throw new Error("Portal element not found"); + } const { whileElementsMounted: _whileElementsMounted, ...restFloatingOptions diff --git a/packages/react/src/editor/BlockNoteContext.ts b/packages/react/src/editor/BlockNoteContext.ts index 59bbdd9554..5ee613e5dc 100644 --- a/packages/react/src/editor/BlockNoteContext.ts +++ b/packages/react/src/editor/BlockNoteContext.ts @@ -18,12 +18,6 @@ export type BlockNoteContextValue< setContentEditableProps?: ReturnType>>[1]; // copy type of setXXX from useState editor?: BlockNoteEditor; colorSchemePreference?: "light" | "dark"; - /** - * A portal container element rendered at `document.body` level, used by - * floating UI elements (menus, toolbars) to escape `overflow: hidden` - * ancestors. Has the same theming classes as the editor container. - */ - portalRoot?: HTMLElement | null; }; export const BlockNoteContext = createContext< diff --git a/packages/react/src/editor/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx index 9af87ea621..dd5fe957fb 100644 --- a/packages/react/src/editor/BlockNoteView.tsx +++ b/packages/react/src/editor/BlockNoteView.tsx @@ -34,7 +34,6 @@ import { import { Portals, getContentComponent } from "./EditorContent.js"; import { ElementRenderer } from "./ElementRenderer.js"; -import { mergeRefs } from "../util/mergeRefs.js"; import "./styles.css"; const emptyFn = () => { @@ -91,20 +90,6 @@ export type BlockNoteViewProps< children?: ReactNode; ref?: Ref | undefined; // only here to get types working with the generics. Regular form doesn't work - - /** - * An element to portal floating UI elements (menus, toolbars) into, - * escaping any `overflow: hidden` ancestors. The caller is responsible - * for theming the element (adding `bn-root`, color scheme classes, etc.). - * - * When omitted, a default portal container is created at `document.body` or the shadowRoot of the editor. - */ - portalRoot?: HTMLElement; - - /** - * A ref to the portal root element. - */ - portalRootRef?: Ref; } & BlockNoteDefaultUIProps; function BlockNoteViewComponent< @@ -137,8 +122,6 @@ function BlockNoteViewComponent< comments, autoFocus, renderEditor = true, - portalRoot, - portalRootRef, ...rest } = props; @@ -165,58 +148,17 @@ function BlockNoteViewComponent< [editor], ); - // Portal container for floating UI elements (menus, toolbars) to render - // into, escaping any overflow:hidden ancestors. - // When portalContainer is provided externally, use it directly. - // Otherwise, create an internal element eagerly (no state, no rerender). - const internalPortalRoot = useMemo( - () => portalRoot ?? document.createElement("div"), - [portalRoot], - ); - useEffect(() => { - if (typeof portalRootRef === "function") { - portalRootRef(internalPortalRoot); - } else if (portalRootRef) { - portalRootRef.current = internalPortalRoot; + if (!editor.portalElement) { + throw new Error("Portal element not found"); } - }, [portalRootRef, internalPortalRoot]); - - // const portalRoot = portalContainer ?? internalPortalEl; - - useEffect(() => { - editor.portalElement = internalPortalRoot; - internalPortalRoot.className = mergeCSSClasses( + editor.portalElement.className = mergeCSSClasses( "bn-root", editorColorScheme, className || "", ); - internalPortalRoot.setAttribute("data-color-scheme", editorColorScheme); - }, [internalPortalRoot, editorColorScheme, className]); - - const internalRef = useCallback( - (element: HTMLDivElement | null) => { - if (portalRoot) { - // container was passed externally, - // we're not responsible for attaching it to a parent - return; - } - // container was created internally, attach it to the rootNode of the editor - const root = element?.getRootNode(); - const container = root instanceof ShadowRoot ? root : document.body; - if (container) { - container.appendChild(internalPortalRoot); - } else { - internalPortalRoot.remove(); - } - }, - [portalRoot, internalPortalRoot], - ); - - const mergedRef = useMemo( - () => mergeRefs([ref, internalRef]), - [ref, internalRef], - ); + editor.portalElement.setAttribute("data-color-scheme", editorColorScheme); + }, [editor, editorColorScheme, className]); // The BlockNoteContext makes sure the editor and some helper methods // are always available to nesteed compoenents @@ -226,9 +168,8 @@ function BlockNoteViewComponent< editor, setContentEditableProps, colorSchemePreference: editorColorScheme, - portalRoot: internalPortalRoot, }; - }, [existingContext, editor, editorColorScheme, internalPortalRoot]); + }, [existingContext, editor, editorColorScheme]); // We set defaultUIProps and editorProps on a different context, the BlockNoteViewContext. // This BlockNoteViewContext is used to render the editor and the default UI. @@ -272,7 +213,7 @@ function BlockNoteViewComponent< className={className} renderEditor={renderEditor} editorColorScheme={editorColorScheme} - ref={mergedRef} + ref={ref} {...rest} > {children} From c93b4682bf061dc71ed4811dda3b7cff0140e904 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 8 Apr 2026 13:11:27 +0200 Subject: [PATCH 11/11] fix tests --- packages/core/src/editor/BlockNoteEditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 7defaa4e87..411d5c4c53 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -681,7 +681,7 @@ export class BlockNoteEditor< */ public mount = (element: HTMLElement) => { const root = element.getRootNode(); - if (root instanceof ShadowRoot) { + if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot) { root.appendChild(this.portalElement); } else { document.body.appendChild(this.portalElement);