Files
radzen-blazor/Radzen.Blazor/RadzenDialog.razor
Martin Hans 6bff178e2c Add CanClose callback to prevent dialog dismissal (#2477)
* Add CanClose callback to prevent dialog dismissal

Closes #2476

* Revert TryCloseSideAsync to use sync CloseSide

* Add CloseSidec() to RadzenDialog @code.

Mirrors Close() handling on DialogContainer.
2026-03-16 11:02:33 +02:00

369 lines
12 KiB
Plaintext

@implements IDisposable
@implements IAsyncDisposable
@using Microsoft.JSInterop
@using Radzen.Blazor.Rendering
@inject IJSRuntime JSRuntime
@{
var sideOptions = sideDialogOptions;
var sideDialogId = $"rz-dialog-side-{sideOptions?.GetHashCode()}";
var sideDialogLabelId = $"{sideDialogId}-label";
var sideShowTitle = sideOptions?.ShowTitle == true;
var sideAriaAttributes = new Dictionary<string, object?>();
if (sideShowTitle)
{
sideAriaAttributes["aria-labelledby"] = sideDialogLabelId;
}
else
{
sideAriaAttributes["aria-label"] = sideOptions?.AriaLabel ?? sideDialog?.Title ?? sideOptions?.Title;
}
if (sideOptions?.ShowMask == true)
{
sideAriaAttributes["aria-modal"] = "true";
}
}
@foreach (var dialog in dialogs)
{
<DialogContainer @key=@dialog Dialog=@dialog ShowMask=@(dialog==dialogs.LastOrDefault()) />
}
@if (isSideDialogOpen)
{
<aside
class="@GetSideDialogCssClass()"
role="dialog"
@ref="sideDialogElement"
tabindex="0"
style="@GetSideDialogStyle()"
@attributes="sideAriaAttributes">
@if (sideOptions?.Resizable == true)
{
<div @ref="resizeBarElement"
class="@GetResizeBarCssClass()"
title="@sideOptions?.ResizeBarTitle" aria-label="@sideOptions?.ResizeBarAriaLabel">
<span class="rz-resize" aria-hidden="true"></span>
</div>
}
@if (sideShowTitle)
{
<div class="rz-dialog-side-titlebar">
<div class="rz-dialog-side-title" style="display: inline" id="@sideDialogLabelId">
@if (sideOptions?.TitleContent != null)
{
@sideOptions.TitleContent(Service!)
}
else if (!string.IsNullOrEmpty(sideDialog?.Title))
{
@sideDialog.Title
}
else if (!string.IsNullOrEmpty(sideOptions?.Title))
{
@sideOptions.Title
}
</div>
@if (sideOptions?.ShowClose == true)
{
<button id="@($"rz-dialog-side-{sideOptions?.GetHashCode() ?? 0}-cl")" type="button" aria-label="@CloseSideDialogAriaLabel" class="rz-dialog-side-titlebar-close" @onclick="CloseSide" tabindex="@(sideOptions?.CloseTabIndex ?? 0)">
<span class="notranslate rzi rzi-times"></span>
</button>
}
</div>
}
<div class="@SideDialogContentCssClass" style="@sideOptions?.Style">
@sideDialogContent
</div>
</aside>
@if (dialogs.Count == 0 && sideOptions?.ShowMask == true)
{
@if (sideOptions.CloseDialogOnOverlayClick)
{
<div @onclick="CloseSide" class="rz-dialog-mask"
aria-hidden="true" tabindex="-1"></div>
}
else
{
<div class="rz-dialog-mask" aria-hidden="true" tabindex="-1"></div>
}
}
}
@code {
#nullable enable
private string SideDialogContentCssClass
{
get
{
var baseCss = "rz-dialog-side-content";
return string.IsNullOrEmpty(sideDialogOptions?.ContentCssClass)
? baseCss
: $"{baseCss} {sideDialogOptions.ContentCssClass}";
}
}
[Inject] private DialogService? Service { get; set; }
/// <summary>
/// Gets or sets the close side dialog aria label text.
/// </summary>
/// <value>The close side dialog aria label text.</value>
[Parameter]
public string CloseSideDialogAriaLabel { get; set; } = "Close side dialog";
private List<Dialog> dialogs = new List<Dialog>();
private bool isSideDialogOpen;
private RenderFragment? sideDialogContent;
private SideDialogOptions? sideDialogOptions;
private Dialog? sideDialog;
private ElementReference? resizeBarElement;
private ElementReference? sideDialogElement;
private IJSObjectReference? sideDialogResizeHandleJsModule;
/// <summary>
/// Opens a new dialog with specified parameters and options.
/// </summary>
/// <param name="title">The title of the dialog.</param>
/// <param name="type">The content type of the dialog, usually a component type.</param>
/// <param name="parameters">A dictionary of parameters to pass to the dialog content.</param>
/// <param name="options">Additional configuration options for the dialog, such as size or behavior.</param>
/// <returns>A Task that represents the asynchronous operation of opening the dialog.</returns>
public async Task Open(string? title,
Type type,
Dictionary<string, object?> parameters,
DialogOptions options)
{
dialogs.Add(new Dialog() { Title = title, Type = type, Parameters = parameters, Options = options });
await InvokeAsync(() => { StateHasChanged(); });
}
/// <summary>
/// Closes the currently open dialog and returns the specified result.
/// </summary>
/// <param name="result">The result to return when the dialog is closed. This can contain any data that needs to be passed back to the caller.</param>
/// <returns>A Task that represents the asynchronous operation of closing the dialog.</returns>
public async Task Close(dynamic result)
{
var lastDialog = dialogs.LastOrDefault();
if (lastDialog != null)
{
dialogs.Remove(lastDialog);
if (dialogs.Count == 0) await JSRuntime.InvokeAsync<string>("Radzen.closeDialog");
}
await InvokeAsync(() =>
{
StateHasChanged();
});
}
/// <inheritdoc />
public void Dispose()
{
if (Service != null)
{
Service.OnOpen -= OnOpen;
Service.OnClose -= OnClose;
Service.OnSideOpen -= OnSideOpen;
Service.OnSideClose -= OnSideClose;
}
_ = DisposeSideDialogResizeHandleAsync();
}
/// <inheritdoc />
protected override void OnInitialized()
{
if (Service is null) return;
Service.OnOpen += OnOpen;
Service.OnClose += OnClose;
Service.OnSideOpen += OnSideOpen;
Service.OnSideClose += OnSideClose;
}
private void OnSideOpen(Type sideComponent, Dictionary<string, object?> parameters, SideDialogOptions options)
{
sideDialogOptions = options;
// Create a DialogOptions from SideDialogOptions by copying common properties
var dialogOptions = new DialogOptions()
{
ShowTitle = options.ShowTitle,
ShowClose = options.ShowClose,
Width = options.Width,
Height = options.Height,
Style = options.Style,
CloseDialogOnOverlayClick = options.CloseDialogOnOverlayClick,
CssClass = options.CssClass,
WrapperCssClass = options.WrapperCssClass,
ContentCssClass = options.ContentCssClass,
CloseTabIndex = options.CloseTabIndex,
TitleContent = options.TitleContent,
AriaLabel = options.AriaLabel,
CloseAriaLabel = options.CloseAriaLabel
};
// Create a Dialog object for the side dialog to support cascading parameter
sideDialog = new Dialog()
{
Title = options.Title, Type = sideComponent, Parameters = parameters, Options = dialogOptions
};
// Wrap the content in a CascadingValue to provide the Dialog object to child components
sideDialogContent = new RenderFragment(builder =>
{
// Open CascadingValue
builder.OpenComponent<CascadingValue<Dialog>>(0);
builder.AddAttribute(1, "Value", sideDialog);
builder.AddAttribute(2, "IsFixed", true);
builder.AddAttribute(3, "ChildContent", (RenderFragment)((builder2) =>
{
// Open the actual component
builder2.OpenComponent(0, sideComponent);
foreach (var parameter in parameters)
{
builder2.AddAttribute(1, parameter.Key, parameter.Value);
}
builder2.CloseComponent();
}));
builder.CloseComponent(); // Close CascadingValue
});
isSideDialogOpen = true;
StateHasChanged();
}
private async Task CloseSide()
{
if (Service is not null)
{
await Service!.TryCloseSideAsync();
}
}
private bool sideDialogClosing = false;
private async Task OnSideCloseAsync()
{
sideDialogClosing = true;
StateHasChanged();
await Task.Delay(300);
isSideDialogOpen = false;
sideDialogClosing = false;
StateHasChanged();
Service?.OnSideCloseComplete();
}
private void OnSideClose(dynamic _)
{
if (isSideDialogOpen)
{
InvokeAsync(OnSideCloseAsync);
}
}
private void OnOpen(string? title,
Type type,
Dictionary<string, object?> parameters,
DialogOptions options)
{
Open(title, type, parameters, options).ConfigureAwait(false);
}
private void OnClose(dynamic result)
{
Close(result).ConfigureAwait(false);
}
private string GetSideDialogCssClass() => ClassList.Create("rz-dialog-side")
.Add($"rz-dialog-side-position-{sideDialogOptions?.Position.ToString().ToLower()}")
.Add(sideDialogOptions?.CssClass)
.Add("rz-open", !sideDialogClosing)
.Add("rz-close", sideDialogClosing)
.ToString();
private string GetResizeBarCssClass() => ClassList.Create("rz-dialog-resize-bar")
.ToString();
private string GetSideDialogStyle()
{
string widthStyle = string.IsNullOrEmpty(sideDialogOptions?.Width)
? string.Empty
: $"width: {sideDialogOptions.Width};";
string heightStyle = string.IsNullOrEmpty(sideDialogOptions?.Height)
? string.Empty
: $"height: {sideDialogOptions.Height};";
return $"{widthStyle}{heightStyle}{sideDialogOptions?.Style}";
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (isSideDialogOpen)
{
await JSRuntime.InvokeAsync<string>("Radzen.openSideDialog", sideDialogOptions);
if (sideDialogOptions is { Resizable: true } && resizeBarElement.HasValue)
{
sideDialogResizeHandleJsModule =
await JSRuntime.InvokeAsync<IJSObjectReference>("Radzen.createSideDialogResizer",
resizeBarElement, sideDialogElement, sideDialogOptions);
}
}
}
/// <summary>
/// Disposes resources used by the dialog asynchronously.
/// This method is typically called to clean up unmanaged resources or
/// perform cleanup tasks for JavaScript interop involved in the dialog component.
/// </summary>
/// <returns>A ValueTask that represents the asynchronous disposal operation.</returns>
public async ValueTask DisposeAsync()
{
Dispose();
await DisposeSideDialogResizeHandleAsync();
}
private async Task DisposeSideDialogResizeHandleAsync()
{
if (sideDialogResizeHandleJsModule == null)
{
return;
}
try
{
await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose");
}
catch
{
/* Ignore */
}
try
{
await sideDialogResizeHandleJsModule.DisposeAsync();
}
catch
{
/* Ignore */
}
sideDialogResizeHandleJsModule = null;
}
}