Skip to content

fix(playground-ui): make popup portal container resolution null-safe#17560

Merged
damien-schneider merged 1 commit into
mainfrom
fix/portal-container-null-sentinel
Jun 4, 2026
Merged

fix(playground-ui): make popup portal container resolution null-safe#17560
damien-schneider merged 1 commit into
mainfrom
fix/portal-container-null-sentinel

Conversation

@damien-schneider

@damien-schneider damien-schneider commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up hardening to #17556. Base UI's FloatingPortal treats container={null} as "not ready — render nothing", while undefined means "portal to document.body". Our PortalContainerContext defaulted to null, so popups opened outside a SideDialog (the model & provider pickers live in the chat composer, which has no PortalContainerProvider ancestor) resolved their portal container to null and mounted to no DOM node — they opened in state but rendered nowhere.

Change

  • usePortalContainer(container?) now resolves explicit → provider → undefined and never returns null; the "no container" sentinel is undefined to match Base UI's contract.
  • Dropped the scattered ?? undefined band-aids in Combobox, Select, Popover, and DropdownMenu — they now just call usePortalContainer(container).

Tests

  • portal-container.test.tsx guards the resolver contract (never null; normalizes a null provider; explicit wins).
  • combobox.test.tsx asserts the popup portals into document.body.
  • Verified these fail under the old null-leaking behavior (combobox open/select tests go red) and pass with the fix.

ELI5

Imagine a magical doorway that decides where popup menus (like dropdowns and options) should appear on the screen. This PR fixes a broken doorway that prevented popups from showing up when opened outside certain panels—now the doorway always works and shows popups in the right place (the main document area) as a fallback.

Problem

Base UI's FloatingPortal treats container={null} as "not ready — render nothing" while undefined means "portal to document.body". The PortalContainerContext was defaulting to null, which caused popups opened outside a SideDialog (such as model and provider pickers in the chat composer) to fail rendering because there was no valid DOM node to attach to.

Solution

The usePortalContainer hook was redesigned to:

  • Accept an optional container parameter that can explicitly override the default behavior
  • Resolve the final portal target with precedence: explicit argument → context provider value → undefined
  • Never return null; always return undefined as the "no container" sentinel to match Base UI's expectations

The PortalContainerContext now treats undefined as the default (fall back to document.body) and normalizes any incoming null values to undefined.

Changes Made

  • portal-container.tsx: Added PortalContainer type, updated usePortalContainer to accept an optional container override and exclude null from its return type, and normalized null values in the provider context
  • Combobox, Select, Popover, and DropdownMenu: Replaced manual fallback logic (container ?? portalContainer ?? undefined) with direct calls to usePortalContainer(container)
  • portal-container.test.tsx: Added comprehensive tests asserting the resolver contract (never returns null, normalizes null provider to undefined, explicit values override provider values)
  • combobox.test.tsx: Added test verifying popups mount into document.body when no portal container provider is present

Testing

Tests were added to verify the contract of the portal-container resolver and confirm that popups now mount correctly when opened outside a SideDialog. Tests were verified to fail under the previous null-leaking behavior and pass with the fix.

…r null

Base UI's FloatingPortal treats container={null} as "not ready, render
nothing", so Combobox/Select/Popover/DropdownMenu popups opened outside a
SideDialog mounted to no DOM node — the model and provider pickers in the
chat composer opened in state but rendered nowhere.

Move the null->undefined coercion into usePortalContainer so the resolver
can never emit null, and drop the per-consumer `?? undefined` band-aids.
Add regression tests for the resolver contract and the combobox portal
target. Follow-up hardening to #17556.
@changeset-bot

changeset-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2468b9e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@mastra/playground-ui Patch
@internal/playground Patch
mastra Patch
create-mastra Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment Jun 4, 2026 8:32am
mastra-playground-ui Ready Ready Preview, Comment Jun 4, 2026 8:32am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ba04740-5b51-4ab0-b6e0-f734709eda61

📥 Commits

Reviewing files that changed from the base of the PR and between 5532141 and 2468b9e.

📒 Files selected for processing (8)
  • .changeset/proud-gifts-appear.md
  • packages/playground-ui/src/ds/components/Combobox/combobox.test.tsx
  • packages/playground-ui/src/ds/components/Combobox/combobox.tsx
  • packages/playground-ui/src/ds/components/DropdownMenu/dropdown-menu.tsx
  • packages/playground-ui/src/ds/components/Popover/popover.tsx
  • packages/playground-ui/src/ds/components/Select/select.tsx
  • packages/playground-ui/src/ds/primitives/portal-container.test.tsx
  • packages/playground-ui/src/ds/primitives/portal-container.tsx

Walkthrough

Portal container resolution is refactored to normalize null to undefined, preventing silent popup failures. All portal-using components (Combobox, DropdownMenu, Popover, Select) are updated to use the new usePortalContainer(container) hook pattern, establishing consistent fallback behavior where popups default to document.body when no provider exists.

Changes

Portal Container and Component Portal Resolution

Layer / File(s) Summary
Portal container hook contract and type system
packages/playground-ui/src/ds/primitives/portal-container.tsx, packages/playground-ui/src/ds/primitives/portal-container.test.tsx
New PortalContainer type and updated usePortalContainer hook that accepts an optional override parameter, normalizes null to undefined, and guarantees a non-null return. Provider normalizes incoming null values. Test suite enforces the contract: no provider returns undefined, null values normalize to undefined, argument overrides provider value, and explicit container is returned as-is.
Component portal resolution updates
packages/playground-ui/src/ds/components/Combobox/combobox.tsx, packages/playground-ui/src/ds/components/DropdownMenu/dropdown-menu.tsx, packages/playground-ui/src/ds/components/Popover/popover.tsx, packages/playground-ui/src/ds/components/Select/select.tsx
Combobox, DropdownMenu (both submenu and main content), Popover, and Select replace previous manual fallback chains with unified usePortalContainer(container) calls, ensuring consistent portal container resolution across all dropdown and popover components.
Combobox portal rendering regression test
packages/playground-ui/src/ds/components/Combobox/combobox.test.tsx
New test case verifies that Combobox popup correctly portals into document.body when no portal container provider is present, confirming the fix for popups that previously failed to render outside a side panel.
Release documentation
.changeset/proud-gifts-appear.md
Changeset entry documents the patch fix for dropdown/combobox/select/popover popups failing to render when opened outside a side panel, noting that the portal-container resolver now falls back to document.body instead of returning an unrenderable value.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • mastra-ai/mastra#17556: Combobox dropdown fix and test updates directly overlap with the main PR's usePortalContainer null-to-undefined portal resolution changes.
  • mastra-ai/mastra#17479: Portal-container behavior refinements in this PR build on the retrieved PR's SideDialog-driven portal-container design for keeping popups clickable inside the drawer.
  • mastra-ai/mastra#16918: Both PRs modify SelectContent portal and container handling, with this PR switching to usePortalContainer(container) semantics.

Suggested labels

tests: green ✅, complexity: medium

Suggested reviewers

  • mfrachet
  • greglobinski
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(playground-ui): make popup portal container resolution null-safe' is concise (68 chars), uses imperative mood, includes a clear prefix, and accurately describes the main change: fixing portal container resolution to be null-safe.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/portal-container-null-sentinel

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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 and usage tips.

@dane-ai-mastra dane-ai-mastra Bot added the complexity: low Low-complexity PR label Jun 4, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for core contributor @damien-schneider.


PR complexity score

Factor Value Score impact
Files changed 8 +16
Lines changed 140 +6
Author merged PRs 87 -20
Test files changed Yes -10
Final score -8

Applied label: complexity: low


Changed test gate

Changed Test Gate is pending. The Changed Test Gate / changed-tests check will update the test label when it completes.

@damien-schneider

damien-schneider commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Some context on the root cause: base-ui's FloatingPortal treats container={null} and container={undefined} differently. null hits an early-return that intentionally renders nothing (the comment says "Wait for the container to be resolved if explicitly null"): https://github.com/mui/base-ui/blob/master/packages/react/src/floating-ui-react/components/FloatingPortal.tsx#L94-L129. That branch is for lazy refs, a container that isn't mounted yet, so it avoids falling back to body in the meantime. undefined is the "no container, use document.body" path.

We were the ones emitting null: PortalContainerContext defaulted to null (#17479's createContext<HTMLElement | null>(null)) and container ?? portalContainer forwarded it, so outside a SideDialog the popup rendered <X.Portal container={null}> and never mounted.

This fixes it at the source: the sentinel is undefined and usePortalContainer never returns null. We also collapse null to undefined in the provider since we don't rely on base-ui's lazy-wait (popups open after the drawer host resolves).

@damien-schneider damien-schneider merged commit 9140963 into main Jun 4, 2026
73 checks passed
@damien-schneider damien-schneider deleted the fix/portal-container-null-sentinel branch June 4, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: low Low-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants