Skip to content

refactor(react): one vocabulary and simpler plumbing for portal elements - #3053

Draft
YousefED wants to merge 2 commits into
portals-contextfrom
portals-cleanup-v2
Draft

refactor(react): one vocabulary and simpler plumbing for portal elements#3053
YousefED wants to merge 2 commits into
portals-contextfrom
portals-cleanup-v2

Conversation

@YousefED

@YousefED YousefED commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

A rework of #3046, opened against portals-context so the diff is only the delta: 42 files, +503 / −389. (#3052 is the same branch opened against main, where the diff also contains the mobile-toolbar stack underneath.)

It keeps #3046's model — portal elements registered on the editor, themed roots for foreign targets, portalElements reaching every UI element — and changes how it is named and wired.

Rationale

Two things came out of reviewing #3046. The vocabulary had four stems for what are really two concepts — portal element / portal root / portal context / editor portal. And the mounting logic was doing more React work than it needed to, in a place where re-render and ordering bugs are expensive and hard to spot.

Changes

One vocabulary, two concepts.

  • portalElement everywhere: PortalElement, portalElements, resolvePortalElement, usePortalElement, PortalElementOverride, editor.registerPortalElement / unregisterPortalElement, mount(el, { portalElement }), and the portalElement prop on every popover, menu and select (was portalRoot). Where a forwarded prop and the ambient element coexist in one scope they are portalElementProp and portalElement.
  • Themed root (.bn-root) becomes a single function rather than a props object: applyThemedRoot(element) on BlockNoteViewContext. ThemedRootProps is removed.

Theming goes back to where it was.

BlockNoteTheme.ts is restored byte-identical to mobile-toolbar-demoapplyBlockNoteCSSVariablesFromTheme and removeBlockNoteCSSVariables untouched, including their set/unset behaviour. The mantine view keeps its existing ref={applyThemeVariables} for the container and passes the same logic down as applyThemedRoot for portal roots, replacing the useEffect that themed editor.portalElement. Net: base plus 12 lines, instead of a rewrite.

Simpler plumbing.

  • usePortalElement derives the default from the editor's own container, so BlockNoteViewContainer returns to its original shape — no useState, no merged refs, no provider wrapper, no useEditorDOMElement coupling in BlockNoteView.
  • PortalElementOverride creates its themed root directly and mounts it in a layout effect: three plain effects (append, theme, register). That removes the createPortal of an empty div, the ref/state round-trip, the needsContainer state, and the closest(".bn-root") DOM read during render.

Impact

Behavioural improvement: the default portal element now resolves to the editor's .bn-container rather than the contenteditable's parentElement. Under renderEditor={false} those differ — the app's own wrapper is the parent — so floating UI could otherwise be clipped by that wrapper's overflow.

Testing

Measured in browser mode (chromium):

  • Theming reaches portal roots and follows changes: a root in a foreign target goes bn-root light bn-mantinebn-root dark bn-mantine, data-color-scheme and data-mantine-color-scheme both following a live toggle — the bug feat: Portals cleanup #3046 set out to fix, preserved.
  • Override path: one render, never null, attached before paint, zero children left after unmount.
  • Default path: resolves to the real .bn-container in the plain case, in the renderEditor={false} + wrapper case, and yields to an explicit override.
  • Render cost: a portal consumer renders twice on mount (null → attached), against three on this base. mobile-toolbar-demo renders once but hands over a detached element, with no notification when it attaches.

E2E chromium: 294 passed, 1 failed — static.test.tsx timing out at 90s. It passes standalone in 2.4s and in CI, which shards chromium into halves; it only times out when all 48 files share one local container.

Not yet run: firefox/webkit, and the mobile end-to-end suite, which lives above this layer.

Open question

The default portal element costs consumers one extra render at mount. Making it an imperatively created div inside the container removes that (measured: one render, never null, attached before paint) at the cost of one empty DOM node — the node mobile-toolbar-demo already had. About 15 lines if we want that trade.

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.
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
blocknote Error Error Sep 4, 2026 5:07pm UTC
blocknote-website Error Error Sep 4, 2026 5:07pm UTC

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@3053

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@3053

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@3053

@blocknote/diagram-block

npm i https://pkg.pr.new/@blocknote/diagram-block@3053

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@3053

@blocknote/math-block

npm i https://pkg.pr.new/@blocknote/math-block@3053

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@3053

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@3053

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@3053

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@3053

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@3053

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@3053

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@3053

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@3053

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@3053

commit: 85b9029

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://TypeCellOS.github.io/BlockNote/pr-preview/pr-3053/

Built to branch gh-pages at 2026-09-04 17:07 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

…aries

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant