Repository navigation
docs: name the mobile toolbar layouts + add layout toggle to the example - #2985
Conversation
Introduce "scrolling document" (default) vs "pinned scroll container" (opt-in) as the names for the two page layouts the mobile formatting toolbar supports, and restructure the docs section around them. - Docs: simple-first rewrite of the Mobile Formatting Toolbar section (default layout, then the opt-in layout with its two CSS rules). - Example: stop embedding it in the docs (`docs: false`) - its page-level CSS (html/body overflow, full-viewport fixed scroll host, `.prose` rules) leaks into the docs page since examples render inline. Link to the standalone playground example instead. - Example: add a nav-bar switch that toggles the pinned scroll container layout via a class on <html>, so both layouts can be compared. - Playground: `.mantine-AppShell-root` width 100vw -> 100%, which caused a horizontal scrollbar on any example taller than the viewport. - Align README, JSDoc and example comments with the new naming; regenerate examples.gen.tsx.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/diagram-block
@blocknote/mantine
@blocknote/math-block
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
|
matthewlipski
left a comment
There was a problem hiding this comment.
Good changes, left 2 comments
* Added demo * Replaced demo toolbar with actual toolbar * Properly fixed dropdowns * - Fixed link button popover close dismissing virtual keyboard - Made mobile toolbar no longer experimental & part of default UI - Updated example * Added transition to toolbar * Replaced hook with pure CSS * Added docs * Removed redundant `direction` prop * Refactored `useVirtualViewportRect` * Fixed some SSR issues * Removed `portalRoot` prop * Implemented PR feedback * Removed unused component * Small fix * Increased mobile formatting toolbar tap target size * - Fixed `getActiveStyles` with empty selections - Removed hover styles for mobile - Gated comment and link buttons to only show when selection is not empty * Refactored `FormattingToolbarController` to handle whether the desktop or mobile version should be used * Made mobile toolbar only appear when attached editor instance is focused * Fixed toolbar select desktop regression * Increased required threshold to detect orientation change * Fixed formatting toolbar not opening when using touch-capable device with external keyboard * Made mobile formatting toolbar center-aligned * Small fix * docs: name the mobile toolbar layouts + add layout toggle to the example (#2985) * docs: name the mobile toolbar layouts, add layout toggle to example Introduce "scrolling document" (default) vs "pinned scroll container" (opt-in) as the names for the two page layouts the mobile formatting toolbar supports, and restructure the docs section around them. - Docs: simple-first rewrite of the Mobile Formatting Toolbar section (default layout, then the opt-in layout with its two CSS rules). - Example: stop embedding it in the docs (`docs: false`) - its page-level CSS (html/body overflow, full-viewport fixed scroll host, `.prose` rules) leaks into the docs page since examples render inline. Link to the standalone playground example instead. - Example: add a nav-bar switch that toggles the pinned scroll container layout via a class on <html>, so both layouts can be compared. - Playground: `.mantine-AppShell-root` width 100vw -> 100%, which caused a horizontal scrollbar on any example taller than the viewport. - Align README, JSDoc and example comments with the new naming; regenerate examples.gen.tsx. * Implemented PR feedback --------- Co-authored-by: Matthew Lipski <matthewlipski@gmail.com> * Made `.bn-scroll-host` class name apply all necessary styles automatically * Added second editor to example * Cached `isTouchDevice` result * Cleaned up portal element context * Moved injected styles to stylesheet * Added `hide` middleware to `GenericPopover` * Minor fixes * Fixed popovers * Implemented PR feedback * Small fix * Fixed Android gap * Small fixes * Small docs change * Updated docs * Made formatting toolbar smooth in docs & playground * Added warning when `interactive-widget` is not set * chore(docs): allow only the private 172.16/12 range for dev origins Each `*` in `allowedDevOrigins` matches one address segment, so `172.*.*.*` admitted all of 172/8 rather than the private range the comment describes. Spell out the sixteen second segments of 172.16.0.0/12 instead. * feat(react): warn about the viewport meta tag from the view, in development only The check for `interactive-widget=resizes-content` moves out of `useVirtualKeyboard` into `BlockNoteViewEditor`'s mount effect, so it reaches developers on desktop and with a custom formatting toolbar alike. It lives in its own module (not re-exported from the package) and returns early when `process.env.NODE_ENV` is "production": the library build leaves that expression in place for the consumer's bundler, so production builds stay silent and a page without a bundler still gets the hint. * docs(examples): show the mobile toolbar example in the playground The playground already puts `bn-scroll-container` on its root, so the example's own container is nested inside it, and its "scrolling document" switch has to turn the page's container off. Both are CSS in the example: a nested container defers to the outer one, and a body class set by the switch neutralises any container on the page. The library stylesheet and the playground stay as they are. --------- Co-authored-by: Yousef <yousefdardiry@gmail.com>
…nts (#3052) * Added demo * Replaced demo toolbar with actual toolbar * Properly fixed dropdowns * - Fixed link button popover close dismissing virtual keyboard - Made mobile toolbar no longer experimental & part of default UI - Updated example * Added transition to toolbar * Replaced hook with pure CSS * Added docs * Removed redundant `direction` prop * Refactored `useVirtualViewportRect` * Fixed some SSR issues * Removed `portalRoot` prop * Implemented PR feedback * Removed unused component * Small fix * Increased mobile formatting toolbar tap target size * - Fixed `getActiveStyles` with empty selections - Removed hover styles for mobile - Gated comment and link buttons to only show when selection is not empty * Refactored `FormattingToolbarController` to handle whether the desktop or mobile version should be used * Made mobile toolbar only appear when attached editor instance is focused * Fixed toolbar select desktop regression * Increased required threshold to detect orientation change * Fixed formatting toolbar not opening when using touch-capable device with external keyboard * Made mobile formatting toolbar center-aligned * Small fix * docs: name the mobile toolbar layouts + add layout toggle to the example (#2985) * docs: name the mobile toolbar layouts, add layout toggle to example Introduce "scrolling document" (default) vs "pinned scroll container" (opt-in) as the names for the two page layouts the mobile formatting toolbar supports, and restructure the docs section around them. - Docs: simple-first rewrite of the Mobile Formatting Toolbar section (default layout, then the opt-in layout with its two CSS rules). - Example: stop embedding it in the docs (`docs: false`) - its page-level CSS (html/body overflow, full-viewport fixed scroll host, `.prose` rules) leaks into the docs page since examples render inline. Link to the standalone playground example instead. - Example: add a nav-bar switch that toggles the pinned scroll container layout via a class on <html>, so both layouts can be compared. - Playground: `.mantine-AppShell-root` width 100vw -> 100%, which caused a horizontal scrollbar on any example taller than the viewport. - Align README, JSDoc and example comments with the new naming; regenerate examples.gen.tsx. * Implemented PR feedback --------- Co-authored-by: Matthew Lipski <matthewlipski@gmail.com> * Made `.bn-scroll-host` class name apply all necessary styles automatically * Added second editor to example * Cached `isTouchDevice` result * Cleaned up portal element context * Moved injected styles to stylesheet * Added `hide` middleware to `GenericPopover` * Minor fixes * Fixed popovers * Implemented PR feedback * Small fix * Fixed Android gap * Small fixes * Small docs change * Updated docs * Made formatting toolbar smooth in docs & playground * Added warning when `interactive-widget` is not set * Added `PortalContext` * Refactored `PortalContext` to `PortalTarget` * Refactored `PortalContext` to `PortalTarget` * Fixed build * Updated naming * Fixed build * Fixed naming omissions * Always pass `editorPortalElement` to `portalRoot` * refactor(react): one vocabulary and simpler plumbing for portal elements Reworks the portal consolidation from #3046, keeping its model — portal elements registered on the editor, themed roots for foreign targets — and changing how it is named and wired. Naming: one stem, `portalElement`, from `editor.registerPortalElement` and `mount(el, { portalElement })` through `resolvePortalElement`, `usePortalElement` and `PortalElementOverride` to the `portalElement` prop on every popover, menu and select. `portalRoot`, `portalTarget`, `portalContext` and `editorPortal` are gone. Where a forwarded prop and the ambient element coexist they are `portalElementProp` and `portalElement`. Theming: `ThemedRootProps` is replaced by a single `applyThemedRoot(element)` on `BlockNoteViewContext`, composed from the base classes and whatever the UI library adds. The mantine wrapper keeps base's `applyThemeVariables` ref for the editor container and passes the same function down for portal roots, so `BlockNoteTheme.ts` is untouched. Plumbing: the default portal element is derived in `usePortalElement` from the editor's own container, so `BlockNoteViewContainer` needs no state, no merged refs and no provider wrapper. `PortalElementOverride` creates its themed root directly and mounts it in a layout effect, replacing a `createPortal` of an empty div, a ref/state round-trip and a `closest(".bn-root")` read during render. * refactor(ui): keep the portalElement prop's plain name in the UI libraries Inside a component that receives a `portalElement` prop, `portalElement` now means that prop, and the surrounding default from `usePortalElement` takes the qualified name. Reads more directly, and keeps the props destructures on one line as they are on the base branch. * Added tests * refactor(core)!: drop the portal option from editor.mount BREAKING: `editor.mount(element, { portalTarget })` loses its options argument. The option decided where `editor.portalElement` was appended, and that element no longer exists; it had already been reduced to an alias for `registerPortalElement`, which callers can call directly and explicitly: editor.mount(element); editor.registerPortalElement(someContainer); Only needed when floating UI renders outside the editor's DOM tree — UI next to the contenteditable already counts as within the editor. * small fixes * refactor: require portalRoot and never fall back to the body `portalRoot` is now required on `Menu.Root`, `Popover.Root` and the toolbar select. The UI-library adapters never read the editor's context, so they cannot fall back to it themselves; making the prop required lets the compiler enforce what was a convention. Call sites pass the ambient element as-is — `null` before the editor has mounted — instead of coercing it to `undefined`. What each adapter does with `null` follows its library, so none of them portal to the document body: - shadcn passes it through: Base UI waits for a container. - ariakit toggles `portal` off: it renders inline until there is one (Ariakit appends a fresh div to the body for null and undefined alike). - mantine already rendered inline on a falsy value. The shadcn tooltips (`ToolbarButton`, `Badge`) keep reading the ambient element through `useEditorPortalElement`. That is the one documented exception: mantine and ariakit tooltips render inline and would ignore a passed element, so a prop would buy nothing there. * fix: pass the required portal element in the SettingsSelect examples `vp run build` typechecks example projects that lint doesn't cover: the three `SettingsSelect` copies render a `Toolbar.Select` without the now required prop. They sit inside `BlockNoteView`, so they read the ambient element and pass it. Also drops the last `?? undefined` on a portal value: the shadcn `Badge` tooltip passes the element as-is, like `ToolbarButton` already does, so nothing falls back to the body. * test(mantine): pin the render profile of BlockNoteView around portals Portal setups — a `portalElements` default, a per-element target, a controller's `portalElement` — must add no React commits or child renders at mount, and cost the same as the default setup on an unrelated parent re-render, with and without StrictMode. Counts are relative to the default setup measured in the same run, so unrelated editor render changes don't break them. Colocated browser-mode test rather than end-to-end: it needs a real browser (jsdom schedules commits differently) but nothing from the e2e harness, and renders with `createRoot` + `act` since `vitest-browser-react` only exists in the tests package. Red on the previous portal implementation (5 commits vs 3 at mount with an external default target), green here and on the base branch. * fix(mantine,ariakit): style portalled menus by their own class Menus now portal out of the toolbar, side menu and table handle that open them, so rules scoped on those ancestors (`.bn-toolbar .mantine-Menu-item`, `.bn-side-menu .mantine-Menu-dropdown`, ...) stopped applying: 14px items, a missing min-width, an ariakit gap on every popover. Scope on the dropdowns' own classes instead, and keep the ariakit gap for form popovers only. The e2e drag-handle menu selector no longer assumes nesting either. * feat(react): render a floating component's menus inside its wrapper Every menu, popover and form a floating component (toolbar, side menu, table handle, ...) opens now portals into a zero-size anchor next to that component, inside the wrapper floating-ui positions. So they share its stacking context and visibility (the ariakit colors submenu paints above the drag handle without a z-index override; ariakit and shadcn dropdowns hide with their toolbar instead of staying orphaned when it scrolls away), follow it when `portalElements` relocates it, and, for the mobile toolbar, sit outside its scroll strip, which iOS WebKit would otherwise not paint. `GenericPopover`'s closing snapshot must ignore the anchor's holder, or a popover whose children are already gone would snapshot an empty wrapper and vanish instead of fading out. The adapter-private contexts in ariakit and shadcn are renamed so they no longer share a name with the react package's context. * fix(mantine): drop the toolbar focus trap Mantine's `useFocusTrap`, armed once focus was within the toolbar, moved focus back into the toolbar a tick after a menu or form opened. With those now portalled next to the toolbar rather than inside it, that stole focus from the link form's URL field (0 ms clicks in e2e, and always for the link toolbar's Edit button). Tab now moves through the buttons and on, as in the other skins. The color menu's deferred `editor.focus()` existed only to work around the trap. * test(mantine): let commits settle before measuring the render profile Mounting schedules an update from an effect that can commit after `act` has returned. On Linux WebKit whether it does varies from mount to mount, so the setup measured second sometimes counted one commit fewer or more than the baseline and the comparison failed (CI's webkit shard, 1 of 3 runs locally in Docker). Wait until no commit has landed for 50 ms before reading the counts, at mount and after the parent re-render, so every mount is measured once things have settled. * fix(react): portal floating UI next to the editor element by default `usePortalElement` fell back to the editor's `bn-container`, which is the editor element's parent in the default layout but not when a layout renders `BlockNoteViewEditor` itself: there the container may also hold a sidebar, and the table's extend button, sized to the table's full width, escaped the editor's scrolling pane and painted over the sidebar while the table stayed clipped. Fall back to the editor element's parent, as `mount()` did before: floating UI clips and scrolls with the editor, and `portalElements` remains the way to escape. * docs(react): drop the null portal target from the doc comments `PortalElement` is `HTMLElement | string` and `PortalElementOverride` takes `HTMLElement | undefined`, but two doc comments still promised that `null` means `document.body`. Nothing accepts it any more. * chore(docs): allow only the private 172.16/12 range for dev origins Each `*` in `allowedDevOrigins` matches one address segment, so `172.*.*.*` admitted all of 172/8 rather than the private range the comment describes. Spell out the sixteen second segments of 172.16.0.0/12 instead. * feat(react): warn about the viewport meta tag from the view, in development only The check for `interactive-widget=resizes-content` moves out of `useVirtualKeyboard` into `BlockNoteViewEditor`'s mount effect, so it reaches developers on desktop and with a custom formatting toolbar alike. It lives in its own module (not re-exported from the package) and returns early when `process.env.NODE_ENV` is "production": the library build leaves that expression in place for the consumer's bundler, so production builds stay silent and a page without a bundler still gets the hint. * docs(examples): show the mobile toolbar example in the playground The playground already puts `bn-scroll-container` on its root, so the example's own container is nested inside it, and its "scrolling document" switch has to turn the page's container off. Both are CSS in the example: a nested container defers to the outer one, and a body class set by the switch neutralises any container on the page. The library stylesheet and the playground stay as they are. * fix(react): a nested BlockNoteView resolves its own portal element A view rendered inside another view's floating UI (the comments composer, an editor in a custom block's popover) inherited the outer view's portal anchor or override through context. That element is registered with the outer editor only, so the nested editor's own menus and popovers counted as outside it for isWithinEditor and the focus tracking built on it. BlockNoteViewContainer now resets the portal element for its subtree to the editor's own default. * docs(react): state the preventFocusOnOpen contract When set, the UI library must not move focus into the surface when it opens; an input inside that asks for focus itself still gets it. * test: link toolbar create and edit flows per skin The link form tests now run for mantine, ariakit and shadcn: each skin's popover decides on its own how the URL field takes focus. The per-skin screenshot tests also wait for the link toolbar before comparing, since a missing toolbar stays within the 2% screenshot tolerance. --------- Co-authored-by: Matthew Lipski <matthewlipski@gmail.com>
Follow-up to #2939, targeting its branch. Docs/example-only — no behavior changes in
@blocknote/react(two JSDoc comments reworded).Why
The opt-in "smooth scrolling" setup was described three different ways (the docs called it a "workaround", the code comments "non-scrolling document", the example
.scroll-host), which made the docs section hard to follow. This PR gives the two page layouts names and restructures the section around them:html/bodylocked, a container pinned to the visual viewport scrolls instead, toolbar is truly fixed."Pinned scroll container" names the one thing the host app has to add; "locked document" would name the precondition instead, and "jank-free mode" the outcome.
Changes
Docs –
formatting-toolbar.mdxSimple-first rewrite of Mobile Formatting Toolbar: intro (on by default on touch devices, nothing to set up) → two-bullet summary naming both layouts →
### Scrolling document→### Pinned scroll containerwith the same two CSS rules as before, the--bn-vv-*vars (now also listing--bn-vv-scale) and the actionable trade-off (pull-to-refresh etc.). Headings give stable anchors (#scrolling-document,#pinned-scroll-container).The embedded example broke the docs page →
docs: falseThe docs render examples inline (
dynamic()import, no iframe), so this example's page-level CSS leaked into the docs page:html, body { overflow: hidden }, the full-viewportposition: fixed.scroll-host,.bn-container { height: 300px }(hits the other two examples on the page) and.prose h2/p(collides with fumadocs' typography class). On the #2939 Vercel preview the lorem ipsum and demo editor are fixed over the prose and the page can't scroll.Since a pinned-scroll-container layout inherently owns the whole page, it can't be embedded. Following the
editor-scrollable/comments.mdxprecedent, the example is nowdocs: falseand the docs link tohttps://playground.blocknotejs.org/ui-components/mobile-formatting-toolbar?hideMenu=true. If you'd rather keep an embed, it needs an iframe-based one.Example – layout toggle
A "Pinned scroll container" switch in the demo nav bar toggles the layout so both can be compared on a device. It's pure CSS:
App.tsxtoggles apinned-scroll-containerclass on<html>(on<html>rather than<body>so the lock rule can scope bothhtmlandbodywithout:has()), andstyle.cssscopes the two rules under it, labelled "step 1" / "step 2". Defaults to on; comments say a real app applies the rules unconditionally. Verified in iPhone emulation: class/overflow/positionflip correctly and the toolbar still lands above the keyboard with the host resized to the visual viewport.Known/inherent: with the playground menu visible, the full-viewport fixed
.scroll-hostsits under the playground sidebar (hence?hideMenu=truein the docs link).Playground –
.mantine-AppShell-root { width: 100vw }→100%Separable.
100vwincludes the vertical scrollbar, so any playground example taller than the viewport shows a horizontal scrollbar with classic/always-visible scrollbars (this example exposes it when the toggle is off; pinned mode only masks it because the document can't scroll). Reproduced with real scrollbars (clientWidth1265 vsscrollWidth1280) and verified fixed.Housekeeping
MobileFormattingToolbarController/useVirtualKeyboard, and example comments aligned with the naming (also fixes the staleDemoChrome→DummyUI.tsxreference).examples.gen.tsxregenerated viavp run gen— it was already stale vs. the example README on the branch.Verification
node docs/validate-links.mjs→ 0 errors; docs page renders (200) with the new anchors, no overlay.pnpm run lintclean.🤖 Generated with Claude Code