Skip to content

feat: Remove useRegisterPortalRoot from ShadCN - #3054

Open
matthewlipski wants to merge 3 commits into
portals-contextfrom
portal-root-always-pass
Open

feat: Remove useRegisterPortalRoot from ShadCN#3054
matthewlipski wants to merge 3 commits into
portals-contextfrom
portal-root-always-pass

Conversation

@matthewlipski

Copy link
Copy Markdown
Collaborator

Summary

Generally, the components in the ariakit, mantine, and shadcn packages that are used for the ComponentsContext are supposed to be "dumb" adapters for primitives like buttons, menus, etc. In #3046, this contract was broken somewhat as ShadCN components required the use of useEditorPortalElement. This is because unlike the other 2 UI libs, which render dropdowns as siblings of their trigger elements, ShadCN portals them to body by default. This doesn't work for us as that puts them outside container elements which provide additional theming and styling (basically the same issue #3046 addresses). Therefore, the default portal location was overridden to the return of useEditorPortalElement.

To ensure consistency across UI libs then, this PR makes it so that the portalRoot prop, which tells an Ariakit/Mantine/ShadCN component if & where it should portal its dropdown to, is always passed the element returned by useEditorPortalElement.

Additionally, the preventFocusOnOpen prop has been added. This is so that for mobile, we can disable dropdowns from grabbing focus from the editor on open, as this would close the virtual keyboard. But on desktop, this should be enabled for better accessibility. This was previously set based on if portalRoot was defined, which isn't quite right. Yes, it works in our case because the only time we'd actually set portalRoot was for the mobile formatting toolbar. But there is no direct correlation between a dropdown needing to be portalled, and preventing focus from moving to a dropdown on open, so these 2 concerns have been separated.

Rationale

See above.

Changes

See above.

Impact

N/A

Testing

E2E snapshots will need updates, but I'm holding off on this until we decide whether this change is smth we want.

Screenshots/Video

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 1b03f78b-2cfe-46f6-8f7f-bfc9ccc31208

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 5, 2026 7:48am UTC
blocknote-website Error Error Sep 5, 2026 7:48am 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@3054

@blocknote/code-block

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

@blocknote/core

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

@blocknote/diagram-block

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

@blocknote/mantine

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

@blocknote/math-block

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

@blocknote/react

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

@blocknote/server-util

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

@blocknote/shadcn

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

@blocknote/xl-ai

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

@blocknote/xl-docx-exporter

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

@blocknote/xl-email-exporter

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

@blocknote/xl-multi-column

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

@blocknote/xl-odt-exporter

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

@blocknote/xl-pdf-exporter

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

commit: d47ab49

@YousefED YousefED left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this works it's SO much cleaner. Love both improvements!!

trapFocus={portalRoot ? false : undefined}
// Do not move focus to the dropdown when requested (mobile), as it blurs
// the editor's contentEditable and dismisses the on-screen keyboard.
trapFocus={preventFocusOnOpen ? false : undefined}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice improvement, so much better to have explicit naming like this

Comment thread packages/shadcn/src/menu/Menu.tsx Outdated
const container = portalRoot ?? editorPortalElement ?? undefined;
// The `portalRoot` supplied at the call site is a themed `.bn-root`, so the
// menu inherits light/dark mode instead of the document body's.
const container = useContext(PortalRootContext) ?? undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double check; but all scenarios keep working like this? because it feels like one option has been dropped here compared to container = portalRoot ?? editorPortalElement ?? undefined;

// Portal the tooltip into the ambient portal target (a themed `.bn-root`)
// so it inherits the editor's light/dark color scheme instead of the
// document body's.
const editorPortalElement = useEditorPortalElement();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's still a dependency on useEditorPortalElement here. can you check all occurences?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried, but was cumbersome for Toolbar and Badges. Let's keep these exceptions for now (added two comments in code)

@@ -48,6 +48,13 @@ type ToolbarSelectType = {
}[];
isDisabled?: boolean;
portalRoot?: HTMLElement | null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the pattern is now that we always pass portalRoot. In that case, maybe let's make this non-optional so we can validate the codebase does this correctly?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, see commit

`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.
`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.
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.

2 participants