Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
03e86dd
Added demo
matthewlipski Aug 3, 2026
e45f5a1
Replaced demo toolbar with actual toolbar
matthewlipski Aug 6, 2026
d9902bf
Properly fixed dropdowns
matthewlipski Aug 7, 2026
af12646
- Fixed link button popover close dismissing virtual keyboard
matthewlipski Aug 7, 2026
1d4fbe3
Added transition to toolbar
matthewlipski Aug 12, 2026
07d9ff7
Replaced hook with pure CSS
matthewlipski Aug 13, 2026
faa72b5
Added docs
matthewlipski Aug 13, 2026
d070f14
Removed redundant `direction` prop
matthewlipski Aug 13, 2026
ece1f83
Merged main
matthewlipski Aug 13, 2026
e23b2f3
Refactored `useVirtualViewportRect`
matthewlipski Aug 14, 2026
57b68bf
Fixed some SSR issues
matthewlipski Aug 17, 2026
dfb89ac
Removed `portalRoot` prop
matthewlipski Aug 17, 2026
5a5fcd1
Merge branch 'main' into mobile-toolbar-demo
matthewlipski Aug 17, 2026
4b920a1
Implemented PR feedback
matthewlipski Aug 17, 2026
52933c2
Removed unused component
matthewlipski Aug 17, 2026
8a03207
Small fix
matthewlipski Aug 17, 2026
f779a4b
Increased mobile formatting toolbar tap target size
matthewlipski Aug 17, 2026
19428d4
- Fixed `getActiveStyles` with empty selections
matthewlipski Aug 20, 2026
a174fef
Refactored `FormattingToolbarController` to handle whether the deskto…
matthewlipski Aug 20, 2026
51aa828
Made mobile toolbar only appear when attached editor instance is focused
matthewlipski Aug 20, 2026
9c748cc
Fixed toolbar select desktop regression
matthewlipski Aug 20, 2026
3a95797
Increased required threshold to detect orientation change
matthewlipski Aug 20, 2026
69c6a71
Fixed formatting toolbar not opening when using touch-capable device …
matthewlipski Aug 20, 2026
a42e93c
Made mobile formatting toolbar center-aligned
matthewlipski Aug 20, 2026
1d0ef95
Small fix
matthewlipski Aug 20, 2026
e441113
docs: name the mobile toolbar layouts + add layout toggle to the exam…
YousefED Aug 20, 2026
09692f4
Merge remote-tracking branch 'origin/mobile-toolbar-demo' into mobile…
matthewlipski Aug 20, 2026
d4079d4
Made `.bn-scroll-host` class name apply all necessary styles automati…
matthewlipski Aug 20, 2026
b05f7ab
Added second editor to example
matthewlipski Aug 20, 2026
cef137d
Cached `isTouchDevice` result
matthewlipski Aug 20, 2026
be8838e
Cleaned up portal element context
matthewlipski Aug 21, 2026
51b89d0
Moved injected styles to stylesheet
matthewlipski Aug 21, 2026
83c2a00
Added `hide` middleware to `GenericPopover`
matthewlipski Aug 21, 2026
22c7645
Minor fixes
matthewlipski Aug 25, 2026
9a9f218
Fixed popovers
matthewlipski Aug 25, 2026
384ffd2
Implemented PR feedback
matthewlipski Aug 25, 2026
5696c41
Small fix
matthewlipski Aug 26, 2026
b54dcf5
Fixed Android gap
matthewlipski Aug 27, 2026
2656d73
Small fixes
matthewlipski Aug 28, 2026
0f80e42
Small docs change
matthewlipski Aug 28, 2026
852849f
Merge branch 'main' into mobile-toolbar-demo
YousefED Aug 28, 2026
0ec8997
Updated docs
matthewlipski Aug 31, 2026
b43ca12
Made formatting toolbar smooth in docs & playground
matthewlipski Aug 31, 2026
9feb488
Added warning when `interactive-widget` is not set
matthewlipski Sep 1, 2026
e0e3e90
Added `PortalContext`
matthewlipski Sep 2, 2026
3773025
Refactored `PortalContext` to `PortalTarget`
matthewlipski Sep 3, 2026
9b2b740
Refactored `PortalContext` to `PortalTarget`
matthewlipski Sep 3, 2026
3c9c0b6
Fixed build
matthewlipski Sep 4, 2026
72fce8f
Updated naming
matthewlipski Sep 4, 2026
0fdf553
Fixed build
matthewlipski Sep 4, 2026
543523d
Fixed naming omissions
matthewlipski Sep 4, 2026
f69f5c9
Always pass `editorPortalElement` to `portalRoot`
matthewlipski Sep 4, 2026
85b9029
refactor(react): one vocabulary and simpler plumbing for portal elements
YousefED Sep 4, 2026
473642d
refactor(ui): keep the portalElement prop's plain name in the UI libr…
YousefED Sep 4, 2026
4ca626d
Added tests
matthewlipski Sep 4, 2026
9e7dfa5
refactor(core)!: drop the portal option from editor.mount
YousefED Sep 5, 2026
bbb5ddf
small fixes
YousefED Sep 5, 2026
ceb93bd
refactor: require portalRoot and never fall back to the body
YousefED Sep 5, 2026
3eb01f7
Merge branch 'portal-root-always-pass' into portals-cleanup-v2
YousefED Sep 5, 2026
c63532a
fix: pass the required portal element in the SettingsSelect examples
YousefED Sep 5, 2026
ea7aef9
Merge branch 'portals-context' into portals-cleanup-v2
YousefED Sep 5, 2026
17eca72
test(mantine): pin the render profile of BlockNoteView around portals
YousefED Sep 5, 2026
e3dde99
Merge branch 'main' into mobile-toolbar-demo
YousefED Sep 5, 2026
355ddc4
Merge branch 'mobile-toolbar-demo' into portals-cleanup-v2
YousefED Sep 5, 2026
7c50b00
fix(mantine,ariakit): style portalled menus by their own class
YousefED Sep 5, 2026
4150a35
feat(react): render a floating component's menus inside its wrapper
YousefED Sep 5, 2026
4dbe297
fix(mantine): drop the toolbar focus trap
YousefED Sep 5, 2026
4d3a67c
test(mantine): let commits settle before measuring the render profile
YousefED Sep 5, 2026
9cfade6
fix(react): portal floating UI next to the editor element by default
YousefED Sep 5, 2026
ef72729
docs(react): drop the null portal target from the doc comments
YousefED Sep 6, 2026
3351b2e
chore(docs): allow only the private 172.16/12 range for dev origins
YousefED Sep 6, 2026
27f48e8
Merge branch 'mobile-toolbar-demo' into portals-cleanup-v2
YousefED Sep 6, 2026
05d2e3b
feat(react): warn about the viewport meta tag from the view, in devel…
YousefED Sep 6, 2026
aaef894
docs(examples): show the mobile toolbar example in the playground
YousefED Sep 6, 2026
bd0537a
fix(react): a nested BlockNoteView resolves its own portal element
YousefED Sep 6, 2026
36e4a47
docs(react): state the preventFocusOnOpen contract
YousefED Sep 6, 2026
d94a980
test: link toolbar create and edit flows per skin
YousefED Sep 6, 2026
261d5f6
Merge branch 'mobile-toolbar-demo' into portals-cleanup-v2
YousefED Sep 7, 2026
3265ebc
Merge branch 'mobile-toolbar-demo' into portals-cleanup-v2
matthewlipski Sep 18, 2026
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
Prev Previous commit
Next Next commit
Refactored FormattingToolbarController to handle whether the deskto…
…p or mobile version should be used
  • Loading branch information
matthewlipski committed Aug 20, 2026
commit a174fef451a04cd35d0499e7c8dd8356a7a08646
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
blockHasType,
BlockSchema,
defaultProps,
DefaultProps,
InlineContentSchema,
StyleSchema,
} from "@blocknote/core";
import { FormattingToolbarExtension } from "@blocknote/core/extensions";
import { flip, offset, shift } from "@floating-ui/react";
import { FC, useMemo } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useEditorState } from "../../hooks/useEditorState.js";
import { useExtension, useExtensionState } from "../../hooks/useExtension.js";
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
import { PositionPopover } from "../Popovers/PositionPopover.js";
import { FormattingToolbar } from "./FormattingToolbar.js";
import { FormattingToolbarProps } from "./FormattingToolbarProps.js";

const textAlignmentToPlacement = (
textAlignment: DefaultProps["textAlignment"],
) => {
switch (textAlignment) {
case "left":
return "top-start";
case "center":
return "top";
case "right":
return "top-end";
default:
return "top-start";
}
};

export const DesktopFormattingToolbarController = (props: {
formattingToolbar?: FC<FormattingToolbarProps>;
floatingUIOptions?: FloatingUIOptions;
/**
* Override the DOM node this floating element portals into. Falls back to
* `editor.portalElement` (which by default is mounted inside `bn-container`)
* when omitted.
*/
portalElement?: HTMLElement | null;
}) => {
const editor = useBlockNoteEditor<
BlockSchema,
InlineContentSchema,
StyleSchema
>();
const formattingToolbar = useExtension(FormattingToolbarExtension, {
editor,
});
const show = useExtensionState(FormattingToolbarExtension, {
editor,
});

const position = useEditorState({
editor,
selector: ({ editor }) =>
formattingToolbar.store.state
? {
from: editor.prosemirrorState.selection.from,
to: editor.prosemirrorState.selection.to,
}
: undefined,
});

const placement = useEditorState({
editor,
selector: ({ editor }) => {
const block = editor.getTextCursorPosition().block;

if (
!blockHasType(block, editor, block.type, {
textAlignment: defaultProps.textAlignment,
})
) {
return "top-start";
} else {
return textAlignmentToPlacement(block.props.textAlignment);
}
},
});

const floatingUIOptions = useMemo<FloatingUIOptions>(
() => ({
...props.floatingUIOptions,
useFloatingOptions: {
open: show,
// Needed as hooks like `useDismiss` call `onOpenChange` to change the
// open state.
onOpenChange: (open, _event, reason) => {
formattingToolbar.store.setState(open);

if (reason === "escape-key") {
editor.focus();
}
},
placement,
middleware: [offset(10), shift(), flip()],
...props.floatingUIOptions?.useFloatingOptions,
},
focusManagerProps: {
disabled: true,
...props.floatingUIOptions?.focusManagerProps,
},
elementProps: {
style: {
zIndex: 40,
},
...props.floatingUIOptions?.elementProps,
},
}),
[show, placement, props.floatingUIOptions, formattingToolbar.store, editor],
);

const Component = props.formattingToolbar || FormattingToolbar;

return (
<PositionPopover
position={position}
portalElement={props.portalElement}
{...floatingUIOptions}
>
{show && <Component />}
</PositionPopover>
);
};
Original file line number Diff line number Diff line change
@@ -1,37 +1,10 @@
import {
blockHasType,
BlockSchema,
defaultProps,
DefaultProps,
InlineContentSchema,
StyleSchema,
} from "@blocknote/core";
import { FormattingToolbarExtension } from "@blocknote/core/extensions";
import { flip, offset, shift } from "@floating-ui/react";
import { FC, useMemo } from "react";
import { isTouchDevice } from "@blocknote/core";
import { FC } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useEditorState } from "../../hooks/useEditorState.js";
import { useExtension, useExtensionState } from "../../hooks/useExtension.js";
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
import { PositionPopover } from "../Popovers/PositionPopover.js";
import { FormattingToolbar } from "./FormattingToolbar.js";
import { DesktopFormattingToolbarController } from "./DesktopFormattingToolbarController.js";
import { FormattingToolbarProps } from "./FormattingToolbarProps.js";

const textAlignmentToPlacement = (
textAlignment: DefaultProps["textAlignment"],
) => {
switch (textAlignment) {
case "left":
return "top-start";
case "center":
return "top";
case "right":
return "top-end";
default:
return "top-start";
}
};
import { MobileFormattingToolbarController } from "./MobileFormattingToolbarController.js";

export const FormattingToolbarController = (props: {
formattingToolbar?: FC<FormattingToolbarProps>;
Expand All @@ -43,87 +16,13 @@ export const FormattingToolbarController = (props: {
*/
portalElement?: HTMLElement | null;
}) => {
const editor = useBlockNoteEditor<
BlockSchema,
InlineContentSchema,
StyleSchema
>();
const formattingToolbar = useExtension(FormattingToolbarExtension, {
editor,
});
const show = useExtensionState(FormattingToolbarExtension, {
editor,
});

const position = useEditorState({
editor,
selector: ({ editor }) =>
formattingToolbar.store.state
? {
from: editor.prosemirrorState.selection.from,
to: editor.prosemirrorState.selection.to,
}
: undefined,
});

const placement = useEditorState({
editor,
selector: ({ editor }) => {
const block = editor.getTextCursorPosition().block;

if (
!blockHasType(block, editor, block.type, {
textAlignment: defaultProps.textAlignment,
})
) {
return "top-start";
} else {
return textAlignmentToPlacement(block.props.textAlignment);
}
},
});

const floatingUIOptions = useMemo<FloatingUIOptions>(
() => ({
...props.floatingUIOptions,
useFloatingOptions: {
open: show,
// Needed as hooks like `useDismiss` call `onOpenChange` to change the
// open state.
onOpenChange: (open, _event, reason) => {
formattingToolbar.store.setState(open);

if (reason === "escape-key") {
editor.focus();
}
},
placement,
middleware: [offset(10), shift(), flip()],
...props.floatingUIOptions?.useFloatingOptions,
},
focusManagerProps: {
disabled: true,
...props.floatingUIOptions?.focusManagerProps,
},
elementProps: {
style: {
zIndex: 40,
},
...props.floatingUIOptions?.elementProps,
},
}),
[show, placement, props.floatingUIOptions, formattingToolbar.store, editor],
);

const Component = props.formattingToolbar || FormattingToolbar;
if (isTouchDevice()) {
return (
<MobileFormattingToolbarController
formattingToolbar={props.formattingToolbar}
/>
);
}

return (
<PositionPopover
position={position}
portalElement={props.portalElement}
{...floatingUIOptions}
>
{show && <Component />}
</PositionPopover>
);
return <DesktopFormattingToolbarController {...props} />;
};
9 changes: 2 additions & 7 deletions packages/react/src/editor/BlockNoteDefaultUI.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isTouchDevice } from "@blocknote/core";
import { CommentsExtension } from "@blocknote/core/comments";
import {
FilePanelExtension,
Expand All @@ -12,7 +11,6 @@ import { lazy, Suspense } from "react";

import { FilePanelController } from "../components/FilePanel/FilePanelController.js";
import { FormattingToolbarController } from "../components/FormattingToolbar/FormattingToolbarController.js";
import { MobileFormattingToolbarController } from "../components/FormattingToolbar/MobileFormattingToolbarController.js";
import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarController.js";
import { SideMenuController } from "../components/SideMenu/SideMenuController.js";
import { AttributionTooltipController } from "../components/AttributionTooltip/AttributionTooltipController.js";
Expand Down Expand Up @@ -121,14 +119,11 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) {
return (
<>
{editor.getExtension(FormattingToolbarExtension) &&
props.formattingToolbar !== false &&
(isTouchDevice() ? (
<MobileFormattingToolbarController />
) : (
props.formattingToolbar !== false && (
<FormattingToolbarController
portalElement={formattingToolbarPortal}
/>
))}
)}
{editor.getExtension(LinkToolbarExtension) &&
props.linkToolbar !== false && (
<LinkToolbarController portalElement={linkToolbarPortal} />
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export * from "./components/FormattingToolbar/DefaultButtons/TableCellMergeButto
export * from "./components/FormattingToolbar/DefaultButtons/TextAlignButton.js";
export * from "./components/FormattingToolbar/DefaultSelects/BlockTypeSelect.js";
export * from "./components/FormattingToolbar/FormattingToolbar.js";
export * from "./components/FormattingToolbar/DesktopFormattingToolbarController.js";
export * from "./components/FormattingToolbar/FormattingToolbarController.js";
export * from "./components/FormattingToolbar/MobileFormattingToolbarController.js";
export * from "./editor/PortalContext.js";
Expand Down