Fix positioning issues of callouts (#11183)#12302
Fix positioning issues of callouts (#11183)#12302msynk wants to merge 3 commits intobitfoundation:developfrom
Conversation
WalkthroughMultiple 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 ChangesCallout Repositioning & Overlay Tracking Refactor
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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. |
| Callouts.moveCalloutToBody(calloutId, callout, overlayId); | ||
|
|
There was a problem hiding this comment.
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
📒 Files selected for processing (23)
src/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razorsrc/BlazorUI/Bit.BlazorUI/Components/Buttons/MenuButton/BitMenuButton.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/CircularTimePicker/BitCircularTimePicker.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razorsrc/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razorsrc/BlazorUI/Bit.BlazorUI/Components/Navs/Breadcrumb/BitBreadcrumb.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razorsrc/BlazorUI/Bit.BlazorUI/Components/Navs/DropMenu/BitDropMenu.razor.cssrc/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razorsrc/BlazorUI/Bit.BlazorUI/Components/Surfaces/Callout/BitCallout.razor.cssrc/BlazorUI/Bit.BlazorUI/Extensions/JsInterop/CalloutsJsRuntimeExtensions.cssrc/BlazorUI/Bit.BlazorUI/Scripts/Callouts.tssrc/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Navs/DropMenu/BitDropMenuTests.cs
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 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 razorRepository: bitfoundation/bitplatform
Length of output: 16874
🏁 Script executed:
cat -n src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.ts | head -250 | tail -100Repository: bitfoundation/bitplatform
Length of output: 4877
🏁 Script executed:
rg -n "restoreCalloutToOriginalParent" src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.tsRepository: bitfoundation/bitplatform
Length of output: 351
🏁 Script executed:
sed -n '40,60p' src/BlazorUI/Bit.BlazorUI/Scripts/Callouts.tsRepository: 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.tsRepository: 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.
| 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.
closes #11183
Summary by CodeRabbit