Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 7 additions & 15 deletions SUPERDIALOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Listens to `SuperDialogService.OnShow`. Renders a Bootstrap modal with a single
|---|---|---|---|
| `Width` | `string?` | `null` | Custom max-width (e.g. `"500px"`, `"80%"`) |
| `Height` | `string?` | `null` | Custom modal body height with auto overflow |
| `CloseOnBackdropClick` | `bool` | `true` | Click outside closes the modal (returns `null`) |
| `CloseOnBackdropClick` | `bool` | `false` | Click outside closes the modal when enabled (returns `null`) |
| `ShowCloseButton` | `bool` | `true` | Shows the `×` icon in the header |
| `CssClass` | `string?` | `null` | Extra CSS class on `.modal-content` |
| `Size` | `DialogSize` | `Default` | `Default`, `Small`, `Large`, `ExtraLarge` |
Expand Down Expand Up @@ -270,13 +270,12 @@ var options = new DialogOptions
await DialogService.OpenAsync<EditorDialog>("Edit", null, options);
```

### 5. Disable backdrop close (force a choice)
### 5. Keep the dialog open on backdrop click (default)

```csharp
await DialogService.OpenAsync<TermsDialog>("Terms of service", null,
new DialogOptions
{
CloseOnBackdropClick = false,
ShowCloseButton = false
});
```
Expand Down Expand Up @@ -342,17 +341,11 @@ await DialogService.Confirm(
}
```

### 10. Re-entrancy guard
### 10. Reopening a dynamic dialog

```csharp
try
{
await DialogService.OpenAsync<EditorDialog>("Edit");
}
catch (InvalidOperationException)
{
// a dialog is already open — ignore or queue
}
// If another dynamic dialog is still open, it is closed with a null result first.
await DialogService.OpenAsync<EditorDialog>("Edit");
```

---
Expand All @@ -363,7 +356,7 @@ catch (InvalidOperationException)
- ✅ For destructive actions, prefer **`SuperConfirmationButton`** (see [SUPERBUTTONS.md](SUPERBUTTONS.md)) — it wraps the confirm flow for free.
- ✅ Make dialog components self-sufficient: inject `SuperDialogService` and call `Close(value)` to return their result.
- ✅ Use **`DialogSize.ExtraLarge`** + custom `Width` for editor-style dialogs.
- ⚠️ Only **one** dialog of each kind (confirm / dynamic) can be open at a time — opening another throws `InvalidOperationException`.
- ⚠️ Only **one** dialog of each kind (confirm / dynamic) can be visible at a time. Opening a dynamic dialog closes the previous dynamic dialog first.
- ⚠️ Avoid heavy initialization inside dialog components; they are mounted on every open.

---
Expand All @@ -373,9 +366,8 @@ catch (InvalidOperationException)
| Symptom | Cause | Fix |
|---|---|---|
| `InvalidOperationException: No dialog host is registered` | `<SuperDialog />` not placed in layout | Add it once to `MainLayout.razor` |
| `InvalidOperationException: A dialog is already open` | Concurrent `OpenAsync` calls | Await previous call or guard with try/catch |
| Dialog never returns | Component never calls `DialogService.Close(...)` | Ensure all close paths call `Close(result)` |
| Backdrop click discards user input | Default behavior | Set `CloseOnBackdropClick = false` |
| Backdrop click does not close the dialog | Default behavior | Set `CloseOnBackdropClick = true` |
| `result` is `null` when expected | User dismissed via backdrop/× | Check for `null` and treat as cancellation |

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class DialogOptions
/// <summary>
/// Indique si la modale peut être fermée en cliquant sur le fond.
/// </summary>
public bool CloseOnBackdropClick { get; set; } = true;
public bool CloseOnBackdropClick { get; set; } = false;

/// <summary>
/// Indique si la modale affiche un bouton de fermeture.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<h5 class="modal-title">@_title</h5>
@if (_options?.ShowCloseButton ?? true)
{
<button type="button" class="btn-close" aria-label="Fermer" @onclick="CloseDialog"></button>
<button type="button" class="btn-close" aria-label="Fermer" @onclick="() => DialogService.Close(null)"></button>
}
</div>
<div class="modal-body" style="@GetBodyStyle()">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private async Task CloseDialog()

private async Task OnBackdropClick()
{
if (_options?.CloseOnBackdropClick ?? true)
if (_options?.CloseOnBackdropClick ?? false)
{
await DialogService.Close(null);
}
Expand Down Expand Up @@ -82,4 +82,4 @@ public void Dispose()
DialogService.OnOpenDialog -= ShowDialog;
DialogService.OnCloseDialog -= CloseDialog;
}
}
}
2 changes: 1 addition & 1 deletion src/SuperBlazorComponents/Services/SuperDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ internal void SetResult(bool result)
{
if (_dialogTcs is { Task.IsCompleted: false })
{
throw new InvalidOperationException("A dialog is already open.");
await Close(null);
}

if (OnOpenDialog is null)
Expand Down
2 changes: 1 addition & 1 deletion src/SuperBlazorComponents/SuperBlazorComponents.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>2.0.5</VersionPrefix>
<VersionPrefix>2.0.6</VersionPrefix>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<RootNamespace>SuperBlazorComponents</RootNamespace>
<AssemblyName>SuperBlazorComponents</AssemblyName>
Expand Down
111 changes: 111 additions & 0 deletions tests/SuperBlazorComponents.Tests/SuperDialogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SuperBlazorComponents.Components.Dialogs;
using SuperBlazorComponents.Services;

namespace SuperBlazorComponents.Tests;

[TestClass]
public sealed class SuperDialogTests : BunitContext
{
[TestInitialize]
public void Setup()
{
Services.AddLocalization();
Services.AddScoped<SuperDialogService>();
}

[TestCleanup]
public void Cleanup()
{
Dispose();
}

[TestMethod]
public async Task CloseButton_CompletesOpenAsync()
{
var cut = Render<SuperDialog>();
var dialogService = Services.GetRequiredService<SuperDialogService>();

var openTask = cut.InvokeAsync(() => dialogService.OpenAsync<TestDialog>("Title"));

cut.WaitForAssertion(() => Assert.AreEqual("Title", cut.Find(".modal-title").TextContent));

cut.Find(".btn-close").Click();

var result = await openTask.WaitAsync(TimeSpan.FromSeconds(1));
Assert.IsNull(result);
Assert.AreEqual(0, cut.FindAll(".modal").Count);
}

[TestMethod]
public async Task BackdropClick_Default_KeepsDialogOpen()
{
var cut = Render<SuperDialog>();
var dialogService = Services.GetRequiredService<SuperDialogService>();

var openTask = cut.InvokeAsync(() => dialogService.OpenAsync<TestDialog>("Title"));

cut.WaitForAssertion(() => Assert.AreEqual("Title", cut.Find(".modal-title").TextContent));

cut.Find(".modal").Click();

Assert.IsFalse(openTask.IsCompleted);
Assert.AreEqual("Title", cut.Find(".modal-title").TextContent);

await cut.InvokeAsync(() => dialogService.Close(null));
await openTask.WaitAsync(TimeSpan.FromSeconds(1));
}

[TestMethod]
public async Task BackdropClick_WhenEnabled_CompletesOpenAsync()
{
var cut = Render<SuperDialog>();
var dialogService = Services.GetRequiredService<SuperDialogService>();

var openTask = cut.InvokeAsync(() => dialogService.OpenAsync<TestDialog>(
"Title",
options: new DialogOptions { CloseOnBackdropClick = true }));

cut.WaitForAssertion(() => Assert.AreEqual("Title", cut.Find(".modal-title").TextContent));

cut.Find(".modal").Click();

var result = await openTask.WaitAsync(TimeSpan.FromSeconds(1));
Assert.IsNull(result);
Assert.AreEqual(0, cut.FindAll(".modal").Count);
}

[TestMethod]
public async Task OpenAsync_WhenDialogIsAlreadyOpen_ClosesPreviousDialog()
{
var cut = Render<SuperDialog>();
var dialogService = Services.GetRequiredService<SuperDialogService>();

var firstOpenTask = cut.InvokeAsync(() => dialogService.OpenAsync<TestDialog>("First"));

cut.WaitForAssertion(() => Assert.AreEqual("First", cut.Find(".modal-title").TextContent));

var secondOpenTask = cut.InvokeAsync(() => dialogService.OpenAsync<TestDialog>("Second"));

var firstResult = await firstOpenTask.WaitAsync(TimeSpan.FromSeconds(1));
Assert.IsNull(firstResult);
cut.WaitForAssertion(() => Assert.AreEqual("Second", cut.Find(".modal-title").TextContent));

await cut.InvokeAsync(() => dialogService.Close("done"));

var secondResult = await secondOpenTask.WaitAsync(TimeSpan.FromSeconds(1));
Assert.AreEqual("done", secondResult);
}

private sealed class TestDialog : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "Test dialog");
}
}
}
Loading