Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
482feb8
DialogOptions are attached to the dialog reference on creation.
Jan 13, 2026
174f38b
Added CloseOnNavigation property to determine if the dialog should be…
Jan 13, 2026
c579082
Implement DialogOptions.CloseOnNavigation
Jan 13, 2026
77cf33a
Formatting
Jan 13, 2026
b63a2c2
Merge branch 'MudBlazor:dev' into fix-#10397
aaronleev Jan 13, 2026
3b9d5b3
Merge branch 'MudBlazor:dev' into fix-#10397
aaronleev Jan 14, 2026
89d6df5
Update DialogTests.cs
aaronleev Jan 14, 2026
e8262e7
Fix build issue.
Jan 14, 2026
5116f05
Fix whitespace.
Jan 14, 2026
919ba1c
Added NotifyLocationChanged so LocationChanged events can be tested.
Jan 14, 2026
094b100
DialogOptions can now be injected into the reference for future use.
Jan 14, 2026
eff238d
added test navigation.
Jan 14, 2026
0ecf569
Add doc example of the feature.
Jan 14, 2026
01cf77d
Split Unit test
Jan 14, 2026
64ea638
Revert
Jan 14, 2026
033f9ce
Revert
Jan 14, 2026
668eb68
Since mud services, specificly MudDialogProvider actaully interact wi…
Jan 14, 2026
88abb94
Merge remote-tracking branch 'origin/fix-#10397' into fix-#10397
Jan 14, 2026
4f52097
Added testable check for CloseOnNavigation usage.
Jan 14, 2026
721c1a0
Add tests for InjectOptions, SetOptions, and ShouldDismissOnNavigation
Jan 14, 2026
fa6fea7
Whitespace.
Jan 14, 2026
c916666
Code coverage.
Jan 14, 2026
481e6c3
Whitespace.
Jan 14, 2026
df6e6df
Code coverage.
Jan 14, 2026
b601329
Test for null CloseOnNavigation
Jan 14, 2026
e52e787
Whitespace.
Jan 14, 2026
5feaf46
Merge branch 'dev' into fix-#10397
aaronleev Jan 14, 2026
3695a5a
Merge branch 'dev' into fix-#10397
aaronleev Jan 16, 2026
4883b85
Update src/MudBlazor/Services/Dialog/IDialogReference.cs
aaronleev Jan 19, 2026
a547685
Clearn up formatting of tests.
Jan 31, 2026
92350c5
Added test to handle null options.
Jan 31, 2026
867f685
Use null-coalescing op, suggested by co-pilot.
Jan 31, 2026
5abe682
Added .Where(...) to foreach as it implicitly filters its target sequ…
Jan 31, 2026
915d2b9
Merge remote-tracking branch 'origin/fix-#10397' into fix-#10397
Jan 31, 2026
aa7d8a4
Merge branch 'dev' into fix-#10397
danielchalmers Feb 2, 2026
166c04d
Add special handling for when options is null or CloseOnNavigation is…
Feb 4, 2026
79ae8af
Add tests for special handling of null options and CloseOnNavigation …
Feb 4, 2026
a008355
Add TriState to Checkbox as the null state is valid and has special h…
Feb 4, 2026
df687e5
Merge remote-tracking branch 'origin/fix-#10397' into fix-#10397
Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@namespace MudBlazor.Docs.Examples
@using System.Web
@namespace MudBlazor.Docs.Examples

@inject NavigationManager NavigationManager

<MudDialog>
<DialogContent>
Expand All @@ -7,6 +10,8 @@
<MudButton OnClick="ToggleCloseButtonAsync">Toggle Close Button</MudButton>
<MudButton OnClick="ToggleFullWidthAsync">Toggle Full Width</MudButton>
<MudButton OnClick="ToggleHeaderAsync">Toggle Header</MudButton>
<MudCheckBox TriState="true" T="bool?" Value="MudDialog.Options.CloseOnNavigation" ValueChanged="@(async v => await CloseOnNavigationChangedAsync(v))">Close On Navigation</MudCheckBox>
<MudButton OnClick="ChangeUrl">Change Url</MudButton>
</div>
</DialogContent>
<DialogActions>
Expand All @@ -18,6 +23,13 @@
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; }

[SupplyParameterFromQuery(Name = "example")]
public int? QueryParameterExample { get; set; }

protected override void OnParametersSet() {
QueryParameterExample ??= 0;
}

private void Close() => MudDialog.Close(DialogResult.Ok(true));

private Task ChangeTitleAsync() => MudDialog.SetTitleAsync($"Current time is: {DateTime.Now}");
Expand Down Expand Up @@ -51,4 +63,26 @@

return MudDialog.SetOptionsAsync(options);
}

private void ChangeUrl() {
var uri = new Uri(NavigationManager.Uri);

var query = HttpUtility.ParseQueryString(uri.Query);

query["example"] = (QueryParameterExample + 1).ToString();

var newUri = $"{uri.GetLeftPart(UriPartial.Path)}?{query}";

NavigationManager.NavigateTo(newUri);
Comment on lines 1 to 76
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

@using System.Web / HttpUtility.ParseQueryString introduces a dependency that isn't referenced in the docs project (and is generally avoided in Blazor). Since MudBlazor.Docs already references Microsoft.AspNetCore.WebUtilities, consider using QueryHelpers.AddQueryString (or UriBuilder) to update the query string without relying on System.Web APIs.

Copilot uses AI. Check for mistakes.
}

private Task CloseOnNavigationChangedAsync(bool? closeOnNavigation)
{
var options = MudDialog.Options with
{
CloseOnNavigation = closeOnNavigation
};

return MudDialog.SetOptionsAsync(options);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using MudBlazor.Services;
using MudBlazor.UnitTests.Shared.Mocks;

namespace MudBlazor.UnitTests.Shared.Extensions
{
Expand All @@ -11,7 +11,7 @@ public static class TestContextExtensions
public static void AddTestServices(this BunitContext ctx)
{
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddSingleton<NavigationManager>(new MockNavigationManager());
ctx.Services.AddSingleton<NavigationManager>(new BunitNavigationManager(ctx));
ctx.Services.AddMudServices(options =>
{
options.SnackbarConfiguration.ShowTransitionDuration = 0;
Expand Down
200 changes: 199 additions & 1 deletion src/MudBlazor.UnitTests/Components/DialogTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AwesomeAssertions;
using System.Web;
using AwesomeAssertions;
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
Expand Down Expand Up @@ -91,6 +92,39 @@ public async Task Simple()
result.Canceled.Should().BeFalse();
}

/// <summary>
/// Opening and closing dialogs via navigation.
/// </summary>
[Test]
public async Task CloseOnNavigationTest()
{
var comp = Context.Render<MudDialogProvider>();
comp.Markup.Trim().Should().BeEmpty();
var dialogService = Context.Services.GetRequiredService<IDialogService>();
dialogService.Should().NotBe(null);
var navigationManager = Context.Services.GetRequiredService<NavigationManager>();
navigationManager.Should().NotBe(null);

//create 2 instances and dismiss all except for one with CloseOnNavigation = false
var closeOnNavigationOptions = new DialogOptions
{
CloseOnNavigation = false
};
await comp.InvokeAsync(async () => _ = await dialogService.ShowAsync<DialogOkCancel>("test", options: closeOnNavigationOptions));
await comp.InvokeAsync(async () => _ = await dialogService.ShowAsync<DialogOkCancel>());
var cont = comp.FindAll("div.mud-dialog-container");
cont.Count.Should().Be(2);

var uri = new Uri(navigationManager.Uri);
var query = HttpUtility.ParseQueryString(uri.Query);
query["query"] = Guid.NewGuid().ToString();
var newUri = $"{uri.GetLeftPart(UriPartial.Path)}?{query}";
await comp.InvokeAsync(() => navigationManager.NavigateTo(newUri));
Comment on lines 1 to 122
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Introducing using System.Web; / HttpUtility.ParseQueryString adds a new dependency that does not appear to be referenced in the test project and may fail to compile on modern .NET targets. Prefer a framework-native approach (e.g., UriBuilder, simple string concat, or Microsoft.AspNetCore.WebUtilities.QueryHelpers) to construct a URI with updated query parameters.

Copilot uses AI. Check for mistakes.

cont = comp.FindAll("div.mud-dialog-container");
Comment on lines +121 to +124
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

This test expects only 1 dialog to remain after changing only the query string, but the new CloseOnNavigation == null behavior is documented to not close dialogs when only query/fragment changes. After fixing _currentUri initialization, this test will likely fail; update it to assert the intended default behavior (query change keeps dialogs open, path change closes unless CloseOnNavigation == false).

Suggested change
var newUri = $"{uri.GetLeftPart(UriPartial.Path)}?{query}";
await comp.InvokeAsync(() => navigationManager.NavigateTo(newUri));
cont = comp.FindAll("div.mud-dialog-container");
var newUriWithQueryChange = $"{uri.GetLeftPart(UriPartial.Path)}?{query}";
await comp.InvokeAsync(() => navigationManager.NavigateTo(newUriWithQueryChange));
// Changing only the query string should not close dialogs when CloseOnNavigation is null (default)
cont = comp.FindAll("div.mud-dialog-container");
cont.Count.Should().Be(2);
// Changing the path should close dialogs with default CloseOnNavigation, but keep those with CloseOnNavigation = false
var newUriWithPathChange = $"{uri.GetLeftPart(UriPartial.Path)}-other";
await comp.InvokeAsync(() => navigationManager.NavigateTo(newUriWithPathChange));
cont = comp.FindAll("div.mud-dialog-container");

Copilot uses AI. Check for mistakes.
cont.Count.Should().Be(1);
}

/// <summary>
/// <para>Opening and closing an inline dialog. Click on open will open the inlined dialog.</para>
/// <para>
Expand Down Expand Up @@ -1497,6 +1531,170 @@ await comp.InvokeAsync(async () =>
closeBtn.Should().NotBeNull();
closeBtn.GetAttribute("blazor:onmousedown:preventdefault").Should().Be("");
}

/// <summary>
/// InjectOptions() should set the options of the calling IDialogReference.
/// </summary>
[Test]
public async Task InjectOptions_ShouldNotBeNull()
{
var service = Context.Services.GetRequiredService<IDialogService>();

var reference = await service.ShowAsync<DialogOkCancel>();

reference.InjectOptions(new DialogOptions());
reference.Options.Should().NotBe(null);
}

/// <summary>
/// SetOptions() should set the options of the dialog reference that corresponds to the given id.
/// </summary>
[Test]
public async Task SetOptions_ShouldNotBeNull()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var provider = Context.Render<MudDialogProvider>();

var reference = await service.ShowAsync<DialogOkCancel>();

provider.Instance.SetOptions(reference.Id, new DialogOptions());
reference.Options.Should().NotBe(null);
}

/// <summary>
/// ShouldDismissOnNavigation() should return true if the dialog reference's options CloseOnNavigation is set to true.
/// </summary>
[Test]
public async Task ShouldDismissOnNavigation_ShouldBeTrue()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var provider = Context.Render<MudDialogProvider>();

var options = new DialogOptions
{
CloseOnNavigation = true
};
var reference = await service.ShowAsync<DialogOkCancel>("test", options: options);

provider.Instance.ShouldDismissOnNavigation(reference, "/test").Should().BeTrue();
}

/// <summary>
/// ShouldDismissOnNavigation() should return false if the dialog reference's options CloseOnNavigation is set to false.
/// </summary>
[Test]
public async Task ShouldDismissOnNavigation_ShouldBeFalse()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var provider = Context.Render<MudDialogProvider>();

var reference = await service.ShowAsync<DialogOkCancel>();
var options = new DialogOptions
{
CloseOnNavigation = false
};
reference.InjectOptions(options);

provider.Instance.ShouldDismissOnNavigation(reference, "/test").Should().BeFalse();
}

/// <summary>
/// HasRouteChanged() should return true if the dialog reference's options
/// CloseOnNavigation is set to null and the absolute path has changed.
/// </summary>
[Test]
public async Task HasRouteChanged_ShouldBeTrueWhenCloseOnNavigationIsNullAndRouteChanged()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var navigationManager = Context.Services.GetRequiredService<NavigationManager>();
var provider = Context.Render<MudDialogProvider>();

var currentRoute = navigationManager.ToAbsoluteUri($"/test/{Guid.NewGuid()}").ToString();
navigationManager.NavigateTo(currentRoute);

var reference = await service.ShowAsync<DialogOkCancel>();
var options = new DialogOptions
{
CloseOnNavigation = null
};
reference.InjectOptions(options);

var changedRoute = navigationManager.ToAbsoluteUri($"{currentRoute}/{Guid.NewGuid()}").AbsolutePath.TrimEnd('/');
provider.Instance.HasRouteChanged(changedRoute).Should().BeTrue();
}

/// <summary>
/// HasRouteChanged() should return false if the dialog reference's options
/// CloseOnNavigation is set to null and only the query or fragment has changed.
/// </summary>
[Test]
public async Task HasRouteChanged_ShouldBeFalseWhenCloseOnNavigationIsNullAndQueryOrFragmentChanged()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var navigationManager = Context.Services.GetRequiredService<NavigationManager>();
var provider = Context.Render<MudDialogProvider>();

var currentRoute = navigationManager.ToAbsoluteUri($"/test/{Guid.NewGuid()}").ToString();
navigationManager.NavigateTo(currentRoute);

var reference = await service.ShowAsync<DialogOkCancel>();
var options = new DialogOptions
{
CloseOnNavigation = null
};
reference.InjectOptions(options);

var changedQuery = navigationManager.ToAbsoluteUri($"{currentRoute}?query={Guid.NewGuid()}").AbsolutePath;
provider.Instance.HasRouteChanged(changedQuery).Should().BeFalse();

var changedFragment = navigationManager.ToAbsoluteUri($"{currentRoute}#{Guid.NewGuid()}").AbsolutePath;
provider.Instance.HasRouteChanged(changedFragment).Should().BeFalse();
}

/// <summary>
/// HasRouteChanged() should return true if the dialog reference's options is set to null
/// and the absolute path has changed.
/// </summary>
[Test]
public async Task HasRouteChanged_ShouldBeTrueWhenOptionsIsNullAndRouteChanged()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var navigationManager = Context.Services.GetRequiredService<NavigationManager>();
var provider = Context.Render<MudDialogProvider>();

var currentRoute = navigationManager.ToAbsoluteUri($"/test/{Guid.NewGuid()}").ToString();
navigationManager.NavigateTo(currentRoute);

var reference = await service.ShowAsync<DialogOkCancel>();
reference.InjectOptions(null);

Comment on lines +1668 to +1670
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

InjectOptions(null) does not compile with the current IDialogReference.InjectOptions(DialogOptions options) signature. Either make InjectOptions accept nullable options, or update these tests to avoid passing null (e.g., verify behavior with CloseOnNavigation = null on a real DialogOptions instance).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@aaronleev aaronleev Feb 4, 2026

Choose a reason for hiding this comment

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

This is a test, options are not required on construction of a reference, neither is the RenderFragment or the Dialog instance itself. This is edge case test for a poorly configured IDialogReference.

It will compile it just has a warning on the test case.

(e.g., verify behavior with CloseOnNavigation = null on a real DialogOptions instance). This is verified in other tests in the same file.

var changedRoute = navigationManager.ToAbsoluteUri($"{currentRoute}/{Guid.NewGuid()}").AbsolutePath.TrimEnd('/');
provider.Instance.HasRouteChanged(changedRoute).Should().BeTrue();
}

/// <summary>
/// HasRouteChanged() should return false if the dialog reference's options is set to null
/// and only the query or fragment has changed.
/// </summary>
[Test]
public async Task HasRouteChanged_ShouldBeFalseWhenOptionsIsNullAndQueryOrFragmentChanged()
{
var service = Context.Services.GetRequiredService<IDialogService>();
var navigationManager = Context.Services.GetRequiredService<NavigationManager>();
var provider = Context.Render<MudDialogProvider>();

var currentRoute = navigationManager.ToAbsoluteUri($"/test/{Guid.NewGuid()}").ToString();
navigationManager.NavigateTo(currentRoute);

var reference = await service.ShowAsync<DialogOkCancel>();
reference.InjectOptions(null);

Comment on lines +1689 to +1691
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

InjectOptions(null) does not compile with the current IDialogReference.InjectOptions(DialogOptions options) signature. Either make InjectOptions accept nullable options, or update these tests to avoid passing null (e.g., verify behavior with CloseOnNavigation = null on a real DialogOptions instance).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a test, options are not required on construction of a reference, neither is the RenderFragment or the Dialog instance itself. This is edge case test for a poorly configured IDialogReference.

It will compile it just has a warning on the test case.

(e.g., verify behavior with CloseOnNavigation = null on a real DialogOptions instance). This is verified in other tests in the same file.

var changedQuery = navigationManager.ToAbsoluteUri($"{currentRoute}?query={Guid.NewGuid()}").AbsolutePath;
provider.Instance.HasRouteChanged(changedQuery).Should().BeFalse();

var changedFragment = navigationManager.ToAbsoluteUri($"{currentRoute}#{Guid.NewGuid()}").AbsolutePath;
provider.Instance.HasRouteChanged(changedFragment).Should().BeFalse();
}
}
internal class CustomDialogService : DialogService
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ public async ValueTask DisposeAsync()
async Task IMudDialogInstance.SetOptionsAsync(DialogOptions options)
{
await _dialogOptionsState.SetValueAsync(options);
Parent.SetOptions(Id, options);
await InvokeAsync(StateHasChanged);
}

Expand Down
38 changes: 37 additions & 1 deletion src/MudBlazor/Components/Dialog/MudDialogProvider.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public partial class MudDialogProvider : IDisposable
{
private DialogOptions _globalDialogOptions = new();
private readonly List<IDialogReference> _dialogs = [];
private string? _currentUri;

[Inject]
private IDialogService DialogService { get; set; } = null!;
Expand Down Expand Up @@ -154,13 +155,40 @@ protected override Task OnAfterRenderAsync(bool firstRender)
return base.OnAfterRenderAsync(firstRender);
}

internal void SetOptions(Guid id, DialogOptions options)
{
var reference = GetDialogReference(id);
if (reference != null)
reference.InjectOptions(options);
}

internal void DismissInstance(Guid id, DialogResult result)
{
var reference = GetDialogReference(id);
if (reference != null)
DismissInstance(reference, result);
}

internal bool ShouldDismissOnNavigation(IDialogReference dialog, string newUri)
{
if (dialog.Options?.CloseOnNavigation == null)
{
return HasRouteChanged(newUri);
}

return dialog.Options.CloseOnNavigation.Value;
}

internal bool HasRouteChanged(string newUri)
{
if (_currentUri == null)
{
return true;
}

return !string.Equals(_currentUri, newUri, StringComparison.OrdinalIgnoreCase);
}

private Task AddInstanceAsync(IDialogReference dialog)
{
_dialogs.Add(dialog);
Expand Down Expand Up @@ -194,7 +222,15 @@ private void DismissInstance(IDialogReference dialog, DialogResult? result)

private void LocationChanged(object? sender, LocationChangedEventArgs args)
{
DismissAll();
var newUri = NavigationManager.ToAbsoluteUri(args.Location).AbsolutePath.TrimEnd('/');

foreach (var dialog in _dialogs.ToArray().Where(d => ShouldDismissOnNavigation(d, newUri)))
{
DismissInstance(dialog, DialogResult.Cancel());
}

_currentUri = newUri;
StateHasChanged();
}

protected virtual void Dispose(bool disposing)
Expand Down
9 changes: 9 additions & 0 deletions src/MudBlazor/Services/Dialog/DialogOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ public record DialogOptions
/// </summary>
public bool? CloseOnEscapeKey { get; init; }

/// <summary>
/// Determines if the dialog should close on navigation.
/// </summary>
/// <remarks>
/// Defaults to <c>null</c>.
/// When <c>null</c> this option is considered <c>true</c> if the absolute path changes and <c>false</c> if only the query or fragment changes.
/// </remarks>
public bool? CloseOnNavigation { get; init; }

/// <summary>
/// Hides the dialog header.
/// </summary>
Expand Down
Loading
Loading