Skip to content

Fix positioning issues of callouts (#11183)#12302

Open
msynk wants to merge 3 commits intobitfoundation:developfrom
msynk:11183-blazorui-callout-positioning-issues
Open

Fix positioning issues of callouts (#11183)#12302
msynk wants to merge 3 commits intobitfoundation:developfrom
msynk:11183-blazorui-callout-positioning-issues

Conversation

@msynk
Copy link
Copy Markdown
Member

@msynk msynk commented May 4, 2026

closes #11183

Summary by CodeRabbit

  • Refactor
    • Restructured internal DOM organization and overlay element management across popover-based components (menu buttons, dropdowns, date pickers, time pickers, breadcrumbs, and search boxes). Changes maintain all existing functionality while improving how overlays and callouts are positioned within the component hierarchy.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Walkthrough

Multiple Blazor callout-based components now render their overlay and callout elements as DOM siblings outside their root wrappers instead of nesting them inside. Each component tracks a dedicated _overlayId and passes it to JavaScript interop, enabling the JS layer to move callouts and overlays to document.body when opened and restore them when closed, addressing styling issues in nested callout scenarios.

Changes

Callout Repositioning & Overlay Tracking Refactor

Layer / File(s) Summary
Field Addition
src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor.cs
New private _overlayId field added to each component to track a dedicated overlay element identifier.
Lifecycle Initialization
Same 10 .razor.cs files
During OnInitialized() or OnInitializedAsync(), each component now initializes _overlayId (e.g., "BitMenuButton-{UniqueId}-overlay", "{_datePickerId}-overlay").
Razor Markup Restructure
src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor, src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor, src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor, src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor, src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor, src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor, src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor, src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor
Overlay and callout <div> elements moved from being nested inside root containers to rendering as DOM siblings after the root container closes. Each overlay now includes id="@_overlayId".
DatePicker Formatting
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor
Whitespace and formatting reformatting; no functional changes to conditional rendering or UI logic.
JS Interop Contract
src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalloutsJsRuntimeExtensions.cs
BitCalloutToggleCallout<T> method signature extended with new string overlayId parameter, passed to JavaScript toggle call.
Core Toggle Implementation
src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts
Callouts.toggle now moves callout and overlay to document.body on open (via moveCalloutToBody) and restores them to original parent on close (via restoreCalloutToOriginalParent). New _calloutOriginalParents map tracks original DOM positions and next sibling references. Callouts.replaceCurrent also restores previous callout instead of only hiding it.
Test Updates
src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/DropMenu/BitDropMenuTests.cs
Expected markup in BitDropMenuShouldRespectHtmlAttributes test updated to reflect new overlay id attribute and adjusted class whitespace formatting.

Sequence Diagram

sequenceDiagram
    participant User as User/UI
    participant Component as Blazor Component
    participant DOM as Original DOM
    participant JS as JavaScript Layer
    participant Body as document.body

    Note over Component: Open Callout
    User->>Component: Toggle Callout
    Component->>Component: Initialize _overlayId (if needed)
    Component->>JS: BitCalloutToggleCallout(calloutId, overlayId, isOpen=true)
    
    rect rgba(100, 200, 100, 0.5)
    Note over JS: Move to body on open
    JS->>JS: moveCalloutToBody(calloutId, overlayId)
    JS->>JS: Store original parent & nextSibling
    JS->>DOM: Save callout's original placement
    JS->>DOM: Save overlay's original placement
    JS->>Body: appendChild(callout)
    JS->>Body: appendChild(overlay)
    end
    
    JS->>JS: Position & size callout
    JS->>Component: Callout positioned at body level
    Component->>User: Overlay + Callout visible

    Note over Component: Close Callout
    User->>Component: Close Callout
    Component->>JS: BitCalloutToggleCallout(calloutId, overlayId, isOpen=false)
    
    rect rgba(200, 100, 100, 0.5)
    Note over JS: Restore from body on close
    JS->>JS: restoreCalloutToOriginalParent(calloutId)
    JS->>JS: Retrieve original parent & nextSibling
    JS->>DOM: Reinsert callout at original position
    JS->>DOM: Reinsert overlay at original position
    JS->>JS: reset() - hide callout & overlay
    end
    
    JS->>Component: Restored & hidden
    Component->>User: Callout back in original DOM location
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hopping through the DOM so free,
Callouts dance from branch to tree,
To document.body they leap and bound,
Restoring peace where nesting's found!
No more style wars, no more pain—
Just siblings shining clear as rain. 🌙

🚥 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 'Fix positioning issues of callouts' directly addresses the main objective of the PR, which is to resolve callout positioning/styling issues when components are nested.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #11183 by restructuring DOM placement and adding overlay tracking across multiple components, enabling proper callout positioning when nested.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing callout positioning: DOM restructuring, overlay ID tracking, and JS interop updates. No unrelated functionality or refactoring detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@msynk msynk marked this pull request as ready for review May 5, 2026 20:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses nested Bit component callout styling/positioning issues (e.g., BitSearchBox inside BitPanel) by ensuring callout elements (and their overlays) can be moved out of potentially clipping/positioned parent DOM contexts and positioned consistently.

Changes:

  • Update the shared callout JS logic to temporarily move callout (and overlay) elements to document.body, then restore them to their original DOM positions when closed.
  • Extend the callout JS interop contract to pass an overlayId, and update multiple components to generate/render unique overlay element IDs.
  • Adjust a bUnit markup test to reflect the updated DOM structure for BitDropMenu.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/DropMenu/BitDropMenuTests.cs Updates expected rendered markup to match new overlay/callout DOM placement.
src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts Moves callout/overlay nodes to body on open and restores them on close; updates current-callout replacement behavior.
src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalloutsJsRuntimeExtensions.cs Adds overlayId to the JS interop method signature and invocation.
src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor.cs Generates a stable overlay element ID and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor Renders overlay with an id so JS can move/restore it alongside the callout.
src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor Renders overlay with an id and places overlay/callout outside the root wrapper.
src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor Renders overlay with an id and places overlay/callout outside the root wrapper.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor Renders overlay with an id so JS can move/restore it.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor Renders overlay with an id and places overlay/callout outside the root wrapper.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor Renders overlay with an id and places overlay/callout outside the root wrapper.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor Renders overlay with an id so JS can move/restore it.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor Renders overlay with an id so JS can move/restore it.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor Renders overlay with an id so JS can move/restore it.
src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor.cs Adds overlay ID generation and passes it to JS toggle.
src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor Renders overlay with an id and places overlay/callout outside the root wrapper.

Comment on lines +54 to +55
Callouts.moveCalloutToBody(calloutId, callout, overlayId);

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts`:
- Around line 196-207: The current insertBefore calls can throw NotFoundError if
recorded siblings were removed; update the insertion logic around
callout/original.overlay so that before calling insertBefore you verify the
sibling is still a child of the expected parent (e.g., check
original.nextSibling && original.nextSibling.parentNode === original.parent) and
only then call original.parent.insertBefore(callout, original.nextSibling),
otherwise fall back to original.parent.appendChild(callout); apply the same
guard for original.overlayNextSibling/original.overlayParent when inserting
original.overlay to avoid using a stale reference.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9ff7efca-49cf-4c04-91f1-45d2174ef4d8

📥 Commits

Reviewing files that changed from the base of the PR and between beefafa and 1f2a760.

📒 Files selected for processing (23)
  • src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor
  • src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalloutsJsRuntimeExtensions.cs
  • src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts
  • src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/DropMenu/BitDropMenuTests.cs

Comment on lines +196 to +207
if (original.nextSibling) {
original.parent.insertBefore(callout, original.nextSibling);
} else {
original.parent.appendChild(callout);
}

if (original.overlay && original.overlayParent) {
if (original.overlayNextSibling) {
original.overlayParent.insertBefore(original.overlay, original.overlayNextSibling);
} else {
original.overlayParent.appendChild(original.overlay);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all callout/overlay sibling orderings and nearby structure in Razor files.
# This helps confirm whether saved nextSibling references can point to moved nodes.
rg -n -C2 '_calloutId|_overlayId' --type-add 'razor:*.razor' -t razor

Repository: bitfoundation/bitplatform

Length of output: 16874


🏁 Script executed:

cat -n src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts | head -250 | tail -100

Repository: bitfoundation/bitplatform

Length of output: 4877


🏁 Script executed:

rg -n "restoreCalloutToOriginalParent" src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts

Repository: bitfoundation/bitplatform

Length of output: 351


🏁 Script executed:

sed -n '40,60p' src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts

Repository: bitfoundation/bitplatform

Length of output: 894


🏁 Script executed:

sed -n '1,100p' src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts | grep -A 5 -B 5 "try\|catch"

Repository: bitfoundation/bitplatform

Length of output: 51


🏁 Script executed:

rg -n "try|catch|NotFoundError" src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts

Repository: bitfoundation/bitplatform

Length of output: 51


Guard insertBefore against stale reference siblings.

original.nextSibling and original.overlayNextSibling may no longer be children of their recorded parent (e.g., if they were removed from the DOM), and insertBefore will throw NotFoundError, breaking callout close and switch flows.

Suggested hardening
-            if (original.nextSibling) {
-                original.parent.insertBefore(callout, original.nextSibling);
-            } else {
-                original.parent.appendChild(callout);
-            }
+            if (original.nextSibling && original.parent.contains(original.nextSibling)) {
+                original.parent.insertBefore(callout, original.nextSibling);
+            } else {
+                original.parent.appendChild(callout);
+            }

             if (original.overlay && original.overlayParent) {
-                if (original.overlayNextSibling) {
-                    original.overlayParent.insertBefore(original.overlay, original.overlayNextSibling);
-                } else {
-                    original.overlayParent.appendChild(original.overlay);
-                }
+                if (original.overlayNextSibling && original.overlayParent.contains(original.overlayNextSibling)) {
+                    original.overlayParent.insertBefore(original.overlay, original.overlayNextSibling);
+                } else {
+                    original.overlayParent.appendChild(original.overlay);
+                }
             }
📝 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
if (original.nextSibling) {
original.parent.insertBefore(callout, original.nextSibling);
} else {
original.parent.appendChild(callout);
}
if (original.overlay && original.overlayParent) {
if (original.overlayNextSibling) {
original.overlayParent.insertBefore(original.overlay, original.overlayNextSibling);
} else {
original.overlayParent.appendChild(original.overlay);
}
if (original.nextSibling && original.parent.contains(original.nextSibling)) {
original.parent.insertBefore(callout, original.nextSibling);
} else {
original.parent.appendChild(callout);
}
if (original.overlay && original.overlayParent) {
if (original.overlayNextSibling && original.overlayParent.contains(original.overlayNextSibling)) {
original.overlayParent.insertBefore(original.overlay, original.overlayNextSibling);
} else {
original.overlayParent.appendChild(original.overlay);
}
}
🤖 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/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts` around lines 196 - 207, The
current insertBefore calls can throw NotFoundError if recorded siblings were
removed; update the insertion logic around callout/original.overlay so that
before calling insertBefore you verify the sibling is still a child of the
expected parent (e.g., check original.nextSibling &&
original.nextSibling.parentNode === original.parent) and only then call
original.parent.insertBefore(callout, original.nextSibling), otherwise fall back
to original.parent.appendChild(callout); apply the same guard for
original.overlayNextSibling/original.overlayParent when inserting
original.overlay to avoid using a stale reference.

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.

Styling issue when using Bit components with Callout

2 participants