Skip to content

fix(menu): portal via ThemeContext refs only (#1100)#2020

Open
kotAPI wants to merge 1 commit into
mainfrom
fix/issue-1100-menu-portal-queryselector
Open

fix(menu): portal via ThemeContext refs only (#1100)#2020
kotAPI wants to merge 1 commit into
mainfrom
fix/issue-1100-menu-portal-queryselector

Conversation

@kotAPI

@kotAPI kotAPI commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Menu portal uses ThemeContext refs with document.body fallback
  • Removes querySelector usage
  • Updates renderWithPortal test helper to use Theme

Fixes #1100

Test plan

  • npm test -- --testPathPatterns=MenuPrimitive.test
  • npm test -- --testPathPatterns=DropdownMenu.test

Summary by CodeRabbit

  • Refactor
    • Enhanced portal root selection logic to support theme-aware container configuration.
    • Improved test utilities for more robust portal rendering validation in test environments.

Remove querySelector fallbacks and resolve menu portal root from
ThemeContext portalRootRef and containerRef. Update portal test helper
to wrap renders in Theme.

Fixes #1100
@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1171016

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

MenuPrimitivePortal is refactored to accept an optional container prop and resolve its portal root from that prop or ThemeContext refs (portalRootRef/containerRef), falling back to document.body, replacing the prior static querySelector. Render gating switches from rootElementFound to an isMounted flag set via useEffect. The renderWithPortal test utility is updated to wrap UI in Theme, call a new mockMatchMedia stub, and locate the portal root via a [data-rad-ui-portal-root] query.

Changes

Portal Root Selection and Test Utility

Layer / File(s) Summary
MenuPrimitivePortal: container prop, ThemeContext root, and isMounted gating
src/core/primitives/Menu/fragments/MenuPrimitivePortal.tsx
Adds container?: Element | null to MenuPrimitivePortalProps. Imports ThemeContext and replaces the one-shot document.querySelector('#rad-ui-theme-container') root selection with a useEffect that sets rootElementRef from container, ThemeContext.portalRootRef, ThemeContext.containerRef, or document.body in order, then sets isMounted. Render guard changes from rootElementFound to isMounted.
renderWithPortal: Theme wrapping, mockMatchMedia, data-attribute lookup
src/test-utils/portal.ts
Adds mockMatchMedia helper that stubs window.matchMedia with no-op listener methods. Updates renderWithPortal to call mockMatchMedia, render inside Theme, and retrieve the portal root via document.querySelector('[data-rad-ui-portal-root]'). Cleanup no longer removes a manually created container div.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • rad-ui/ui#1276: Directly modifies MenuPrimitivePortal.tsx with prior portal root selection logic that this PR supersedes.
  • rad-ui/ui#1540: Introduced the original renderWithPortal and related portal test helpers in src/test-utils/portal.ts that this PR updates.

Suggested reviewers

  • GoldGroove06

Poem

🐰 Hop, hop, the portal leaps,
No more stale queries while the theme sleeps!
ThemeContext whispers, "Use my ref!"
isMounted guards each rendered chef.
The test wraps snug in Theme's embrace,
A rabbit cheers from the right place! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically describes the main change: updating the menu portal to use ThemeContext refs only, eliminating reliance on DOM queries.
Linked Issues check ✅ Passed The PR addresses #1100 by updating portal root resolution to use ThemeContext refs (portalRootRef/containerRef) with document.body fallback instead of querySelector, ensuring portal components render as children of the provided theme root.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing portal root resolution in MenuPrimitivePortal and updating the test helper to support Theme-aware portal rendering, staying within the scope of issue #1100.

✏️ 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/issue-1100-menu-portal-queryselector

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

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

This report compares the PR with the base branch. "Δ" shows how the PR affects each metric.

Metric PR Δ
Statements 75.73% +0.00%
Branches 59.2% +0.03%
Functions 61.75% +0.00%
Lines 77.27% +0.01%

Coverage improved or stayed the same. Great job!

Run npm run coverage:ci locally for detailed reports and target untested areas to raise these numbers.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test-utils/portal.ts`:
- Line 21: The querySelector on the portalRoot variable is force-cast to
HTMLElement without verifying the element exists, which defers failures to
downstream code making them harder to diagnose. After the querySelector call in
the getPortalRoot function, add a null/undefined check and throw a descriptive
error immediately if the element with the data-rad-ui-portal-root attribute is
not found. This ensures failures are caught early at the source with a clear
error message.
- Around line 7-14: The mockMatchMedia function checks if matchMedia exists as a
function and returns early, but doesn't validate that the matchMedia
implementation provides the required methods (addEventListener and
removeEventListener) that are called by Theme. Enhance the validation logic to
not only check if matchMedia is a function, but also verify that it returns an
object with both addEventListener and removeEventListener methods before
deciding to skip the mock. Only skip the mock setup if the complete API shape is
present; otherwise, proceed with the jest.fn mock implementation to ensure all
required methods are available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc174966-1418-4883-bd4b-5f57c10305b5

📥 Commits

Reviewing files that changed from the base of the PR and between fe87b36 and 1171016.

📒 Files selected for processing (2)
  • src/core/primitives/Menu/fragments/MenuPrimitivePortal.tsx
  • src/test-utils/portal.ts

Comment thread src/test-utils/portal.ts
Comment on lines +7 to +14
if ('matchMedia' in window && typeof window.matchMedia === 'function') return;
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(() => ({
matches: false,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether other test setup files provide an incompatible partial matchMedia mock.
fd -i 'setup*.{ts,tsx,js,jsx}' . | xargs -r rg -n -C2 'matchMedia|addEventListener|removeEventListener|addListener|removeListener'

Repository: rad-ui/ui

Length of output: 464


🏁 Script executed:

# Fix 1: Inspect the actual portal.ts file to understand current implementation
cat -n src/test-utils/portal.ts

Repository: rad-ui/ui

Length of output: 3008


🏁 Script executed:

# Fix 2: Search for Theme's usage of matchMedia to understand the dependency
rg -n 'matchMedia|addEventListener.*media|removeEventListener.*media' --type ts --type tsx -A 2 -B 2 | head -100

Repository: rad-ui/ui

Length of output: 175


🏁 Script executed:

# Fix 3: Find setup files using correct glob patterns
fd -g '**/*setup*' -e ts -e tsx -e js -e jsx . | head -20

Repository: rad-ui/ui

Length of output: 167


🏁 Script executed:

# Fix 4: Search for other matchMedia mocks in test files
rg -n 'matchMedia' -A 5 -B 2 --type ts --type tsx --type js --type jsx | head -150

Repository: rad-ui/ui

Length of output: 175


🏁 Script executed:

# Search for Theme component to understand matchMedia usage
find . -name "Theme.ts*" -o -name "Theme.js*" | grep -i theme | head -5

Repository: rad-ui/ui

Length of output: 183


🏁 Script executed:

# Search for addEventListener/removeEventListener usage with matchMedia
rg -n 'addEventListener|removeEventListener' -B 5 -A 2 | grep -A 7 -B 5 'matchMedia'

Repository: rad-ui/ui

Length of output: 147


🏁 Script executed:

# Check setupTests.ts for other matchMedia mocks
cat -n src/setupTests.ts

Repository: rad-ui/ui

Length of output: 2876


🏁 Script executed:

# Broader search for all matchMedia references in codebase
rg 'matchMedia' -n -B 2 -A 2

Repository: rad-ui/ui

Length of output: 147


🏁 Script executed:

# Read the Theme component to understand matchMedia usage
cat -n ./src/components/ui/Theme/Theme.tsx

Repository: rad-ui/ui

Length of output: 4082


🏁 Script executed:

# Try a simpler rg search for matchMedia
rg 'matchMedia' -n

Repository: rad-ui/ui

Length of output: 147


🏁 Script executed:

# Check for addEventListener calls in the codebase
rg 'addEventListener' -n -A 1 -B 1 | head -50

Repository: rad-ui/ui

Length of output: 147


mockMatchMedia should validate API shape, not only function presence.

Line 7 returns early when matchMedia exists, but Theme (lines 62, 67) calls addEventListener and removeEventListener on the media query object. If the environment provides a partial matchMedia implementation lacking these methods, tests will fail with a runtime error.

Suggested fix
 const mockMatchMedia = () => {
-    if ('matchMedia' in window && typeof window.matchMedia === 'function') return;
-    Object.defineProperty(window, 'matchMedia', {
+    const hasCompatibleMatchMedia = (() => {
+        if (!('matchMedia' in window) || typeof window.matchMedia !== 'function') return false;
+        const media = window.matchMedia('(prefers-color-scheme: dark)');
+        return !!media && typeof media.addEventListener === 'function' && typeof media.removeEventListener === 'function';
+    })();
+
+    if (hasCompatibleMatchMedia) return;
+
+    Object.defineProperty(window, 'matchMedia', {
         writable: true,
         value: jest.fn().mockImplementation(() => ({
             matches: false,
-            addEventListener: jest.fn(),
-            removeEventListener: jest.fn()
+            addEventListener: jest.fn(),
+            removeEventListener: jest.fn(),
+            addListener: jest.fn(),
+            removeListener: jest.fn()
         }))
     });
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test-utils/portal.ts` around lines 7 - 14, The mockMatchMedia function
checks if matchMedia exists as a function and returns early, but doesn't
validate that the matchMedia implementation provides the required methods
(addEventListener and removeEventListener) that are called by Theme. Enhance the
validation logic to not only check if matchMedia is a function, but also verify
that it returns an object with both addEventListener and removeEventListener
methods before deciding to skip the mock. Only skip the mock setup if the
complete API shape is present; otherwise, proceed with the jest.fn mock
implementation to ensure all required methods are available.

Comment thread src/test-utils/portal.ts
const result = render(ui);
mockMatchMedia();
const result = render(React.createElement(Theme, null, ui));
const portalRoot = document.querySelector('[data-rad-ui-portal-root]') as HTMLElement;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fail fast when portal root is not found.

Line 21 force-casts query result to HTMLElement; if missing, downstream failures become harder to diagnose.

Suggested fix
-    const portalRoot = document.querySelector('[data-rad-ui-portal-root]') as HTMLElement;
+    const portalRoot = document.querySelector('[data-rad-ui-portal-root]');
+    if (!portalRoot) {
+        throw new Error('renderWithPortal: [data-rad-ui-portal-root] not found. Ensure Theme rendered correctly.');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const portalRoot = document.querySelector('[data-rad-ui-portal-root]') as HTMLElement;
const portalRoot = document.querySelector('[data-rad-ui-portal-root]');
if (!portalRoot) {
throw new Error('renderWithPortal: [data-rad-ui-portal-root] not found. Ensure Theme rendered correctly.');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test-utils/portal.ts` at line 21, The querySelector on the portalRoot
variable is force-cast to HTMLElement without verifying the element exists,
which defers failures to downstream code making them harder to diagnose. After
the querySelector call in the getPortalRoot function, add a null/undefined check
and throw a descriptive error immediately if the element with the
data-rad-ui-portal-root attribute is not found. This ensures failures are caught
early at the source with a clear error message.

@kotAPI

kotAPI commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Code review

LGTM. Matches project patterns for portal Theme refs, Floating UI prop merge, controlled-switch/lazy-mount/RTL tests, or focused bug fixes. No changes requested.

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.

BUG: Alert Dialog not spawning as a child to the provided root element

1 participant