From e53f90dfdfb8ff1e5c922ceae76cc802e88f5322 Mon Sep 17 00:00:00 2001 From: Martin Hans Date: Wed, 29 Oct 2025 23:18:21 +0100 Subject: [PATCH 1/5] Add resizable option to side dialog. --- Radzen.Blazor/DialogService.cs | 55 ++++++------ Radzen.Blazor/RadzenDialog.razor | 49 ++++++++++- .../themes/components/blazor/_dialog.scss | 80 ++++++++++++++++++ Radzen.Blazor/wwwroot/Radzen.Blazor.js | 83 +++++++++++++++++++ RadzenBlazorDemos/Pages/DialogSide.razor | 7 +- 5 files changed, 242 insertions(+), 32 deletions(-) diff --git a/Radzen.Blazor/DialogService.cs b/Radzen.Blazor/DialogService.cs index a22950266e1..83800690c79 100644 --- a/Radzen.Blazor/DialogService.cs +++ b/Radzen.Blazor/DialogService.cs @@ -12,7 +12,7 @@ namespace Radzen { /// - /// Class DialogService. Contains various methods with options to open and close dialogs. + /// Class DialogService. Contains various methods with options to open and close dialogs. /// Should be added as scoped service in the application services and RadzenDialog should be added in application main layout. /// /// @@ -54,7 +54,7 @@ internal DotNetObjectReference Reference return reference; } } - + /// /// Gets or sets the URI helper. /// @@ -384,7 +384,7 @@ public virtual Task OpenAsync(RenderFragment titleConten // register the cancellation token if (cancellationToken.HasValue) cancellationToken.Value.Register(() => task.TrySetCanceled()); - + tasks.Add(task); options ??= new DialogOptions(); @@ -484,8 +484,8 @@ public void Dispose() options.Width = !String.IsNullOrEmpty(options.Width) ? options.Width : ""; // Width is set to 600px by default by OpenAsync options.Style = !String.IsNullOrEmpty(options.Style) ? options.Style : ""; options.CssClass = !String.IsNullOrEmpty(options.CssClass) ? $"rz-dialog-confirm {options.CssClass}" : "rz-dialog-confirm"; - options.WrapperCssClass = !String.IsNullOrEmpty(options.WrapperCssClass) ? $"rz-dialog-wrapper {options.WrapperCssClass}" : "rz-dialog-wrapper"; - + options.WrapperCssClass = !String.IsNullOrEmpty(options.WrapperCssClass) ? $"rz-dialog-wrapper {options.WrapperCssClass}" : "rz-dialog-wrapper"; + return await OpenAsync(title, ds => { RenderFragment content = b => @@ -578,7 +578,7 @@ public void Dispose() // Validate and set default values for the dialog options options ??= new(); options.OkButtonText = !String.IsNullOrEmpty(options.OkButtonText) ? options.OkButtonText : "Ok"; - options.Width = !String.IsNullOrEmpty(options.Width) ? options.Width : ""; + options.Width = !String.IsNullOrEmpty(options.Width) ? options.Width : ""; options.Style = !String.IsNullOrEmpty(options.Style) ? options.Style : ""; options.CssClass = !String.IsNullOrEmpty(options.CssClass) ? $"rz-dialog-alert {options.CssClass}" : "rz-dialog-alert"; options.WrapperCssClass = !String.IsNullOrEmpty(options.WrapperCssClass) ? $"rz-dialog-wrapper {options.WrapperCssClass}" : "rz-dialog-wrapper"; @@ -848,6 +848,7 @@ public int CloseTabIndex } private RenderFragment titleContent; + private bool resizable; /// /// Gets or sets the title content. @@ -865,6 +866,23 @@ public RenderFragment TitleContent } } } + + /// + /// Gets or sets a value indicating whether the dialog is resizable. Set to false by default. + /// + /// true if resizable; otherwise, false. + public bool Resizable + { + get => resizable; + set + { + if (resizable != value) + { + resizable = value; + OnPropertyChanged(nameof(Resizable)); + } + } + } } /// @@ -992,25 +1010,6 @@ public class DialogOptions : DialogOptionsBase /// public string IconStyle { get; set; } = "margin-right: 0.75rem"; - - private bool resizable; - /// - /// Gets or sets a value indicating whether the dialog is resizable. Set to false by default. - /// - /// true if resizable; otherwise, false. - public bool Resizable - { - get => resizable; - set - { - if (resizable != value) - { - resizable = value; - OnPropertyChanged(nameof(Resizable)); - } - } - } - private Action resize; /// @@ -1147,7 +1146,7 @@ public RenderFragment ChildContent private bool autoFocusFirstElement = true; /// - /// Gets or sets a value indicating whether to focus the first focusable HTML element. + /// Gets or sets a value indicating whether to focus the first focusable HTML element. /// /// true if the first focusable element is focused; otherwise, false. Default is true. public bool AutoFocusFirstElement @@ -1237,7 +1236,7 @@ public string CancelButtonText public class Dialog : INotifyPropertyChanged { private string title; - + /// /// Gets or sets the title. /// @@ -1326,4 +1325,4 @@ protected virtual void OnPropertyChanged(string propertyName) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } -} +} \ No newline at end of file diff --git a/Radzen.Blazor/RadzenDialog.razor b/Radzen.Blazor/RadzenDialog.razor index 6eac3af6908..d91395a2b24 100644 --- a/Radzen.Blazor/RadzenDialog.razor +++ b/Radzen.Blazor/RadzenDialog.razor @@ -12,10 +12,26 @@ { - /// The close side dialog aria label text. - [Parameter] - public string CloseSideDialogAriaLabel { get; set; } = "Close side dialog"; - - List dialogs = new List(); - bool isSideDialogOpen = false; - RenderFragment sideDialogContent; - SideDialogOptions sideDialogOptions; - Dialog sideDialog; - ElementReference? sideDialogResizeHandle; - ElementReference? sideDialogHandle; - IJSObjectReference sideDialogResizeHandleJsModule; - - public async Task Open(string title, Type type, Dictionary parameters, DialogOptions options) - { - dialogs.Add(new Dialog() { Title = title, Type = type, Parameters = parameters, Options = options }); - - await InvokeAsync(() => { StateHasChanged(); }); - } - - public async Task Close(dynamic result) - { - var lastDialog = dialogs.LastOrDefault(); - if (lastDialog != null) - { - dialogs.Remove(lastDialog); - if (dialogs.Count==0) await JSRuntime.InvokeAsync("Radzen.closeDialog"); - } - - await InvokeAsync(() => - { - StateHasChanged(); - }); - } - - public void Dispose() - { - Service.OnOpen -= OnOpen; - Service.OnClose -= OnClose; - - Service.OnSideOpen -= OnSideOpen; - Service.OnSideClose -= OnSideClose; - } - - protected override void OnInitialized() - { - Service.OnOpen += OnOpen; - Service.OnClose += OnClose; - - Service.OnSideOpen += OnSideOpen; - Service.OnSideClose += OnSideClose; - } - - void OnSideOpen(Type sideComponent, Dictionary 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 - }; - - // 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>(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(); - } - - bool sideDialogClosing = false; - - async Task OnSideCloseAsync() - { - sideDialogClosing = true; - - StateHasChanged(); - - await Task.Delay(300); - - isSideDialogOpen = false; - sideDialogClosing = false; - - StateHasChanged(); - - Service.OnSideCloseComplete(); - } - - void OnSideClose(dynamic _) - { - if (isSideDialogOpen) - { - InvokeAsync(OnSideCloseAsync); - } - } - - void OnOpen(string title, Type type, Dictionary parameters, DialogOptions options) - { - Open(title, type, parameters, options).ConfigureAwait(false); - } - - void OnClose(dynamic result) - { - Close(result).ConfigureAwait(false); - } - - 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(); - - string GetResizBarCssClass() => ClassList.Create("rz-dialog-resize-bar") - .ToString(); - - 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}"; - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (isSideDialogOpen) - { - await JSRuntime.InvokeAsync("Radzen.openSideDialog", sideDialogOptions); - - if (sideDialogOptions.Resizable && sideDialogResizeHandle.HasValue) - { - sideDialogResizeHandleJsModule = await JSRuntime.InvokeAsync("Radzen.initSideDialogResize", sideDialogResizeHandle, sideDialogHandle); - } - } - } - - public async ValueTask DisposeAsync() - { - try - { - if (sideDialogResizeHandleJsModule != null) - { - await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose"); - } - } - catch - { - /* Ignore */ - } - } +#nullable enable } diff --git a/Radzen.Blazor/RadzenDialog.razor.cs b/Radzen.Blazor/RadzenDialog.razor.cs new file mode 100644 index 00000000000..ad2eef6b54e --- /dev/null +++ b/Radzen.Blazor/RadzenDialog.razor.cs @@ -0,0 +1,254 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Radzen.Blazor.Rendering; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Radzen.Blazor; + +/// +/// Represents a dialog component in the Radzen framework. +/// RadzenDialog is a reusable component that facilitates the creation of modal dialogs. +/// It can be used to display custom content, collect user input or confirm/cancel operations. +/// +public partial class RadzenDialog +{ + 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; } + + /// + /// Gets or sets the close side dialog aria label text. + /// + /// The close side dialog aria label text. + [Parameter] + public string CloseSideDialogAriaLabel { get; set; } = "Close side dialog"; + + private List dialogs = new List(); + private bool isSideDialogOpen = false; + private RenderFragment sideDialogContent; + private SideDialogOptions sideDialogOptions; + private Dialog sideDialog; + private ElementReference? sideDialogResizeHandle; + private ElementReference? sideDialogHandle; + private IJSObjectReference sideDialogResizeHandleJsModule; + + /// + /// Opens a new dialog with specified parameters and options. + /// + /// The title of the dialog. + /// The content type of the dialog, usually a component type. + /// A dictionary of parameters to pass to the dialog content. + /// Additional configuration options for the dialog, such as size or behavior. + /// A Task that represents the asynchronous operation of opening the dialog. + public async Task Open(string title, + Type type, + Dictionary parameters, + DialogOptions options) + { + dialogs.Add(new Dialog() { Title = title, Type = type, Parameters = parameters, Options = options }); + + await InvokeAsync(() => { StateHasChanged(); }); + } + + /// + /// Closes the currently open dialog and returns the specified result. + /// + /// The result to return when the dialog is closed. This can contain any data that needs to be passed back to the caller. + /// A Task that represents the asynchronous operation of closing the dialog. + public async Task Close(dynamic result) + { + var lastDialog = dialogs.LastOrDefault(); + if (lastDialog != null) + { + dialogs.Remove(lastDialog); + if (dialogs.Count == 0) await JSRuntime.InvokeAsync("Radzen.closeDialog"); + } + + await InvokeAsync(() => + { + StateHasChanged(); + }); + } + + /// + public void Dispose() + { + if (Service is null) return; + Service.OnOpen -= OnOpen; + Service.OnClose -= OnClose; + Service.OnSideOpen -= OnSideOpen; + Service.OnSideClose -= OnSideClose; + } + + /// + 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 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 + }; + + // 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>(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 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 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}"; + } + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (isSideDialogOpen) + { + await JSRuntime.InvokeAsync("Radzen.openSideDialog", sideDialogOptions); + + if (sideDialogOptions is { Resizable: true } && sideDialogResizeHandle.HasValue) + { + sideDialogResizeHandleJsModule = + await JSRuntime.InvokeAsync("Radzen.initSideDialogResize", + sideDialogResizeHandle, sideDialogHandle); + } + } + } + + /// + /// 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. + /// + /// A ValueTask that represents the asynchronous disposal operation. + public async ValueTask DisposeAsync() + { + try + { + if (sideDialogResizeHandleJsModule != null) + { + await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose"); + } + } + catch + { + /* Ignore */ + } + } +} \ No newline at end of file From 8d24b49026b9d0f9d15366a1b2c66893529c28da Mon Sep 17 00:00:00 2001 From: Martin Hans Date: Wed, 5 Nov 2025 16:16:25 +0100 Subject: [PATCH 3/5] Add tests for resizable side dialog. --- Radzen.Blazor.Tests/DialogServiceTests.cs | 78 ++++++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/Radzen.Blazor.Tests/DialogServiceTests.cs b/Radzen.Blazor.Tests/DialogServiceTests.cs index 9476a4b7ebb..5a7c332b38e 100644 --- a/Radzen.Blazor.Tests/DialogServiceTests.cs +++ b/Radzen.Blazor.Tests/DialogServiceTests.cs @@ -1,11 +1,15 @@ -using System; +using Bunit; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Radzen.Blazor.Tests { - public class DialogServiceTests + public class DialogServiceTests : ComponentBase { public class OpenDialogTests { @@ -124,13 +128,81 @@ public async Task OpenAsync_DynamicComponentType_Reflective_Calls_Resolve() var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []); dialogService.Close(); await openTask; - + // Assert Assert.Equal("Dynamic Open", resultingTitle); Assert.Equal(typeof(RadzenButton), resultingType); } } + public class OpenSideDialogTests + { + [Fact(DisplayName = "SideDialogOptions resizable option is retained after OpenSideDialog call")] + public void SideDialogOptions_Resizable_AreRetained_AfterOpenSideDialogCall() + { + // Arrange + var options = new SideDialogOptions { Resizable = true }; + SideDialogOptions resultingOptions = null; + var dialogService = new DialogService(null, null); + dialogService.OnSideOpen += (_, _, sideOptions) => resultingOptions = sideOptions; + + // Act + dialogService.OpenSide("Test", [], options); + + // Assert + Assert.NotNull(resultingOptions); + Assert.Same(options, resultingOptions); + Assert.True(resultingOptions.Resizable); + } + + [Fact(DisplayName = "Side dialog shows resize bar when Resizable is true")] + public void SideDialog_Resizable_ShowsResizeBar() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.Services.AddScoped(); + + // Render the dialog host + var cut = ctx.RenderComponent(); + + // Open a side dialog with Resizable=true + var dialogService = ctx.Services.GetRequiredService(); + cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton), + new Dictionary(), new SideDialogOptions { Resizable = true })); + + // Assert: the resize bar element is present + cut.WaitForAssertion(() => + { + var markup = cut.Markup; + Assert.Contains("rz-dialog-resize-bar", markup); + // Optionally ensure the inner handle exists too + Assert.Contains("rz-resize", markup); + }); + } + + [Fact(DisplayName = "Side dialog hides resize bar when Resizable is false")] + public void SideDialog_NonResizable_HidesResizeBar() + { + using var ctx = new TestContext(); + ctx.JSInterop.Mode = JSRuntimeMode.Loose; + ctx.Services.AddScoped(); + + // Render the dialog host + var cut = ctx.RenderComponent(); + + // Open a side dialog with Resizable=false + var dialogService = ctx.Services.GetRequiredService(); + cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton), + new Dictionary(), new SideDialogOptions())); + + // Assert: the resize bar element is not present + cut.WaitForAssertion(() => + { + var markup = cut.Markup; + Assert.DoesNotContain("rz-dialog-resize-bar", markup); + }); + } + } public class ConfirmTests { [Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")] From 454ea9f78456cfd277feb40f49e49477b1db4154 Mon Sep 17 00:00:00 2001 From: Martin Hans Date: Wed, 5 Nov 2025 16:18:22 +0100 Subject: [PATCH 4/5] Rename element references. --- Radzen.Blazor/RadzenDialog.razor | 9 ++------- Radzen.Blazor/RadzenDialog.razor.cs | 8 ++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Radzen.Blazor/RadzenDialog.razor b/Radzen.Blazor/RadzenDialog.razor index 1b4a2d5634b..96b4d496701 100644 --- a/Radzen.Blazor/RadzenDialog.razor +++ b/Radzen.Blazor/RadzenDialog.razor @@ -12,13 +12,13 @@ {