feat: Implement full-width search results dropdown with dynamic positioning

This commit is contained in:
2026-06-21 11:52:47 +01:00
parent 49f636a7d7
commit 0c8b3a0e5d
4 changed files with 78 additions and 3 deletions

View File

@@ -155,5 +155,6 @@
<script src="js/software-cover-batch-upload.js"></script>
<script src="js/software-screenshot-upload.js"></script>
<script src="js/software-screenshot-batch-upload.js"></script>
<script src="js/search-dropdown-positioning.js"></script>
</body>
</html>

View File

@@ -10,7 +10,9 @@
@inject SearchService SearchService
@inject NavigationManager Nav
@inject StringLocalizer<NavMenu> L
@inject IJSRuntime JS
<div id="search-bar-anchor">
<MudAutocomplete T="SearchResultDto"
@bind-Value="_selected"
@bind-Value:after="OnSelected"
@@ -32,6 +34,7 @@
AdornmentColor="Color.Default"
OnAdornmentClick="OnAdornmentClick"
OnKeyDown="OnKeyDown"
RelativeWidth="DropdownWidth.Ignore"
PopoverClass="search-popover-fullwidth"
Style="background-color: rgba(255,255,255,0.15); border-radius: 4px;"
@ref="_autocomplete">
@@ -81,12 +84,18 @@
</MudStack>
</NoItemsTemplate>
</MudAutocomplete>
</div>
@code {
SearchResultDto _selected;
string _lastQuery = string.Empty;
MudAutocomplete<SearchResultDto> _autocomplete;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender) await JS.InvokeVoidAsync("SearchDropdownPositioning.attach", "search-bar-anchor");
}
Task<IEnumerable<SearchResultDto>> SearchAsync(string value, CancellationToken ct)
=> SearchAsyncCore(value, ct);

View File

@@ -300,10 +300,13 @@
}
/* Full-width search results dropdown in top bar.
MudAutocomplete normally anchors the popover to match the input width.
This class overrides that to span from the search box to the right edge of the screen. */
These are just the initial values for first paint — MudBlazor's own JS
actively rewrites this popover's inline `left`/`width` on open, resize, and
via its internal flip/collision logic, fighting any CSS-only override.
js/search-dropdown-positioning.js watches this element and re-asserts
`left` (anchored to the search box) and `right: 0; width: auto` (stretched
to the viewport's right edge) every time MudBlazor changes it. */
.search-popover-fullwidth {
left: auto !important;
right: 0 !important;
width: auto !important;
max-width: none !important;

View File

@@ -0,0 +1,62 @@
// Keeps the top-bar search results popover anchored to the search box's own
// left edge while letting it stretch to the right edge of the viewport.
//
// MudAutocomplete's popover JS actively re-measures and rewrites the popover's
// inline `left`/`width` on open, on resize, and via its own collision-avoidance
// ("flip") logic — it does this regardless of any CSS we apply, so a CSS-only
// `left` override gets clobbered moments after render. We instead watch the
// popover element itself and correct its inline style back to "anchored to the
// search box, stretched to the right edge" every time MudBlazor changes it.
window.SearchDropdownPositioning = (function () {
let attached = false;
function reposition(anchor, popover) {
const desiredLeft = anchor.getBoundingClientRect().left + "px";
if (popover.style.left !== desiredLeft) popover.style.setProperty("left", desiredLeft, "important");
if (popover.style.right !== "0px") popover.style.setProperty("right", "0px", "important");
if (popover.style.width !== "auto") popover.style.setProperty("width", "auto", "important");
if (popover.style.maxWidth !== "none") popover.style.setProperty("max-width", "none", "important");
}
function watch(anchor, popover) {
reposition(anchor, popover);
// Re-assert our positioning whenever MudBlazor's own JS touches the
// popover's style/class (open, close, resize, flip recalculation).
// Each call is a no-op once values already match, so this can't loop.
new MutationObserver(() => reposition(anchor, popover))
.observe(popover, { attributes: true, attributeFilter: ["style", "class"] });
window.addEventListener("resize", () => reposition(anchor, popover));
}
return {
attach: function (anchorId) {
if (attached) return;
const anchor = document.getElementById(anchorId);
if (!anchor) return;
const existing = document.querySelector(".search-popover-fullwidth");
if (existing) {
watch(anchor, existing);
attached = true;
return;
}
// The popover element doesn't exist until MudBlazor first renders
// it (typically on first focus/keystroke), so wait for it to show
// up in the DOM before attaching the real observer.
const bodyObserver = new MutationObserver(() => {
const popover = document.querySelector(".search-popover-fullwidth");
if (!popover) return;
bodyObserver.disconnect();
watch(anchor, popover);
attached = true;
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
}
};
})();