From c1829484ec49b9f80a3de90710f2b970e2251355 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 18 Sep 2026 14:13:43 +0200 Subject: [PATCH 1/3] Updated documentation --- .../docs/getting-started/vanilla-js.mdx | 4 ++- .../react/components/formatting-toolbar.mdx | 10 ++++--- docs/content/docs/react/components/index.mdx | 4 ++- docs/content/docs/react/overview.mdx | 27 +++++++++++++++++++ .../react/styling-theming/overriding-css.mdx | 4 ++- docs/content/docs/reference/editor/events.mdx | 21 +++++++++++++++ .../reference/editor/manipulating-content.mdx | 4 +-- .../docs/reference/editor/overview.mdx | 10 ++++++- .../20-portal-elements/README.md | 1 + packages/core/src/editor/BlockNoteEditor.ts | 7 ++--- .../core/src/editor/managers/EventManager.ts | 18 ++++++------- playground/src/examples.gen.tsx | 2 +- 12 files changed, 89 insertions(+), 23 deletions(-) diff --git a/docs/content/docs/getting-started/vanilla-js.mdx b/docs/content/docs/getting-started/vanilla-js.mdx index 2ec0e2fbb5..2b937925ea 100644 --- a/docs/content/docs/getting-started/vanilla-js.mdx +++ b/docs/content/docs/getting-started/vanilla-js.mdx @@ -42,7 +42,9 @@ Now, you'll have a plain BlockNote instance on your page. However, it's missing ## Creating your own UI elements -Because you can't use the built-in React [UI Components](/docs/react/components), you'll need to create and register your own UI elements. +Because you can't use the built-in React [UI Components](/docs/react/components), you'll need to create your own UI elements. + +Make sure to call `editor.registerPortalElement(container)`/`editor.unregisterPortalElement(container)` when each element is added/removed to/from the DOM. This ensures that `editor.isWithinEditor` works correctly. Each UI element is backed by an [extension](/docs/features/extensions). You can retrieve an extension instance from the editor with `editor.getExtension(...)`, and each one exposes a store that holds its current state (visibility, position, and any element-specific data). A store is a small observable container with these primary members: `state` to read the current value, `setState` to update it, and `subscribe` to be notified of changes. The available UI element extensions are: diff --git a/docs/content/docs/react/components/formatting-toolbar.mdx b/docs/content/docs/react/components/formatting-toolbar.mdx index 569e298d6a..3069bfc087 100644 --- a/docs/content/docs/react/components/formatting-toolbar.mdx +++ b/docs/content/docs/react/components/formatting-toolbar.mdx @@ -1,11 +1,11 @@ --- title: Formatting Toolbar -description: The Formatting Toolbar appears whenever you highlight text in the editor. +description: Format selected text or use the mobile toolbar above the on-screen keyboard. --- # Formatting Toolbar -The Formatting Toolbar appears whenever you highlight text in the editor. +The Formatting Toolbar appears above or below highlighted text on desktop. On touch devices with the on-screen keyboard open, it appears above the keyboard instead. ` and cover its full area. BlockNo Your app should only ever have a single `bn-scroll-container` element. It's pinned to the visual viewport with `position: fixed`, so multiple containers would overlap each other. Wrap all your scrollable page content in one. + +The standalone [Mobile Formatting Toolbar example](https://playground.blocknotejs.org/ui-components/mobile-formatting-toolbar?hideMenu=true) lets you switch between document scrolling and the pinned scroll container to compare their behavior. diff --git a/docs/content/docs/react/components/index.mdx b/docs/content/docs/react/components/index.mdx index c068e16ec7..79dd203ddb 100644 --- a/docs/content/docs/react/components/index.mdx +++ b/docs/content/docs/react/components/index.mdx @@ -31,6 +31,8 @@ By default, the floating components (formatting toolbar, side menu, slash menu, /> ``` -Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. All keys, including `default`, update reactively. See the [Portal Targets example](/examples/ui-components/portal-elements). +Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`, `attributionTooltip`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. All keys, including `default`, update reactively. See the [Portal Targets example](/examples/ui-components/portal-elements). + +The [mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar) always portals to `document.body`, including its menus and popovers. It does not use the configured formatting-toolbar or default target. When a target sits outside the editor's DOM (like `document.body`), BlockNote automatically renders a themed wrapper element inside it, so floating UI keeps the editor's styling and theming wherever it's portalled. diff --git a/docs/content/docs/react/overview.mdx b/docs/content/docs/react/overview.mdx index b85d29ab21..b744a65913 100644 --- a/docs/content/docs/react/overview.mdx +++ b/docs/content/docs/react/overview.mdx @@ -133,6 +133,33 @@ declare function useEditorSelectionChange( ): BlockNoteEditor; ``` +### useEditorFocus + +The `useEditorFocus` hook returns a boolean and re-renders your component when the editor gains or loses focus. By default, it tracks the content area. Pass `includeEditorUI: true` to keep it `true` while focus moves into the editor's toolbars, menus, or popovers. + +```tsx twoslash +/** + * See the [Editor API reference](/docs/reference/editor/overview) for more details + */ +type BlockNoteEditor = object; +/** + * This hook tracks whether the editor is focused. + */ +// ---cut--- +declare function useEditorFocus( + options?: { + /** + * Include focus within the editor's toolbars, menus, and popovers. + * Defaults to false. + */ + includeEditorUI?: boolean; + }, + editor?: BlockNoteEditor, +): boolean; +``` + +The options and editor arguments are optional. When called inside `BlockNoteView`, the hook can use the editor from context. To run a side effect on focus changes, subscribe to [`editor.onFocusChange`](/docs/reference/editor/events#onfocuschange). + ## Next Steps The editor is now ready to use! Start typing and explore the various block types and formatting options available in the toolbar. diff --git a/docs/content/docs/react/styling-theming/overriding-css.mdx b/docs/content/docs/react/styling-theming/overriding-css.mdx index bece286f4d..c8f3d23549 100644 --- a/docs/content/docs/react/styling-theming/overriding-css.mdx +++ b/docs/content/docs/react/styling-theming/overriding-css.mdx @@ -21,7 +21,7 @@ BlockNote uses classes with the `bn-` prefix to style editor elements. Here are #### Editor Structure -- `.bn-root`: Container class both the floating menus / toolbars and the editor +- `.bn-root`: Themed root for the editor or portalled UI. An editor can have multiple roots. - `.bn-container`: Container around `.bn-editor` - `.bn-editor`: Main editor element (the "contenteditable"). - `.bn-block`: Individual block element (including nested). @@ -36,6 +36,8 @@ BlockNote uses classes with the `bn-` prefix to style editor elements. Here are - `.bn-drag-handle-menu`: Drag handle menu. - `.bn-suggestion-menu`: Suggestion menu. +Menus and popovers may be portalled outside the toolbar or button that opened them. Target their own classes under `.bn-root` rather than relying on a toolbar or `.bn-container` ancestor. See [Configuring Portal Targets](/docs/react/components#configuring-portal-targets). + ### BlockNote CSS Attributes BlockNote uses data attributes to target specific block types and properties: diff --git a/docs/content/docs/reference/editor/events.mdx b/docs/content/docs/reference/editor/events.mdx index 137c479b55..facac2a8f4 100644 --- a/docs/content/docs/reference/editor/events.mdx +++ b/docs/content/docs/reference/editor/events.mdx @@ -14,6 +14,7 @@ The editor emits events for: - **Editor lifecycle** - When the editor is created, mounted, unmounted, etc. - **Content changes** - When blocks are inserted, updated, or deleted - **Selection changes** - When the cursor position or selection changes +- **Focus changes** - When focus enters or leaves the editor ## `onMount` @@ -52,6 +53,26 @@ editor.onSelectionChange((editor) => { }); ``` +## `onFocusChange` + +The `onFocusChange` callback receives the editor and a context containing `focused` (a boolean) and `event` (the triggering DOM `FocusEvent`). By default, it reports focus entering or leaving the content area. + +Pass `includeEditorUI: true` to count focus within the editor's toolbars, menus, and popovers too. In this mode, the callback runs only when that combined state changes, allowing focus to settle so moving from the content area into a popover input does not report a blur. + +```typescript +const unsubscribe = editor.onFocusChange( + (editor, { focused }) => { + console.log("Interacting with editor:", focused); + }, + { includeEditorUI: true }, +); + +// When you no longer need the listener: +unsubscribe(); +``` + +For the current focus state, use [`editor.isFocused()`](/docs/reference/editor/overview#focus). In React, use [`useEditorFocus`](/docs/react/overview#useeditorfocus) when focus determines what to render. + ## `onChange` The `onChange` callback is called whenever the editor's content changes. This is the primary way to track modifications to the document. diff --git a/docs/content/docs/reference/editor/manipulating-content.mdx b/docs/content/docs/reference/editor/manipulating-content.mdx index 1a9c97c222..27b9bc0e70 100644 --- a/docs/content/docs/reference/editor/manipulating-content.mdx +++ b/docs/content/docs/reference/editor/manipulating-content.mdx @@ -390,7 +390,7 @@ if (selectedText) { getActiveStyles(): Styles ``` -Returns the active text styles at the current cursor position or at the end of the current selection. +Returns the active text styles at the current cursor position or at the end of the current selection. With an empty selection, this includes styles enabled for upcoming text: for example, turning on bold before typing makes `activeStyles.bold` true even when the surrounding text is not bold. ```typescript const activeStyles = editor.getActiveStyles(); @@ -413,7 +413,7 @@ if (activeStyles.textColor) { getSelectedLinkUrl(): string | undefined ``` -Returns the URL of the last link in the current selection, or `undefined` if no links are selected. +Returns the URL of the link the current selection starts in, or `undefined` if it does not start in a link. ```typescript const linkUrl = editor.getSelectedLinkUrl(); diff --git a/docs/content/docs/reference/editor/overview.mdx b/docs/content/docs/reference/editor/overview.mdx index 086d91c754..a754ddcd4b 100644 --- a/docs/content/docs/reference/editor/overview.mdx +++ b/docs/content/docs/reference/editor/overview.mdx @@ -24,12 +24,20 @@ To focus the editor, you can use the `focus` method. editor.focus(); ``` -Check if the editor has focus. +Check if the editor's content area has focus: ```ts const isFocused = editor.isFocused(); ``` +By default, `isFocused()` returns `false` when focus moves into a toolbar, menu, or popover. Pass `includeEditorUI: true` to include the editor's UI: + +```ts +const isInteractingWithEditor = editor.isFocused({ includeEditorUI: true }); +``` + +These methods read the current focus state. To respond to focus changes, use [`onFocusChange`](/docs/reference/editor/events#onfocuschange), or [`useEditorFocus`](/docs/react/overview#useeditorfocus) to render React UI based on focus. + ## Undo/Redo To undo the last operation, you can use the `undo` method. diff --git a/examples/03-ui-components/20-portal-elements/README.md b/examples/03-ui-components/20-portal-elements/README.md index 63e1ba7c55..1c98052d4a 100644 --- a/examples/03-ui-components/20-portal-elements/README.md +++ b/examples/03-ui-components/20-portal-elements/README.md @@ -11,3 +11,4 @@ This example renders two editors side-by-side, both wrapped in a small `overflow **Relevant Docs:** - [UI Components](/docs/react/components) +- [Mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 6c08091468..bc30d58092 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1397,12 +1397,13 @@ export class BlockNoteEditor< } /** - * A callback function that runs whenever the editor's content area gains or - * loses DOM focus. + * A callback function that runs when focus changes. By default, this reports + * when the editor's content area gains or loses DOM focus. * * Note that `focused: false` only means the content area itself blurred — * focus may have moved into the editor's own UI (e.g. a toolbar - * popover's input). + * popover's input). Pass `includeEditorUI: true` to report changes to + * combined content and UI focus, allowing focus handoffs to settle. * * @param callback The callback to execute. * @returns A function to remove the callback. diff --git a/packages/core/src/editor/managers/EventManager.ts b/packages/core/src/editor/managers/EventManager.ts index dccf1b57f7..3da09afb74 100644 --- a/packages/core/src/editor/managers/EventManager.ts +++ b/packages/core/src/editor/managers/EventManager.ts @@ -21,8 +21,8 @@ export type Unsubscribe = () => void; */ export type EditorFocusOptions = { /** - * When true, the editor's own UI - toolbars, menus and popovers, i.e. - * everything portalled into `editor.portalElement` - counts as focused, + * When true, the editor's own UI - toolbars, menus and popovers inside + * its DOM boundary or registered portal elements - counts as focused, * answering "is the user still interacting with this editor?" rather than * "does the content area hold DOM focus?". * @@ -112,9 +112,9 @@ export class EventManager< * Settled focus-within-UI tracking. Document-level listeners (attached on * editor mount, detached on unmount — a no-op per focus event is too * cheap to be worth gating on subscribers) cover the case tiptap events - * can't: focus moving from the editor's own UI (which lives in - * `editor.portalElement`, outside the content area) to somewhere else - * entirely. Blur-side changes are re-checked a frame later because + * can't: focus moving from the editor's own UI outside the content area + * (including registered portal elements) to somewhere else entirely. + * Blur-side changes are re-checked after the current task because * `document.activeElement` transiently becomes `` during focus * handoffs (and `relatedTarget` is unreliable on mobile). */ @@ -215,13 +215,13 @@ export class EventManager< } /** - * Register a callback that will be called when the editor's content area - * gains or loses DOM focus. + * Register a callback for focus changes. By default, this reports when the + * editor's content area gains or loses DOM focus. * * Note that `focused: false` only means the content area itself blurred — * focus may have moved into the editor's own UI (e.g. a toolbar - * popover's input). Consumers that need to distinguish should check where - * `document.activeElement` ended up. + * popover's input). Pass `includeEditorUI: true` to report changes to + * combined content and UI focus, allowing focus handoffs to settle. */ public onFocusChange( callback: ( diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 734bb05299..d477e40852 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -889,7 +889,7 @@ export const examples = { slug: "ui-components", }, readme: - "By default, BlockNote's floating UI elements (formatting toolbar, slash menu, table handles, etc.) mount inside the editor's `bn-container`. The `portalElements` prop on `BlockNoteView` lets you change that — globally via `default`, or per element by key.\n\nThis example renders two editors side-by-side, both wrapped in a small `overflow: hidden` container. The left editor uses the default — the slash menu is clipped by the editor's bounds. The right editor passes `portalElements={{ default: document.body }}` so floating UI escapes the wrapper and renders fully.\n\n```tsx\n\n```\n\n**Relevant Docs:**\n\n- [UI Components](/docs/react/components)", + "By default, BlockNote's floating components (formatting toolbar, slash menu, table handles, etc.) mount next to the editor, inside its `bn-container` (or inside whatever you render `BlockNoteViewEditor` into). The `portalElements` prop on `BlockNoteView` lets you change that: globally via `default`, or per component by key. The menus and popovers a floating component opens follow it wherever it mounts.\n\nThis example renders two editors side-by-side, both wrapped in a small `overflow: hidden` container. The left editor uses the default, so the slash menu is clipped by the editor's bounds. The right editor passes `portalElements={{ default: document.body }}` so the floating components escape the wrapper and render fully.\n\n```tsx\n\n```\n\n**Relevant Docs:**\n\n- [UI Components](/docs/react/components)\n- [Mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar)", }, ], }, From b5face04795d7035ecf693a36abd6160b531ebea Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 18 Sep 2026 15:36:08 +0200 Subject: [PATCH 2/3] Implemented PR feedback --- docs/content/docs/getting-started/index.mdx | 2 +- docs/content/docs/react/components/formatting-toolbar.mdx | 4 ++-- .../05-side-menu-drag-handle-items/src/App.tsx | 2 +- .../index.html | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/getting-started/index.mdx b/docs/content/docs/getting-started/index.mdx index 8030c5f89a..012ab45c07 100644 --- a/docs/content/docs/getting-started/index.mdx +++ b/docs/content/docs/getting-started/index.mdx @@ -69,7 +69,7 @@ For more information about the `useCreateBlockNote` hook and the `BlockNoteView` ## Mobile compatibility -If your app is used on touch devices, it's good practice to add `interactive-widget=resizes-content` to your page's viewport meta tag: +If your app is used on touch devices, we recommend adding `interactive-widget=resizes-content` to your page's viewport meta tag: ```html ` and cover its full area. BlockNote will keep its size in sync with the [visual viewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport) and render the Formatting Toolbar outside of it, eliminating any jitter/lag. - Your app should only ever have a single `bn-scroll-container` element. It's pinned to the visual viewport with `position: fixed`, so multiple containers would overlap each other. Wrap all your scrollable page content in one. + Your app should only ever have a single `bn-scroll-container` element. Wrap all your scrollable page content in one. The standalone [Mobile Formatting Toolbar example](https://playground.blocknotejs.org/ui-components/mobile-formatting-toolbar?hideMenu=true) lets you switch between document scrolling and the pinned scroll container to compare their behavior. diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx index ec50019e0c..d1e073134e 100644 --- a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx +++ b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx @@ -13,7 +13,7 @@ import { import { ResetBlockTypeItem } from "./ResetBlockTypeItem"; -// To avoid rendering issues, it's good practice to define your custom drag +// To avoid rendering issues, we recommend defining your custom drag // handle menu in a separate component, instead of inline within the `sideMenu` // prop of `SideMenuController`. const CustomDragHandleMenu = () => ( diff --git a/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html b/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html index 4d59fc32bf..875c480942 100644 --- a/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html +++ b/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html @@ -1,7 +1,10 @@ - + Exporting documents to PDF (react-pdf, deprecated)