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/editor/managers/ExtensionManager/extensions.ts b/packages/core/src/editor/managers/ExtensionManager/extensions.ts
index 3e87553d31..4364afaaa0 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 d9a3191c49..635929a756 100644
--- a/packages/core/src/extensions/SideMenu/SideMenu.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenu.ts
@@ -612,9 +612,6 @@ 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!;
-
// 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 (
@@ -623,11 +620,8 @@ export class SideMenuView<
// An element is hovered
event &&
event.target &&
- // Element is outside the editor
- !(
- editorWrapper === event.target ||
- editorWrapper.contains(event.target as HTMLElement)
- )
+ // 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/mantine/src/BlockNoteView.tsx b/packages/mantine/src/BlockNoteView.tsx
index 3b92da582a..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,17 +38,23 @@ export const BlockNoteView = <
};
},
) => {
- const { className, theme, ...rest } = props;
+ const { className, theme, editor, ...rest } = props;
const existingContext = useBlockNoteContext();
const systemColorScheme = usePrefersColorScheme();
const defaultColorScheme =
existingContext?.colorSchemePreference || systemColorScheme;
- const ref = useCallback(
- (node: HTMLDivElement | null) => {
+ const finalTheme =
+ typeof theme === "string"
+ ? theme
+ : defaultColorScheme !== "no-preference"
+ ? defaultColorScheme
+ : "light";
+
+ const applyThemeVariables = useCallback(
+ (node: HTMLElement | null) => {
if (!node) {
- // todo: clean variables?
return;
}
@@ -70,14 +76,15 @@ export const BlockNoteView = <
[defaultColorScheme, theme],
);
- const mantineContext = useContext(MantineContext);
+ 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 finalTheme =
- typeof theme === "string"
- ? theme
- : defaultColorScheme !== "no-preference"
- ? defaultColorScheme
- : "light";
+ const mantineContext = useContext(MantineContext);
const view = (
@@ -85,8 +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={ref}
+ ref={applyThemeVariables}
/>
);
diff --git a/packages/mantine/src/popover/Popover.tsx b/packages/mantine/src/popover/Popover.tsx
index 29564585ce..882a715cc0 100644
--- a/packages/mantine/src/popover/Popover.tsx
+++ b/packages/mantine/src/popover/Popover.tsx
@@ -11,18 +11,18 @@ 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..db078703f2 100644
--- a/packages/react/src/components/Comments/EmojiPicker.tsx
+++ b/packages/react/src/components/Comments/EmojiPicker.tsx
@@ -1,10 +1,8 @@
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";
-import { createPortal } from "react-dom";
-import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
export const EmojiPicker = (props: {
onEmojiSelect: (emoji: { native: string }) => void;
@@ -14,11 +12,15 @@ export const EmojiPicker = (props: {
const [open, setOpen] = useState(false);
const Components = useComponentsContext()!;
- const editor = useBlockNoteEditor();
- const blockNoteContext = useBlockNoteContext();
+ const blockNoteContext = useBlockNoteContext()!;
+ const portalRoot = blockNoteContext.editor?.portalElement;
+
+ if (!portalRoot) {
+ throw new Error("Portal root not found");
+ }
return (
-
+
{
@@ -39,28 +41,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 eda9ce3b5e..88defa0607 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,
UseFloatingOptions,
@@ -12,6 +13,7 @@ import {
} from "@floating-ui/react";
import { HTMLAttributes, ReactNode, useEffect, useRef } from "react";
+import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { FloatingUIOptions } from "./FloatingUIOptions.js";
export type GenericPopoverReference =
@@ -109,6 +111,11 @@ export const GenericPopover = (
children: ReactNode;
},
) => {
+ const editor = useBlockNoteEditor();
+ const portalRoot = editor?.portalElement;
+ if (!portalRoot) {
+ throw new Error("Portal element not found");
+ }
const {
whileElementsMounted: _whileElementsMounted,
...restFloatingOptions
@@ -186,7 +193,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,
},
@@ -203,27 +210,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/BlockNoteView.tsx b/packages/react/src/editor/BlockNoteView.tsx
index d810fafcbe..dd5fe957fb 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";
@@ -147,6 +148,18 @@ function BlockNoteViewComponent<
[editor],
);
+ useEffect(() => {
+ if (!editor.portalElement) {
+ throw new Error("Portal element not found");
+ }
+ editor.portalElement.className = mergeCSSClasses(
+ "bn-root",
+ editorColorScheme,
+ className || "",
+ );
+ 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
const blockNoteContext: BlockNoteContextValue = useMemo(() => {
@@ -226,7 +239,12 @@ 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;
}