From f35282ab4cc6eb541a20a1a2be92325539201af1 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 14 Dec 2024 11:47:45 +0100 Subject: [PATCH 01/57] Add FluentDialogBody --- .../Dialog/Examples/DialogBodyDefault.razor | 11 ++++ .../Components/Dialog/FluentDialog.md | 12 ++++ .../Components/Dialog/FluentDialogBody.razor | 23 ++++++++ .../Dialog/FluentDialogBody.razor.cs | 55 +++++++++++++++++++ .../Dialog/FluentDialogBody.razor.css | 24 ++++++++ 5 files changed, 125 insertions(+) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md create mode 100644 src/Core/Components/Dialog/FluentDialogBody.razor create mode 100644 src/Core/Components/Dialog/FluentDialogBody.razor.cs create mode 100644 src/Core/Components/Dialog/FluentDialogBody.razor.css diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor new file mode 100644 index 0000000000..39146c49a0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor @@ -0,0 +1,11 @@ + + + Hello Body + + + + One + Two + + + diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md new file mode 100644 index 0000000000..4fa175806c --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -0,0 +1,12 @@ +--- +title: Dialog +route: /Dialog +--- + +# Dialog + +{{ DialogBodyDefault }} + +## API FluentDialogBody + +{{ API Type=FluentDialogBody }} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor new file mode 100644 index 0000000000..ba210cca3e --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -0,0 +1,23 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@inherits FluentComponentBase + + + + @if (TitleTemplate is not null) + { +
@TitleTemplate
+ } + + @if (TitleActionTemplate is not null) + { +
@TitleActionTemplate
+ } + + @ChildContent + + @if (ActionTemplate is not null) + { +
@ActionTemplate
+ } + +
diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.cs b/src/Core/Components/Dialog/FluentDialogBody.razor.cs new file mode 100644 index 0000000000..01ada030ce --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// The dialog component is a window overlaid on either the primary window or another dialog window. +/// Windows under a modal dialog are inert. +/// +public partial class FluentDialogBody : FluentComponentBase +{ + /// + /// Initializes a new instance of . + /// + public FluentDialogBody() + { + Id = Identifier.NewId(); + } + + /// + protected string? ClassValue => new CssBuilder(Class) + .Build(); + + /// + protected string? StyleValue => new StyleBuilder(Style) + .Build(); + + /// + /// Gets or sets the content to be rendered inside the component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Gets or sets the content for the title element. + /// + [Parameter] + public RenderFragment? TitleTemplate { get; set; } + + /// + /// Gets or sets the content for the action element in the title. + /// + [Parameter] + public RenderFragment? TitleActionTemplate { get; set; } + + /// + /// Gets or sets the content for the action element. + /// + [Parameter] + public RenderFragment? ActionTemplate { get; set; } +} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css new file mode 100644 index 0000000000..136e15d10a --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -0,0 +1,24 @@ +@media (min-width: 480px) { + fluent-dialog-body .dialog-actions { + background: red; + align-items: center; + flex-direction: row; + justify-content: flex-end; + margin-block-start: calc(var(--spacingVerticalS)* -1); + padding-block-start: var(--spacingVerticalS); + } +} + +fluent-dialog-body .dialog-actions { + box-sizing: border-box; + background: var(--colorNeutralBackground1); + display: flex; + flex-direction: column; + gap: var(--spacingVerticalS); + inset-block-end: 0; + margin-block-end: calc(var(--spacingVerticalXXL)* -1); + padding-block-end: var(--spacingVerticalXXL); + padding-block-start: var(--spacingVerticalL); + position: sticky; + z-index: 2; +} From 0a052becdd5b751b289e7abb1723566edc1b071a Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 14 Dec 2024 17:32:19 +0100 Subject: [PATCH 02/57] Add JS Sample method --- examples/Demo/FluentUI.Demo/MyLocalizer.cs | 39 ++++++++++++++++++++++ examples/Demo/FluentUI.Demo/Program.cs | 32 +----------------- src/Core.Scripts/src/ExportedMethods.ts | 22 ++++++++++++ src/Core.Scripts/src/index.ts | 7 ++++ 4 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo/MyLocalizer.cs create mode 100644 src/Core.Scripts/src/ExportedMethods.ts diff --git a/examples/Demo/FluentUI.Demo/MyLocalizer.cs b/examples/Demo/FluentUI.Demo/MyLocalizer.cs new file mode 100644 index 0000000000..4c9c36c986 --- /dev/null +++ b/examples/Demo/FluentUI.Demo/MyLocalizer.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.FluentUI.AspNetCore.Components; +using System.Globalization; + +namespace FluentUI.Demo; + +/// +/// Sample of a Custom Localizer, +/// used with `builder.Services.AddFluentUIComponents`. +/// +internal class MyLocalizer : IFluentLocalizer +{ + public string this[string key, params object[] arguments] + { + get + { + // Need to add + // - builder.Services.AddLocalization(); + // - app.UseRequestLocalization(new RequestLocalizationOptions().AddSupportedUICultures(["fr"])); + var language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + + // Returns the French version of the string + if (language == "fr") + { + return key switch + { + "FluentSample_Hello" => "Bonjour", + _ => IFluentLocalizer.GetDefault(key, arguments), + }; + } + + // By default, returns the English version of the string + return IFluentLocalizer.GetDefault(key, arguments); + } + } +} diff --git a/examples/Demo/FluentUI.Demo/Program.cs b/examples/Demo/FluentUI.Demo/Program.cs index c8d32b5718..303bebda93 100644 --- a/examples/Demo/FluentUI.Demo/Program.cs +++ b/examples/Demo/FluentUI.Demo/Program.cs @@ -2,7 +2,6 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ -using System.Globalization; using FluentUI.Demo.Client; using Microsoft.FluentUI.AspNetCore.Components; @@ -22,7 +21,7 @@ // Add FluentUI services builder.Services.AddFluentUIComponents(config => { - config.Localizer = new MyLocalizer(); + config.Localizer = new FluentUI.Demo.MyLocalizer(); }); // Add Demo server services @@ -56,32 +55,3 @@ .AddAdditionalAssemblies(typeof(FluentUI.Demo.Client._Imports).Assembly); app.Run(); - -internal class MyLocalizer : IFluentLocalizer -{ - public string this[string key, params object[] arguments] - { - get - { - // Need to add - // - builder.Services.AddLocalization(); - // - app.UseRequestLocalization(new RequestLocalizationOptions().AddSupportedUICultures(["fr"])); - var language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; - - Console.WriteLine(language); - - // Returns the French version of the string - if (language == "fr") - { - return key switch - { - "FluentSample_Hello" => "Bonjour", - _ => IFluentLocalizer.GetDefault(key, arguments), - }; - } - - // By default, returns the English version of the string - return IFluentLocalizer.GetDefault(key, arguments); - } - } -} diff --git a/src/Core.Scripts/src/ExportedMethods.ts b/src/Core.Scripts/src/ExportedMethods.ts new file mode 100644 index 0000000000..e284f63ea7 --- /dev/null +++ b/src/Core.Scripts/src/ExportedMethods.ts @@ -0,0 +1,22 @@ +import { Microsoft as LoggerFile } from './Utilities/Logger'; + +export namespace Microsoft.FluentUI.Blazor.ExportedMethods { + + /** + * Initializes the common methods to use with the Fluent UI Blazor library. + */ + export function initialize() { + + // Create the Microsoft.FluentUI.Blazor namespace + (window as any).Microsoft = (window as any).Microsoft || {}; + (window as any).Microsoft.FluentUI = (window as any).Microsoft.FluentUI || {}; + (window as any).Microsoft.FluentUI.Blazor = (window as any).Microsoft.FluentUI.Blazor || {}; + + // Utilities methods (Logger) + (window as any).Microsoft.FluentUI.Blazor.Utilities = (window as any).Microsoft.FluentUI.Blazor.Utilities || {}; + (window as any).Microsoft.FluentUI.Blazor.Utilities.Logger = LoggerFile.FluentUI.Blazor.Utilities.Logger; + + // [^^^ Add your other exported methods before this line ^^^] + } +} + diff --git a/src/Core.Scripts/src/index.ts b/src/Core.Scripts/src/index.ts index e12d4ce2cc..3d13a5af64 100644 --- a/src/Core.Scripts/src/index.ts +++ b/src/Core.Scripts/src/index.ts @@ -13,6 +13,10 @@ - All FluentUI WebComponents are defined and initialized in the `FluentUIWebComponents` file. */ +/* ***********************************************/ +/* ⚠️ PLEASE DO NOT MODIFY ANYTHING IN THIS FILE */ +/* ***********************************************/ + import { Microsoft as StartupFile } from './Startup'; import Startup = StartupFile.FluentUI.Blazor.Startup; @@ -25,3 +29,6 @@ export const afterWebStarted = Startup.afterWebStarted; export const afterServerStarted = Startup.afterServerStarted; export const afterWebAssemblyStarted = Startup.afterWebAssemblyStarted; +// Initialize the common methods to use with the Fluent UI Blazor library. +import { Microsoft as ExportedMethodsFile } from './ExportedMethods'; +ExportedMethodsFile.FluentUI.Blazor.ExportedMethods.initialize(); From e184f9877f0e29d62d3064185cc61b692411df0f Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 14 Dec 2024 17:55:03 +0100 Subject: [PATCH 03/57] Update styles --- .../Dialog/Examples/DialogBodyDefault.razor | 7 ++++- .../Components/Dialog/FluentDialogBody.razor | 2 +- .../Dialog/FluentDialogBody.razor.css | 31 ++++++------------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor index 39146c49a0..465facb5b0 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor @@ -1,10 +1,15 @@  + + + My Title + + Hello Body - One + One Two diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index ba210cca3e..4cc410476e 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -17,7 +17,7 @@ @if (ActionTemplate is not null) { -
@ActionTemplate
+
@ActionTemplate
} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css index 136e15d10a..9b62c59465 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.css +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -1,24 +1,13 @@ -@media (min-width: 480px) { - fluent-dialog-body .dialog-actions { - background: red; - align-items: center; - flex-direction: row; - justify-content: flex-end; - margin-block-start: calc(var(--spacingVerticalS)* -1); - padding-block-start: var(--spacingVerticalS); - } -} - -fluent-dialog-body .dialog-actions { - box-sizing: border-box; - background: var(--colorNeutralBackground1); +/* Desktop and Mobile */ +fluent-dialog-body .actions-container { display: flex; - flex-direction: column; gap: var(--spacingVerticalS); - inset-block-end: 0; - margin-block-end: calc(var(--spacingVerticalXXL)* -1); - padding-block-end: var(--spacingVerticalXXL); - padding-block-start: var(--spacingVerticalL); - position: sticky; - z-index: 2; + flex-direction: column; +} + +/* Desktop */ +@container (min-width: 480px) { + fluent-dialog-body .actions-container { + flex-direction: row; + } } From 4e16f19b6cda1dd31e695103dba74cb4cd4438ae Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 09:51:05 +0100 Subject: [PATCH 04/57] Update Styles --- src/Core/Components/Dialog/FluentDialogBody.razor | 2 +- src/Core/Components/Dialog/FluentDialogBody.razor.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index 4cc410476e..c2dc4e529c 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -17,7 +17,7 @@ @if (ActionTemplate is not null) { -
@ActionTemplate
+
@ActionTemplate
} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css index 9b62c59465..dd15348c91 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.css +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -1,5 +1,5 @@ /* Desktop and Mobile */ -fluent-dialog-body .actions-container { +fluent-dialog-body > div[slot="action"] { display: flex; gap: var(--spacingVerticalS); flex-direction: column; @@ -7,7 +7,7 @@ fluent-dialog-body .actions-container { /* Desktop */ @container (min-width: 480px) { - fluent-dialog-body .actions-container { + fluent-dialog-body > div[slot="action"] { flex-direction: row; } } From 28172a1618c7c18f1b69b601a008279cfc38abeb Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 09:52:40 +0100 Subject: [PATCH 05/57] Remove unused sub Id --- src/Core/Components/Dialog/FluentDialogBody.razor | 6 +++--- src/Core/Components/Dialog/FluentDialogBody.razor.cs | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index c2dc4e529c..dde57cc729 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -5,19 +5,19 @@ @if (TitleTemplate is not null) { -
@TitleTemplate
+
@TitleTemplate
} @if (TitleActionTemplate is not null) { -
@TitleActionTemplate
+
@TitleActionTemplate
} @ChildContent @if (ActionTemplate is not null) { -
@ActionTemplate
+
@ActionTemplate
} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.cs b/src/Core/Components/Dialog/FluentDialogBody.razor.cs index 01ada030ce..d2bc3f1aa7 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.cs @@ -13,14 +13,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components; ///
public partial class FluentDialogBody : FluentComponentBase { - /// - /// Initializes a new instance of . - /// - public FluentDialogBody() - { - Id = Identifier.NewId(); - } - /// protected string? ClassValue => new CssBuilder(Class) .Build(); From 5449f5e5795bcbc9c89d907ea7d5bb292dd25d47 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 17:05:56 +0100 Subject: [PATCH 06/57] Add draft of Dialog classes --- src/Core/Components/Base/FluentServiceBase.cs | 96 +++++++++++++++++++ .../Components/Base/IFluentServiceBase.cs | 45 +++++++++ src/Core/Components/Dialog/FluentDialog.razor | 14 +++ .../Components/Dialog/FluentDialog.razor.cs | 26 +++++ .../Components/Dialog/FluentDialogBody.razor | 5 +- .../Dialog/FluentDialogProvider.razor | 12 +++ .../Dialog/FluentDialogProvider.razor.cs | 56 +++++++++++ .../Dialog/Services/DialogInstance.cs | 37 +++++++ .../Dialog/Services/DialogParameters.cs | 16 ++++ .../Dialog/Services/DialogReference.cs | 41 ++++++++ .../Dialog/Services/DialogResult.cs | 28 ++++++ .../Dialog/Services/DialogService.cs | 22 +++++ .../Services/IDialogContentComponent.cs | 24 +++++ .../Dialog/Services/IDialogReference.cs | 23 +++++ .../Dialog/Services/IDialogService.cs | 25 +++++ src/Core/Utilities/ZIndex.cs | 16 ++++ 16 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 src/Core/Components/Base/FluentServiceBase.cs create mode 100644 src/Core/Components/Base/IFluentServiceBase.cs create mode 100644 src/Core/Components/Dialog/FluentDialog.razor create mode 100644 src/Core/Components/Dialog/FluentDialog.razor.cs create mode 100644 src/Core/Components/Dialog/FluentDialogProvider.razor create mode 100644 src/Core/Components/Dialog/FluentDialogProvider.razor.cs create mode 100644 src/Core/Components/Dialog/Services/DialogInstance.cs create mode 100644 src/Core/Components/Dialog/Services/DialogParameters.cs create mode 100644 src/Core/Components/Dialog/Services/DialogReference.cs create mode 100644 src/Core/Components/Dialog/Services/DialogResult.cs create mode 100644 src/Core/Components/Dialog/Services/DialogService.cs create mode 100644 src/Core/Components/Dialog/Services/IDialogContentComponent.cs create mode 100644 src/Core/Components/Dialog/Services/IDialogReference.cs create mode 100644 src/Core/Components/Dialog/Services/IDialogService.cs create mode 100644 src/Core/Utilities/ZIndex.cs diff --git a/src/Core/Components/Base/FluentServiceBase.cs b/src/Core/Components/Base/FluentServiceBase.cs new file mode 100644 index 0000000000..215c398288 --- /dev/null +++ b/src/Core/Components/Base/FluentServiceBase.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// +/// +/// +public abstract class FluentServiceBase : IFluentServiceBase where TComponent : FluentComponentBase +{ + private readonly ReaderWriterLockSlim _lock = new(); + private readonly IList _list = []; + + /// + /// + /// + public virtual string? ProviderId { get; set; } + + /// + /// + /// + public virtual IEnumerable Items + { + get + { + _lock.EnterReadLock(); + try + { + return _list; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + /// + /// + /// + public virtual Func OnUpdatedAsync { get; set; } = (item) => Task.CompletedTask; + + /// + /// + /// + public virtual void Add(TComponent item) + { + _lock.EnterWriteLock(); + try + { + _list.Add(item); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// + /// + public virtual void Remove(TComponent item) + { + _lock.EnterWriteLock(); + try + { + var firstItem = _list.FirstOrDefault(i => string.Equals(i.Id, item.Id, StringComparison.OrdinalIgnoreCase)); + if (firstItem != null) + { + _list.Remove(firstItem); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// + /// + public virtual void Clear() + { + _lock.EnterWriteLock(); + try + { + _list.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } +} diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs new file mode 100644 index 0000000000..b1c30e6baa --- /dev/null +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Common interface for a service provider (DialogService, MenuService, ...). +/// +/// +public interface IFluentServiceBase where TComponent : FluentComponentBase +{ + /// + /// Gets or sets the Provider ID. + /// This value is set by the provider and will be empty if the provider is not initialized. + /// + public string? ProviderId { get; set; } + + /// + /// Gets the list of . + /// + IEnumerable Items { get; } + + /// + /// Action to be called when the is updated. + /// + Func OnUpdatedAsync { get; set; } + + /// + /// Add a to the list. + /// + /// + void Add(TComponent item); + + /// + /// Remove a from the list. + /// + /// + void Remove(TComponent item); + + /// + /// Clear the list of . + /// + void Clear(); +} diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor new file mode 100644 index 0000000000..9df411afe2 --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -0,0 +1,14 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@inherits FluentComponentBase + + + @* Content *@ + @if (Instance is not null) + { + + } + else + { + @ChildContent + } + diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs new file mode 100644 index 0000000000..8ec8d6c9fa --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// The dialog component is a window overlaid on either the primary window or another dialog window. +/// Windows under a modal dialog are inert. +/// +public partial class FluentDialog : FluentComponentBase +{ + /// + /// Gets or sets the instance used by the . + /// + [Parameter] + public DialogInstance? Instance { get; set; } + + /// + /// Used when not calling the to show a dialog. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } +} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index dde57cc729..5cf681ab1c 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -13,7 +13,10 @@
@TitleActionTemplate
} - @ChildContent + @if (ChildContent is not null) + { +
@ChildContent
+ } @if (ActionTemplate is not null) { diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor new file mode 100644 index 0000000000..ed14a09baf --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -0,0 +1,12 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@inherits FluentComponentBase + +
+ @if (DialogService != null) + { + @foreach (var item in DialogService.Items) + { + + } + } +
diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs new file mode 100644 index 0000000000..798237dd25 --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Globalization; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public partial class FluentDialogProvider : FluentComponentBase +{ + private IDialogService? _dialogService; + + /// + public FluentDialogProvider() + { + Id = Identifier.NewId(); + } + + /// + internal string? ClassValue => new CssBuilder(Class) + .AddClass("fluent-dialog-provider") + .Build(); + + /// + internal string? StyleValue => new StyleBuilder(Style) + .AddStyle("z-index", ZIndex.Dialog.ToString(CultureInfo.InvariantCulture)) + .Build(); + + /// + /// Gets or sets the injected service provider. + /// + [Inject] + public IServiceProvider? ServiceProvider { get; set; } + + /// + protected virtual IDialogService? DialogService => _dialogService ??= ServiceProvider?.GetService(); + + /// + protected IEnumerable? Dialogs => DialogService?.Items; + + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + if (DialogService is not null) + { + DialogService.ProviderId = Id; + DialogService.OnUpdatedAsync = async (item) => await InvokeAsync(StateHasChanged); + } + } +} diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs new file mode 100644 index 0000000000..655b6a82d5 --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public sealed class DialogInstance +{ + /// + public DialogInstance(Type? type, DialogParameters parameters, object content) + { + ContentType = type; + Parameters = parameters; + Content = content; + } + + /// + public Type? ContentType { get; } + + /// + public object Content { get; internal set; } = default!; + + /// + public DialogParameters Parameters { get; internal set; } + + /// + internal Dictionary? GetParameterDictionary() + { + if (Content is null) + { + return null; + } + + return new Dictionary(StringComparer.Ordinal) { { "Content", Content } }; + } +} diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogParameters.cs new file mode 100644 index 0000000000..9ae9e2531b --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogParameters.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Parameters for configuring a dialog. +/// +public class DialogParameters +{ + /// + /// Gets or sets the title of the dialog. + /// + public string? Title { get; set; } +} diff --git a/src/Core/Components/Dialog/Services/DialogReference.cs b/src/Core/Components/Dialog/Services/DialogReference.cs new file mode 100644 index 0000000000..bce1d34657 --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogReference.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public class DialogReference : IDialogReference +{ + private readonly IDialogService _dialogService; + private readonly TaskCompletionSource _resultCompletion = new(); + + /// + public DialogReference(Guid dialogInstanceId, DialogInstance dialogInstance, IDialogService dialogService) + { + DialogInstanceId = dialogInstanceId; + Instance = dialogInstance; + _dialogService = dialogService; + } + + /// + internal Guid DialogInstanceId { get; } + + /// + public DialogInstance Instance { get; set; } + + /// + public Task Result => _resultCompletion.Task; + + /// + public Task CloseAsync() + { + return _dialogService.CloseAsync(this, DialogResult.Cancel()); + } + + /// + public Task CloseAsync(DialogResult result) + { + return _dialogService.CloseAsync(this, result); + } +} diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs new file mode 100644 index 0000000000..85a7a51a9e --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public class DialogResult +{ + /// + protected internal DialogResult(object? data, bool cancelled) + { + Data = data; + Cancelled = cancelled; + } + + /// + public object? Data { get; set; } + + /// + public bool Cancelled { get; } + + /// + public static DialogResult Ok(T result) => new(result, cancelled: false); + + /// + public static DialogResult Cancel(object? data = null) => new(data ?? default, cancelled: true); +} diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs new file mode 100644 index 0000000000..fd61d61ecf --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public partial class DialogService : FluentServiceBase, IDialogService +{ + /// + public Task CloseAsync(DialogReference dialog, DialogResult result) + { + return Task.CompletedTask; + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0025:Implement the functionality instead of throwing NotImplementedException", Justification = "Under development")] + public Task ShowDialogAsync(Type dialogComponent, TData data, DialogParameters parameters) where TData : class + { + throw new NotImplementedException(); + } +} diff --git a/src/Core/Components/Dialog/Services/IDialogContentComponent.cs b/src/Core/Components/Dialog/Services/IDialogContentComponent.cs new file mode 100644 index 0000000000..cd100b5dbf --- /dev/null +++ b/src/Core/Components/Dialog/Services/IDialogContentComponent.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// A component implementing this interface can be used as dialog content. +/// +public interface IDialogContentComponent +{ +} + +/// +/// A component implementing this interface can be used as dialog content. +/// +/// +public interface IDialogContentComponent : IDialogContentComponent +{ + /// + /// Gets or sets the content to display in the dialog. + /// + TContent Content { get; set; } +} diff --git a/src/Core/Components/Dialog/Services/IDialogReference.cs b/src/Core/Components/Dialog/Services/IDialogReference.cs new file mode 100644 index 0000000000..f161681174 --- /dev/null +++ b/src/Core/Components/Dialog/Services/IDialogReference.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Interface for DialogReference +/// +public interface IDialogReference +{ + /// + Task Result { get; } + + /// + DialogInstance Instance { get; set; } + + /// + Task CloseAsync(); + + /// + Task CloseAsync(DialogResult result); +} diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs new file mode 100644 index 0000000000..7abc16c5ce --- /dev/null +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Interface for DialogService +/// +public partial interface IDialogService : IFluentServiceBase +{ + /// + Task CloseAsync(DialogReference dialog, DialogResult result); + + /// + /// Shows a dialog with the component type as the body, + /// passing the specified + /// + /// Type of content to pass to component being displayed. + /// Type of component to display. + /// Content to pass to component being displayed. + /// Parameters to configure the dialog component. + Task ShowDialogAsync(Type dialogComponent, TData data, DialogParameters parameters) + where TData : class; +} diff --git a/src/Core/Utilities/ZIndex.cs b/src/Core/Utilities/ZIndex.cs new file mode 100644 index 0000000000..edcd96ed93 --- /dev/null +++ b/src/Core/Utilities/ZIndex.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// ZIndex values for FluentUI components +/// +public static class ZIndex +{ + /// + /// ZIndex for the component. + /// + public static int Dialog { get; set; } = 999; +} From 94eb0dd99d2c942d33224d5bfbf4978e29bd2449 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 21:34:05 +0100 Subject: [PATCH 07/57] Add samples and update Provider --- .../Examples/DialogServiceDefault.razor | 30 ++++++++++++ .../Dialog/Examples/SimpleDialog.razor | 13 +++++ .../Layout/DemoMainLayout.razor | 3 ++ src/Core/Components/Base/FluentServiceBase.cs | 26 +++++++--- .../Base/FluentServiceProviderException.cs | 20 ++++++++ .../Components/Base/IFluentServiceBase.cs | 4 +- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Components/Dialog/FluentDialog.razor.cs | 23 +++++++++ .../Dialog/FluentDialogProvider.razor | 2 +- .../Dialog/Services/DialogService.cs | 23 +++++++-- .../Dialog/Services/IDialogService.cs | 14 ++++-- .../Extensions/ServiceCollectionExtensions.cs | 48 ++++++++++++------- .../Extensions/ServiceProviderExtensions.cs | 17 +++++++ .../Infrastructure/LibraryConfiguration.cs | 9 ++++ 14 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor create mode 100644 src/Core/Components/Base/FluentServiceProviderException.cs create mode 100644 src/Core/Extensions/ServiceProviderExtensions.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor new file mode 100644 index 0000000000..8e460e28e4 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -0,0 +1,30 @@ +@inject IDialogService DialogService + + + Open Dialog + + +@code +{ + private async Task OpenDialogAsync() + { + DialogParameters parameters = new() + { + Title = $"My title", + }; + + var data = new SimpleDialog.MySimpleData(); + var dialog = await DialogService.ShowDialogAsync(data, parameters); + var result = await dialog.Result; + + if (result.Data is not null) + { + var simplePerson = result.Data as SimpleDialog.MySimpleData; + Console.WriteLine($"Dialog closed with {simplePerson?.Name} - Canceled: {result.Cancelled}"); + } + else + { + Console.WriteLine($"Dialog closed - Canceled: {result.Cancelled}"); + } + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor new file mode 100644 index 0000000000..7aed7ea283 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -0,0 +1,13 @@ +@implements IDialogContentComponent + +Hello World + +@code { + [CascadingParameter] + public FluentDialog? Dialog { get; set; } + + public class MySimpleData + { + public string? Name { get; set; } + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Layout/DemoMainLayout.razor b/examples/Demo/FluentUI.Demo.Client/Layout/DemoMainLayout.razor index 2f7ab6843d..398a6a2301 100644 --- a/examples/Demo/FluentUI.Demo.Client/Layout/DemoMainLayout.razor +++ b/examples/Demo/FluentUI.Demo.Client/Layout/DemoMainLayout.razor @@ -75,3 +75,6 @@
© Microsoft @DateTime.Now.Year. All rights reserved. + + + diff --git a/src/Core/Components/Base/FluentServiceBase.cs b/src/Core/Components/Base/FluentServiceBase.cs index 215c398288..23a226de29 100644 --- a/src/Core/Components/Base/FluentServiceBase.cs +++ b/src/Core/Components/Base/FluentServiceBase.cs @@ -16,12 +16,12 @@ public abstract class FluentServiceBase : IFluentServiceBase /// ///
- public virtual string? ProviderId { get; set; } + string? IFluentServiceBase.ProviderId { get; set; } /// /// /// - public virtual IEnumerable Items + IEnumerable IFluentServiceBase.Items { get { @@ -40,12 +40,12 @@ public virtual IEnumerable Items /// /// /// - public virtual Func OnUpdatedAsync { get; set; } = (item) => Task.CompletedTask; + Func IFluentServiceBase.OnUpdatedAsync { get; set; } = (item) => Task.CompletedTask; /// /// /// - public virtual void Add(TComponent item) + void IFluentServiceBase.Add(TComponent item) { _lock.EnterWriteLock(); try @@ -61,7 +61,7 @@ public virtual void Add(TComponent item) /// /// /// - public virtual void Remove(TComponent item) + void IFluentServiceBase.Remove(TComponent item) { _lock.EnterWriteLock(); try @@ -81,7 +81,7 @@ public virtual void Remove(TComponent item) /// /// /// - public virtual void Clear() + void IFluentServiceBase.Clear() { _lock.EnterWriteLock(); try @@ -93,4 +93,18 @@ public virtual void Clear() _lock.ExitWriteLock(); } } + + /// + /// Gets the current instance of the service, + /// converted to the interface. + /// + internal IFluentServiceBase InternalService => this; + + /// + /// + /// + public void Dispose() + { + InternalService.Clear(); + } } diff --git a/src/Core/Components/Base/FluentServiceProviderException.cs b/src/Core/Components/Base/FluentServiceProviderException.cs new file mode 100644 index 0000000000..43dadd97a7 --- /dev/null +++ b/src/Core/Components/Base/FluentServiceProviderException.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Exception thrown when a service provider is not available. +/// +public class FluentServiceProviderException : Exception +{ + /// + /// Creates a new instance of the class. + /// + public FluentServiceProviderException() + : base($"{typeof(TProvider).Name} needs to be added to the main layout of your application/site.") + { + + } +} diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs index b1c30e6baa..8224bb4323 100644 --- a/src/Core/Components/Base/IFluentServiceBase.cs +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -8,13 +8,13 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// Common interface for a service provider (DialogService, MenuService, ...). ///
/// -public interface IFluentServiceBase where TComponent : FluentComponentBase +public interface IFluentServiceBase : IDisposable where TComponent : FluentComponentBase { /// /// Gets or sets the Provider ID. /// This value is set by the provider and will be empty if the provider is not initialized. /// - public string? ProviderId { get; set; } + string? ProviderId { get; set; } /// /// Gets the list of . diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 9df411afe2..4adfc369d5 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -1,7 +1,7 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - + @* Content *@ @if (Instance is not null) { diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 8ec8d6c9fa..f78f49c559 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -12,6 +12,10 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial class FluentDialog : FluentComponentBase { + /// + [Inject] + private IDialogService? DialogService { get; set; } + /// /// Gets or sets the instance used by the . /// @@ -23,4 +27,23 @@ public partial class FluentDialog : FluentComponentBase /// [Parameter] public RenderFragment? ChildContent { get; set; } + + ///// + ///// Initializes the component. + ///// + ///// + //protected override void OnInitialized() + //{ + // if (DialogService != null) + // { + // if (DialogService.ProviderNotAvailable()) + // { + // throw new FluentServiceProviderException(); + // } + + // DialogService.Add(this); + // } + + // base.OnInitialized(); + //} } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index ed14a09baf..986f699d9f 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -6,7 +6,7 @@ { @foreach (var item in DialogService.Items) { - + } } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index fd61d61ecf..3d93e84e19 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -13,10 +13,27 @@ public Task CloseAsync(DialogReference dialog, DialogResult result) return Task.CompletedTask; } + /// + public virtual Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters) + { + if (!typeof(IDialogContentComponent).IsAssignableFrom(dialogComponent)) + { + throw new ArgumentException($"{dialogComponent.FullName} must be a Dialog Component", nameof(dialogComponent)); + } + + if (this.ProviderNotAvailable()) + { + throw new FluentServiceProviderException(); + } + + throw new InvalidOperationException("Hello"); + //IDialogReference? dialogReference = new DialogReference(parameters.Id, this); + //return await OnShowAsync.Invoke(dialogReference, dialogComponent, parameters, data); + } + /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0025:Implement the functionality instead of throwing NotImplementedException", Justification = "Under development")] - public Task ShowDialogAsync(Type dialogComponent, TData data, DialogParameters parameters) where TData : class + public Task ShowDialogAsync(object data, DialogParameters parameters) where TDialog : IDialogContentComponent { - throw new NotImplementedException(); + return ShowDialogAsync(typeof(TDialog), data, parameters); } } diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 7abc16c5ce..99ae3a249c 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -16,10 +16,18 @@ public partial interface IDialogService : IFluentServiceBase /// Shows a dialog with the component type as the body, /// passing the specified /// - /// Type of content to pass to component being displayed. /// Type of component to display. /// Content to pass to component being displayed. /// Parameters to configure the dialog component. - Task ShowDialogAsync(Type dialogComponent, TData data, DialogParameters parameters) - where TData : class; + Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters); + + /// + /// Shows a dialog with the component type as the body, + /// passing the specified + /// + /// Type of component to display. + /// Content to pass to component being displayed. + /// Parameters to configure the dialog component. + Task ShowDialogAsync(object data, DialogParameters parameters) + where TDialog : IDialogContentComponent; } diff --git a/src/Core/Extensions/ServiceCollectionExtensions.cs b/src/Core/Extensions/ServiceCollectionExtensions.cs index 4074f1536c..06fd05a11d 100644 --- a/src/Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Extensions/ServiceCollectionExtensions.cs @@ -19,26 +19,42 @@ public static class ServiceCollectionExtensions /// Library configuration public static IServiceCollection AddFluentUIComponents(this IServiceCollection services, LibraryConfiguration? configuration = null) { - /* - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - var options = configuration ?? new(); - if (options.UseTooltipServiceProvider) - { - services.AddScoped(); - } - services.AddSingleton(options); - */ - - services.AddScoped(provider => configuration?.Localizer ?? FluentLocalizerInternal.Default); + var options = configuration ?? new(); + + var serviceLifetime = options?.ServiceLifetime ?? ServiceLifetime.Scoped; + if (serviceLifetime == ServiceLifetime.Transient) + { + throw new NotSupportedException("Transient lifetime is not supported for Fluent UI services."); + } + + // Add services + services.Add(provider => options ?? new(), serviceLifetime); + services.Add(provider => new DialogService(), serviceLifetime); + services.Add(provider => options?.Localizer ?? FluentLocalizerInternal.Default, serviceLifetime); return services; } + /// + /// Add a service to the service collection + /// + /// + /// + /// + /// + /// + /// + private static IServiceCollection Add(this IServiceCollection services, Func implementationFactory, ServiceLifetime lifetime) + where TService : class + { + return lifetime switch + { + ServiceLifetime.Singleton => services.AddSingleton(implementationFactory), + ServiceLifetime.Scoped => services.AddScoped(implementationFactory), + _ => throw new NotSupportedException($"Service lifetime {lifetime} is not supported.") + }; + } + /// /// Add common services required by the Fluent UI Web Components for Blazor library /// diff --git a/src/Core/Extensions/ServiceProviderExtensions.cs b/src/Core/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 0000000000..e1ef57cb68 --- /dev/null +++ b/src/Core/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +internal static class ServiceProviderExtensions +{ + /// + /// Gets a value indicating whether the provider was added by the user and is available. + /// + public static bool ProviderNotAvailable(this IFluentServiceBase provider) + where TComponent : FluentComponentBase + { + return string.IsNullOrEmpty(provider.ProviderId); + } +} diff --git a/src/Core/Infrastructure/LibraryConfiguration.cs b/src/Core/Infrastructure/LibraryConfiguration.cs index 236917f0f9..ab5a7e93a1 100644 --- a/src/Core/Infrastructure/LibraryConfiguration.cs +++ b/src/Core/Infrastructure/LibraryConfiguration.cs @@ -2,6 +2,8 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using Microsoft.Extensions.DependencyInjection; + namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -21,6 +23,13 @@ public class LibraryConfiguration /// public bool UseTooltipServiceProvider { get; set; } = true; + /// + /// Gets or sets the service lifetime for the library services, when using Fluent UI in WebAssembly, it can make sense to use . + /// Default is . + /// Only and are supported. + /// + public ServiceLifetime ServiceLifetime { get; set; } = ServiceLifetime.Scoped; + /// /// Gets or sets the FluentLocalizer instance used to localize the library components. /// From 1130a11765f4cbb4847b100859d5e3c6e38a6681 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 22:24:04 +0100 Subject: [PATCH 08/57] Fix missing properties --- .../Dialog/Examples/SimpleDialog.razor | 3 + .../Components/Dialog/FluentDialog.md | 4 + src/Core/Components/Base/FluentServiceBase.cs | 79 ++----------------- .../Components/Base/IFluentServiceBase.cs | 23 +----- .../Components/Dialog/FluentDialog.razor.cs | 12 +++ .../Dialog/FluentDialogProvider.razor | 2 + .../Dialog/FluentDialogProvider.razor.cs | 5 +- .../Dialog/Services/DialogService.cs | 14 +++- 8 files changed, 48 insertions(+), 94 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 7aed7ea283..55c4199bd3 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -6,6 +6,9 @@ Hello World [CascadingParameter] public FluentDialog? Dialog { get; set; } + [Parameter] + public object? Content { get; set; } + public class MySimpleData { public string? Name { get; set; } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index 4fa175806c..396e3702c1 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -7,6 +7,10 @@ route: /Dialog {{ DialogBodyDefault }} +## Services + +{{ DialogServiceDefault }} + ## API FluentDialogBody {{ API Type=FluentDialogBody }} diff --git a/src/Core/Components/Base/FluentServiceBase.cs b/src/Core/Components/Base/FluentServiceBase.cs index 23a226de29..7cde51b59f 100644 --- a/src/Core/Components/Base/FluentServiceBase.cs +++ b/src/Core/Components/Base/FluentServiceBase.cs @@ -2,16 +2,17 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using System.Collections.Concurrent; + namespace Microsoft.FluentUI.AspNetCore.Components; /// /// /// /// -public abstract class FluentServiceBase : IFluentServiceBase where TComponent : FluentComponentBase +public abstract class FluentServiceBase : IFluentServiceBase { - private readonly ReaderWriterLockSlim _lock = new(); - private readonly IList _list = []; + private readonly ConcurrentBag _list = []; /// /// @@ -21,79 +22,13 @@ public abstract class FluentServiceBase : IFluentServiceBase /// /// - IEnumerable IFluentServiceBase.Items - { - get - { - _lock.EnterReadLock(); - try - { - return _list; - } - finally - { - _lock.ExitReadLock(); - } - } - } - + ConcurrentBag IFluentServiceBase.Items => _list; + /// /// /// Func IFluentServiceBase.OnUpdatedAsync { get; set; } = (item) => Task.CompletedTask; - /// - /// - /// - void IFluentServiceBase.Add(TComponent item) - { - _lock.EnterWriteLock(); - try - { - _list.Add(item); - } - finally - { - _lock.ExitWriteLock(); - } - } - - /// - /// - /// - void IFluentServiceBase.Remove(TComponent item) - { - _lock.EnterWriteLock(); - try - { - var firstItem = _list.FirstOrDefault(i => string.Equals(i.Id, item.Id, StringComparison.OrdinalIgnoreCase)); - if (firstItem != null) - { - _list.Remove(firstItem); - } - } - finally - { - _lock.ExitWriteLock(); - } - } - - /// - /// - /// - void IFluentServiceBase.Clear() - { - _lock.EnterWriteLock(); - try - { - _list.Clear(); - } - finally - { - _lock.ExitWriteLock(); - } - } - /// /// Gets the current instance of the service, /// converted to the interface. @@ -105,6 +40,6 @@ void IFluentServiceBase.Clear() /// public void Dispose() { - InternalService.Clear(); + InternalService.Items.Clear(); } } diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs index 8224bb4323..aac88339a8 100644 --- a/src/Core/Components/Base/IFluentServiceBase.cs +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -2,13 +2,15 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using System.Collections.Concurrent; + namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Common interface for a service provider (DialogService, MenuService, ...). /// /// -public interface IFluentServiceBase : IDisposable where TComponent : FluentComponentBase +public interface IFluentServiceBase : IDisposable { /// /// Gets or sets the Provider ID. @@ -19,27 +21,10 @@ public interface IFluentServiceBase : IDisposable where TComponent : /// /// Gets the list of . /// - IEnumerable Items { get; } + ConcurrentBag Items { get; } /// /// Action to be called when the is updated. /// Func OnUpdatedAsync { get; set; } - - /// - /// Add a to the list. - /// - /// - void Add(TComponent item); - - /// - /// Remove a from the list. - /// - /// - void Remove(TComponent item); - - /// - /// Clear the list of . - /// - void Clear(); } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index f78f49c559..920a6df995 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -12,6 +12,18 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial class FluentDialog : FluentComponentBase { + /// + public FluentDialog() + { + } + + /// + internal FluentDialog(IDialogService? dialogService, DialogInstance? instance) + { + DialogService = dialogService; + Instance = instance; + } + /// [Inject] private IDialogService? DialogService { get; set; } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 986f699d9f..2a1a78c55d 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -4,6 +4,8 @@
@if (DialogService != null) { +
@DialogService.Items.Count
+ @foreach (var item in DialogService.Items) { diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index 798237dd25..7936816b49 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -50,7 +50,10 @@ protected override void OnInitialized() if (DialogService is not null) { DialogService.ProviderId = Id; - DialogService.OnUpdatedAsync = async (item) => await InvokeAsync(StateHasChanged); + DialogService.OnUpdatedAsync = async (item) => + { + await InvokeAsync(StateHasChanged); + }; } } } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 3d93e84e19..11602eeece 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -14,7 +14,8 @@ public Task CloseAsync(DialogReference dialog, DialogResult result) } /// - public virtual Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] + public virtual async Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters) { if (!typeof(IDialogContentComponent).IsAssignableFrom(dialogComponent)) { @@ -26,7 +27,16 @@ public virtual Task ShowDialogAsync(Type dialogComponent, obje throw new FluentServiceProviderException(); } - throw new InvalidOperationException("Hello"); + var dialogInstance = new DialogInstance(dialogComponent, parameters, data); + var dialogReference = new DialogReference(Guid.NewGuid(), dialogInstance, this); + var dialog = new FluentDialog(this, dialogInstance); + + InternalService.Items.Add(dialog); + + await InternalService.OnUpdatedAsync.Invoke(dialog); + + return dialogReference; + //throw new InvalidOperationException("Hello"); //IDialogReference? dialogReference = new DialogReference(parameters.Id, this); //return await OnShowAsync.Invoke(dialogReference, dialogComponent, parameters, data); } From 97cab25248600f0ea5da36e10a247b980cd6d6c0 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 16 Dec 2024 22:29:21 +0100 Subject: [PATCH 09/57] Add Id --- src/Core/Components/Dialog/FluentDialog.razor.cs | 2 ++ src/Core/Components/Dialog/FluentDialogProvider.razor | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 920a6df995..9f146a1274 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -20,6 +21,7 @@ public FluentDialog() /// internal FluentDialog(IDialogService? dialogService, DialogInstance? instance) { + Id = Identifier.NewId(); DialogService = dialogService; Instance = instance; } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 2a1a78c55d..ac6a48ea61 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -6,9 +6,9 @@ {
@DialogService.Items.Count
- @foreach (var item in DialogService.Items) + @foreach (var dialog in DialogService.Items) { - + } }
From b3a9aeafc06195ca3e3dfc8e6a12fdd99b90a7e1 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 11:13:00 +0100 Subject: [PATCH 10/57] Add show and hide methods --- .../Dialog/Examples/SimpleDialog.razor | 17 ++++- .../appsettings.Development.json | 3 +- ...entUI.AspNetCore.Components.Scripts.esproj | 2 +- .../src/Components/Dialog/FluentDialog.ts | 22 ++++++ src/Core.Scripts/src/ExportedMethods.ts | 5 ++ .../Components/Base/FluentComponentBase.cs | 2 +- src/Core/Components/Dialog/FluentDialog.razor | 4 +- .../Components/Dialog/FluentDialog.razor.cs | 67 +++++++++++++------ .../Dialog/FluentDialogProvider.razor | 2 - .../Dialog/Services/DialogInstance.cs | 7 +- .../Dialog/Services/DialogService.cs | 15 ++++- .../Extensions/ServiceCollectionExtensions.cs | 41 +++++++----- 12 files changed, 138 insertions(+), 49 deletions(-) create mode 100644 src/Core.Scripts/src/Components/Dialog/FluentDialog.ts diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 55c4199bd3..710bdfb916 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,6 +1,21 @@ @implements IDialogContentComponent -Hello World + + + + My Title + + + + Hello Body + + + + One + Two + + + @code { [CascadingParameter] diff --git a/examples/Demo/FluentUI.Demo/appsettings.Development.json b/examples/Demo/FluentUI.Demo/appsettings.Development.json index 0c208ae918..f8063ef188 100644 --- a/examples/Demo/FluentUI.Demo/appsettings.Development.json +++ b/examples/Demo/FluentUI.Demo/appsettings.Development.json @@ -4,5 +4,6 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "DetailedErrors": true } diff --git a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj index e796a50a46..78d87e5074 100644 --- a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj +++ b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts new file mode 100644 index 0000000000..f7ca6626fa --- /dev/null +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -0,0 +1,22 @@ +export namespace Microsoft.FluentUI.Blazor.Components.Dialog { + + /** + * Display the fluent-dialog with the given id + * @param id The id of the fluent-dialog to display + */ + export function Show(id: string): void { + const dialog = document.getElementById(id) as any; + console.log('Show dialog ' + id, dialog); + dialog?.show(); + } + + /** + * Display the fluent-dialog with the given id + * @param id The id of the fluent-dialog to display + */ + export function Hide(id: string): void { + console.log('Hide dialog ' + id); + const dialog = document.getElementById(id) as any; + dialog?.hide(); + } +} diff --git a/src/Core.Scripts/src/ExportedMethods.ts b/src/Core.Scripts/src/ExportedMethods.ts index e284f63ea7..2440ffc7f9 100644 --- a/src/Core.Scripts/src/ExportedMethods.ts +++ b/src/Core.Scripts/src/ExportedMethods.ts @@ -1,4 +1,5 @@ import { Microsoft as LoggerFile } from './Utilities/Logger'; +import { Microsoft as FluentDialogFile } from './Components/Dialog/FluentDialog'; export namespace Microsoft.FluentUI.Blazor.ExportedMethods { @@ -16,6 +17,10 @@ export namespace Microsoft.FluentUI.Blazor.ExportedMethods { (window as any).Microsoft.FluentUI.Blazor.Utilities = (window as any).Microsoft.FluentUI.Blazor.Utilities || {}; (window as any).Microsoft.FluentUI.Blazor.Utilities.Logger = LoggerFile.FluentUI.Blazor.Utilities.Logger; + // Dialog methods + (window as any).Microsoft.FluentUI.Blazor.Components = (window as any).Microsoft.FluentUI.Blazor.Components || {}; + (window as any).Microsoft.FluentUI.Blazor.Components.Dialog = FluentDialogFile.FluentUI.Blazor.Components.Dialog; + // [^^^ Add your other exported methods before this line ^^^] } } diff --git a/src/Core/Components/Base/FluentComponentBase.cs b/src/Core/Components/Base/FluentComponentBase.cs index c1d5c37499..1b8e1dcd47 100644 --- a/src/Core/Components/Base/FluentComponentBase.cs +++ b/src/Core/Components/Base/FluentComponentBase.cs @@ -17,7 +17,7 @@ public abstract class FluentComponentBase : ComponentBase, IAsyncDisposable, IFl /// [Inject] - private IJSRuntime JSRuntime { get; set; } = default!; + protected IJSRuntime JSRuntime { get; set; } = default!; /// [Inject] diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 4adfc369d5..9239253b66 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -5,10 +5,12 @@ @* Content *@ @if (Instance is not null) { - + } else { @ChildContent } + diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 9f146a1274..6d81038278 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -3,7 +3,9 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components.Utilities; +using Microsoft.JSInterop; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -16,14 +18,22 @@ public partial class FluentDialog : FluentComponentBase /// public FluentDialog() { + Id = Identifier.NewId(); } - /// - internal FluentDialog(IDialogService? dialogService, DialogInstance? instance) + /// + /// internal constructor used by the + /// and the . + /// + /// + /// + /// + internal FluentDialog(IServiceProvider serviceProvider, IDialogService? dialogService, DialogInstance? instance) + : this() { - Id = Identifier.NewId(); DialogService = dialogService; Instance = instance; + JSRuntime = serviceProvider.GetRequiredService(); } /// @@ -42,22 +52,37 @@ internal FluentDialog(IDialogService? dialogService, DialogInstance? instance) [Parameter] public RenderFragment? ChildContent { get; set; } - ///// - ///// Initializes the component. - ///// - ///// - //protected override void OnInitialized() - //{ - // if (DialogService != null) - // { - // if (DialogService.ProviderNotAvailable()) - // { - // throw new FluentServiceProviderException(); - // } - - // DialogService.Add(this); - // } - - // base.OnInitialized(); - //} + /// + /// + /// + /// + /// + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && LaunchedFromService) + { + return ShowAsync(); + } + + return Task.CompletedTask; + } + + /// + /// + /// + public async Task ShowAsync() + { + await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); + } + + /// + /// + /// + public async Task HideAsync() + { + await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Hide", Id); + } + + /// + private bool LaunchedFromService => Instance is not null; } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index ac6a48ea61..21f088a81e 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -4,8 +4,6 @@
@if (DialogService != null) { -
@DialogService.Items.Count
- @foreach (var dialog in DialogService.Items) { diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 655b6a82d5..88a1720cc7 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -19,7 +19,7 @@ public DialogInstance(Type? type, DialogParameters parameters, object content) public Type? ContentType { get; } /// - public object Content { get; internal set; } = default!; + public object? Content { get; internal set; } /// public DialogParameters Parameters { get; internal set; } @@ -32,6 +32,9 @@ public DialogInstance(Type? type, DialogParameters parameters, object content) return null; } - return new Dictionary(StringComparer.Ordinal) { { "Content", Content } }; + return new Dictionary(StringComparer.Ordinal) + { + { nameof(Content), Content }, + }; } } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 11602eeece..ddcbcc9d20 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -7,6 +7,17 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial class DialogService : FluentServiceBase, IDialogService { + private readonly IServiceProvider _serviceProvider; + + /// + /// + /// + /// + public DialogService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + /// public Task CloseAsync(DialogReference dialog, DialogResult result) { @@ -29,12 +40,14 @@ public virtual async Task ShowDialogAsync(Type dialogComponent var dialogInstance = new DialogInstance(dialogComponent, parameters, data); var dialogReference = new DialogReference(Guid.NewGuid(), dialogInstance, this); - var dialog = new FluentDialog(this, dialogInstance); + var dialog = new FluentDialog(_serviceProvider, this, dialogInstance); InternalService.Items.Add(dialog); await InternalService.OnUpdatedAsync.Invoke(dialog); + //await dialog.ShowAsync(); + return dialogReference; //throw new InvalidOperationException("Hello"); //IDialogReference? dialogReference = new DialogReference(parameters.Id, this); diff --git a/src/Core/Extensions/ServiceCollectionExtensions.cs b/src/Core/Extensions/ServiceCollectionExtensions.cs index 06fd05a11d..ed9c9734f7 100644 --- a/src/Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Extensions/ServiceCollectionExtensions.cs @@ -29,21 +29,26 @@ public static IServiceCollection AddFluentUIComponents(this IServiceCollection s // Add services services.Add(provider => options ?? new(), serviceLifetime); - services.Add(provider => new DialogService(), serviceLifetime); + services.Add(serviceLifetime); services.Add(provider => options?.Localizer ?? FluentLocalizerInternal.Default, serviceLifetime); return services; } /// - /// Add a service to the service collection + /// Add common services required by the Fluent UI Web Components for Blazor library /// - /// - /// - /// - /// - /// - /// + /// Service collection + /// Library configuration + public static IServiceCollection AddFluentUIComponents(this IServiceCollection services, Action configuration) + { + LibraryConfiguration options = new(); + configuration.Invoke(options); + + return AddFluentUIComponents(services, options); + } + + /// private static IServiceCollection Add(this IServiceCollection services, Func implementationFactory, ServiceLifetime lifetime) where TService : class { @@ -55,16 +60,16 @@ private static IServiceCollection Add(this IServiceCollection services }; } - /// - /// Add common services required by the Fluent UI Web Components for Blazor library - /// - /// Service collection - /// Library configuration - public static IServiceCollection AddFluentUIComponents(this IServiceCollection services, Action configuration) + /// + private static IServiceCollection Add(this IServiceCollection services, ServiceLifetime lifetime) + where TService : class + where TImplementation : class, TService { - LibraryConfiguration options = new(); - configuration.Invoke(options); - - return AddFluentUIComponents(services, options); + return lifetime switch + { + ServiceLifetime.Singleton => services.AddSingleton(), + ServiceLifetime.Scoped => services.AddScoped(), + _ => throw new NotSupportedException($"Service lifetime {lifetime} is not supported.") + }; } } From a8f156b8f7f2e33b1b58c1f0a1c4aeb4a0b7de2c Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 12:39:34 +0100 Subject: [PATCH 11/57] Add Dialog events --- .../src/Components/Dialog/FluentDialog.ts | 2 - src/Core.Scripts/src/FluentUICustomEvents.ts | 31 ++++++--- src/Core.Scripts/src/Startup.ts | 2 +- src/Core/Components/Dialog/DialogEventArgs.cs | 66 +++++++++++++++++++ src/Core/Components/Dialog/FluentDialog.razor | 7 +- .../Components/Dialog/FluentDialog.razor.cs | 23 ++++++- src/Core/Enums/DialogState.cs | 31 +++++++++ src/Core/Events/DialogToggleEventArgs.cs | 31 +++++++++ src/Core/Events/EventHandlers.cs | 34 ++++++++++ 9 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 src/Core/Components/Dialog/DialogEventArgs.cs create mode 100644 src/Core/Enums/DialogState.cs create mode 100644 src/Core/Events/DialogToggleEventArgs.cs create mode 100644 src/Core/Events/EventHandlers.cs diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts index f7ca6626fa..b435285741 100644 --- a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -6,7 +6,6 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { */ export function Show(id: string): void { const dialog = document.getElementById(id) as any; - console.log('Show dialog ' + id, dialog); dialog?.show(); } @@ -15,7 +14,6 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { * @param id The id of the fluent-dialog to display */ export function Hide(id: string): void { - console.log('Hide dialog ' + id); const dialog = document.getElementById(id) as any; dialog?.hide(); } diff --git a/src/Core.Scripts/src/FluentUICustomEvents.ts b/src/Core.Scripts/src/FluentUICustomEvents.ts index 948a821097..4c1d77c2d1 100644 --- a/src/Core.Scripts/src/FluentUICustomEvents.ts +++ b/src/Core.Scripts/src/FluentUICustomEvents.ts @@ -8,21 +8,34 @@ export namespace Microsoft.FluentUI.Blazor.FluentUICustomEvents { * E.g. `FluentUICustomEvents.RadioGroup(blazor);` */ + export function DialogToggle(blazor: Blazor) { - // TODO: Example of custom event for "not yet existing" RadioGroup component - export function RadioGroup(blazor: Blazor) { - - blazor.registerCustomEventType('radiogroupclick', { - browserEventName: 'click', + blazor.registerCustomEventType('dialogbeforetoggle', { + browserEventName: 'beforetoggle', createEventArgs: event => { - if (event.target.readOnly || event.target.disabled) { - return null; - } return { - value: event.target.value + id: event.target.id, + type: event.type, + oldState: event.detail.oldState, + newState: event.detail.newState, }; } }); + blazor.registerCustomEventType('dialogtoggle', { + browserEventName: 'toggle', + createEventArgs: event => { + return { + id: event.target.id, + type: event.type, + oldState: event.detail.oldState, + newState: event.detail.newState, + }; + } + }); } + + + // [^^^ Add your other custom events before this line ^^^] + } diff --git a/src/Core.Scripts/src/Startup.ts b/src/Core.Scripts/src/Startup.ts index 064c097995..95748c4ca6 100644 --- a/src/Core.Scripts/src/Startup.ts +++ b/src/Core.Scripts/src/Startup.ts @@ -49,7 +49,7 @@ export namespace Microsoft.FluentUI.Blazor.Startup { // [^^^ Add your other custom components before this line ^^^] // Register all custom events - FluentUICustomEvents.RadioGroup(blazor); + FluentUICustomEvents.DialogToggle(blazor); // [^^^ Add your other custom events before this line ^^^] // Finishing diff --git a/src/Core/Components/Dialog/DialogEventArgs.cs b/src/Core/Components/Dialog/DialogEventArgs.cs new file mode 100644 index 0000000000..fba8c083e8 --- /dev/null +++ b/src/Core/Components/Dialog/DialogEventArgs.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Event arguments for the FluentDialog component. +/// +public class DialogEventArgs : EventArgs +{ + /// + internal DialogEventArgs(FluentDialog dialog, DialogToggleEventArgs args) + : this(dialog, args.Id, args.Type, args.OldState, args.NewState) + { + } + + /// + internal DialogEventArgs(FluentDialog dialog, string? id, string? eventType, string? oldState, string? newState) + { + Id = id ?? string.Empty; + Instance = dialog.Instance; + + if (string.Equals(eventType, "toggle", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(newState, "open", StringComparison.OrdinalIgnoreCase)) + { + State = DialogState.Open; + } + else if (string.Equals(newState, "closed", StringComparison.OrdinalIgnoreCase)) + { + State = DialogState.Closed; + } + } + else if (string.Equals(eventType, "beforetoggle", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(oldState, "closed", StringComparison.OrdinalIgnoreCase)) + { + State = DialogState.Opening; + } + else if (string.Equals(newState, "open", StringComparison.OrdinalIgnoreCase)) + { + State = DialogState.Closing; + } + } + else + { + State = DialogState.Closed; + } + } + + /// + /// Gets the ID of the FluentDialog component. + /// + public string Id { get; } + + /// + /// Gets the state of the FluentDialog component. + /// + public DialogState State { get; set; } + + /// + /// Gets the instance used by the . + /// + public DialogInstance? Instance { get; set; } +} diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 9239253b66..f5f3607603 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -1,7 +1,12 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - + @* Content *@ @if (Instance is not null) { diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 6d81038278..3a22a47696 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -52,6 +52,12 @@ internal FluentDialog(IServiceProvider serviceProvider, IDialogService? dialogSe [Parameter] public RenderFragment? ChildContent { get; set; } + /// + /// Command executed when the user clicks on the button. + /// + [Parameter] + public EventCallback OnStateChange { get; set; } + /// /// /// @@ -67,8 +73,21 @@ protected override Task OnAfterRenderAsync(bool firstRender) return Task.CompletedTask; } + private Task OnBeforeToggleAsync(DialogToggleEventArgs args) + { + var dialogEventArgs = new DialogEventArgs(this, args); + return OnStateChange.InvokeAsync(dialogEventArgs); + } + + /// + private Task OnToggleAsync(DialogToggleEventArgs args) + { + var dialogEventArgs = new DialogEventArgs(this, args); + return OnStateChange.InvokeAsync(dialogEventArgs); + } + /// - /// + /// Displays the dialog. /// public async Task ShowAsync() { @@ -76,7 +95,7 @@ public async Task ShowAsync() } /// - /// + /// Hide the dialog. /// public async Task HideAsync() { diff --git a/src/Core/Enums/DialogState.cs b/src/Core/Enums/DialogState.cs new file mode 100644 index 0000000000..d8ae8b02aa --- /dev/null +++ b/src/Core/Enums/DialogState.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Represents the state of a dialog. +/// +public enum DialogState +{ + /// + /// The dialog is hidden. + /// + Closed, + + /// + /// The dialog is showing. + /// + Opening, + + /// + /// The dialog is shown. + /// + Open, + + /// + /// The dialog is closing. + /// + Closing, +} diff --git a/src/Core/Events/DialogToggleEventArgs.cs b/src/Core/Events/DialogToggleEventArgs.cs new file mode 100644 index 0000000000..e1f4fe3c91 --- /dev/null +++ b/src/Core/Events/DialogToggleEventArgs.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Event arguments for the Dialog toggle event. +/// +internal class DialogToggleEventArgs : EventArgs +{ + /// + /// Gets or sets the ID of the dialog. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the old state of the dialog. + /// + public string? OldState { get; set; } + + /// + /// Gets or sets the new state of the dialog. + /// + public string? NewState { get; set; } + + /// + /// Gets or sets the type of the dialog. + /// + public string? Type { get; set; } +} diff --git a/src/Core/Events/EventHandlers.cs b/src/Core/Events/EventHandlers.cs new file mode 100644 index 0000000000..78411b02c6 --- /dev/null +++ b/src/Core/Events/EventHandlers.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/* Blazor supports custom event arguments, which enable you to pass arbitrary data to .NET event handlers with custom events. + * https://learn.microsoft.com/aspnet/core/blazor/components/event-handling#custom-event-arguments + * + * In the Components C# project + * ---------------------------- + * 1. In this `Events` folder, create a class that derives from `EventArgs`. Ex. DialogToggleEventArgs.cs + * 2. Add the `EventHandler` attribute (prefixed by "on") to the `EventHandlers` class in this file. Ex. "ondialogtoggle" + * + * In the Components.Scripts project + * --------------------------------- + * 3. Define a registering method the Custom Event Type in the `FluentUICustomEvents.ts` file. Ex. "dialogtoggle" + * 4. Call the Registering method in the `Startup.ts` file. + * + * In the C# component + * ------------------- + * 5. Use this new event in the component: `@ondialogtoggle="@(e => ...)"` + */ + +/// +/// List of custom events to associate an event argument type with an event attribute name. +/// +[EventHandler("ondialogbeforetoggle", typeof(DialogToggleEventArgs), enableStopPropagation: true, enablePreventDefault: true)] +[EventHandler("ondialogtoggle", typeof(DialogToggleEventArgs), enableStopPropagation: true, enablePreventDefault: true)] +public static class EventHandlers +{ +} From 774068395d27afac1d48afef2144f18ecb370742 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 15:17:28 +0100 Subject: [PATCH 12/57] Refactoring --- .../Examples/DialogServiceDefault.razor | 4 ++++ src/Core/Components/Base/FluentServiceBase.cs | 4 ++-- .../Components/Base/IFluentServiceBase.cs | 2 +- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Components/Dialog/FluentDialog.razor.cs | 21 ++++++++++++------- .../Dialog/FluentDialogProvider.razor | 4 ++-- .../Dialog/FluentDialogProvider.razor.cs | 5 ++++- .../Dialog/Services/DialogParameters.cs | 5 +++++ .../Dialog/Services/DialogReference.cs | 6 +----- .../Dialog/Services/DialogService.cs | 4 ++-- 10 files changed, 36 insertions(+), 21 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 8e460e28e4..cc676e36c8 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -11,6 +11,10 @@ DialogParameters parameters = new() { Title = $"My title", + OnStateChange = (e) => + { + Console.WriteLine($"Dialog state changed: {e.State}"); + } }; var data = new SimpleDialog.MySimpleData(); diff --git a/src/Core/Components/Base/FluentServiceBase.cs b/src/Core/Components/Base/FluentServiceBase.cs index 7cde51b59f..bb79043183 100644 --- a/src/Core/Components/Base/FluentServiceBase.cs +++ b/src/Core/Components/Base/FluentServiceBase.cs @@ -12,7 +12,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public abstract class FluentServiceBase : IFluentServiceBase { - private readonly ConcurrentBag _list = []; + private readonly ConcurrentDictionary _list = []; /// /// @@ -22,7 +22,7 @@ public abstract class FluentServiceBase : IFluentServiceBase /// /// - ConcurrentBag IFluentServiceBase.Items => _list; + ConcurrentDictionary IFluentServiceBase.Items => _list; /// /// diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs index aac88339a8..7f34131c1d 100644 --- a/src/Core/Components/Base/IFluentServiceBase.cs +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -21,7 +21,7 @@ public interface IFluentServiceBase : IDisposable /// /// Gets the list of . /// - ConcurrentBag Items { get; } + ConcurrentDictionary Items { get; } /// /// Action to be called when the is updated. diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index f5f3607603..fad2b7a0ac 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -4,7 +4,7 @@ @* Content *@ diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 3a22a47696..052ec657ad 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -73,17 +73,24 @@ protected override Task OnAfterRenderAsync(bool firstRender) return Task.CompletedTask; } - private Task OnBeforeToggleAsync(DialogToggleEventArgs args) - { - var dialogEventArgs = new DialogEventArgs(this, args); - return OnStateChange.InvokeAsync(dialogEventArgs); - } - /// private Task OnToggleAsync(DialogToggleEventArgs args) { var dialogEventArgs = new DialogEventArgs(this, args); - return OnStateChange.InvokeAsync(dialogEventArgs); + var dialogId = dialogEventArgs?.Id ?? string.Empty; + + if (OnStateChange.HasDelegate) + { + return OnStateChange.InvokeAsync(dialogEventArgs); + } + + // Remove the HTML code from the Provider + if (LaunchedFromService && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) + { + (DialogService as DialogService)?.InternalService.Items.TryRemove(dialogId, out _); + } + + return Task.CompletedTask; } /// diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 21f088a81e..37f2620412 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -4,9 +4,9 @@
@if (DialogService != null) { - @foreach (var dialog in DialogService.Items) + @foreach (var dialog in DialogService.Items.Values) { - + } }
diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index 7936816b49..354920ac28 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -40,7 +40,7 @@ public FluentDialogProvider() protected virtual IDialogService? DialogService => _dialogService ??= ServiceProvider?.GetService(); /// - protected IEnumerable? Dialogs => DialogService?.Items; + protected IEnumerable? Dialogs => DialogService?.Items.Values; /// protected override void OnInitialized() @@ -56,4 +56,7 @@ protected override void OnInitialized() }; } } + + /// + private static Action EmptyOnStateChange => (_) => { }; } diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogParameters.cs index 9ae9e2531b..2dc2450088 100644 --- a/src/Core/Components/Dialog/Services/DialogParameters.cs +++ b/src/Core/Components/Dialog/Services/DialogParameters.cs @@ -13,4 +13,9 @@ public class DialogParameters /// Gets or sets the title of the dialog. /// public string? Title { get; set; } + + /// + /// Gets or sets the action raised when the dialog is opened or closed. + /// + public Action? OnStateChange { get; set; } } diff --git a/src/Core/Components/Dialog/Services/DialogReference.cs b/src/Core/Components/Dialog/Services/DialogReference.cs index bce1d34657..f9c6016b12 100644 --- a/src/Core/Components/Dialog/Services/DialogReference.cs +++ b/src/Core/Components/Dialog/Services/DialogReference.cs @@ -11,16 +11,12 @@ public class DialogReference : IDialogReference private readonly TaskCompletionSource _resultCompletion = new(); /// - public DialogReference(Guid dialogInstanceId, DialogInstance dialogInstance, IDialogService dialogService) + public DialogReference(DialogInstance dialogInstance, IDialogService dialogService) { - DialogInstanceId = dialogInstanceId; Instance = dialogInstance; _dialogService = dialogService; } - /// - internal Guid DialogInstanceId { get; } - /// public DialogInstance Instance { get; set; } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index ddcbcc9d20..e2fa90e26c 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -39,10 +39,10 @@ public virtual async Task ShowDialogAsync(Type dialogComponent } var dialogInstance = new DialogInstance(dialogComponent, parameters, data); - var dialogReference = new DialogReference(Guid.NewGuid(), dialogInstance, this); + var dialogReference = new DialogReference(dialogInstance, this); var dialog = new FluentDialog(_serviceProvider, this, dialogInstance); - InternalService.Items.Add(dialog); + InternalService.Items.TryAdd(dialog?.Id ?? "", dialog ?? throw new InvalidOperationException("Failed to create FluentDialog.")); await InternalService.OnUpdatedAsync.Invoke(dialog); From 32ee054f654573480d999c517c106ce701c91bd0 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 16:41:46 +0100 Subject: [PATCH 13/57] Fix event --- src/Core/Components/Dialog/DialogEventArgs.cs | 2 +- src/Core/Components/Dialog/FluentDialog.razor.cs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Core/Components/Dialog/DialogEventArgs.cs b/src/Core/Components/Dialog/DialogEventArgs.cs index fba8c083e8..eb52aeeef9 100644 --- a/src/Core/Components/Dialog/DialogEventArgs.cs +++ b/src/Core/Components/Dialog/DialogEventArgs.cs @@ -38,7 +38,7 @@ internal DialogEventArgs(FluentDialog dialog, string? id, string? eventType, str { State = DialogState.Opening; } - else if (string.Equals(newState, "open", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(oldState, "open", StringComparison.OrdinalIgnoreCase)) { State = DialogState.Closing; } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 052ec657ad..6ec5703882 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -74,23 +74,21 @@ protected override Task OnAfterRenderAsync(bool firstRender) } /// - private Task OnToggleAsync(DialogToggleEventArgs args) + private async Task OnToggleAsync(DialogToggleEventArgs args) { var dialogEventArgs = new DialogEventArgs(this, args); var dialogId = dialogEventArgs?.Id ?? string.Empty; if (OnStateChange.HasDelegate) { - return OnStateChange.InvokeAsync(dialogEventArgs); + await OnStateChange.InvokeAsync(dialogEventArgs); } - // Remove the HTML code from the Provider + // Remove the HTML code from the DialogProvider if (LaunchedFromService && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) { (DialogService as DialogService)?.InternalService.Items.TryRemove(dialogId, out _); } - - return Task.CompletedTask; } /// From c69b904aa1f4e52382af37e7bb371e677c8efdba Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 17:28:05 +0100 Subject: [PATCH 14/57] Refactoring --- .../Examples/DialogServiceDefault.razor | 46 +++++++++++++------ .../Dialog/Examples/SimpleDialog.razor | 21 +++++++-- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Dialog/Services/DialogInstance.cs | 22 +-------- .../Dialog/Services/DialogParameters.cs | 19 ++++++++ .../Dialog/Services/DialogService.cs | 10 ++-- .../Dialog/Services/IDialogService.cs | 10 ++-- 7 files changed, 80 insertions(+), 50 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index cc676e36c8..027269af7f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -1,30 +1,50 @@ @inject IDialogService DialogService - - Open Dialog - +Open Dialog + +@data["Name"] +@((data["Data"] as SimpleDialog.MyData)?.Name) + +Refresh @code { + private Dictionary data = new() + { + { nameof(SimpleDialog.Name), "John" }, + { nameof(SimpleDialog.Data), new SimpleDialog.MyData() + { + Name = "John" + } }, + }; + private async Task OpenDialogAsync() { - DialogParameters parameters = new() + // var parameters = new DialogParameters() + // { + // Title = $"My title", + // OnStateChange = (e) => + // { + // Console.WriteLine($"Dialog state changed: {e.State}"); + // } + // }; + + var parameters = new DialogParameters(factory => { - Title = $"My title", - OnStateChange = (e) => + factory.Title = "My title"; + factory.Data = data; + factory.OnStateChange = (e) => { Console.WriteLine($"Dialog state changed: {e.State}"); - } - }; - - var data = new SimpleDialog.MySimpleData(); - var dialog = await DialogService.ShowDialogAsync(data, parameters); + }; + }); + + var dialog = await DialogService.ShowDialogAsync(parameters); var result = await dialog.Result; if (result.Data is not null) { - var simplePerson = result.Data as SimpleDialog.MySimpleData; - Console.WriteLine($"Dialog closed with {simplePerson?.Name} - Canceled: {result.Cancelled}"); + Console.WriteLine($"Dialog closed - Canceled: {result.Cancelled}"); } else { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 710bdfb916..3f1baaba4b 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -8,11 +8,15 @@ Hello Body +
+ Name: @Name + Data: @Data.Name +
One - Two + Update Name @@ -22,10 +26,19 @@ public FluentDialog? Dialog { get; set; } [Parameter] - public object? Content { get; set; } + public string Name { get; set; } = string.Empty; - public class MySimpleData + [Parameter] + public MyData Data { get; set; } = new(); + + private void UpdateName() + { + Name = "Denis"; + Data.Name = "Denis"; + } + + public class MyData { - public string? Name { get; set; } + public string Name { get; set; } = string.Empty; } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index fad2b7a0ac..0aa4e880d8 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -11,7 +11,7 @@ @if (Instance is not null) { + Parameters="Instance.Parameters.Data" /> } else { diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 88a1720cc7..e73297d508 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -8,33 +8,15 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public sealed class DialogInstance { /// - public DialogInstance(Type? type, DialogParameters parameters, object content) + public DialogInstance(Type? type, DialogParameters parameters) { ContentType = type; Parameters = parameters; - Content = content; } /// public Type? ContentType { get; } /// - public object? Content { get; internal set; } - - /// - public DialogParameters Parameters { get; internal set; } - - /// - internal Dictionary? GetParameterDictionary() - { - if (Content is null) - { - return null; - } - - return new Dictionary(StringComparer.Ordinal) - { - { nameof(Content), Content }, - }; - } + public DialogParameters Parameters { get; internal set; } } diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogParameters.cs index 2dc2450088..126541d28e 100644 --- a/src/Core/Components/Dialog/Services/DialogParameters.cs +++ b/src/Core/Components/Dialog/Services/DialogParameters.cs @@ -9,11 +9,30 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public class DialogParameters { + private static readonly Dictionary EmptyData = new(StringComparer.Ordinal); + + /// + public DialogParameters() + { + + } + + /// + public DialogParameters(Action implementationFactory) + { + implementationFactory.Invoke(this); + } + /// /// Gets or sets the title of the dialog. /// public string? Title { get; set; } + /// + /// Gets the content of the dialog. + /// + public IDictionary Data { get; set; } = EmptyData; + /// /// Gets or sets the action raised when the dialog is opened or closed. /// diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index e2fa90e26c..2a8f0b9816 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -24,9 +24,9 @@ public Task CloseAsync(DialogReference dialog, DialogResult result) return Task.CompletedTask; } - /// + /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] - public virtual async Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters) + public virtual async Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters) { if (!typeof(IDialogContentComponent).IsAssignableFrom(dialogComponent)) { @@ -38,7 +38,7 @@ public virtual async Task ShowDialogAsync(Type dialogComponent throw new FluentServiceProviderException(); } - var dialogInstance = new DialogInstance(dialogComponent, parameters, data); + var dialogInstance = new DialogInstance(dialogComponent, parameters); var dialogReference = new DialogReference(dialogInstance, this); var dialog = new FluentDialog(_serviceProvider, this, dialogInstance); @@ -55,8 +55,8 @@ public virtual async Task ShowDialogAsync(Type dialogComponent } /// - public Task ShowDialogAsync(object data, DialogParameters parameters) where TDialog : IDialogContentComponent + public Task ShowDialogAsync(DialogParameters parameters) where TDialog : IDialogContentComponent { - return ShowDialogAsync(typeof(TDialog), data, parameters); + return ShowDialogAsync(typeof(TDialog), parameters); } } diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 99ae3a249c..8d7c3fca98 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -14,20 +14,16 @@ public partial interface IDialogService : IFluentServiceBase /// /// Shows a dialog with the component type as the body, - /// passing the specified /// /// Type of component to display. - /// Content to pass to component being displayed. /// Parameters to configure the dialog component. - Task ShowDialogAsync(Type dialogComponent, object data, DialogParameters parameters); + Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters); /// - /// Shows a dialog with the component type as the body, - /// passing the specified + /// Shows a dialog with the component type as the body. /// /// Type of component to display. - /// Content to pass to component being displayed. /// Parameters to configure the dialog component. - Task ShowDialogAsync(object data, DialogParameters parameters) + Task ShowDialogAsync(DialogParameters parameters) where TDialog : IDialogContentComponent; } From 1c68deb802499f700c9d679c2b8b6e3a28692acd Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 17:50:39 +0100 Subject: [PATCH 15/57] Refactoring --- .../Dialog/Examples/SimpleDialog.razor | 4 +- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Components/Dialog/FluentDialog.razor.cs | 5 +-- .../Dialog/Services/DialogInstance.cs | 31 +++++++++++++--- .../Dialog/Services/DialogReference.cs | 37 ------------------- .../Dialog/Services/DialogService.cs | 28 ++++++-------- ...IDialogReference.cs => IDialogInstance.cs} | 5 +-- .../Dialog/Services/IDialogService.cs | 10 +++-- 8 files changed, 49 insertions(+), 73 deletions(-) delete mode 100644 src/Core/Components/Dialog/Services/DialogReference.cs rename src/Core/Components/Dialog/Services/{IDialogReference.cs => IDialogInstance.cs} (84%) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 3f1baaba4b..59ec1d8cf3 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,6 +1,4 @@ -@implements IDialogContentComponent - - + My Title diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 0aa4e880d8..c5287990c8 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -10,7 +10,7 @@ @* Content *@ @if (Instance is not null) { - } else diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 6ec5703882..3f27d48cab 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -26,12 +26,11 @@ public FluentDialog() /// and the . /// /// - /// /// - internal FluentDialog(IServiceProvider serviceProvider, IDialogService? dialogService, DialogInstance? instance) + internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) : this() { - DialogService = dialogService; + DialogService = instance.DialogService; Instance = instance; JSRuntime = serviceProvider.GetRequiredService(); } diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index e73297d508..ca717a71a4 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -5,18 +5,39 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -public sealed class DialogInstance +public class DialogInstance : IDialogInstance { + private readonly TaskCompletionSource _resultCompletion = new(); + /// - public DialogInstance(Type? type, DialogParameters parameters) + public DialogInstance(IDialogService dialogService, Type componentType, DialogParameters parameters) { - ContentType = type; + ComponentType = componentType; Parameters = parameters; + DialogService = dialogService; } /// - public Type? ContentType { get; } + internal Type ComponentType { get; } + + /// + internal IDialogService DialogService { get; } + + /// + public DialogParameters Parameters { get; internal set; } + + /// + public Task Result => _resultCompletion.Task; /// - public DialogParameters Parameters { get; internal set; } + public Task CloseAsync() + { + return DialogService.CloseAsync(this, DialogResult.Cancel()); + } + + /// + public Task CloseAsync(DialogResult result) + { + return DialogService.CloseAsync(this, result); + } } diff --git a/src/Core/Components/Dialog/Services/DialogReference.cs b/src/Core/Components/Dialog/Services/DialogReference.cs deleted file mode 100644 index f9c6016b12..0000000000 --- a/src/Core/Components/Dialog/Services/DialogReference.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------ -// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------------------ - -namespace Microsoft.FluentUI.AspNetCore.Components; - -/// -public class DialogReference : IDialogReference -{ - private readonly IDialogService _dialogService; - private readonly TaskCompletionSource _resultCompletion = new(); - - /// - public DialogReference(DialogInstance dialogInstance, IDialogService dialogService) - { - Instance = dialogInstance; - _dialogService = dialogService; - } - - /// - public DialogInstance Instance { get; set; } - - /// - public Task Result => _resultCompletion.Task; - - /// - public Task CloseAsync() - { - return _dialogService.CloseAsync(this, DialogResult.Cancel()); - } - - /// - public Task CloseAsync(DialogResult result) - { - return _dialogService.CloseAsync(this, result); - } -} diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 2a8f0b9816..31b524bea3 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -2,6 +2,8 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using Microsoft.AspNetCore.Components; + namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -19,18 +21,18 @@ public DialogService(IServiceProvider serviceProvider) } /// - public Task CloseAsync(DialogReference dialog, DialogResult result) + public Task CloseAsync(DialogInstance dialog, DialogResult result) { return Task.CompletedTask; } /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] - public virtual async Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "TODO")] + public virtual async Task ShowDialogAsync(Type componentType, DialogParameters parameters) { - if (!typeof(IDialogContentComponent).IsAssignableFrom(dialogComponent)) + if (!componentType.IsSubclassOf(typeof(ComponentBase))) { - throw new ArgumentException($"{dialogComponent.FullName} must be a Dialog Component", nameof(dialogComponent)); + throw new ArgumentException($"{componentType.FullName} must be a Blazor Component", nameof(componentType)); } if (this.ProviderNotAvailable()) @@ -38,24 +40,18 @@ public virtual async Task ShowDialogAsync(Type dialogComponent throw new FluentServiceProviderException(); } - var dialogInstance = new DialogInstance(dialogComponent, parameters); - var dialogReference = new DialogReference(dialogInstance, this); - var dialog = new FluentDialog(_serviceProvider, this, dialogInstance); + var instance = new DialogInstance(this, componentType, parameters); + var dialog = new FluentDialog(_serviceProvider, instance); + // Add the dialog to the service, and render it. InternalService.Items.TryAdd(dialog?.Id ?? "", dialog ?? throw new InvalidOperationException("Failed to create FluentDialog.")); - await InternalService.OnUpdatedAsync.Invoke(dialog); - //await dialog.ShowAsync(); - - return dialogReference; - //throw new InvalidOperationException("Hello"); - //IDialogReference? dialogReference = new DialogReference(parameters.Id, this); - //return await OnShowAsync.Invoke(dialogReference, dialogComponent, parameters, data); + return instance; } /// - public Task ShowDialogAsync(DialogParameters parameters) where TDialog : IDialogContentComponent + public Task ShowDialogAsync(DialogParameters parameters) where TDialog : ComponentBase { return ShowDialogAsync(typeof(TDialog), parameters); } diff --git a/src/Core/Components/Dialog/Services/IDialogReference.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs similarity index 84% rename from src/Core/Components/Dialog/Services/IDialogReference.cs rename to src/Core/Components/Dialog/Services/IDialogInstance.cs index f161681174..f131b20fc4 100644 --- a/src/Core/Components/Dialog/Services/IDialogReference.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -7,14 +7,11 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Interface for DialogReference /// -public interface IDialogReference +public interface IDialogInstance { /// Task Result { get; } - /// - DialogInstance Instance { get; set; } - /// Task CloseAsync(); diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 8d7c3fca98..2e6df5b792 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -2,6 +2,8 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using Microsoft.AspNetCore.Components; + namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -10,20 +12,20 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public partial interface IDialogService : IFluentServiceBase { /// - Task CloseAsync(DialogReference dialog, DialogResult result); + Task CloseAsync(DialogInstance dialog, DialogResult result); /// /// Shows a dialog with the component type as the body, /// /// Type of component to display. /// Parameters to configure the dialog component. - Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters); + Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters); /// /// Shows a dialog with the component type as the body. /// /// Type of component to display. /// Parameters to configure the dialog component. - Task ShowDialogAsync(DialogParameters parameters) - where TDialog : IDialogContentComponent; + Task ShowDialogAsync(DialogParameters parameters) + where TDialog : ComponentBase; } From 350e29f6a751656a9b9118f60299e04df2d8071d Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 18:04:17 +0100 Subject: [PATCH 16/57] refactoring --- .../Dialog/Examples/DialogServiceDefault.razor | 14 +++++++------- .../Components/Dialog/Examples/SimpleDialog.razor | 6 +++--- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Components/Dialog/Services/DialogParameters.cs | 3 +-- .../Components/Dialog/Services/DialogResult.cs | 8 ++++---- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 027269af7f..b96b7da55e 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -2,17 +2,17 @@ Open Dialog -@data["Name"] -@((data["Data"] as SimpleDialog.MyData)?.Name) +@items["Name"] +@((items["Content"] as SimpleDialog.MyData)?.Name) -Refresh +Refresh @code { - private Dictionary data = new() + private Dictionary items = new() { { nameof(SimpleDialog.Name), "John" }, - { nameof(SimpleDialog.Data), new SimpleDialog.MyData() + { nameof(SimpleDialog.Content), new SimpleDialog.MyData() { Name = "John" } }, @@ -32,7 +32,7 @@ var parameters = new DialogParameters(factory => { factory.Title = "My title"; - factory.Data = data; + factory.Content = items; factory.OnStateChange = (e) => { Console.WriteLine($"Dialog state changed: {e.State}"); @@ -42,7 +42,7 @@ var dialog = await DialogService.ShowDialogAsync(parameters); var result = await dialog.Result; - if (result.Data is not null) + if (result.Content is not null) { Console.WriteLine($"Dialog closed - Canceled: {result.Cancelled}"); } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 59ec1d8cf3..ad9d5b5876 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -8,7 +8,7 @@ Hello Body
Name: @Name - Data: @Data.Name + Data: @Content.Name
@@ -27,12 +27,12 @@ public string Name { get; set; } = string.Empty; [Parameter] - public MyData Data { get; set; } = new(); + public MyData Content { get; set; } = new(); private void UpdateName() { Name = "Denis"; - Data.Name = "Denis"; + Content.Name = "Denis"; } public class MyData diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index c5287990c8..a195804712 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -11,7 +11,7 @@ @if (Instance is not null) { + Parameters="Instance.Parameters.Content" /> } else { diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogParameters.cs index 126541d28e..2596bc02a6 100644 --- a/src/Core/Components/Dialog/Services/DialogParameters.cs +++ b/src/Core/Components/Dialog/Services/DialogParameters.cs @@ -14,7 +14,6 @@ public class DialogParameters /// public DialogParameters() { - } /// @@ -31,7 +30,7 @@ public DialogParameters(Action implementationFactory) /// /// Gets the content of the dialog. /// - public IDictionary Data { get; set; } = EmptyData; + public IDictionary Content { get; set; } = EmptyData; /// /// Gets or sets the action raised when the dialog is opened or closed. diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs index 85a7a51a9e..dc9996bb70 100644 --- a/src/Core/Components/Dialog/Services/DialogResult.cs +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -8,14 +8,14 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public class DialogResult { /// - protected internal DialogResult(object? data, bool cancelled) + protected internal DialogResult(object? content, bool cancelled) { - Data = data; + Content = content; Cancelled = cancelled; } /// - public object? Data { get; set; } + public object? Content { get; set; } /// public bool Cancelled { get; } @@ -24,5 +24,5 @@ protected internal DialogResult(object? data, bool cancelled) public static DialogResult Ok(T result) => new(result, cancelled: false); /// - public static DialogResult Cancel(object? data = null) => new(data ?? default, cancelled: true); + public static DialogResult Cancel(object? content = null) => new(content ?? default, cancelled: true); } From c0bde70e9f06d8e0570d57c2f53e6f79443dee7f Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 18:16:56 +0100 Subject: [PATCH 17/57] Refacroring --- .../Components/Dialog/Examples/DialogServiceDefault.razor | 2 +- src/Core/Components/Base/IFluentServiceBase.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index b96b7da55e..1c80c3fcfd 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -38,7 +38,7 @@ Console.WriteLine($"Dialog state changed: {e.State}"); }; }); - + var dialog = await DialogService.ShowDialogAsync(parameters); var result = await dialog.Result; diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs index 7f34131c1d..cb47810aee 100644 --- a/src/Core/Components/Base/IFluentServiceBase.cs +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -13,18 +13,18 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public interface IFluentServiceBase : IDisposable { /// - /// Gets or sets the Provider ID. + /// Gets the Provider ID. /// This value is set by the provider and will be empty if the provider is not initialized. /// - string? ProviderId { get; set; } + public string? ProviderId { get; internal set; } /// /// Gets the list of . /// - ConcurrentDictionary Items { get; } + internal ConcurrentDictionary Items { get; } /// /// Action to be called when the is updated. /// - Func OnUpdatedAsync { get; set; } + internal Func OnUpdatedAsync { get; set; } } From 113d21d5309f64835ac88d321662f833df2832f2 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 17 Dec 2024 18:45:32 +0100 Subject: [PATCH 18/57] Refactoring --- .../Examples/DialogServiceDefault.razor | 34 ++++--------------- .../Dialog/Examples/SimpleDialog.razor | 23 +++---------- src/Core/Components/Base/FluentServiceBase.cs | 4 +-- .../Components/Base/IFluentServiceBase.cs | 2 +- .../Components/Dialog/FluentDialog.razor.cs | 7 ++-- .../Dialog/Services/DialogInstance.cs | 10 ++++-- .../Dialog/Services/DialogResult.cs | 4 +-- .../Dialog/Services/DialogService.cs | 10 ++++-- .../Dialog/Services/IDialogInstance.cs | 3 ++ .../Components/Grid/FluentGridItem.razor.cs | 2 +- .../Components/Layout/FluentLayout.razor.cs | 4 +-- 11 files changed, 42 insertions(+), 61 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 1c80c3fcfd..c5b45e71d6 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -2,37 +2,17 @@ Open Dialog -@items["Name"] -@((items["Content"] as SimpleDialog.MyData)?.Name) - -Refresh - @code { - private Dictionary items = new() - { - { nameof(SimpleDialog.Name), "John" }, - { nameof(SimpleDialog.Content), new SimpleDialog.MyData() - { - Name = "John" - } }, - }; - private async Task OpenDialogAsync() { - // var parameters = new DialogParameters() - // { - // Title = $"My title", - // OnStateChange = (e) => - // { - // Console.WriteLine($"Dialog state changed: {e.State}"); - // } - // }; - var parameters = new DialogParameters(factory => { factory.Title = "My title"; - factory.Content = items; + + factory.Content.Add(nameof(SimpleDialog.Name), "John"); + factory.Content.Add(nameof(SimpleDialog.Age), 20); + factory.OnStateChange = (e) => { Console.WriteLine($"Dialog state changed: {e.State}"); @@ -42,13 +22,13 @@ var dialog = await DialogService.ShowDialogAsync(parameters); var result = await dialog.Result; - if (result.Content is not null) + if (result.Cancelled) { - Console.WriteLine($"Dialog closed - Canceled: {result.Cancelled}"); + Console.WriteLine($"Dialog closed - Canceled"); } else { - Console.WriteLine($"Dialog closed - Canceled: {result.Cancelled}"); + Console.WriteLine($"Dialog closed - OK"); } } } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index ad9d5b5876..f3607e1edf 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -5,16 +5,12 @@ - Hello Body -
- Name: @Name - Data: @Content.Name -
+ Hello @Name (@Age)
- One - Update Name + OK + Cancel @@ -27,16 +23,5 @@ public string Name { get; set; } = string.Empty; [Parameter] - public MyData Content { get; set; } = new(); - - private void UpdateName() - { - Name = "Denis"; - Content.Name = "Denis"; - } - - public class MyData - { - public string Name { get; set; } = string.Empty; - } + public int Age { get; set; } } diff --git a/src/Core/Components/Base/FluentServiceBase.cs b/src/Core/Components/Base/FluentServiceBase.cs index bb79043183..95088a609f 100644 --- a/src/Core/Components/Base/FluentServiceBase.cs +++ b/src/Core/Components/Base/FluentServiceBase.cs @@ -33,13 +33,13 @@ public abstract class FluentServiceBase : IFluentServiceBase interface. ///
- internal IFluentServiceBase InternalService => this; + internal IFluentServiceBase ServiceProvider => this; /// /// /// public void Dispose() { - InternalService.Items.Clear(); + ServiceProvider.Items.Clear(); } } diff --git a/src/Core/Components/Base/IFluentServiceBase.cs b/src/Core/Components/Base/IFluentServiceBase.cs index cb47810aee..b03c4cbd58 100644 --- a/src/Core/Components/Base/IFluentServiceBase.cs +++ b/src/Core/Components/Base/IFluentServiceBase.cs @@ -13,7 +13,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public interface IFluentServiceBase : IDisposable { /// - /// Gets the Provider ID. + /// Gets the ServiceProvider ID. /// This value is set by the provider and will be empty if the provider is not initialized. /// public string? ProviderId { get; internal set; } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 3f27d48cab..6dd2243ef9 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -28,8 +28,8 @@ public FluentDialog() /// /// internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) - : this() { + Id = instance.Id; DialogService = instance.DialogService; Instance = instance; JSRuntime = serviceProvider.GetRequiredService(); @@ -84,9 +84,10 @@ private async Task OnToggleAsync(DialogToggleEventArgs args) } // Remove the HTML code from the DialogProvider - if (LaunchedFromService && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) + if (LaunchedFromService && Instance is not null && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) { - (DialogService as DialogService)?.InternalService.Items.TryRemove(dialogId, out _); + var service = DialogService as DialogService; + service?.CloseAsync(Instance, DialogResult.Cancel()); } } diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index ca717a71a4..04319aec1c 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -2,12 +2,14 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + namespace Microsoft.FluentUI.AspNetCore.Components; /// public class DialogInstance : IDialogInstance { - private readonly TaskCompletionSource _resultCompletion = new(); + internal readonly TaskCompletionSource ResultCompletion = new(); /// public DialogInstance(IDialogService dialogService, Type componentType, DialogParameters parameters) @@ -15,6 +17,7 @@ public DialogInstance(IDialogService dialogService, Type componentType, DialogPa ComponentType = componentType; Parameters = parameters; DialogService = dialogService; + Id = Identifier.NewId(); } /// @@ -27,7 +30,10 @@ public DialogInstance(IDialogService dialogService, Type componentType, DialogPa public DialogParameters Parameters { get; internal set; } /// - public Task Result => _resultCompletion.Task; + public Task Result => ResultCompletion.Task; + + /// + public string Id { get; } /// public Task CloseAsync() diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs index dc9996bb70..70c62ed3b8 100644 --- a/src/Core/Components/Dialog/Services/DialogResult.cs +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -10,12 +10,12 @@ public class DialogResult /// protected internal DialogResult(object? content, bool cancelled) { - Content = content; + Value = content; Cancelled = cancelled; } /// - public object? Content { get; set; } + public object? Value { get; set; } /// public bool Cancelled { get; } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 31b524bea3..77d02b4379 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -23,6 +23,12 @@ public DialogService(IServiceProvider serviceProvider) /// public Task CloseAsync(DialogInstance dialog, DialogResult result) { + // Remove the HTML code from the DialogProvider + ServiceProvider.Items.TryRemove(dialog.Id, out _); + + // Set the result of the dialog + dialog.ResultCompletion.SetResult(result); + return Task.CompletedTask; } @@ -44,8 +50,8 @@ public virtual async Task ShowDialogAsync(Type componentType, D var dialog = new FluentDialog(_serviceProvider, instance); // Add the dialog to the service, and render it. - InternalService.Items.TryAdd(dialog?.Id ?? "", dialog ?? throw new InvalidOperationException("Failed to create FluentDialog.")); - await InternalService.OnUpdatedAsync.Invoke(dialog); + ServiceProvider.Items.TryAdd(dialog?.Id ?? "", dialog ?? throw new InvalidOperationException("Failed to create FluentDialog.")); + await ServiceProvider.OnUpdatedAsync.Invoke(dialog); return instance; } diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index f131b20fc4..8a10e94171 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -9,6 +9,9 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public interface IDialogInstance { + /// + string Id { get; } + /// Task Result { get; } diff --git a/src/Core/Components/Grid/FluentGridItem.razor.cs b/src/Core/Components/Grid/FluentGridItem.razor.cs index d15ab67c4b..3c388b4c85 100644 --- a/src/Core/Components/Grid/FluentGridItem.razor.cs +++ b/src/Core/Components/Grid/FluentGridItem.razor.cs @@ -9,7 +9,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Content placed within a layout using the component. +/// Value placed within a layout using the component. /// public partial class FluentGridItem : FluentComponentBase { diff --git a/src/Core/Components/Layout/FluentLayout.razor.cs b/src/Core/Components/Layout/FluentLayout.razor.cs index 7c25dacb37..e9c0d258de 100644 --- a/src/Core/Components/Layout/FluentLayout.razor.cs +++ b/src/Core/Components/Layout/FluentLayout.razor.cs @@ -8,8 +8,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Component that defines a layout for a page, using a grid composed of a Header, a Footer and 3 columns: Menu, Content and Aside Pane. -/// For mobile devices (< 768px), the layout is a single column with the Menu, Content and Footer panes stacked vertically. +/// Component that defines a layout for a page, using a grid composed of a Header, a Footer and 3 columns: Menu, Value and Aside Pane. +/// For mobile devices (< 768px), the layout is a single column with the Menu, Value and Footer panes stacked vertically. /// public partial class FluentLayout { From 9ff60e32d42bfeecdc0db445e574f29fc53f5ab4 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Thu, 19 Dec 2024 18:00:06 +0100 Subject: [PATCH 19/57] Add DialogAlignment --- .../Examples/DialogServiceDefault.razor | 6 +++-- .../Dialog/Examples/SimpleDialog.razor | 9 +++++-- src/Core/Components/Dialog/FluentDialog.razor | 8 +++--- .../Components/Dialog/FluentDialog.razor.cs | 20 ++++++++++++++ .../Components/Dialog/FluentDialog.razor.css | 16 ++++++++++++ .../Dialog/FluentDialogBody.razor.css | 5 ++++ .../Dialog/Services/DialogParameters.cs | 9 ++++--- src/Core/Enums/DialogAlignment.cs | 26 +++++++++++++++++++ 8 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 src/Core/Components/Dialog/FluentDialog.razor.css create mode 100644 src/Core/Enums/DialogAlignment.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index c5b45e71d6..76ac3892e6 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -1,14 +1,16 @@ @inject IDialogService DialogService -Open Dialog +Open Panel +Open Dialog @code { - private async Task OpenDialogAsync() + private async Task OpenDialogAsync(DialogAlignment alignment) { var parameters = new DialogParameters(factory => { factory.Title = "My title"; + factory.Alignment = alignment; factory.Content.Add(nameof(SimpleDialog.Name), "John"); factory.Content.Add(nameof(SimpleDialog.Age), 20); diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index f3607e1edf..12d5f89d92 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,7 +1,7 @@  - My Title + @Dialog?.Parameters.Title @@ -17,11 +17,16 @@ @code { [CascadingParameter] - public FluentDialog? Dialog { get; set; } + public DialogInstance? Dialog { get; set; } [Parameter] public string Name { get; set; } = string.Empty; [Parameter] public int Age { get; set; } + + protected override void OnInitialized() + { + Console.WriteLine("OnInitialized: " + Dialog?.Parameters.Title); + } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index a195804712..5e05f66021 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -4,18 +4,20 @@ @* Content *@ @if (Instance is not null) { - + + + } else { @ChildContent } - diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 6dd2243ef9..844e2ff3e7 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -51,6 +51,12 @@ internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) [Parameter] public RenderFragment? ChildContent { get; set; } + /// + /// Gets or sets the alignment of the dialog (center, left, right). + /// + [Parameter] + public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; + /// /// Command executed when the user clicks on the button. /// @@ -109,4 +115,18 @@ public async Task HideAsync() /// private bool LaunchedFromService => Instance is not null; + + /// + private string? GetAlignmentAttribute() + { + // Get the alignment from the DialogService (if used) or the Alignment property. + var alignment = Instance?.Parameters.Alignment ?? Alignment; + + return alignment switch + { + DialogAlignment.Start => "start", + DialogAlignment.End => "end", + _ => null + }; + } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.css b/src/Core/Components/Dialog/FluentDialog.razor.css new file mode 100644 index 0000000000..1c41adcc81 --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialog.razor.css @@ -0,0 +1,16 @@ +fluent-dialog[panel]::part(dialog) { + height: 100%; + max-height: 100%; + margin-top: 0px; + margin-bottom: 0px; + border-radius: 0px; + max-width: 530px; +} + +fluent-dialog[panel='start']::part(dialog) { + margin-left: 0px; +} + +fluent-dialog[panel='end']::part(dialog) { + margin-right: 0px; +} diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css index dd15348c91..75d887bc02 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.css +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -1,3 +1,8 @@ +fluent-dialog-body { + grid-template-rows: auto 1fr auto; + height: 100%; +} + /* Desktop and Mobile */ fluent-dialog-body > div[slot="action"] { display: flex; diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogParameters.cs index 2596bc02a6..4ecd8407ab 100644 --- a/src/Core/Components/Dialog/Services/DialogParameters.cs +++ b/src/Core/Components/Dialog/Services/DialogParameters.cs @@ -9,8 +9,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public class DialogParameters { - private static readonly Dictionary EmptyData = new(StringComparer.Ordinal); - /// public DialogParameters() { @@ -27,10 +25,15 @@ public DialogParameters(Action implementationFactory) /// public string? Title { get; set; } + /// + /// Gets or sets the dialog alignment. + /// + public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; + /// /// Gets the content of the dialog. /// - public IDictionary Content { get; set; } = EmptyData; + public IDictionary Content { get; set; } = new Dictionary(StringComparer.Ordinal); /// /// Gets or sets the action raised when the dialog is opened or closed. diff --git a/src/Core/Enums/DialogAlignment.cs b/src/Core/Enums/DialogAlignment.cs new file mode 100644 index 0000000000..dc547353d1 --- /dev/null +++ b/src/Core/Enums/DialogAlignment.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// The alignment of the dialog. +/// +public enum DialogAlignment +{ + /// + /// Default alignment (center). + /// + Default, + + /// + /// Panel of the left. + /// + Start, + + /// + /// Panel of the right. + /// + End, +} From e88e7303fcc1a05423c366496001f9cdd4b2c0e1 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Thu, 19 Dec 2024 20:23:47 +0100 Subject: [PATCH 20/57] Add fluent-drawer --- src/Core/Components/Dialog/FluentDialog.razor | 18 ++++++++++-------- .../Components/Dialog/FluentDialog.razor.cs | 9 +++++++++ .../Components/Dialog/FluentDialog.razor.css | 17 ++--------------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 5e05f66021..940366a688 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -1,13 +1,15 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - + @* Content *@ @if (Instance is not null) { @@ -20,4 +22,4 @@ { @ChildContent } - + diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 844e2ff3e7..5ab94c1b08 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -57,6 +57,12 @@ internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) [Parameter] public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; + ///// + ///// Gets or sets the type of modal dialog. + ///// + //[Parameter] + //public bool Modal { get; set; } + /// /// Command executed when the user clicks on the button. /// @@ -129,4 +135,7 @@ public async Task HideAsync() _ => null }; } + + /// + private bool IsPanel() => GetAlignmentAttribute() != null; } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.css b/src/Core/Components/Dialog/FluentDialog.razor.css index 1c41adcc81..ccfc633448 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.css +++ b/src/Core/Components/Dialog/FluentDialog.razor.css @@ -1,16 +1,3 @@ -fluent-dialog[panel]::part(dialog) { - height: 100%; - max-height: 100%; - margin-top: 0px; - margin-bottom: 0px; - border-radius: 0px; - max-width: 530px; -} - -fluent-dialog[panel='start']::part(dialog) { - margin-left: 0px; -} - -fluent-dialog[panel='end']::part(dialog) { - margin-right: 0px; +fluent-drawer::part(dialog) { + max-width: revert; } From 261e1028aed6069a54aefe79b833cb92a976ff6c Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 10:01:04 +0100 Subject: [PATCH 21/57] Refactoring --- .../Examples/DialogServiceDefault.razor | 15 ++++++------ .../Components/Dialog/FluentDialog.razor.css | 2 +- .../Components/Dialog/FluentDialogBody.razor | 2 +- .../Dialog/FluentDialogBody.razor.css | 16 ++++++------- .../Dialog/FluentDialogProvider.razor | 9 ++++++- .../Dialog/Services/DialogInstance.cs | 6 ++--- .../{DialogParameters.cs => DialogOptions.cs} | 21 +++++++++++++--- .../Dialog/Services/DialogService.cs | 18 +++++++++----- .../Services/IDialogContentComponent.cs | 24 ------------------- .../Dialog/Services/IDialogService.cs | 16 +++++++++---- 10 files changed, 70 insertions(+), 59 deletions(-) rename src/Core/Components/Dialog/Services/{DialogParameters.cs => DialogOptions.cs} (69%) delete mode 100644 src/Core/Components/Dialog/Services/IDialogContentComponent.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 76ac3892e6..6315308489 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -7,21 +7,20 @@ { private async Task OpenDialogAsync(DialogAlignment alignment) { - var parameters = new DialogParameters(factory => + var dialog = await DialogService.ShowDialogAsync(options => { - factory.Title = "My title"; - factory.Alignment = alignment; + options.Title = "My title"; + options.Alignment = alignment; - factory.Content.Add(nameof(SimpleDialog.Name), "John"); - factory.Content.Add(nameof(SimpleDialog.Age), 20); + options.Content.Add(nameof(SimpleDialog.Name), "John"); + options.Content.Add(nameof(SimpleDialog.Age), 20); - factory.OnStateChange = (e) => + options.OnStateChange = (e) => { Console.WriteLine($"Dialog state changed: {e.State}"); }; }); - - var dialog = await DialogService.ShowDialogAsync(parameters); + var result = await dialog.Result; if (result.Cancelled) diff --git a/src/Core/Components/Dialog/FluentDialog.razor.css b/src/Core/Components/Dialog/FluentDialog.razor.css index ccfc633448..2d8864c49c 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.css +++ b/src/Core/Components/Dialog/FluentDialog.razor.css @@ -1,3 +1,3 @@ -fluent-drawer::part(dialog) { +fluent-drawer[fuib]::part(dialog) { max-width: revert; } diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index 5cf681ab1c..eb4c13a775 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -1,7 +1,7 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - + @if (TitleTemplate is not null) { diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css index 75d887bc02..ae0bb53ebb 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.css +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -1,18 +1,18 @@ -fluent-dialog-body { +fluent-dialog-body[fuib] { grid-template-rows: auto 1fr auto; height: 100%; } -/* Desktop and Mobile */ -fluent-dialog-body > div[slot="action"] { - display: flex; - gap: var(--spacingVerticalS); - flex-direction: column; -} + /* Desktop and Mobile */ + fluent-dialog-body[fuib] > div[slot="action"] { + display: flex; + gap: var(--spacingVerticalS); + flex-direction: column; + } /* Desktop */ @container (min-width: 480px) { - fluent-dialog-body > div[slot="action"] { + fluent-dialog-body[fuib] > div[slot="action"] { flex-direction: row; } } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 37f2620412..6236e11610 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -6,7 +6,14 @@ { @foreach (var dialog in DialogService.Items.Values) { - + var parameters = dialog.Instance?.Parameters; + } }
diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 04319aec1c..ed6a6005f5 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -12,12 +12,12 @@ public class DialogInstance : IDialogInstance internal readonly TaskCompletionSource ResultCompletion = new(); /// - public DialogInstance(IDialogService dialogService, Type componentType, DialogParameters parameters) + internal DialogInstance(IDialogService dialogService, Type componentType, DialogOptions parameters) { ComponentType = componentType; Parameters = parameters; DialogService = dialogService; - Id = Identifier.NewId(); + Id = string.IsNullOrEmpty(parameters.Id) ? Identifier.NewId() : parameters.Id; } /// @@ -27,7 +27,7 @@ public DialogInstance(IDialogService dialogService, Type componentType, DialogPa internal IDialogService DialogService { get; } /// - public DialogParameters Parameters { get; internal set; } + public DialogOptions Parameters { get; internal set; } /// public Task Result => ResultCompletion.Task; diff --git a/src/Core/Components/Dialog/Services/DialogParameters.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs similarity index 69% rename from src/Core/Components/Dialog/Services/DialogParameters.cs rename to src/Core/Components/Dialog/Services/DialogOptions.cs index 4ecd8407ab..2a834e6c00 100644 --- a/src/Core/Components/Dialog/Services/DialogParameters.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -7,19 +7,34 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Parameters for configuring a dialog. /// -public class DialogParameters +public class DialogOptions : IFluentComponentBase { /// - public DialogParameters() + public DialogOptions() { } /// - public DialogParameters(Action implementationFactory) + public DialogOptions(Action implementationFactory) { implementationFactory.Invoke(this); } + /// + public string? Id { get; set; } + + /// + public string? Class { get; set; } + + /// + public string? Style { get; set; } + + /// + public object? Data { get; set; } + + /// + public IReadOnlyDictionary? AdditionalAttributes { get; set; } + /// /// Gets or sets the title of the dialog. /// diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 77d02b4379..22fcafb1e4 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -32,9 +32,9 @@ public Task CloseAsync(DialogInstance dialog, DialogResult result) return Task.CompletedTask; } - /// + /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "TODO")] - public virtual async Task ShowDialogAsync(Type componentType, DialogParameters parameters) + public virtual async Task ShowDialogAsync(Type componentType, DialogOptions options) { if (!componentType.IsSubclassOf(typeof(ComponentBase))) { @@ -46,7 +46,7 @@ public virtual async Task ShowDialogAsync(Type componentType, D throw new FluentServiceProviderException(); } - var instance = new DialogInstance(this, componentType, parameters); + var instance = new DialogInstance(this, componentType, options); var dialog = new FluentDialog(_serviceProvider, instance); // Add the dialog to the service, and render it. @@ -56,9 +56,15 @@ public virtual async Task ShowDialogAsync(Type componentType, D return instance; } - /// - public Task ShowDialogAsync(DialogParameters parameters) where TDialog : ComponentBase + /// + public Task ShowDialogAsync(DialogOptions options) where TDialog : ComponentBase + { + return ShowDialogAsync(typeof(TDialog), options); + } + + /// + public Task ShowDialogAsync(Action options) where TDialog : ComponentBase { - return ShowDialogAsync(typeof(TDialog), parameters); + return ShowDialogAsync(typeof(TDialog), new DialogOptions(options)); } } diff --git a/src/Core/Components/Dialog/Services/IDialogContentComponent.cs b/src/Core/Components/Dialog/Services/IDialogContentComponent.cs deleted file mode 100644 index cd100b5dbf..0000000000 --- a/src/Core/Components/Dialog/Services/IDialogContentComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------ -// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------------------ - -namespace Microsoft.FluentUI.AspNetCore.Components; - -/// -/// A component implementing this interface can be used as dialog content. -/// -public interface IDialogContentComponent -{ -} - -/// -/// A component implementing this interface can be used as dialog content. -/// -/// -public interface IDialogContentComponent : IDialogContentComponent -{ - /// - /// Gets or sets the content to display in the dialog. - /// - TContent Content { get; set; } -} diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 2e6df5b792..f3ed49ad74 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -18,14 +18,22 @@ public partial interface IDialogService : IFluentServiceBase /// Shows a dialog with the component type as the body, /// /// Type of component to display. - /// Parameters to configure the dialog component. - Task ShowDialogAsync(Type dialogComponent, DialogParameters parameters); + /// Parameters to configure the dialog component. + Task ShowDialogAsync(Type dialogComponent, DialogOptions options); /// /// Shows a dialog with the component type as the body. /// /// Type of component to display. - /// Parameters to configure the dialog component. - Task ShowDialogAsync(DialogParameters parameters) + /// Parameters to configure the dialog component. + Task ShowDialogAsync(DialogOptions options) + where TDialog : ComponentBase; + + /// + /// Shows a dialog with the component type as the body. + /// + /// Type of component to display. + /// Parameters to configure the dialog component. + Task ShowDialogAsync(Action options) where TDialog : ComponentBase; } From 38f5fd961165da9e6375ddfac408bbc289afe841 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 12:05:05 +0100 Subject: [PATCH 22/57] Refactoring --- .../Dialog/Examples/SimpleDialog.razor | 4 +- src/Core/Components/Dialog/FluentDialog.razor | 8 ++-- .../Components/Dialog/FluentDialog.razor.cs | 47 +++++++++++++++---- .../Dialog/FluentDialogProvider.razor | 10 ++-- .../Dialog/Services/DialogInstance.cs | 8 ++-- .../Dialog/Services/DialogOptions.cs | 11 ++++- .../Dialog/Services/IDialogService.cs | 6 +-- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 12d5f89d92..553b1dae87 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,7 +1,7 @@  - @Dialog?.Parameters.Title + @Dialog?.Options.Title @@ -27,6 +27,6 @@ protected override void OnInitialized() { - Console.WriteLine("OnInitialized: " + Dialog?.Parameters.Title); + Console.WriteLine("OnInitialized: " + Dialog?.Options.Title); } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 940366a688..fa6ef560dd 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -6,20 +6,22 @@ id="@Id" class="@Class" style="@Style" - position="@(IsPanel() ? GetAlignmentAttribute() : null)" + position="@GetAlignmentAttribute()" + type="@GetModalAttribute()" @ondialogbeforetoggle="@OnToggleAsync" @ondialogtoggle="@OnToggleAsync" @attributes="AdditionalAttributes"> - @* Content *@ + @if (Instance is not null) { + Parameters="Instance.Options.Content" /> } else { @ChildContent } + diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 5ab94c1b08..5fc8193a5b 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -58,8 +58,12 @@ internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; ///// - ///// Gets or sets the type of modal dialog. + ///// Gets or sets a value indicating whether this dialog is displayed modally. ///// + ///// + ///// When a dialog is displayed modally, no input (keyboard or mouse click) can occur except to objects on the modal dialog. + ///// The program must hide or close a modal dialog (usually in response to some user action) before input to another dialog can occur. + ///// //[Parameter] //public bool Modal { get; set; } @@ -125,17 +129,42 @@ public async Task HideAsync() /// private string? GetAlignmentAttribute() { - // Get the alignment from the DialogService (if used) or the Alignment property. - var alignment = Instance?.Parameters.Alignment ?? Alignment; + // Alignment is only used when the dialog is a panel. + if (IsPanel()) + { + // Get the alignment from the DialogService (if used) or the Alignment property. + var alignment = Instance?.Options.Alignment ?? Alignment; + + return alignment switch + { + DialogAlignment.Start => "start", + DialogAlignment.End => "end", + _ => null + }; + } - return alignment switch + return null; + } + + /// + private string? GetModalAttribute() + { + switch (IsPanel()) { - DialogAlignment.Start => "start", - DialogAlignment.End => "end", - _ => null - }; + // Dialog + case false: + // TODO: To find a way to catch the click event outside the dialog. + return "modal"; + + // Panels + case true: + // TODO: To find a way to catch the click event outside the dialog. + return "modal"; + + } } /// - private bool IsPanel() => GetAlignmentAttribute() != null; + private bool IsPanel() => (Instance?.Options.Alignment ?? Alignment) != DialogAlignment.Default; + } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 6236e11610..1118ed72ef 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -6,13 +6,13 @@ { @foreach (var dialog in DialogService.Items.Values) { - var parameters = dialog.Instance?.Parameters; + var options = dialog.Instance?.Options; } } diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index ed6a6005f5..e95a633cd7 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -12,12 +12,12 @@ public class DialogInstance : IDialogInstance internal readonly TaskCompletionSource ResultCompletion = new(); /// - internal DialogInstance(IDialogService dialogService, Type componentType, DialogOptions parameters) + internal DialogInstance(IDialogService dialogService, Type componentType, DialogOptions options) { ComponentType = componentType; - Parameters = parameters; + Options = options; DialogService = dialogService; - Id = string.IsNullOrEmpty(parameters.Id) ? Identifier.NewId() : parameters.Id; + Id = string.IsNullOrEmpty(options.Id) ? Identifier.NewId() : options.Id; } /// @@ -27,7 +27,7 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog internal IDialogService DialogService { get; } /// - public DialogOptions Parameters { get; internal set; } + public DialogOptions Options { get; internal set; } /// public Task Result => ResultCompletion.Task; diff --git a/src/Core/Components/Dialog/Services/DialogOptions.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs index 2a834e6c00..793d9e212d 100644 --- a/src/Core/Components/Dialog/Services/DialogOptions.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -5,7 +5,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Parameters for configuring a dialog. +/// Options for configuring a dialog. /// public class DialogOptions : IFluentComponentBase { @@ -40,6 +40,15 @@ public DialogOptions(Action implementationFactory) /// public string? Title { get; set; } + ///// + ///// Gets or sets a value indicating whether this dialog is displayed modally. + ///// + ///// + ///// When a dialog is displayed modally, no input (keyboard or mouse click) can occur except to objects on the modal dialog. + ///// The program must hide or close a modal dialog (usually in response to some user action) before input to another dialog can occur. + ///// + //public bool Modal { get; set; } + /// /// Gets or sets the dialog alignment. /// diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index f3ed49ad74..b73002ee62 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -18,14 +18,14 @@ public partial interface IDialogService : IFluentServiceBase /// Shows a dialog with the component type as the body, /// /// Type of component to display. - /// Parameters to configure the dialog component. + /// Options to configure the dialog component. Task ShowDialogAsync(Type dialogComponent, DialogOptions options); /// /// Shows a dialog with the component type as the body. /// /// Type of component to display. - /// Parameters to configure the dialog component. + /// Options to configure the dialog component. Task ShowDialogAsync(DialogOptions options) where TDialog : ComponentBase; @@ -33,7 +33,7 @@ Task ShowDialogAsync(DialogOptions options) /// Shows a dialog with the component type as the body. /// /// Type of component to display. - /// Parameters to configure the dialog component. + /// Options to configure the dialog component. Task ShowDialogAsync(Action options) where TDialog : ComponentBase; } From fa30ec791cf46c2a7f2f9d6d29d182a78e116042 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 12:18:30 +0100 Subject: [PATCH 23/57] Add documentation --- .../Examples/DialogServiceDefault.razor | 4 +-- .../Dialog/Examples/SimpleDialog.razor | 2 +- src/Core/Components/Dialog/FluentDialog.razor | 2 +- .../Dialog/Services/DialogInstance.cs | 14 ++++---- .../Dialog/Services/DialogOptions.cs | 15 ++++++--- .../Dialog/Services/DialogResult.cs | 33 +++++++++++++++---- .../Dialog/Services/DialogService.cs | 10 +++--- .../Dialog/Services/IDialogInstance.cs | 25 +++++++++++--- .../Dialog/Services/IDialogService.cs | 7 +++- 9 files changed, 82 insertions(+), 30 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 6315308489..e2aa45520d 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -12,8 +12,8 @@ options.Title = "My title"; options.Alignment = alignment; - options.Content.Add(nameof(SimpleDialog.Name), "John"); - options.Content.Add(nameof(SimpleDialog.Age), 20); + options.Parameters.Add(nameof(SimpleDialog.Name), "John"); + options.Parameters.Add(nameof(SimpleDialog.Age), 20); options.OnStateChange = (e) => { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 553b1dae87..9c288b6a22 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -17,7 +17,7 @@ @code { [CascadingParameter] - public DialogInstance? Dialog { get; set; } + public IDialogInstance? Dialog { get; set; } [Parameter] public string Name { get; set; } = string.Empty; diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index fa6ef560dd..03dee9bd68 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -16,7 +16,7 @@ { + Parameters="Instance.Options.Parameters" /> } else diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index e95a633cd7..64ec3b2d29 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -6,7 +6,9 @@ namespace Microsoft.FluentUI.AspNetCore.Components; -/// +/// +/// Represents a dialog instance used with the . +/// public class DialogInstance : IDialogInstance { internal readonly TaskCompletionSource ResultCompletion = new(); @@ -26,22 +28,22 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// internal IDialogService DialogService { get; } - /// + /// public DialogOptions Options { get; internal set; } - /// + /// public Task Result => ResultCompletion.Task; - /// + /// " public string Id { get; } - /// + /// public Task CloseAsync() { return DialogService.CloseAsync(this, DialogResult.Cancel()); } - /// + /// public Task CloseAsync(DialogResult result) { return DialogService.CloseAsync(this, result); diff --git a/src/Core/Components/Dialog/Services/DialogOptions.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs index 793d9e212d..374414084d 100644 --- a/src/Core/Components/Dialog/Services/DialogOptions.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -9,12 +9,18 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public class DialogOptions : IFluentComponentBase { - /// + /// + /// Initializes a new instance of the class. + /// public DialogOptions() { } - /// + /// + /// Initializes a new instance of the class + /// using the specified implementation factory. + /// + /// public DialogOptions(Action implementationFactory) { implementationFactory.Invoke(this); @@ -55,9 +61,10 @@ public DialogOptions(Action implementationFactory) public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; /// - /// Gets the content of the dialog. + /// Gets a list of dialog parameters. + /// Each parameter must correspond to a `[Parameter]` property defined in the component. /// - public IDictionary Content { get; set; } = new Dictionary(StringComparer.Ordinal); + public IDictionary Parameters { get; set; } = new Dictionary(StringComparer.Ordinal); /// /// Gets or sets the action raised when the dialog is opened or closed. diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs index 70c62ed3b8..a32f446529 100644 --- a/src/Core/Components/Dialog/Services/DialogResult.cs +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -4,25 +4,44 @@ namespace Microsoft.FluentUI.AspNetCore.Components; -/// +/// +/// Represents the result of a dialog. +/// public class DialogResult { - /// + /// + /// Initializes a new instance of the class. + /// + /// + /// protected internal DialogResult(object? content, bool cancelled) { Value = content; Cancelled = cancelled; } - /// - public object? Value { get; set; } + /// + /// Gets the content of the dialog result. + /// + public object? Value { get; } - /// + /// + /// Gets a value indicating whether the dialog was cancelled. + /// public bool Cancelled { get; } - /// + /// + /// Creates a dialog result with the specified content. + /// + /// + /// + /// public static DialogResult Ok(T result) => new(result, cancelled: false); - /// + /// + /// Creates a dialog result with the specified content. + /// + /// + /// public static DialogResult Cancel(object? content = null) => new(content ?? default, cancelled: true); } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 22fcafb1e4..18f50d539e 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -6,21 +6,23 @@ namespace Microsoft.FluentUI.AspNetCore.Components; -/// +/// +/// Service for showing dialogs. +/// public partial class DialogService : FluentServiceBase, IDialogService { private readonly IServiceProvider _serviceProvider; /// - /// + /// Initializes a new instance of the class. /// - /// + /// List of services available in the application. public DialogService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } - /// + /// public Task CloseAsync(DialogInstance dialog, DialogResult result) { // Remove the HTML code from the DialogProvider diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index 8a10e94171..c40f50149d 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -9,15 +9,32 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public interface IDialogInstance { - /// + /// + /// Gets the unique identifier for the dialog. + /// If this value is not set in the , a new identifier is generated. + /// string Id { get; } - /// + /// + /// Gets the options used to configure the dialog. + /// + DialogOptions Options { get; } + + /// + /// Gets the result of the dialog. + /// Task Result { get; } - /// + /// + /// Closes the dialog with a cancel result. + /// + /// Task CloseAsync(); - /// + /// + /// Closes the dialog with the specified result. + /// + /// Result to close the dialog with. + /// Task CloseAsync(DialogResult result); } diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index b73002ee62..4f21c516ca 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -11,7 +11,12 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial interface IDialogService : IFluentServiceBase { - /// + /// + /// Closes the dialog with the specified result. + /// + /// Instance of the dialog to close. + /// Result of closing the dialog box. + /// Task CloseAsync(DialogInstance dialog, DialogResult result); /// From 429d74cd61d2e64972eeff3b2fb1469a9348e9b4 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 13:14:42 +0100 Subject: [PATCH 24/57] Remove the Drawer --- .../Components/Dialog/Examples/SimpleDialog.razor | 6 ++++++ src/Core/Components/Dialog/FluentDialog.razor | 6 +++--- src/Core/Components/Dialog/FluentDialog.razor.css | 12 ++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 9c288b6a22..3318733967 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -6,6 +6,7 @@ Hello @Name (@Age) + @GetVeryLongText() @@ -29,4 +30,9 @@ { Console.WriteLine("OnInitialized: " + Dialog?.Options.Title); } + + private MarkupString GetVeryLongText() + { + return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 03dee9bd68..b740d45535 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -1,7 +1,7 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - - + @if (Instance is not null) { @@ -24,4 +24,4 @@ @ChildContent } - + diff --git a/src/Core/Components/Dialog/FluentDialog.razor.css b/src/Core/Components/Dialog/FluentDialog.razor.css index 2d8864c49c..28cb7ef51c 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.css +++ b/src/Core/Components/Dialog/FluentDialog.razor.css @@ -1,3 +1,11 @@ -fluent-drawer[fuib]::part(dialog) { - max-width: revert; +fluent-dialog[position="end"][fuib]::part(dialog) { + margin-inline-end: 0px; + max-height: 100vh; + border-radius: 0px; +} + +fluent-dialog[position="start"][fuib]::part(dialog) { + margin-inline-start: 0px; + max-height: 100vh; + border-radius: 0px; } From ce28c27abe668507b55829dbf8e26bcc745951a8 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 14:57:44 +0100 Subject: [PATCH 25/57] Add Height and Width --- .../Examples/DialogServiceDefault.razor | 1 + src/Core/Components/Dialog/FluentDialog.razor | 6 +++-- .../Components/Dialog/FluentDialog.razor.cs | 23 +++++++++++++++++++ .../Dialog/FluentDialogBody.razor.css | 2 +- .../Dialog/Services/DialogOptions.cs | 12 ++++++++++ src/Core/Utilities/StyleBuilder.cs | 2 +- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index e2aa45520d..0bb23ebafd 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -11,6 +11,7 @@ { options.Title = "My title"; options.Alignment = alignment; + options.Height = "300px"; options.Parameters.Add(nameof(SimpleDialog.Name), "John"); options.Parameters.Add(nameof(SimpleDialog.Age), 20); diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index b740d45535..9252fe792f 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -4,14 +4,16 @@ + @GetDialogStyle() + @if (Instance is not null) { diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 5fc8193a5b..b5d799eaa1 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -35,6 +35,16 @@ internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) JSRuntime = serviceProvider.GetRequiredService(); } + /// + protected string? ClassValue => new CssBuilder(Class) + .Build(); + + /// + protected string? StyleValue => new StyleBuilder(Style) + .AddStyle("height", Instance?.Options.Height, when: IsDialog()) + .AddStyle("width", Instance?.Options.Width) + .Build(); + /// [Inject] private IDialogService? DialogService { get; set; } @@ -167,4 +177,17 @@ public async Task HideAsync() /// private bool IsPanel() => (Instance?.Options.Alignment ?? Alignment) != DialogAlignment.Default; + /// + private bool IsDialog() => !IsPanel(); + + /// + private MarkupString? GetDialogStyle() + { + if (string.IsNullOrEmpty(StyleValue)) + { + return null; + } + + return (MarkupString)$""; + } } diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.css b/src/Core/Components/Dialog/FluentDialogBody.razor.css index ae0bb53ebb..a4a217e854 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.css +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.css @@ -1,4 +1,4 @@ -fluent-dialog-body[fuib] { +fluent-dialog[position][fuib] fluent-dialog-body[fuib] { grid-template-rows: auto 1fr auto; height: 100%; } diff --git a/src/Core/Components/Dialog/Services/DialogOptions.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs index 374414084d..9cd4caced1 100644 --- a/src/Core/Components/Dialog/Services/DialogOptions.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -60,6 +60,18 @@ public DialogOptions(Action implementationFactory) /// public DialogAlignment Alignment { get; set; } = DialogAlignment.Default; + /// + /// Gets or sets the width of the dialog. Must be a valid CSS width value like '600px' or '3em' + /// Only used if Alignment is set to . + /// + public string? Width { get; set; } + + /// + /// Gets or sets the height of the dialog. Must be a valid CSS height value like '600px' or '3em' + /// Only used if Alignment is set to . + /// + public string? Height { get; set; } + /// /// Gets a list of dialog parameters. /// Each parameter must correspond to a `[Parameter]` property defined in the component. diff --git a/src/Core/Utilities/StyleBuilder.cs b/src/Core/Utilities/StyleBuilder.cs index 89f8c4181a..64ecd59138 100644 --- a/src/Core/Utilities/StyleBuilder.cs +++ b/src/Core/Utilities/StyleBuilder.cs @@ -49,7 +49,7 @@ public StyleBuilder(string? userStyles) /// /// Style to add /// StyleBuilder - public StyleBuilder AddStyle(string prop, string? value) => AddRaw($"{prop}: {value}"); + public StyleBuilder AddStyle(string prop, string? value) => string.IsNullOrEmpty(value) ? this : AddRaw($"{prop}: {value}"); /// /// Adds a conditional in-line style to the builder with space separator and closing semicolon.. From 69d27aff52f6c6a533bc1c487b4131ae9d66a6d7 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 15:23:42 +0100 Subject: [PATCH 26/57] Refactoring --- .../Dialog/Examples/SimpleDialog.razor | 14 +++++++++-- src/Core/Components/Dialog/DialogEventArgs.cs | 2 +- .../Components/Dialog/FluentDialog.razor.cs | 4 ++-- .../Dialog/FluentDialogProvider.razor | 11 ++++----- .../Dialog/FluentDialogProvider.razor.cs | 3 --- .../Dialog/Services/DialogInstance.cs | 5 ++-- .../Dialog/Services/DialogService.cs | 23 +++++++++++-------- .../Dialog/Services/IDialogInstance.cs | 5 ++++ .../Dialog/Services/IDialogService.cs | 4 ++-- .../Extensions/ServiceProviderExtensions.cs | 1 - 10 files changed, 43 insertions(+), 29 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 3318733967..4f1b6ca263 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -10,15 +10,20 @@ - OK + OK Cancel @code { + + // If you want to use this razor component in standalone mode, + // you can use a nullable IDialogInstance property. + // If the value is not null, the component is running using the DialogService. + // `public IDialogInstance? Dialog { get; set; }` [CascadingParameter] - public IDialogInstance? Dialog { get; set; } + public required IDialogInstance Dialog { get; set; } [Parameter] public string Name { get; set; } = string.Empty; @@ -26,6 +31,11 @@ [Parameter] public int Age { get; set; } + private async Task btnOK_Click() + { + await Dialog.CloseAsync(); + } + protected override void OnInitialized() { Console.WriteLine("OnInitialized: " + Dialog?.Options.Title); diff --git a/src/Core/Components/Dialog/DialogEventArgs.cs b/src/Core/Components/Dialog/DialogEventArgs.cs index eb52aeeef9..d206750194 100644 --- a/src/Core/Components/Dialog/DialogEventArgs.cs +++ b/src/Core/Components/Dialog/DialogEventArgs.cs @@ -62,5 +62,5 @@ internal DialogEventArgs(FluentDialog dialog, string? id, string? eventType, str /// /// Gets the instance used by the . /// - public DialogInstance? Instance { get; set; } + public IDialogInstance? Instance { get; set; } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index b5d799eaa1..b0a7706275 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -53,7 +53,7 @@ internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) /// Gets or sets the instance used by the . /// [Parameter] - public DialogInstance? Instance { get; set; } + public IDialogInstance? Instance { get; set; } /// /// Used when not calling the to show a dialog. @@ -110,7 +110,7 @@ private async Task OnToggleAsync(DialogToggleEventArgs args) } // Remove the HTML code from the DialogProvider - if (LaunchedFromService && Instance is not null && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) + if (LaunchedFromService && Instance is not null && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) { var service = DialogService as DialogService; service?.CloseAsync(Instance, DialogResult.Cancel()); diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index 1118ed72ef..d4ab9a70ba 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -6,13 +6,12 @@ { @foreach (var dialog in DialogService.Items.Values) { - var options = dialog.Instance?.Options; } } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index 354920ac28..a1c18fe30c 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -39,9 +39,6 @@ public FluentDialogProvider() /// protected virtual IDialogService? DialogService => _dialogService ??= ServiceProvider?.GetService(); - /// - protected IEnumerable? Dialogs => DialogService?.Items.Values; - /// protected override void OnInitialized() { diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 64ec3b2d29..16fcf753b2 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -11,19 +11,20 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public class DialogInstance : IDialogInstance { + private readonly Type _componentType; internal readonly TaskCompletionSource ResultCompletion = new(); /// internal DialogInstance(IDialogService dialogService, Type componentType, DialogOptions options) { - ComponentType = componentType; + _componentType = componentType; Options = options; DialogService = dialogService; Id = string.IsNullOrEmpty(options.Id) ? Identifier.NewId() : options.Id; } /// - internal Type ComponentType { get; } + Type IDialogInstance.ComponentType => _componentType; /// internal IDialogService DialogService { get; } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 18f50d539e..ee679d4f16 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -9,7 +9,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Service for showing dialogs. /// -public partial class DialogService : FluentServiceBase, IDialogService +public partial class DialogService : FluentServiceBase, IDialogService { private readonly IServiceProvider _serviceProvider; @@ -22,16 +22,20 @@ public DialogService(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - /// - public Task CloseAsync(DialogInstance dialog, DialogResult result) + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] + public async Task CloseAsync(IDialogInstance dialog, DialogResult result) { // Remove the HTML code from the DialogProvider - ServiceProvider.Items.TryRemove(dialog.Id, out _); + if (!ServiceProvider.Items.TryRemove(dialog.Id, out _)) + { + throw new InvalidOperationException($"Failed to remove dialog from DialogProvider: the ID '{dialog.Id}' doesn't exist in the DialogServiceProvider."); + } - // Set the result of the dialog - dialog.ResultCompletion.SetResult(result); + await ServiceProvider.OnUpdatedAsync.Invoke(dialog); - return Task.CompletedTask; + // Set the result of the dialog + (dialog as DialogInstance)?.ResultCompletion.SetResult(result); } /// @@ -49,11 +53,10 @@ public virtual async Task ShowDialogAsync(Type componentType, D } var instance = new DialogInstance(this, componentType, options); - var dialog = new FluentDialog(_serviceProvider, instance); // Add the dialog to the service, and render it. - ServiceProvider.Items.TryAdd(dialog?.Id ?? "", dialog ?? throw new InvalidOperationException("Failed to create FluentDialog.")); - await ServiceProvider.OnUpdatedAsync.Invoke(dialog); + ServiceProvider.Items.TryAdd(instance?.Id ?? "", instance ?? throw new InvalidOperationException("Failed to create FluentDialog.")); + await ServiceProvider.OnUpdatedAsync.Invoke(instance); return instance; } diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index c40f50149d..b9a70b1774 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -9,6 +9,11 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public interface IDialogInstance { + /// + /// Gets the component type of the dialog. + /// + internal Type ComponentType { get; } + /// /// Gets the unique identifier for the dialog. /// If this value is not set in the , a new identifier is generated. diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 4f21c516ca..9406ce692a 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -9,7 +9,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Interface for DialogService /// -public partial interface IDialogService : IFluentServiceBase +public partial interface IDialogService : IFluentServiceBase { /// /// Closes the dialog with the specified result. @@ -17,7 +17,7 @@ public partial interface IDialogService : IFluentServiceBase /// Instance of the dialog to close. /// Result of closing the dialog box. /// - Task CloseAsync(DialogInstance dialog, DialogResult result); + Task CloseAsync(IDialogInstance dialog, DialogResult result); /// /// Shows a dialog with the component type as the body, diff --git a/src/Core/Extensions/ServiceProviderExtensions.cs b/src/Core/Extensions/ServiceProviderExtensions.cs index e1ef57cb68..9959b78187 100644 --- a/src/Core/Extensions/ServiceProviderExtensions.cs +++ b/src/Core/Extensions/ServiceProviderExtensions.cs @@ -10,7 +10,6 @@ internal static class ServiceProviderExtensions /// Gets a value indicating whether the provider was added by the user and is available. /// public static bool ProviderNotAvailable(this IFluentServiceBase provider) - where TComponent : FluentComponentBase { return string.IsNullOrEmpty(provider.ProviderId); } From 025d1e348b612e8347d47f90b9356fce58b74887 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 16:48:37 +0100 Subject: [PATCH 27/57] Update the event orders --- .../Dialog/Examples/DialogBodyDefault.razor | 8 +-- .../Examples/DialogServiceDefault.razor | 8 +-- .../Dialog/Examples/SimpleDialog.razor | 20 +++--- src/Core/Components/Dialog/DialogEventArgs.cs | 12 +++- .../Components/Dialog/FluentDialog.razor.cs | 62 +++++++++++-------- .../Dialog/Services/DialogInstance.cs | 3 + .../Dialog/Services/DialogResult.cs | 24 +++++-- .../Dialog/Services/DialogService.cs | 39 +++++++++--- .../Dialog/Services/IDialogInstance.cs | 2 +- src/Core/Events/DialogToggleEventArgs.cs | 2 +- 10 files changed, 121 insertions(+), 59 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor index 465facb5b0..cfe23e81e5 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogBodyDefault.razor @@ -1,16 +1,16 @@  - My Title + My title - Hello Body + @SampleData.Text.GenerateLoremIpsum(paragraphCount: 1) - One - Two + One + Two diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 0bb23ebafd..5affeef2e4 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -14,11 +14,11 @@ options.Height = "300px"; options.Parameters.Add(nameof(SimpleDialog.Name), "John"); - options.Parameters.Add(nameof(SimpleDialog.Age), 20); + options.Parameters.Add(nameof(SimpleDialog.Age), "20"); options.OnStateChange = (e) => { - Console.WriteLine($"Dialog state changed: {e.State}"); + Console.WriteLine($"State changed: {e.State}"); }; }); @@ -26,11 +26,11 @@ if (result.Cancelled) { - Console.WriteLine($"Dialog closed - Canceled"); + Console.WriteLine($"Dialog Canceled: {result.Value}"); } else { - Console.WriteLine($"Dialog closed - OK"); + Console.WriteLine($"Dialog Saved: {result.Value}"); } } } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 4f1b6ca263..de36943e8f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -5,23 +5,25 @@ - Hello @Name (@Age) - @GetVeryLongText() + + + @LongText OK - Cancel + Cancel @code { + private static MarkupString LongText = GetVeryLongText(); // If you want to use this razor component in standalone mode, // you can use a nullable IDialogInstance property. // If the value is not null, the component is running using the DialogService. - // `public IDialogInstance? Dialog { get; set; }` + // `public IDialogInstance? FluentDialog { get; set; }` [CascadingParameter] public required IDialogInstance Dialog { get; set; } @@ -29,19 +31,19 @@ public string Name { get; set; } = string.Empty; [Parameter] - public int Age { get; set; } + public string Age { get; set; } = string.Empty; private async Task btnOK_Click() { - await Dialog.CloseAsync(); + await Dialog.CloseAsync(DialogResult.Ok("Yes")); } - protected override void OnInitialized() + private async Task btnCancel_Click() { - Console.WriteLine("OnInitialized: " + Dialog?.Options.Title); + await Dialog.CloseAsync(DialogResult.Cancel("No")); } - private MarkupString GetVeryLongText() + private static MarkupString GetVeryLongText() { return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); } diff --git a/src/Core/Components/Dialog/DialogEventArgs.cs b/src/Core/Components/Dialog/DialogEventArgs.cs index d206750194..3253da802a 100644 --- a/src/Core/Components/Dialog/DialogEventArgs.cs +++ b/src/Core/Components/Dialog/DialogEventArgs.cs @@ -49,6 +49,14 @@ internal DialogEventArgs(FluentDialog dialog, string? id, string? eventType, str } } + /// + internal DialogEventArgs(IDialogInstance instance, DialogState state) + { + Id = instance.Id; + Instance = instance; + State = state; + } + /// /// Gets the ID of the FluentDialog component. /// @@ -57,10 +65,10 @@ internal DialogEventArgs(FluentDialog dialog, string? id, string? eventType, str /// /// Gets the state of the FluentDialog component. /// - public DialogState State { get; set; } + public DialogState State { get; } /// /// Gets the instance used by the . /// - public IDialogInstance? Instance { get; set; } + public IDialogInstance? Instance { get; } } diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index b0a7706275..a06c37d535 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -3,7 +3,6 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components.Utilities; using Microsoft.JSInterop; @@ -21,20 +20,6 @@ public FluentDialog() Id = Identifier.NewId(); } - /// - /// internal constructor used by the - /// and the . - /// - /// - /// - internal FluentDialog(IServiceProvider serviceProvider, DialogInstance instance) - { - Id = instance.Id; - DialogService = instance.DialogService; - Instance = instance; - JSRuntime = serviceProvider.GetRequiredService(); - } - /// protected string? ClassValue => new CssBuilder(Class) .Build(); @@ -92,6 +77,12 @@ protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender && LaunchedFromService) { + var instance = Instance as DialogInstance; + if (instance is not null) + { + instance.FluentDialog = this; + } + return ShowAsync(); } @@ -101,22 +92,43 @@ protected override Task OnAfterRenderAsync(bool firstRender) /// private async Task OnToggleAsync(DialogToggleEventArgs args) { - var dialogEventArgs = new DialogEventArgs(this, args); - var dialogId = dialogEventArgs?.Id ?? string.Empty; - - if (OnStateChange.HasDelegate) + // Raise the event received from the Web Component + var dialogEventArgs = await RaiseOnStateChangeAsync(args); + + if (LaunchedFromService) { - await OnStateChange.InvokeAsync(dialogEventArgs); + switch (dialogEventArgs.State) + { + // Set the result of the dialog + case DialogState.Closing: + (Instance as DialogInstance)?.ResultCompletion.TrySetResult(DialogResult.Cancel()); + break; + + // Remove the dialog from the DialogProvider + case DialogState.Closed: + (DialogService as DialogService)?.RemoveDialogFromProviderAsync(Instance); + break; + } } + } - // Remove the HTML code from the DialogProvider - if (LaunchedFromService && Instance is not null && dialogEventArgs?.State == DialogState.Closed && !string.IsNullOrEmpty(dialogId)) + /// + private async Task RaiseOnStateChangeAsync(DialogEventArgs args) + { + if (OnStateChange.HasDelegate) { - var service = DialogService as DialogService; - service?.CloseAsync(Instance, DialogResult.Cancel()); + await OnStateChange.InvokeAsync(args); } + + return args; } + /// + private Task RaiseOnStateChangeAsync(DialogToggleEventArgs args) => RaiseOnStateChangeAsync(new DialogEventArgs(this, args)); + + /// + internal Task RaiseOnStateChangeAsync(IDialogInstance instance, DialogState state) => RaiseOnStateChangeAsync(new DialogEventArgs(instance, state)); + /// /// Displays the dialog. /// @@ -161,7 +173,7 @@ public async Task HideAsync() { switch (IsPanel()) { - // Dialog + // FluentDialog case false: // TODO: To find a way to catch the click event outside the dialog. return "modal"; diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index 16fcf753b2..fe91ac161c 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -29,6 +29,9 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// internal IDialogService DialogService { get; } + /// + internal FluentDialog? FluentDialog { get; set; } + /// public DialogOptions Options { get; internal set; } diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs index a32f446529..6452530bb0 100644 --- a/src/Core/Components/Dialog/Services/DialogResult.cs +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -33,15 +33,27 @@ protected internal DialogResult(object? content, bool cancelled) /// /// Creates a dialog result with the specified content. /// - /// - /// - /// + /// Type of the content. + /// The content of the dialog result. + /// The dialog result. public static DialogResult Ok(T result) => new(result, cancelled: false); /// /// Creates a dialog result with the specified content. /// - /// - /// - public static DialogResult Cancel(object? content = null) => new(content ?? default, cancelled: true); + /// The dialog result. + public static DialogResult Ok() => Ok(result: null); + + /// + /// Creates a dialog result with the specified content. + /// + /// The content of the dialog result. + /// The dialog result. + public static DialogResult Cancel(T result) => new(result, cancelled: true); + + /// + /// Creates a dialog result with the specified content. + /// + /// The dialog result. + public static DialogResult Cancel() => Cancel(result: null); } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index ee679d4f16..b04024535f 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -26,16 +26,19 @@ public DialogService(IServiceProvider serviceProvider) [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] public async Task CloseAsync(IDialogInstance dialog, DialogResult result) { - // Remove the HTML code from the DialogProvider - if (!ServiceProvider.Items.TryRemove(dialog.Id, out _)) - { - throw new InvalidOperationException($"Failed to remove dialog from DialogProvider: the ID '{dialog.Id}' doesn't exist in the DialogServiceProvider."); - } + var dialogInstance = dialog as DialogInstance; + + // Raise the DialogState.Closing event + dialogInstance?.FluentDialog?.RaiseOnStateChangeAsync(dialog, DialogState.Closing); - await ServiceProvider.OnUpdatedAsync.Invoke(dialog); + // Remove the dialog from the DialogProvider + await RemoveDialogFromProviderAsync(dialog); // Set the result of the dialog - (dialog as DialogInstance)?.ResultCompletion.SetResult(result); + dialogInstance?.ResultCompletion.TrySetResult(result); + + // Raise the DialogState.Closed event + dialogInstance?.FluentDialog?.RaiseOnStateChangeAsync(dialog, DialogState.Closed); } /// @@ -72,4 +75,26 @@ public Task ShowDialogAsync(Action opti { return ShowDialogAsync(typeof(TDialog), new DialogOptions(options)); } + + /// + /// Removes the dialog from the DialogProvider. + /// + /// + /// + /// + internal Task RemoveDialogFromProviderAsync(IDialogInstance? dialog) + { + if (dialog is null) + { + return Task.CompletedTask; + } + + // Remove the HTML code from the DialogProvider + if (!ServiceProvider.Items.TryRemove(dialog.Id, out _)) + { + throw new InvalidOperationException($"Failed to remove dialog from DialogProvider: the ID '{dialog.Id}' doesn't exist in the DialogServiceProvider."); + } + + return ServiceProvider.OnUpdatedAsync.Invoke(dialog); + } } diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index b9a70b1774..ceb49b4ef0 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -31,7 +31,7 @@ public interface IDialogInstance Task Result { get; } /// - /// Closes the dialog with a cancel result. + /// Closes the dialog with a Cancel result. /// /// Task CloseAsync(); diff --git a/src/Core/Events/DialogToggleEventArgs.cs b/src/Core/Events/DialogToggleEventArgs.cs index e1f4fe3c91..907a85558c 100644 --- a/src/Core/Events/DialogToggleEventArgs.cs +++ b/src/Core/Events/DialogToggleEventArgs.cs @@ -5,7 +5,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// -/// Event arguments for the Dialog toggle event. +/// Event arguments for the FluentDialog toggle event. /// internal class DialogToggleEventArgs : EventArgs { From 9911d4448eeaac42b653f27053fe12597bb1b337 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 17:04:21 +0100 Subject: [PATCH 28/57] Updating examples --- .../Examples/DialogServiceDefault.razor | 15 ++++++++----- .../Dialog/Examples/SimpleDialog.razor | 22 ++++++++++++++----- .../Dialog/Services/DialogOptions.cs | 9 -------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 5affeef2e4..6baca74c59 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -3,25 +3,30 @@ Open Panel Open Dialog +
+ John: @John +
+ @code { + private SimpleDialog.PersonDetails John = new() { Age = "20" }; + private async Task OpenDialogAsync(DialogAlignment alignment) { var dialog = await DialogService.ShowDialogAsync(options => { options.Title = "My title"; options.Alignment = alignment; - options.Height = "300px"; - - options.Parameters.Add(nameof(SimpleDialog.Name), "John"); - options.Parameters.Add(nameof(SimpleDialog.Age), "20"); + + options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type + options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object options.OnStateChange = (e) => { Console.WriteLine($"State changed: {e.State}"); }; }); - + var result = await dialog.Result; if (result.Cancelled) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index de36943e8f..48324693cf 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -5,8 +5,8 @@ - - + +
@LongText
@@ -27,14 +27,20 @@ [CascadingParameter] public required IDialogInstance Dialog { get; set; } + // A simple type is not updatable + [Parameter] + public string? Name { get; set; } + + // A class is updatable [Parameter] - public string Name { get; set; } = string.Empty; + public PersonDetails Person { get; set; } = new(); + // A nullable type is optional [Parameter] - public string Age { get; set; } = string.Empty; + public int? NotAssignedParam { get; set; } private async Task btnOK_Click() - { + { await Dialog.CloseAsync(DialogResult.Ok("Yes")); } @@ -47,4 +53,10 @@ { return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); } + + public class PersonDetails + { + public string Age { get; set; } = ""; + public override string ToString() => $"Age: {Age}"; + } } diff --git a/src/Core/Components/Dialog/Services/DialogOptions.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs index 9cd4caced1..1245dabdd1 100644 --- a/src/Core/Components/Dialog/Services/DialogOptions.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -46,15 +46,6 @@ public DialogOptions(Action implementationFactory) ///
public string? Title { get; set; } - ///// - ///// Gets or sets a value indicating whether this dialog is displayed modally. - ///// - ///// - ///// When a dialog is displayed modally, no input (keyboard or mouse click) can occur except to objects on the modal dialog. - ///// The program must hide or close a modal dialog (usually in response to some user action) before input to another dialog can occur. - ///// - //public bool Modal { get; set; } - /// /// Gets or sets the dialog alignment. /// From b8ab0aa1eac8182daa823899244da489a031bdcb Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 20 Dec 2024 17:42:25 +0100 Subject: [PATCH 29/57] Update the doc --- .../Components/Dialog/Examples/DialogServiceDefault.razor | 4 +++- .../Documentation/Components/Dialog/FluentDialog.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 6baca74c59..8c824ce6ae 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -3,6 +3,7 @@ Open Panel Open Dialog +
John: @John
@@ -17,7 +18,8 @@ { options.Title = "My title"; options.Alignment = alignment; - + options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; + options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index 396e3702c1..17e3321aaa 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -9,7 +9,7 @@ route: /Dialog ## Services -{{ DialogServiceDefault }} +{{ DialogServiceDefault Files=Code:DialogServiceDefault.razor;SimpleDialog.razor:SimpleDialog.razor }} ## API FluentDialogBody From dd596bc7cfc5255aa020923f7dd3fc8c9e9473ee Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 15:49:16 +0100 Subject: [PATCH 30/57] Add Header and Footer --- .../Examples/DialogServiceCustomized.razor | 43 +++++++++++++ .../Examples/DialogServiceDefault.razor | 9 ++- .../Dialog/Examples/SimpleDialog.razor | 22 ++----- .../Examples/SimpleDialogCustomized.razor | 62 +++++++++++++++++++ .../Components/Dialog/FluentDialog.md | 30 ++++++++- .../Components/Dialog/FluentDialog.razor.cs | 4 +- .../Components/Dialog/FluentDialogBody.razor | 29 ++++++++- .../Dialog/FluentDialogBody.razor.cs | 23 +++++++ .../Dialog/Services/DialogOptions.cs | 9 ++- .../Dialog/Services/DialogOptionsFooter.cs | 33 ++++++++++ .../Services/DialogOptionsFooterAction.cs | 43 +++++++++++++ .../Dialog/Services/DialogOptionsHeader.cs | 34 ++++++++++ src/Core/Components/Icons/CoreIcons.cs | 2 + 13 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor create mode 100644 src/Core/Components/Dialog/Services/DialogOptionsFooter.cs create mode 100644 src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs create mode 100644 src/Core/Components/Dialog/Services/DialogOptionsHeader.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor new file mode 100644 index 0000000000..5fccd3d72c --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor @@ -0,0 +1,43 @@ +@inject IDialogService DialogService + +Open Panel +Open Dialog + +
+
+ John: @John +
+ +@code +{ + private SimpleDialogCustomized.PersonDetails John = new() { Age = "20" }; + + private async Task OpenDialogAsync(DialogAlignment alignment) + { + var dialog = await DialogService.ShowDialogAsync(options => + { + options.Header.Title = "My title"; + options.Alignment = alignment; + options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; + + options.Parameters.Add(nameof(SimpleDialogCustomized.Name), "John"); // Simple type + options.Parameters.Add(nameof(SimpleDialogCustomized.Person), John); // Updatable object + + options.OnStateChange = (e) => + { + Console.WriteLine($"State changed: {e.State}"); + }; + }); + + var result = await dialog.Result; + + if (result.Cancelled) + { + Console.WriteLine($"Dialog Canceled: {result.Value}"); + } + else + { + Console.WriteLine($"Dialog Saved: {result.Value}"); + } + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 8c824ce6ae..bc9e7e3a8b 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -3,6 +3,7 @@ Open Panel Open Dialog +
John: @John @@ -16,9 +17,13 @@ { var dialog = await DialogService.ShowDialogAsync(options => { - options.Title = "My title"; options.Alignment = alignment; - options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; + //options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; + + options.Header.Title = "My title"; + + options.Footer.PrimaryAction.Label = "Save"; + options.Footer.SecondaryAction.Label = "Cancel"; options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 48324693cf..549d5ca993 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,20 +1,8 @@  - - - @Dialog?.Options.Title - - - - -
- @LongText -
- - - OK - Cancel - - + + Switch +
+ @LongText
@code { @@ -33,7 +21,7 @@ // A class is updatable [Parameter] - public PersonDetails Person { get; set; } = new(); + public PersonDetails Person { get; set; } = new(); // A nullable type is optional [Parameter] diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor new file mode 100644 index 0000000000..b4dd7abbf6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor @@ -0,0 +1,62 @@ + + + + @Dialog?.Options.Header.Title + + + + +
+ @LongText +
+ + + OK + Cancel + + +
+ +@code { + private static MarkupString LongText = GetVeryLongText(); + + // If you want to use this razor component in standalone mode, + // you can use a nullable IDialogInstance property. + // If the value is not null, the component is running using the DialogService. + // `public IDialogInstance? FluentDialog { get; set; }` + [CascadingParameter] + public required IDialogInstance Dialog { get; set; } + + // A simple type is not updatable + [Parameter] + public string? Name { get; set; } + + // A class is updatable + [Parameter] + public PersonDetails Person { get; set; } = new(); + + // A nullable type is optional + [Parameter] + public int? NotAssignedParam { get; set; } + + private async Task btnOK_Click() + { + await Dialog.CloseAsync(DialogResult.Ok("Yes")); + } + + private async Task btnCancel_Click() + { + await Dialog.CloseAsync(DialogResult.Cancel("No")); + } + + private static MarkupString GetVeryLongText() + { + return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + } + + public class PersonDetails + { + public string Age { get; set; } = ""; + public override string ToString() => $"Age: {Age}"; + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index 17e3321aaa..b92bc5f924 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -5,12 +5,38 @@ route: /Dialog # Dialog -{{ DialogBodyDefault }} +`FluentDialog` is a window overlaid on either the primary window or another dialog window. +Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. +Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, +attempts to interact with the inert content cause the dialog to close. + +## Best practices + +### Do + - Dialog boxes consist of a header (`TitleTemplate`), content (`ChildContent`), and footer (`ActionTemplate`), + which should all be included inside a body (`FluentDialogBody`). + - Validate that people’s entries are acceptable before closing the dialog. Show an inline validation error near the field they must correct. + - Modal dialogs should be used very sparingly—only when it's critical that people make a choice or provide information before they can proceed. + Thee dialogs are generally used for irreversible or potentially destructive tasks. They're typically paired with an backdrop without a light dismiss. -## Services +### Don't + - Don't use more than three buttons between Dialog'`ActionTemplate`. + - Don't open a `FluentDialog` from a `FluentDialog`. + - Don't use a `FluentDialog` with no focusable elements + +## Usage {{ DialogServiceDefault Files=Code:DialogServiceDefault.razor;SimpleDialog.razor:SimpleDialog.razor }} +## Customized + +{{ DialogServiceCustomized Files=Code:DialogServiceCustomized.razor;SimpleDialogCustomized.razor:SimpleDialogCustomized.razor }} + + +## Without the DialogService + +{{ DialogBodyDefault }} + ## API FluentDialogBody {{ API Type=FluentDialogBody }} diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index a06c37d535..fcc544cb95 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -176,12 +176,12 @@ public async Task HideAsync() // FluentDialog case false: // TODO: To find a way to catch the click event outside the dialog. - return "modal"; + return "alert"; // Panels case true: // TODO: To find a way to catch the click event outside the dialog. - return "modal"; + return "alert"; } } diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor b/src/Core/Components/Dialog/FluentDialogBody.razor index eb4c13a775..cee4796865 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor +++ b/src/Core/Components/Dialog/FluentDialogBody.razor @@ -1,26 +1,53 @@ @namespace Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Rendering @inherits FluentComponentBase + @* Header Title *@ @if (TitleTemplate is not null) {
@TitleTemplate
} + else if (!string.IsNullOrEmpty(Instance?.Options.Header.Title)) + { +
@((MarkupString)(Instance.Options.Header.Title))
+ } + @* Header Action *@ @if (TitleActionTemplate is not null) {
@TitleActionTemplate
} + @* Content *@ @if (ChildContent is not null) {
@ChildContent
} + @* Footer *@ @if (ActionTemplate is not null) {
@ActionTemplate
} - + else if (Instance?.Options.Footer.HasActions == true) + { +
+ + @foreach (var action in Instance.Options.Footer.Actions) + { + if (action.ToDisplay) + { + + @action.Label + + } + } + +
+ } +
diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.cs b/src/Core/Components/Dialog/FluentDialogBody.razor.cs index d2bc3f1aa7..a4ef7f67ab 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.cs @@ -21,6 +21,10 @@ public partial class FluentDialogBody : FluentComponentBase protected string? StyleValue => new StyleBuilder(Style) .Build(); + /// + [CascadingParameter] + private IDialogInstance? Instance { get; set; } + /// /// Gets or sets the content to be rendered inside the component. /// @@ -44,4 +48,23 @@ public partial class FluentDialogBody : FluentComponentBase /// [Parameter] public RenderFragment? ActionTemplate { get; set; } + + /// + private async Task ActionClickHandlerAsync(DialogOptionsFooterAction item) + { + if (item.Disabled || Instance is null) + { + return; + } + + if (item.OnClickAsync is not null ) + { + await item.OnClickAsync(Instance); + } + else + { + var result = item.Appearance == ButtonAppearance.Primary ? DialogResult.Ok() : DialogResult.Cancel(); + await Instance.CloseAsync(result); + } + } } diff --git a/src/Core/Components/Dialog/Services/DialogOptions.cs b/src/Core/Components/Dialog/Services/DialogOptions.cs index 1245dabdd1..07fe8e4a86 100644 --- a/src/Core/Components/Dialog/Services/DialogOptions.cs +++ b/src/Core/Components/Dialog/Services/DialogOptions.cs @@ -42,9 +42,14 @@ public DialogOptions(Action implementationFactory) public IReadOnlyDictionary? AdditionalAttributes { get; set; } /// - /// Gets or sets the title of the dialog. + /// Gets the header title of the dialog. /// - public string? Title { get; set; } + public DialogOptionsHeader Header { get; } = new(); + + /// + /// Gets the footer actions of the dialog. + /// + public DialogOptionsFooter Footer { get; } = new(); /// /// Gets or sets the dialog alignment. diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs new file mode 100644 index 0000000000..83ca4920a1 --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Options for configuring a dialog footer. +/// +public class DialogOptionsFooter +{ + /// + public DialogOptionsFooter() + { + + } + + /// + /// Gets or sets the primary action for the footer. + /// + public DialogOptionsFooterAction PrimaryAction { get; } = new(ButtonAppearance.Primary); + + /// + /// Gets or sets the secondary action for the footer. + /// + public DialogOptionsFooterAction SecondaryAction { get; } = new(ButtonAppearance.Default); + + /// + internal IEnumerable Actions => [SecondaryAction, PrimaryAction]; + + /// + internal bool HasActions => PrimaryAction.ToDisplay || SecondaryAction.ToDisplay; +} diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs new file mode 100644 index 0000000000..6cbb3f7df0 --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Options for configuring a dialog footer action button. +/// +public class DialogOptionsFooterAction +{ + /// + internal DialogOptionsFooterAction(ButtonAppearance appearance) + { + Appearance = appearance; + } + + /// + internal ButtonAppearance Appearance { get; } + + /// + /// Gets or sets the label of the action button. + /// + public string? Label { get; set; } + + /// + /// Gets or sets whether the action button is visible. + /// + public bool Visible { get; set; } = true; + + /// + /// Gets or sets whether the action button is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Gets or sets the action to be performed when the action button is clicked. + /// + public Func? OnClickAsync { get; set; } + + /// + internal bool ToDisplay => !string.IsNullOrEmpty(Label) && Visible; +} diff --git a/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs b/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs new file mode 100644 index 0000000000..853c8a5d8c --- /dev/null +++ b/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Options for configuring a dialog header. +/// +public class DialogOptionsHeader +{ + /// + internal DialogOptionsHeader() + { + + } + + /// + /// Gets or sets the title of the dialog. + /// + public string? Title { get; set; } + + /* + /// + /// Gets or sets whether the close button is visible. + /// + public bool DismissVisible { get; set; } = true; + + /// + /// Gets or sets the icon to use for the close button. + /// + public Icon DismissIcon { get; set; } = new CoreIcons.Regular.Size20.Dismiss(); + */ +} diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index 57e03a2ec2..c58a902f78 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -27,6 +27,8 @@ internal static partial class Regular internal static partial class Size20 { public class LineHorizontal3 : Icon { public LineHorizontal3() : base("LineHorizontal3", IconVariant.Regular, IconSize.Size20, "") { } }; + + public class Dismiss : Icon { public Dismiss() : base("Dismiss", IconVariant.Regular, IconSize.Size20, "") { } }; } } From 66dffb1a516a8fdbb48337d6c75b054a51cb2268 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 18:17:06 +0100 Subject: [PATCH 31/57] Add FluentDialogInstance --- .../Examples/DialogServiceDefault.razor | 6 --- .../Dialog/Examples/SimpleDialog.razor | 41 ++++++--------- .../Components/Dialog/FluentDialogInstance.cs | 51 +++++++++++++++++++ 3 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 src/Core/Components/Dialog/FluentDialogInstance.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index bc9e7e3a8b..f42dac4dc5 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -18,12 +18,6 @@ var dialog = await DialogService.ShowDialogAsync(options => { options.Alignment = alignment; - //options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; - - options.Header.Title = "My title"; - - options.Footer.PrimaryAction.Label = "Save"; - options.Footer.SecondaryAction.Label = "Cancel"; options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index 549d5ca993..d0ec09aeda 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,20 +1,10 @@ - +@inherits FluentDialogInstance + + - Switch -
- @LongText
@code { - private static MarkupString LongText = GetVeryLongText(); - - // If you want to use this razor component in standalone mode, - // you can use a nullable IDialogInstance property. - // If the value is not null, the component is running using the DialogService. - // `public IDialogInstance? FluentDialog { get; set; }` - [CascadingParameter] - public required IDialogInstance Dialog { get; set; } - // A simple type is not updatable [Parameter] public string? Name { get; set; } @@ -23,23 +13,22 @@ [Parameter] public PersonDetails Person { get; set; } = new(); - // A nullable type is optional - [Parameter] - public int? NotAssignedParam { get; set; } - - private async Task btnOK_Click() - { - await Dialog.CloseAsync(DialogResult.Ok("Yes")); - } - - private async Task btnCancel_Click() + protected override void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) { - await Dialog.CloseAsync(DialogResult.Cancel("No")); + header.Title = $"Hello {Name}"; + footer.SecondaryAction.Visible = false; } - private static MarkupString GetVeryLongText() + protected override Task OnActionClickedAsync(bool primary) { - return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + if (primary) + { + return Dialog.CloseAsync(DialogResult.Ok("Yes")); + } + else + { + return Dialog.CloseAsync(DialogResult.Cancel("No")); + } } public class PersonDetails diff --git a/src/Core/Components/Dialog/FluentDialogInstance.cs b/src/Core/Components/Dialog/FluentDialogInstance.cs new file mode 100644 index 0000000000..4ba2b80d2e --- /dev/null +++ b/src/Core/Components/Dialog/FluentDialogInstance.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// +/// +public abstract class FluentDialogInstance : ComponentBase +{ + /// + /// + /// + [CascadingParameter] + public required IDialogInstance Dialog { get; set; } + + /// + /// + /// + /// + /// + protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) + { + + } + + /// + /// + /// + /// + protected override Task OnInitializedAsync() + { + Dialog.Options.Footer.PrimaryAction.Label = "OK"; + Dialog.Options.Footer.PrimaryAction.OnClickAsync = (e) => OnActionClickedAsync(primary: true); + Dialog.Options.Footer.SecondaryAction.Label = "Cancel"; + Dialog.Options.Footer.SecondaryAction.OnClickAsync = (e) => OnActionClickedAsync(primary: false); + + OnInitializeDialog(Dialog.Options.Header, Dialog.Options.Footer); + + return base.OnInitializedAsync(); + } + + /// + /// + /// + /// + protected abstract Task OnActionClickedAsync(bool primary); +} From 1e885229b7cf6fcebce743154a6b9fd80ae29d5e Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 18:28:39 +0100 Subject: [PATCH 32/57] Fix default values --- .../Dialog/Examples/DialogServiceDefault.razor | 1 - .../Components/Dialog/Examples/SimpleDialog.razor | 12 +++++++----- .../Dialog/Examples/SimpleDialogCustomized.razor | 2 +- src/Core/Components/Dialog/FluentDialogInstance.cs | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index f42dac4dc5..3f9a01ee96 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -3,7 +3,6 @@ Open Panel Open Dialog -
John: @John diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor index d0ec09aeda..d0456080a1 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor @@ -1,7 +1,7 @@ @inherits FluentDialogInstance - + @code { @@ -13,21 +13,23 @@ [Parameter] public PersonDetails Person { get; set; } = new(); + // Initialize the dialog protected override void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) { header.Title = $"Hello {Name}"; - footer.SecondaryAction.Visible = false; + footer.SecondaryAction.Visible = true; } - protected override Task OnActionClickedAsync(bool primary) + // Handle the action click + protected override async Task OnActionClickedAsync(bool primary) { if (primary) { - return Dialog.CloseAsync(DialogResult.Ok("Yes")); + await Dialog.CloseAsync(DialogResult.Ok("Yes")); } else { - return Dialog.CloseAsync(DialogResult.Cancel("No")); + await Dialog.CloseAsync(DialogResult.Cancel("No")); } } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor index b4dd7abbf6..e918869751 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor @@ -11,8 +11,8 @@ - OK Cancel + OK diff --git a/src/Core/Components/Dialog/FluentDialogInstance.cs b/src/Core/Components/Dialog/FluentDialogInstance.cs index 4ba2b80d2e..8c20cfd3bb 100644 --- a/src/Core/Components/Dialog/FluentDialogInstance.cs +++ b/src/Core/Components/Dialog/FluentDialogInstance.cs @@ -24,7 +24,6 @@ public abstract class FluentDialogInstance : ComponentBase /// protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) { - } /// @@ -33,10 +32,11 @@ protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOpti /// protected override Task OnInitializedAsync() { - Dialog.Options.Footer.PrimaryAction.Label = "OK"; - Dialog.Options.Footer.PrimaryAction.OnClickAsync = (e) => OnActionClickedAsync(primary: true); - Dialog.Options.Footer.SecondaryAction.Label = "Cancel"; - Dialog.Options.Footer.SecondaryAction.OnClickAsync = (e) => OnActionClickedAsync(primary: false); + var footer = Dialog.Options.Footer; + footer.PrimaryAction.Label ??= "OK"; + footer.PrimaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: true); + footer.SecondaryAction.Label ??= "Cancel"; + footer.SecondaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: false); OnInitializeDialog(Dialog.Options.Header, Dialog.Options.Footer); From be779e7e32afdde164ad46a68804e7d49b9d0a52 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 22:28:18 +0100 Subject: [PATCH 33/57] Add documentation --- .../CustomizedDialog.razor} | 20 +- .../Dialog/Examples/Dialog/PersonDetails.cs | 17 ++ .../Examples/{ => Dialog}/SimpleDialog.razor | 17 +- .../Examples/DialogServiceCustomized.razor | 10 +- .../Examples/DialogServiceDefault.razor | 10 +- .../Components/Dialog/FluentDialog.md | 182 +++++++++++++++++- .../Components/Dialog/FluentDialogInstance.cs | 6 +- .../Dialog/Services/DialogInstance.cs | 14 +- .../Dialog/Services/DialogResult.cs | 38 +++- .../Dialog/Services/IDialogInstance.cs | 13 ++ 10 files changed, 280 insertions(+), 47 deletions(-) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/{SimpleDialogCustomized.razor => Dialog/CustomizedDialog.razor} (71%) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/{ => Dialog}/SimpleDialog.razor (61%) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor similarity index 71% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor index e918869751..c9926a29d7 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialogCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor @@ -5,9 +5,10 @@ - -
- @LongText + + + Description + @Person.VeryLongDescription
@@ -18,8 +19,6 @@ @code { - private static MarkupString LongText = GetVeryLongText(); - // If you want to use this razor component in standalone mode, // you can use a nullable IDialogInstance property. // If the value is not null, the component is running using the DialogService. @@ -48,15 +47,4 @@ { await Dialog.CloseAsync(DialogResult.Cancel("No")); } - - private static MarkupString GetVeryLongText() - { - return (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); - } - - public class PersonDetails - { - public string Age { get; set; } = ""; - public override string ToString() => $"Age: {Age}"; - } } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs new file mode 100644 index 0000000000..f074f48052 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; + +namespace FluentUI.Demo.Client.Documentation.Components.Dialog.Examples.Dialog; + +public class PersonDetails +{ + public string Age { get; set; } = ""; + + public MarkupString VeryLongDescription + => (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + + public override string ToString() => $"Age: {Age}"; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/SimpleDialog.razor similarity index 61% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/SimpleDialog.razor index d0456080a1..74e5b647ef 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/SimpleDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/SimpleDialog.razor @@ -1,7 +1,8 @@ @inherits FluentDialogInstance - + + @code { @@ -16,7 +17,7 @@ // Initialize the dialog protected override void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) { - header.Title = $"Hello {Name}"; + header.Title = $"Dialog Title - {Name}"; footer.SecondaryAction.Visible = true; } @@ -25,17 +26,11 @@ { if (primary) { - await Dialog.CloseAsync(DialogResult.Ok("Yes")); + await DialogInstance.CloseAsync("Yes"); } else - { - await Dialog.CloseAsync(DialogResult.Cancel("No")); + { + await DialogInstance.CancelAsync(); } } - - public class PersonDetails - { - public string Age { get; set; } = ""; - public override string ToString() => $"Age: {Age}"; - } } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor index 5fccd3d72c..508bbb12f2 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor @@ -10,18 +10,18 @@ @code { - private SimpleDialogCustomized.PersonDetails John = new() { Age = "20" }; + private Dialog.PersonDetails John = new() { Age = "20" }; private async Task OpenDialogAsync(DialogAlignment alignment) { - var dialog = await DialogService.ShowDialogAsync(options => + var dialog = await DialogService.ShowDialogAsync(options => { - options.Header.Title = "My title"; + options.Header.Title = "Dialog Title"; options.Alignment = alignment; options.Style = alignment == DialogAlignment.Default ? "max-height: 400px;" : null; - options.Parameters.Add(nameof(SimpleDialogCustomized.Name), "John"); // Simple type - options.Parameters.Add(nameof(SimpleDialogCustomized.Person), John); // Updatable object + options.Parameters.Add(nameof(Dialog.CustomizedDialog.Name), "John"); // Simple type + options.Parameters.Add(nameof(Dialog.CustomizedDialog.Person), John); // Updatable object options.OnStateChange = (e) => { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 3f9a01ee96..0af5a6176c 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -10,23 +10,23 @@ @code { - private SimpleDialog.PersonDetails John = new() { Age = "20" }; + private Dialog.PersonDetails John = new() { Age = "20" }; private async Task OpenDialogAsync(DialogAlignment alignment) { - var dialog = await DialogService.ShowDialogAsync(options => + var dialog = await DialogService.ShowDialogAsync(options => { options.Alignment = alignment; - options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type - options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object + options.Parameters.Add(nameof(Dialog.SimpleDialog.Name), "John"); // Simple type + options.Parameters.Add(nameof(Dialog.SimpleDialog.Person), John); // Updatable object options.OnStateChange = (e) => { Console.WriteLine($"State changed: {e.State}"); }; }); - + var result = await dialog.Result; if (result.Cancelled) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index b92bc5f924..7908d7fad0 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -26,17 +26,195 @@ attempts to interact with the inert content cause the dialog to close. ## Usage -{{ DialogServiceDefault Files=Code:DialogServiceDefault.razor;SimpleDialog.razor:SimpleDialog.razor }} +The simplest way is to use the DialogService to display a dialog box. +By injecting this service, you have `ShowDialogAsync` methods at your disposal. +You can pass the **type of Dialog** to be displayed and the **options** to be passed to that window. + +Once the user closes the dialog window, the `ShowDialogAsync` method returns a `DialogResult` object containing the return data. + +Any Blazor component can be used as a dialog type. + +```csharp +@inject IDialogService DialogService + +var dialog = await DialogService.ShowDialogAsync(options => +{ + // Options +}); + +var result = await dialog.Result; + +if (result.Cancelled == false) +{ + ... +} +``` + +The **SimpleDialog** window can inherit from the `FluentDialogInstance` class to have access to the `DialogInstance` property, which contains +all parameters defined when calling `ShowDialogAsync`, as well as the `CloseAsync` and `CancelAsync` methods. +Using `CloseAsync` you can pass a return object to the parent component. +It is then retrieved in the `DialogResult` like in the example below. + +```xml +@inherits FluentDialogInstance + + + Content dialog + +``` + +> **Note:** The `FluentDialogBody` component is required to display the dialog window correctly. + +By default, the `FluentDialogInstance` class will offer two buttons, one to validate and one to cancel (**OK** and **Cancel**). +You can override the actions of these buttons by overriding the `OnActionClickedAsync` method. + +{{ DialogServiceDefault Files=Code:DialogServiceDefault.razor;SimpleDialog:SimpleDialog.razor;PersonDetails:PersonDetails.cs }} ## Customized -{{ DialogServiceCustomized Files=Code:DialogServiceCustomized.razor;SimpleDialogCustomized.razor:SimpleDialogCustomized.razor }} +The previous `FluentDialogInstance` object is optional. You can create your own custom dialog box in two different ways. + +🔹Using the `ShowDialogAsync` method options to pass parameters to your dialog box. + Several **options** can be used to customize the dialog box, such as styles, title, button text, etc. + +```csharp +var dialog = await DialogService.ShowDialogAsync(options => +{ + options.Header.Title = "Dialog Title"; + options.Alignment = DialogAlignment.Default; + options.Style = "max-height: 400px;"; + options.Parameters.Add("Name", "John"); // Simple data will send to the dialog "Name" property. +}); +``` + +🔹Using the `FluentDialogBody` component to create a custom dialog box. + This component allows you to create a dialog box with: + - `TitleTemplate`: Title of the dialog box. + - `TitleActionTemplate`: (optional) Action to be performed on the title dismiss icon. + - `ChildContent`: Content of the dialog box. + - `ActionsTemplate`: Footer of the dialog box, containing close and cancel buttons. + + To have a reference to the `DialogInstance` object, you must use the `CascadingParameter` attribute. + This object allows you to retrieve the dialog options and to close the dialog box and pass a return object to the parent component. + +```xml + + + + @Dialog?.Options.Header.Title + + + + Content dialog + + + + Cancel + OK + + + + +@code +{ + [CascadingParameter] + public required IDialogInstance Dialog { get; set; } +} +``` + +{{ DialogServiceCustomized Files=Code:DialogServiceCustomized.razor;CustomizedDialog:CustomizedDialog.razor;PersonDetails:PersonDetails.cs }} +## Data exchange between components + +🔹You can easily send data from your main component to the dialog box +by using the `options.Parameters.Add()` method in the `ShowDialogAsync` method options. + +`Parameters` is a dictionary that allows you to send any type of data to the dialog box. +the **key is the name of the property** in the dialog box, and the **value** is the data to be sent. + +**Main component** +```csharp +PersonDetails John = new() { Age = "20" }; + +var dialog = await DialogService.ShowDialogAsync(options => +{ + options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type + options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object +}); +``` + +> **Note:** The `nameof` operator is used to avoid typing errors when passing parameters. +> The key must match the name of the property in the dialog box. +> If the key does not match, an exception will be thrown. + +**SimpleDialog** +```csharp + + ... + + +@code { + // A simple type is not updatable + [Parameter] + public string? Name { get; set; } + + // A class is updatable + [Parameter] + public PersonDetails Person { get; set; } = new(); +} +``` + +In .NET, simple types are passed by value, while objects are passed by reference. +So if you modify an object in the dialog box, the changes will also be visible in the main component. +This is not the case for simple types. + +🔹You can also send data from the dialog box to the main component by using the `CloseAsync` method. +This method allows you to pass a return object to the parent component. + +**Dialog box** +```csharp +protected override async Task OnActionClickedAsync(bool primary) +{ + if (primary) + { + await DialogInstance.CloseAsync("You clicked on the OK button"); + } + else + { + await DialogInstance.CancelAsync(); + } +} +``` + +In the main component, you can retrieve the return object from the `DialogResult` object. +And you can check if the dialog box was closed by clicking on the **OK** or **Cancel** button, using the `Cancelled` property. + +> **Note:** The `CancelAsync` method can not pass a return object to the parent component. +> But the `CloseAsync` method can pass a"Canceled" result object to the parent component. +> `await DialogInstance.CloseAsync(DialogResult.Cancel("You clicked on the Cancel button"));` ## Without the DialogService +You can also use the `FluentDialogBody` component directly in your component, without using the `DialogService` and the `FluentDialog` component. + {{ DialogBodyDefault }} +## API DialogService + +{{ API Type=DialogService }} + +## API DialogInstance + +{{ API Type=DialogInstance }} + +## API DialogResult + +{{ API Type=DialogResult }} + ## API FluentDialogBody {{ API Type=FluentDialogBody }} + +## API FluentDialog + +{{ API Type=FluentDialog }} diff --git a/src/Core/Components/Dialog/FluentDialogInstance.cs b/src/Core/Components/Dialog/FluentDialogInstance.cs index 8c20cfd3bb..9250056ec5 100644 --- a/src/Core/Components/Dialog/FluentDialogInstance.cs +++ b/src/Core/Components/Dialog/FluentDialogInstance.cs @@ -15,7 +15,7 @@ public abstract class FluentDialogInstance : ComponentBase /// ///
[CascadingParameter] - public required IDialogInstance Dialog { get; set; } + public required IDialogInstance DialogInstance { get; set; } /// /// @@ -32,13 +32,13 @@ protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOpti /// protected override Task OnInitializedAsync() { - var footer = Dialog.Options.Footer; + var footer = DialogInstance.Options.Footer; footer.PrimaryAction.Label ??= "OK"; footer.PrimaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: true); footer.SecondaryAction.Label ??= "Cancel"; footer.SecondaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: false); - OnInitializeDialog(Dialog.Options.Header, Dialog.Options.Footer); + OnInitializeDialog(DialogInstance.Options.Header, DialogInstance.Options.Footer); return base.OnInitializedAsync(); } diff --git a/src/Core/Components/Dialog/Services/DialogInstance.cs b/src/Core/Components/Dialog/Services/DialogInstance.cs index fe91ac161c..a0913a2166 100644 --- a/src/Core/Components/Dialog/Services/DialogInstance.cs +++ b/src/Core/Components/Dialog/Services/DialogInstance.cs @@ -41,10 +41,22 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// " public string Id { get; } + /// + public Task CancelAsync() + { + return DialogService.CloseAsync(this, DialogResult.Cancel()); + } + /// public Task CloseAsync() { - return DialogService.CloseAsync(this, DialogResult.Cancel()); + return DialogService.CloseAsync(this, DialogResult.Ok()); + } + + /// + public Task CloseAsync(T result) + { + return DialogService.CloseAsync(this, DialogResult.Ok(result)); } /// diff --git a/src/Core/Components/Dialog/Services/DialogResult.cs b/src/Core/Components/Dialog/Services/DialogResult.cs index 6452530bb0..e1675a0e68 100644 --- a/src/Core/Components/Dialog/Services/DialogResult.cs +++ b/src/Core/Components/Dialog/Services/DialogResult.cs @@ -7,14 +7,14 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// /// Represents the result of a dialog. /// -public class DialogResult +public class DialogResult { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// - protected internal DialogResult(object? content, bool cancelled) + protected internal DialogResult(TContent? content, bool cancelled) { Value = content; Cancelled = cancelled; @@ -23,7 +23,7 @@ protected internal DialogResult(object? content, bool cancelled) /// /// Gets the content of the dialog result. /// - public object? Value { get; } + public TContent? Value { get; } /// /// Gets a value indicating whether the dialog was cancelled. @@ -57,3 +57,33 @@ protected internal DialogResult(object? content, bool cancelled) /// The dialog result. public static DialogResult Cancel() => Cancel(result: null); } + +/// +/// Represents the result of a dialog. +/// +public class DialogResult : DialogResult +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + protected internal DialogResult(object? content, bool cancelled) : base(content, cancelled) + { + } + + /// + /// Gets the content of the dialog result. + /// + /// + /// + public T GetValue() + { + if (Value is T variable) + { + return variable; + } + + return default(T)!; + } +} diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index ceb49b4ef0..4a52eaaab5 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -34,6 +34,12 @@ public interface IDialogInstance /// Closes the dialog with a Cancel result. /// /// + Task CancelAsync(); + + /// + /// Closes the dialog with the specified result. + /// + /// Task CloseAsync(); /// @@ -42,4 +48,11 @@ public interface IDialogInstance /// Result to close the dialog with. /// Task CloseAsync(DialogResult result); + + /// + /// Closes the dialog with the specified result. + /// + /// Result to close the dialog with. + /// + Task CloseAsync(T result); } From 847d27e200ba4bfbd907590db67d8c2980012bd5 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 22:38:26 +0100 Subject: [PATCH 34/57] Fix doc --- .../Documentation/Components/Dialog/FluentDialog.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index 7908d7fad0..f216bfee4f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -203,14 +203,6 @@ You can also use the `FluentDialogBody` component directly in your component, wi {{ API Type=DialogService }} -## API DialogInstance - -{{ API Type=DialogInstance }} - -## API DialogResult - -{{ API Type=DialogResult }} - ## API FluentDialogBody {{ API Type=FluentDialogBody }} From e9ff699c29e633f4c0b651676c61104c57a5a6f5 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 23 Dec 2024 22:46:13 +0100 Subject: [PATCH 35/57] Fix Samples --- .../Dialog/Examples/DialogServiceCustomized.razor | 4 ++-- .../Components/Dialog/Examples/DialogServiceDefault.razor | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor index 508bbb12f2..62e55ef4f7 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor @@ -1,7 +1,7 @@ @inject IDialogService DialogService -Open Panel -Open Dialog +Open Panel +Open Dialog
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 0af5a6176c..4106119370 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -1,7 +1,6 @@ @inject IDialogService DialogService -Open Panel -Open Dialog +Open Dialog
@@ -12,12 +11,10 @@ { private Dialog.PersonDetails John = new() { Age = "20" }; - private async Task OpenDialogAsync(DialogAlignment alignment) + private async Task OpenDialogAsync() { var dialog = await DialogService.ShowDialogAsync(options => { - options.Alignment = alignment; - options.Parameters.Add(nameof(Dialog.SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(Dialog.SimpleDialog.Person), John); // Updatable object From 5b79df7d7a959389233dcc0cb2618392af3e836d Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 24 Dec 2024 11:45:32 +0100 Subject: [PATCH 36/57] Add MessageBox --- .editorconfig | 3 + .../Dialog/Examples/DialogMessageBox.razor | 79 +++++++++++++++ .../Components/Dialog/FluentMessageBox.md | 31 ++++++ src/Core/Components/Base/FluentJSModule.cs | 6 +- .../Dialog/MessageBox/DialogService.cs | 96 +++++++++++++++++++ .../Dialog/MessageBox/FluentMessageBox.razor | 13 +++ .../MessageBox/FluentMessageBox.razor.cs | 31 ++++++ .../MessageBox/FluentMessageBox.razor.css | 16 ++++ .../Dialog/MessageBox/IDialogService.cs | 63 ++++++++++++ .../Dialog/MessageBox/MessageBoxOptions.cs | 41 ++++++++ .../Dialog/Services/DialogService.cs | 2 - src/Core/Components/Icons/CoreIcons.cs | 9 ++ 12 files changed, 383 insertions(+), 7 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md create mode 100644 src/Core/Components/Dialog/MessageBox/DialogService.cs create mode 100644 src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor create mode 100644 src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.cs create mode 100644 src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.css create mode 100644 src/Core/Components/Dialog/MessageBox/IDialogService.cs create mode 100644 src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs diff --git a/.editorconfig b/.editorconfig index 25189e6245..cdbf76b26b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -387,6 +387,9 @@ dotnet_diagnostic.IDE2005.severity = warning # MA0001: StringComparison is missing dotnet_diagnostic.MA0001.severity = warning +# MA0004: Use ConfigureAwait when awaiting a task +dotnet_diagnostic.MA0004.severity = none + # MA0015: Do not call overridable members in constructor dotnet_diagnostic.MA0056.severity = none diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor new file mode 100644 index 0000000000..805b794866 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor @@ -0,0 +1,79 @@ +@inject IDialogService DialogService + +Success +Warning +Error +Information +Confirmation +Long message +Custom message + +

+ Last result: @GetResult() +

+ +@code +{ + private bool? Canceled; + + private async Task ShowSuccessAsync() + { + var result = await DialogService.ShowSuccessAsync("The action was a success"); + Canceled = result.Cancelled; + } + + private async Task ShowWarningAsync() + { + var result = await DialogService.ShowWarningAsync("This is your final warning"); + Canceled = result.Cancelled; + } + + private async Task ShowErrorAsync() + { + var result = await DialogService.ShowErrorAsync("This is an error"); + Canceled = result.Cancelled; + } + + private async Task ShowInformationAsync() + { + var result = await DialogService.ShowInfoAsync("This is a message"); + Canceled = result.Cancelled; + } + + private async Task ShowConfirmationAsync() + { + var result = await DialogService.ShowConfirmationAsync("Are you sure you want to delete this item?
This will also remove any linked items"); + Canceled = result.Cancelled; + } + + private async Task ShowMessageBoxLongAsync() + { + var result = await DialogService.ShowInfoAsync(SampleData.Text.GenerateLoremIpsum(1)); + Canceled = result.Cancelled; + } + + private async Task ShowMessageBoxAsync() + { + var result = await DialogService.ShowMessageBoxAsync(new MessageBoxOptions() + { + Title = "My title", + Message = "My customized message", + Icon = new Icons.Regular.Size24.Games(), + IconColor = Color.Success, + PrimaryAction = "Plus", + SecondaryAction = "Minus", + }); + + Canceled = result.Cancelled; + } + + private string GetResult() + { + return Canceled switch + { + true => "❌ Canceled", + false => "✅ OK", + _ => "" + }; + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md new file mode 100644 index 0000000000..88d5f5aa0d --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md @@ -0,0 +1,31 @@ +--- +title: MessageBox +route: /MessageBox +--- + +# MessageBox + +A **MessageBox** is a dialog that is used to display information with a specific intent to the user. +It uses the `DialogService` to display the dialog. + +The `DialogService` is an injected service that can be used into any component. +It exposes methods to show a dialog. For working with a **MessageBox**, the following methods are available: + +- ShowSuccessAsync +- ShowWarningAsync +- ShowInfoAsync +- ShowErrorAsync +- ShowConfirmationAsync +- ShowMessageBoxAsync + +The **MessageBox** is displayed as a modal dialog box. This means that the user has to click one of the buttons to close the dialog. +It cannot be closed by clicking outside of the dialog. + +Clicking **Primary** button (OK) will return `true` and clicking **Secondary** button (Cancel) will return `false` as the dialog result. +See the Console log for these return values. + +Internally, the `ShowMessageBox` methods call the `ShowDialog` methods. The ShowMessageBox variants are just convenience methods that make ite easier to work. + +## Example + +{{ DialogMessageBox }} \ No newline at end of file diff --git a/src/Core/Components/Base/FluentJSModule.cs b/src/Core/Components/Base/FluentJSModule.cs index 9d451dc7c1..bf97f0d363 100644 --- a/src/Core/Components/Base/FluentJSModule.cs +++ b/src/Core/Components/Base/FluentJSModule.cs @@ -7,8 +7,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components; -#pragma warning disable MA0004 // Use Task.ConfigureAwait - /// /// Base class to manage the JavaScript function from the FluentUI Blazor components. /// @@ -91,6 +89,4 @@ internal virtual ValueTask DisposeAsync(IJSObjectReference? jsModule) { return ValueTask.CompletedTask; } -} - -#pragma warning restore MA0004 // Use Task.ConfigureAwait +} \ No newline at end of file diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs new file mode 100644 index 0000000000..bb7bf58ef2 --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Dialog.MessageBox; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public partial class DialogService : IDialogService +{ + /// + public Task ShowSuccessAsync(string message, string? title = null, string? primaryAction = null) + { + return ShowMessageBoxAsync(new MessageBoxOptions + { + Title = title ?? "Success", + Message = message, + PrimaryAction = primaryAction ?? "OK", + Icon = new CoreIcons.Filled.Size20.CheckmarkCircle(), + IconColor = Color.Success, + }); + } + + /// + public Task ShowWarningAsync(string message, string? title = null, string? primaryAction = null) + { + return ShowMessageBoxAsync(new MessageBoxOptions + { + Title = title ?? "Warning", + Message = message, + PrimaryAction = primaryAction ?? "OK", + Icon = new CoreIcons.Filled.Size20.Warning(), + IconColor = Color.Warning, + }); + } + + /// + public Task ShowErrorAsync(string message, string? title = null, string? primaryAction = null) + { + return ShowMessageBoxAsync(new MessageBoxOptions + { + Title = title ?? "Error", + Message = message, + PrimaryAction = primaryAction ?? "OK", + Icon = new CoreIcons.Filled.Size20.DismissCircle(), + IconColor = Color.Error, + }); + } + + /// + public Task ShowInfoAsync(string message, string? title = null, string? primaryAction = null) + { + return ShowMessageBoxAsync(new MessageBoxOptions + { + Title = title ?? "Information", + Message = message, + PrimaryAction = primaryAction ?? "OK", + Icon = new CoreIcons.Filled.Size20.Info(), + IconColor = Color.Info, + }); + } + + /// + public Task ShowConfirmationAsync(string message, string? title = null, string? primaryAction = null, string? secondaryAction = null) + { + return ShowMessageBoxAsync(new MessageBoxOptions + { + Title = title ?? "Confirmation", + Message = message, + PrimaryAction = primaryAction ?? "Yes", + SecondaryAction = secondaryAction ?? "No", + Icon = new CoreIcons.Regular.Size20.QuestionCircle(), + IconColor = Color.Default, + }); + } + + /// /> + public async Task ShowMessageBoxAsync(MessageBoxOptions options) + { + var dialog = await ShowDialogAsync(config => + { + config.Header.Title = options.Title; + config.Footer.PrimaryAction.Label = options.PrimaryAction; + config.Footer.SecondaryAction.Label = options.SecondaryAction; + config.Parameters.Add(nameof(FluentMessageBox.Message), new MarkupString(options.Message ?? "")); + config.Parameters.Add(nameof(FluentMessageBox.Icon), options.Icon); + config.Parameters.Add(nameof(FluentMessageBox.IconColor), options.IconColor); + }); + + var result = await dialog.Result; + + return result; + } +} diff --git a/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor new file mode 100644 index 0000000000..c77dd4bdfc --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor @@ -0,0 +1,13 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components.Dialog.MessageBox + + +
+ @if (Icon != null) + { + + } +
+ @Message +
+
+
diff --git a/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.cs b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.cs new file mode 100644 index 0000000000..c4bd5f65f7 --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; + +namespace Microsoft.FluentUI.AspNetCore.Components.Dialog.MessageBox; + +/// +/// Represents a message box dialog. +/// +public partial class FluentMessageBox +{ + /// + /// Gets or sets the content of the message box. + /// + [Parameter] + public MarkupString? Message { get; set; } + + /// + /// Gets or sets the icon of the message box. + /// + [Parameter] + public Icon Icon { get; set; } = new CoreIcons.Filled.Size20.CheckmarkCircle(); + + /// + /// Gets or sets the icon color. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Success; +} diff --git a/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.css b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.css new file mode 100644 index 0000000000..f3372fbe28 --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/FluentMessageBox.razor.css @@ -0,0 +1,16 @@ +.fluent-message-box { + display: flex; + margin-top: 10px; + justify-content: start; + align-items: center; + column-gap: 10px; + width: 100%; +} + + .fluent-message-box .icon { + min-width: 32px; + } + + .fluent-message-box .message { + + } diff --git a/src/Core/Components/Dialog/MessageBox/IDialogService.cs b/src/Core/Components/Dialog/MessageBox/IDialogService.cs new file mode 100644 index 0000000000..ef874e7cd4 --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/IDialogService.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +public partial interface IDialogService +{ + + /// + /// Shows a dialog with a success (green) icon, a message and an OK button. + /// + /// Message to display in the dialog. + /// Title to display in the dialog header. + /// Text to display in the primary action button. + /// Result of the dialog. Always `Cancelled = false`. + Task ShowSuccessAsync(string message, string? title = null, string? primaryAction = null); + + /// + /// Shows a dialog with a warning (orange) icon, a message and an OK button. + /// + /// Message to display in the dialog. + /// Title to display in the dialog header. Default is "Success". + /// Text to display in the primary action button. Default is "OK". + /// Result of the dialog. Always `Cancelled = false`. + Task ShowWarningAsync(string message, string? title = null, string? primaryAction = null); + + /// + /// Shows a dialog with an error (red) icon, a message and an OK button. + /// + /// Message to display in the dialog. + /// Title to display in the dialog header. Default is "Error". + /// Text to display in the primary action button. Default is "OK". + /// Result of the dialog. Always `Cancelled = false`. + Task ShowErrorAsync(string message, string? title = null, string? primaryAction = null); + + /// + /// Shows a dialog with an information (gray) icon, a message and an OK button. + /// + /// Message to display in the dialog. + /// Title to display in the dialog header. Default is "Information". + /// Text to display in the primary action button. Default is "OK". + /// Result of the dialog. Always `Cancelled = false`. + Task ShowInfoAsync(string message, string? title = null, string? primaryAction = null); + + /// + /// Shows a dialog with a confirmation icon, a message and a Yes/No buttons. + /// + /// Message to display in the dialog. + /// Title to display in the dialog header. Default is "Confirmation". + /// Text to display in the primary action button. Default is "Yes". + /// Text to display in the secondary action button. Default is "No". + /// Result of the dialog: Yes returns `Cancelled = false`, No returns `Cancelled = true` + Task ShowConfirmationAsync(string message, string? title = null, string? primaryAction = null, string? secondaryAction = null); + + /// + /// Shows a dialog with the specified options. + /// + /// Options to configure the dialog. + /// + Task ShowMessageBoxAsync(MessageBoxOptions options); +} diff --git a/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs new file mode 100644 index 0000000000..01219ffed8 --- /dev/null +++ b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Options for the message box dialog. +/// +public class MessageBoxOptions +{ + /// + /// Gets or sets the message to display in the dialog. + /// + public string? Message { get; set; } + + /// + /// Gets or sets the title to display in the dialog header. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the text to display in the primary action button. + /// + public string? PrimaryAction { get; set; } + + /// + /// Gets or sets the text to display in the secondary action button. + /// + public string? SecondaryAction { get; set; } + + /// + /// Gets or sets the icon to display in the dialog. + /// + public Icon? Icon { get; set; } + + /// + /// Gets or sets the color of the icon. + /// + public Color IconColor { get; set; } = Color.Default; +} diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index b04024535f..a04fcd0bee 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -23,7 +23,6 @@ public DialogService(IServiceProvider serviceProvider) } /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "")] public async Task CloseAsync(IDialogInstance dialog, DialogResult result) { var dialogInstance = dialog as DialogInstance; @@ -42,7 +41,6 @@ public async Task CloseAsync(IDialogInstance dialog, DialogResult result) } /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MA0004:Use Task.ConfigureAwait", Justification = "TODO")] public virtual async Task ShowDialogAsync(Type componentType, DialogOptions options) { if (!componentType.IsSubclassOf(typeof(ComponentBase))) diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index c58a902f78..2fc651a871 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -28,6 +28,8 @@ internal static partial class Size20 { public class LineHorizontal3 : Icon { public LineHorizontal3() : base("LineHorizontal3", IconVariant.Regular, IconSize.Size20, "") { } }; + public class QuestionCircle : Icon { public QuestionCircle() : base("QuestionCircle", IconVariant.Regular, IconSize.Size20, "") { } }; + public class Dismiss : Icon { public Dismiss() : base("Dismiss", IconVariant.Regular, IconSize.Size20, "") { } }; } } @@ -40,6 +42,13 @@ internal static partial class Filled [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class Size20 { + public class CheckmarkCircle : Icon { public CheckmarkCircle() : base("CheckmarkCircle", IconVariant.Filled, IconSize.Size20, "") { } } + + public class Info : Icon { public Info() : base("Info", IconVariant.Filled, IconSize.Size20, "") { } } + + public class Warning : Icon { public Warning() : base("Warning", IconVariant.Filled, IconSize.Size20, "") { } } + + public class DismissCircle : Icon { public DismissCircle() : base("WaDismissCirclerning", IconVariant.Filled, IconSize.Size20, "") { } } } } } From c68f1c4582066328906eeb4eba0ce91ca43dd0e4 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 24 Dec 2024 11:56:32 +0100 Subject: [PATCH 37/57] Refactoring --- .../Dialog/Examples/DialogMessageBox.razor | 6 ++-- .../Components/Dialog/FluentMessageBox.md | 30 ++++++++++++++----- .../Dialog/MessageBox/DialogService.cs | 26 ++++++++-------- .../Dialog/MessageBox/IDialogService.cs | 18 +++++------ .../Dialog/MessageBox/MessageBoxOptions.cs | 4 +-- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor index 805b794866..d221c63cf1 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogMessageBox.razor @@ -18,7 +18,7 @@ private async Task ShowSuccessAsync() { - var result = await DialogService.ShowSuccessAsync("The action was a success"); + var result = await DialogService.ShowSuccessAsync("The action was a success"); Canceled = result.Cancelled; } @@ -60,8 +60,8 @@ Message = "My customized message", Icon = new Icons.Regular.Size24.Games(), IconColor = Color.Success, - PrimaryAction = "Plus", - SecondaryAction = "Minus", + PrimaryButton = "Plus", + SecondaryButton = "Minus", }); Canceled = result.Cancelled; diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md index 88d5f5aa0d..4f9d56bd03 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md @@ -11,12 +11,18 @@ It uses the `DialogService` to display the dialog. The `DialogService` is an injected service that can be used into any component. It exposes methods to show a dialog. For working with a **MessageBox**, the following methods are available: -- ShowSuccessAsync -- ShowWarningAsync -- ShowInfoAsync -- ShowErrorAsync -- ShowConfirmationAsync -- ShowMessageBoxAsync +- **ShowSuccessAsync**: Shows a dialog with a success (green) icon, a message and an OK button. + +- **ShowWarningAsync**: Shows a dialog with a warning (orange) icon, a message and an OK button. + +- **ShowInfoAsync**: Shows a dialog with an information (gray) icon, a message and an OK button. + +- **ShowErrorAsync**: Shows a dialog with an error (red) icon, a message and an OK button. + +- **ShowConfirmationAsync**: Shows a dialog with a confirmation icon, a message and a Yes/No buttons. + This method returns a `DialogResult` object where `Cancelled = false` if **Yes** is pressed and `Cancelled = true` if **No** is pressed. + +- **ShowMessageBoxAsync**: Shows a dialog with the specified options. The **MessageBox** is displayed as a modal dialog box. This means that the user has to click one of the buttons to close the dialog. It cannot be closed by clicking outside of the dialog. @@ -28,4 +34,14 @@ Internally, the `ShowMessageBox` methods call the `ShowDialog` methods. The Show ## Example -{{ DialogMessageBox }} \ No newline at end of file +```csharp +await DialogService.ShowSuccessAsync("The action was a success"); +``` + +```csharp +await DialogService.ShowSuccessAsync(message: "The action was a success", + title: "Success", + button: "Okay"); +``` + +{{ DialogMessageBox }} diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs index bb7bf58ef2..6979ccf096 100644 --- a/src/Core/Components/Dialog/MessageBox/DialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -11,66 +11,66 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public partial class DialogService : IDialogService { /// - public Task ShowSuccessAsync(string message, string? title = null, string? primaryAction = null) + public Task ShowSuccessAsync(string message, string? title = null, string? button = null) { return ShowMessageBoxAsync(new MessageBoxOptions { Title = title ?? "Success", Message = message, - PrimaryAction = primaryAction ?? "OK", + PrimaryButton = button ?? "OK", Icon = new CoreIcons.Filled.Size20.CheckmarkCircle(), IconColor = Color.Success, }); } /// - public Task ShowWarningAsync(string message, string? title = null, string? primaryAction = null) + public Task ShowWarningAsync(string message, string? title = null, string? button = null) { return ShowMessageBoxAsync(new MessageBoxOptions { Title = title ?? "Warning", Message = message, - PrimaryAction = primaryAction ?? "OK", + PrimaryButton = button ?? "OK", Icon = new CoreIcons.Filled.Size20.Warning(), IconColor = Color.Warning, }); } /// - public Task ShowErrorAsync(string message, string? title = null, string? primaryAction = null) + public Task ShowErrorAsync(string message, string? title = null, string? button = null) { return ShowMessageBoxAsync(new MessageBoxOptions { Title = title ?? "Error", Message = message, - PrimaryAction = primaryAction ?? "OK", + PrimaryButton = button ?? "OK", Icon = new CoreIcons.Filled.Size20.DismissCircle(), IconColor = Color.Error, }); } /// - public Task ShowInfoAsync(string message, string? title = null, string? primaryAction = null) + public Task ShowInfoAsync(string message, string? title = null, string? button = null) { return ShowMessageBoxAsync(new MessageBoxOptions { Title = title ?? "Information", Message = message, - PrimaryAction = primaryAction ?? "OK", + PrimaryButton = button ?? "OK", Icon = new CoreIcons.Filled.Size20.Info(), IconColor = Color.Info, }); } /// - public Task ShowConfirmationAsync(string message, string? title = null, string? primaryAction = null, string? secondaryAction = null) + public Task ShowConfirmationAsync(string message, string? title = null, string? primaryButton = null, string? secondaryButton = null) { return ShowMessageBoxAsync(new MessageBoxOptions { Title = title ?? "Confirmation", Message = message, - PrimaryAction = primaryAction ?? "Yes", - SecondaryAction = secondaryAction ?? "No", + PrimaryButton = primaryButton ?? "Yes", + SecondaryButton = secondaryButton ?? "No", Icon = new CoreIcons.Regular.Size20.QuestionCircle(), IconColor = Color.Default, }); @@ -82,8 +82,8 @@ public async Task ShowMessageBoxAsync(MessageBoxOptions options) var dialog = await ShowDialogAsync(config => { config.Header.Title = options.Title; - config.Footer.PrimaryAction.Label = options.PrimaryAction; - config.Footer.SecondaryAction.Label = options.SecondaryAction; + config.Footer.PrimaryAction.Label = options.PrimaryButton; + config.Footer.SecondaryAction.Label = options.SecondaryButton; config.Parameters.Add(nameof(FluentMessageBox.Message), new MarkupString(options.Message ?? "")); config.Parameters.Add(nameof(FluentMessageBox.Icon), options.Icon); config.Parameters.Add(nameof(FluentMessageBox.IconColor), options.IconColor); diff --git a/src/Core/Components/Dialog/MessageBox/IDialogService.cs b/src/Core/Components/Dialog/MessageBox/IDialogService.cs index ef874e7cd4..2c9f0f4f50 100644 --- a/src/Core/Components/Dialog/MessageBox/IDialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/IDialogService.cs @@ -13,27 +13,27 @@ public partial interface IDialogService /// /// Message to display in the dialog. /// Title to display in the dialog header. - /// Text to display in the primary action button. + /// Text to display in the primary action button. /// Result of the dialog. Always `Cancelled = false`. - Task ShowSuccessAsync(string message, string? title = null, string? primaryAction = null); + Task ShowSuccessAsync(string message, string? title = null, string? button = null); /// /// Shows a dialog with a warning (orange) icon, a message and an OK button. /// /// Message to display in the dialog. /// Title to display in the dialog header. Default is "Success". - /// Text to display in the primary action button. Default is "OK". + /// Text to display in the primary action button. Default is "OK". /// Result of the dialog. Always `Cancelled = false`. - Task ShowWarningAsync(string message, string? title = null, string? primaryAction = null); + Task ShowWarningAsync(string message, string? title = null, string? button = null); /// /// Shows a dialog with an error (red) icon, a message and an OK button. /// /// Message to display in the dialog. /// Title to display in the dialog header. Default is "Error". - /// Text to display in the primary action button. Default is "OK". + /// Text to display in the primary action button. Default is "OK". /// Result of the dialog. Always `Cancelled = false`. - Task ShowErrorAsync(string message, string? title = null, string? primaryAction = null); + Task ShowErrorAsync(string message, string? title = null, string? button = null); /// /// Shows a dialog with an information (gray) icon, a message and an OK button. @@ -49,10 +49,10 @@ public partial interface IDialogService /// /// Message to display in the dialog. /// Title to display in the dialog header. Default is "Confirmation". - /// Text to display in the primary action button. Default is "Yes". - /// Text to display in the secondary action button. Default is "No". + /// Text to display in the primary action button. Default is "Yes". + /// Text to display in the secondary action button. Default is "No". /// Result of the dialog: Yes returns `Cancelled = false`, No returns `Cancelled = true` - Task ShowConfirmationAsync(string message, string? title = null, string? primaryAction = null, string? secondaryAction = null); + Task ShowConfirmationAsync(string message, string? title = null, string? primaryButton = null, string? secondaryButton = null); /// /// Shows a dialog with the specified options. diff --git a/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs index 01219ffed8..c0c575100b 100644 --- a/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs +++ b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs @@ -22,12 +22,12 @@ public class MessageBoxOptions /// /// Gets or sets the text to display in the primary action button. /// - public string? PrimaryAction { get; set; } + public string? PrimaryButton { get; set; } /// /// Gets or sets the text to display in the secondary action button. /// - public string? SecondaryAction { get; set; } + public string? SecondaryButton { get; set; } /// /// Gets or sets the icon to display in the dialog. From 2f9cfb3552e5f9404de546870e0fc07a465e46ae Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 24 Dec 2024 12:05:43 +0100 Subject: [PATCH 38/57] aDD lOCALIZATION --- .../Dialog/MessageBox/DialogService.cs | 22 ++++++++--------- .../Dialog/Services/DialogService.cs | 7 +++++- src/Core/Localization/LanguageResource.resx | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs index 6979ccf096..0af6787397 100644 --- a/src/Core/Components/Dialog/MessageBox/DialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -15,9 +15,9 @@ public Task ShowSuccessAsync(string message, string? title = null, { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? "Success", + Title = title ?? Localizer["MessageBox_Success"], Message = message, - PrimaryButton = button ?? "OK", + PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], Icon = new CoreIcons.Filled.Size20.CheckmarkCircle(), IconColor = Color.Success, }); @@ -28,9 +28,9 @@ public Task ShowWarningAsync(string message, string? title = null, { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? "Warning", + Title = title ?? Localizer["MessageBox_Warning"], Message = message, - PrimaryButton = button ?? "OK", + PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], Icon = new CoreIcons.Filled.Size20.Warning(), IconColor = Color.Warning, }); @@ -41,9 +41,9 @@ public Task ShowErrorAsync(string message, string? title = null, s { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? "Error", + Title = title ?? Localizer["MessageBox_Error"], Message = message, - PrimaryButton = button ?? "OK", + PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], Icon = new CoreIcons.Filled.Size20.DismissCircle(), IconColor = Color.Error, }); @@ -54,9 +54,9 @@ public Task ShowInfoAsync(string message, string? title = null, st { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? "Information", + Title = title ?? Localizer["MessageBox_Information"], Message = message, - PrimaryButton = button ?? "OK", + PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], Icon = new CoreIcons.Filled.Size20.Info(), IconColor = Color.Info, }); @@ -67,10 +67,10 @@ public Task ShowConfirmationAsync(string message, string? title = { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? "Confirmation", + Title = title ?? Localizer["MessageBox_Confirmation"], Message = message, - PrimaryButton = primaryButton ?? "Yes", - SecondaryButton = secondaryButton ?? "No", + PrimaryButton = primaryButton ?? Localizer["MessageBox_ButtonYes"], + SecondaryButton = secondaryButton ?? Localizer["MessageBox_ButtonNo"], Icon = new CoreIcons.Regular.Size20.QuestionCircle(), IconColor = Color.Default, }); diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index a04fcd0bee..8734314a6e 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -17,11 +17,16 @@ public partial class DialogService : FluentServiceBase, IDialog /// Initializes a new instance of the class. /// /// List of services available in the application. - public DialogService(IServiceProvider serviceProvider) + /// Localizer for the application. + public DialogService(IServiceProvider serviceProvider, IFluentLocalizer? localizer) { _serviceProvider = serviceProvider; + Localizer = localizer ?? FluentLocalizerInternal.Default; } + /// + protected IFluentLocalizer Localizer { get; } + /// public async Task CloseAsync(IDialogInstance dialog, DialogResult result) { diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index f514534fbe..251dabbbad 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -126,4 +126,28 @@ Required + + Yes + + + OK + + + No + + + Success + + + Warning + + + Error + + + Information + + + Confirmation + \ No newline at end of file From fbf51165f7bf07c10c7518a59a263df879b3d964 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 24 Dec 2024 15:45:51 +0100 Subject: [PATCH 39/57] Update ShowDialogAsync to return DialogResult --- .../Dialog/Examples/DialogServiceCustomized.razor | 4 +--- .../Components/Dialog/Examples/DialogServiceDefault.razor | 4 +--- .../Documentation/Components/Dialog/FluentDialog.md | 8 +++----- src/Core/Components/Dialog/MessageBox/DialogService.cs | 8 ++------ src/Core/Components/Dialog/Services/DialogService.cs | 8 ++++---- src/Core/Components/Dialog/Services/IDialogService.cs | 6 +++--- 6 files changed, 14 insertions(+), 24 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor index 62e55ef4f7..d8b91cd490 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor @@ -14,7 +14,7 @@ private async Task OpenDialogAsync(DialogAlignment alignment) { - var dialog = await DialogService.ShowDialogAsync(options => + var result = await DialogService.ShowDialogAsync(options => { options.Header.Title = "Dialog Title"; options.Alignment = alignment; @@ -29,8 +29,6 @@ }; }); - var result = await dialog.Result; - if (result.Cancelled) { Console.WriteLine($"Dialog Canceled: {result.Value}"); diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor index 4106119370..5fa12a1eed 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceDefault.razor @@ -13,7 +13,7 @@ private async Task OpenDialogAsync() { - var dialog = await DialogService.ShowDialogAsync(options => + var result = await DialogService.ShowDialogAsync(options => { options.Parameters.Add(nameof(Dialog.SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(Dialog.SimpleDialog.Person), John); // Updatable object @@ -24,8 +24,6 @@ }; }); - var result = await dialog.Result; - if (result.Cancelled) { Console.WriteLine($"Dialog Canceled: {result.Value}"); diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index f216bfee4f..69a4d6e234 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -37,13 +37,11 @@ Any Blazor component can be used as a dialog type. ```csharp @inject IDialogService DialogService -var dialog = await DialogService.ShowDialogAsync(options => +var result = await DialogService.ShowDialogAsync(options => { // Options }); -var result = await dialog.Result; - if (result.Cancelled == false) { ... @@ -78,7 +76,7 @@ The previous `FluentDialogInstance` object is optional. You can create your own Several **options** can be used to customize the dialog box, such as styles, title, button text, etc. ```csharp -var dialog = await DialogService.ShowDialogAsync(options => +var result = await DialogService.ShowDialogAsync(options => { options.Header.Title = "Dialog Title"; options.Alignment = DialogAlignment.Default; @@ -136,7 +134,7 @@ the **key is the name of the property** in the dialog box, and the **value** is ```csharp PersonDetails John = new() { Age = "20" }; -var dialog = await DialogService.ShowDialogAsync(options => +var result = await DialogService.ShowDialogAsync(options => { options.Parameters.Add(nameof(SimpleDialog.Name), "John"); // Simple type options.Parameters.Add(nameof(SimpleDialog.Person), John); // Updatable object diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs index 0af6787397..832e932f67 100644 --- a/src/Core/Components/Dialog/MessageBox/DialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -77,9 +77,9 @@ public Task ShowConfirmationAsync(string message, string? title = } /// /> - public async Task ShowMessageBoxAsync(MessageBoxOptions options) + public Task ShowMessageBoxAsync(MessageBoxOptions options) { - var dialog = await ShowDialogAsync(config => + return ShowDialogAsync(config => { config.Header.Title = options.Title; config.Footer.PrimaryAction.Label = options.PrimaryButton; @@ -88,9 +88,5 @@ public async Task ShowMessageBoxAsync(MessageBoxOptions options) config.Parameters.Add(nameof(FluentMessageBox.Icon), options.Icon); config.Parameters.Add(nameof(FluentMessageBox.IconColor), options.IconColor); }); - - var result = await dialog.Result; - - return result; } } diff --git a/src/Core/Components/Dialog/Services/DialogService.cs b/src/Core/Components/Dialog/Services/DialogService.cs index 8734314a6e..a21ece7cdf 100644 --- a/src/Core/Components/Dialog/Services/DialogService.cs +++ b/src/Core/Components/Dialog/Services/DialogService.cs @@ -46,7 +46,7 @@ public async Task CloseAsync(IDialogInstance dialog, DialogResult result) } /// - public virtual async Task ShowDialogAsync(Type componentType, DialogOptions options) + public virtual async Task ShowDialogAsync(Type componentType, DialogOptions options) { if (!componentType.IsSubclassOf(typeof(ComponentBase))) { @@ -64,17 +64,17 @@ public virtual async Task ShowDialogAsync(Type componentType, D ServiceProvider.Items.TryAdd(instance?.Id ?? "", instance ?? throw new InvalidOperationException("Failed to create FluentDialog.")); await ServiceProvider.OnUpdatedAsync.Invoke(instance); - return instance; + return await instance.Result; } /// - public Task ShowDialogAsync(DialogOptions options) where TDialog : ComponentBase + public Task ShowDialogAsync(DialogOptions options) where TDialog : ComponentBase { return ShowDialogAsync(typeof(TDialog), options); } /// - public Task ShowDialogAsync(Action options) where TDialog : ComponentBase + public Task ShowDialogAsync(Action options) where TDialog : ComponentBase { return ShowDialogAsync(typeof(TDialog), new DialogOptions(options)); } diff --git a/src/Core/Components/Dialog/Services/IDialogService.cs b/src/Core/Components/Dialog/Services/IDialogService.cs index 9406ce692a..d2aa8dae5b 100644 --- a/src/Core/Components/Dialog/Services/IDialogService.cs +++ b/src/Core/Components/Dialog/Services/IDialogService.cs @@ -24,14 +24,14 @@ public partial interface IDialogService : IFluentServiceBase /// /// Type of component to display. /// Options to configure the dialog component. - Task ShowDialogAsync(Type dialogComponent, DialogOptions options); + Task ShowDialogAsync(Type dialogComponent, DialogOptions options); /// /// Shows a dialog with the component type as the body. /// /// Type of component to display. /// Options to configure the dialog component. - Task ShowDialogAsync(DialogOptions options) + Task ShowDialogAsync(DialogOptions options) where TDialog : ComponentBase; /// @@ -39,6 +39,6 @@ Task ShowDialogAsync(DialogOptions options) /// /// Type of component to display. /// Options to configure the dialog component. - Task ShowDialogAsync(Action options) + Task ShowDialogAsync(Action options) where TDialog : ComponentBase; } From 5c77cb4796bdc7e9fcfb93c656044bb4932f2af1 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 24 Dec 2024 16:53:35 +0100 Subject: [PATCH 40/57] Add Shortcuts --- .../Components/Dialog/FluentDialog.md | 4 ++ .../Components/Dialog/FluentMessageBox.md | 10 ++++ src/Core/Components/Dialog/FluentDialog.razor | 20 +++---- .../Components/Dialog/FluentDialog.razor.cs | 56 +++++++++++++++++++ .../Dialog/MessageBox/DialogService.cs | 8 +++ .../Dialog/MessageBox/MessageBoxOptions.cs | 10 ++++ .../Services/DialogOptionsFooterAction.cs | 8 +++ 7 files changed, 106 insertions(+), 10 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index 69a4d6e234..fc67102403 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -65,6 +65,8 @@ It is then retrieved in the `DialogResult` like in the example below. By default, the `FluentDialogInstance` class will offer two buttons, one to validate and one to cancel (**OK** and **Cancel**). You can override the actions of these buttons by overriding the `OnActionClickedAsync` method. +By default the shortcut key **Enter** is associated with the **OK** button and the shortcut key **Escape** is associated with the **Cancel** button. +But your can update these shortcuts by overriding the `ShortCut` property. {{ DialogServiceDefault Files=Code:DialogServiceDefault.razor;SimpleDialog:SimpleDialog.razor;PersonDetails:PersonDetails.cs }} @@ -120,6 +122,8 @@ var result = await DialogService.ShowDialogAsync(options => } ``` +> **Notes**: Adding your custom buttons to the dialog box, the default shortcuts **Enter** and **Escape** will no longer work. + {{ DialogServiceCustomized Files=Code:DialogServiceCustomized.razor;CustomizedDialog:CustomizedDialog.razor;PersonDetails:PersonDetails.cs }} ## Data exchange between components diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md index 4f9d56bd03..5e5e24c1ab 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentMessageBox.md @@ -45,3 +45,13 @@ await DialogService.ShowSuccessAsync(message: "The action was a success< ``` {{ DialogMessageBox }} + +## Shortcuts + +By default, the shortcut key **Enter** is associated with the **Primary** button (OK) +and the shortcut key **Escape** is associated with the **Secondary** button (Cancel). + +If a single **OK** button is displayed, the shortcut keys **Enter** or **Escape** will close the dialog. + +For the Confirmation dialog, the shortcut keys **Enter** and **Y** are associated with the **Primary** button (Yes). +And the shortcut keys **Escape** and **N** are associated with the **Secondary** button (No). diff --git a/src/Core/Components/Dialog/FluentDialog.razor b/src/Core/Components/Dialog/FluentDialog.razor index 9252fe792f..0422f004fb 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor +++ b/src/Core/Components/Dialog/FluentDialog.razor @@ -1,16 +1,16 @@ @namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase - + @GetDialogStyle() diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index fcc544cb95..389c77d229 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -148,6 +148,62 @@ public async Task HideAsync() /// private bool LaunchedFromService => Instance is not null; + /// + private async Task OnKeyDownHandlerAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) + { + if (Instance is null) + { + return; + } + + var shortCut = $"{(e.CtrlKey ? "Ctrl+" : string.Empty)}{(e.AltKey ? "Alt+" : string.Empty)}{(e.ShiftKey ? "Shift+" : string.Empty)}{e.Key}"; + + // OK button + var primaryPressed = await ShortCutPressedAsync(Instance.Options.Footer.PrimaryAction, shortCut, Instance.CloseAsync); + if (primaryPressed) + { + return; + } + + // Cancel button + var secondaryPressed = await ShortCutPressedAsync(Instance.Options.Footer.SecondaryAction, shortCut, Instance.CancelAsync); + if (secondaryPressed) + { + return; + } + + // Call the OnClickAsync or defaultAction if the shortcut is the button.ShortCut. + async Task ShortCutPressedAsync(DialogOptionsFooterAction button, string shortCut, Func defaultAction) + { + if (string.IsNullOrEmpty(button.ShortCut) || Instance is null || !button.ToDisplay) + { + return false; + } + + var buttonShortcuts = button.ShortCut.Split(";"); + foreach (var buttonShortcut in buttonShortcuts) + { + + if (string.Equals(buttonShortcut.Trim(), shortCut, StringComparison.OrdinalIgnoreCase)) + { + if (button.OnClickAsync is not null) + { + await button.OnClickAsync.Invoke(Instance); + } + else + { + await defaultAction.Invoke(); + } + + return true; + } + } + + + return false; + } + } + /// private string? GetAlignmentAttribute() { diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs index 832e932f67..bc70e9a017 100644 --- a/src/Core/Components/Dialog/MessageBox/DialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -18,6 +18,7 @@ public Task ShowSuccessAsync(string message, string? title = null, Title = title ?? Localizer["MessageBox_Success"], Message = message, PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.CheckmarkCircle(), IconColor = Color.Success, }); @@ -31,6 +32,7 @@ public Task ShowWarningAsync(string message, string? title = null, Title = title ?? Localizer["MessageBox_Warning"], Message = message, PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.Warning(), IconColor = Color.Warning, }); @@ -44,6 +46,7 @@ public Task ShowErrorAsync(string message, string? title = null, s Title = title ?? Localizer["MessageBox_Error"], Message = message, PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.DismissCircle(), IconColor = Color.Error, }); @@ -57,6 +60,7 @@ public Task ShowInfoAsync(string message, string? title = null, st Title = title ?? Localizer["MessageBox_Information"], Message = message, PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.Info(), IconColor = Color.Info, }); @@ -70,7 +74,9 @@ public Task ShowConfirmationAsync(string message, string? title = Title = title ?? Localizer["MessageBox_Confirmation"], Message = message, PrimaryButton = primaryButton ?? Localizer["MessageBox_ButtonYes"], + PrimaryShortCut = "Enter;Y", SecondaryButton = secondaryButton ?? Localizer["MessageBox_ButtonNo"], + SecondaryShortCut = "Escape;N", Icon = new CoreIcons.Regular.Size20.QuestionCircle(), IconColor = Color.Default, }); @@ -83,7 +89,9 @@ public Task ShowMessageBoxAsync(MessageBoxOptions options) { config.Header.Title = options.Title; config.Footer.PrimaryAction.Label = options.PrimaryButton; + config.Footer.PrimaryAction.ShortCut = options.PrimaryShortCut; config.Footer.SecondaryAction.Label = options.SecondaryButton; + config.Footer.SecondaryAction.ShortCut = options.SecondaryShortCut; config.Parameters.Add(nameof(FluentMessageBox.Message), new MarkupString(options.Message ?? "")); config.Parameters.Add(nameof(FluentMessageBox.Icon), options.Icon); config.Parameters.Add(nameof(FluentMessageBox.IconColor), options.IconColor); diff --git a/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs index c0c575100b..cdad26a076 100644 --- a/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs +++ b/src/Core/Components/Dialog/MessageBox/MessageBoxOptions.cs @@ -29,6 +29,16 @@ public class MessageBoxOptions /// public string? SecondaryButton { get; set; } + /// + /// Gets or sets the text to display in the primary action button. + /// + public string? PrimaryShortCut { get; set; } = "Enter"; + + /// + /// Gets or sets the text to display in the secondary action button. + /// + public string? SecondaryShortCut { get; set; } = "Escape"; + /// /// Gets or sets the icon to display in the dialog. /// diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs index 6cbb3f7df0..ba2e0be2bc 100644 --- a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs @@ -13,6 +13,7 @@ public class DialogOptionsFooterAction internal DialogOptionsFooterAction(ButtonAppearance appearance) { Appearance = appearance; + ShortCut = appearance == ButtonAppearance.Primary ? "Enter" : "Escape"; } /// @@ -23,6 +24,13 @@ internal DialogOptionsFooterAction(ButtonAppearance appearance) /// public string? Label { get; set; } + /// + /// Gets or sets the shortcut key for the action button. + /// By default, "Enter" for the primary action and "Escape" for the secondary action. + /// Example: "Enter", "Escape", "Ctrl+Enter", "Escape;Enter" + /// + public string? ShortCut { get; set; } + /// /// Gets or sets whether the action button is visible. /// From b4931cecaf6b688e7c2b435b6f6623a47182a08d Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 27 Dec 2024 19:53:53 +0100 Subject: [PATCH 41/57] Add first Unit Tests --- ...ts.FluentDialog_Render.verified.razor.html | 9 +++ .../Components/Dialog/FluentDialogTests.razor | 67 +++++++++++++++++++ .../Dialog/Templates/DialogRender.razor | 41 ++++++++++++ .../Dialog/Templates/DialogRenderOptions.cs | 16 +++++ 4 files changed, 133 insertions(+) create mode 100644 tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_Render.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentDialogTests.razor create mode 100644 tests/Core/Components/Dialog/Templates/DialogRender.razor create mode 100644 tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs diff --git a/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_Render.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_Render.verified.razor.html new file mode 100644 index 0000000000..d7c8e2b55d --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_Render.verified.razor.html @@ -0,0 +1,9 @@ + +
+ + +
+ Hello John
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor new file mode 100644 index 0000000000..3d46a11e0e --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -0,0 +1,67 @@ +@using Xunit; +@inherits TestContext +@code +{ + public FluentDialogTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddFluentUIComponents(); + + DialogService = Services.GetRequiredService(); + DialogProvider = RenderComponent(); + } + + /// + /// Gets the dialog service. + /// + public IDialogService DialogService { get; } + + /// + /// Gets the dialog provider. + /// + public IRenderedComponent DialogProvider { get; } + + [Fact] + public async Task FluentDialog_Render() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var result = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact] + public async Task FluentDialog_OpenClose() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions() + { + AutoClose = true, + }; + + // Act + var result = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + + // Wait for the dialog to be closed (autoc-closed) + await result; + + // Assert + Assert.Equal(1, renderOptions.OnInitializedCount); + Assert.Equal(1, renderOptions.OnParametersSetCount); + } +} diff --git a/tests/Core/Components/Dialog/Templates/DialogRender.razor b/tests/Core/Components/Dialog/Templates/DialogRender.razor new file mode 100644 index 0000000000..ddeba9e781 --- /dev/null +++ b/tests/Core/Components/Dialog/Templates/DialogRender.razor @@ -0,0 +1,41 @@ + + Hello @Name + + +@code { + + [CascadingParameter] + public required IDialogInstance Dialog { get; set; } + + [Parameter] + public string? Name { get; set; } + + [Parameter] + public DialogRenderOptions Options { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + await Task.Delay(10); + Options.OnInitializedCount++; + } + + protected override async Task OnParametersSetAsync() + { + await Task.Delay(10); + Options.OnParametersSetCount++; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + return; + } + + if (Options.AutoClose) + { + await Task.Delay(10); + await Dialog.CloseAsync(Options.AutoCloseResult); + } + } +} diff --git a/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs b/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs new file mode 100644 index 0000000000..8b40f0db7b --- /dev/null +++ b/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Dialog.Templates; + +public class DialogRenderOptions +{ + public bool AutoClose { get; set; } + + public DialogResult AutoCloseResult { get; set; } = DialogResult.Ok(true); + + public int OnInitializedCount { get; set; } + + public int OnParametersSetCount { get; set; } +} From 6e49173b15019770ccd4fed58721cc3a8cd56cc2 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 27 Dec 2024 20:32:20 +0100 Subject: [PATCH 42/57] Add ShortCut tests --- .../Services/DialogOptionsFooterAction.cs | 1 + .../Components/Dialog/FluentDialogTests.razor | 62 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs index ba2e0be2bc..db38d9951e 100644 --- a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs @@ -21,6 +21,7 @@ internal DialogOptionsFooterAction(ButtonAppearance appearance) /// /// Gets or sets the label of the action button. + /// By default, this label is not set. So the button will not be displayed. /// public string? Label { get; set; } diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 3d46a11e0e..94d1656647 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -2,6 +2,9 @@ @inherits TestContext @code { + // A timeout can be set when you open a dialog box and do not close it. + private const int TEST_TIMEOUT = 3000; + public FluentDialogTests() { JSInterop.Mode = JSRuntimeMode.Loose; @@ -21,14 +24,14 @@ ///
public IRenderedComponent DialogProvider { get; } - [Fact] + [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentDialog_Render() { // Arrange var renderOptions = new Templates.DialogRenderOptions(); // Act - var result = DialogService.ShowDialogAsync(options => + var dialogTask = DialogService.ShowDialogAsync(options => { options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); @@ -41,7 +44,7 @@ DialogProvider.Verify(); } - [Fact] + [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentDialog_OpenClose() { // Arrange @@ -51,17 +54,64 @@ }; // Act - var result = DialogService.ShowDialogAsync(options => + var dialogTask = DialogService.ShowDialogAsync(options => { options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); }); - // Wait for the dialog to be closed (autoc-closed) - await result; + // Wait for the dialog to be closed (auto-closed) + var result = await dialogTask; // Assert Assert.Equal(1, renderOptions.OnInitializedCount); Assert.Equal(1, renderOptions.OnParametersSetCount); } + + [Theory(Timeout = TEST_TIMEOUT)] + [InlineData(true, null, null, "Escape", false, false, false)] + [InlineData(true, null, "Escape", "Escape", false, false, false)] + [InlineData(true, null, "Escape;Ctrl+Escape", "Escape", true, false, false)] + [InlineData(false, null, null, "Enter", false, false, false)] + [InlineData(false, "Enter", null, "Enter", false, false, false)] + [InlineData(false, "Enter;Ctrl+Enter", null, "Enter", true, false, false)] + public async Task FluentDialog_ShortcutToClose(bool expectedCancelled, string? primaryShortcut, string? secondaryShortcut, string key, bool ctrl, bool shift, bool alt) + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + var keyEventArg = new KeyboardEventArgs() + { + Key = key, + AltKey = alt, + CtrlKey = ctrl, + ShiftKey = shift, + }; + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + + options.Footer.PrimaryAction.Label = "OK"; + if (!string.IsNullOrEmpty(primaryShortcut)) + { + options.Footer.PrimaryAction.ShortCut = primaryShortcut; + } + + options.Footer.SecondaryAction.Label = "Cancel"; + if (!string.IsNullOrEmpty(secondaryShortcut)) + { + options.Footer.SecondaryAction.ShortCut = secondaryShortcut; + } + }); + + // Send a shortcut to close the dialog + DialogProvider.Find("fluent-dialog").KeyDown(keyEventArg); + + var result = await dialogTask; + + // Assert + Assert.Equal(expectedCancelled, result.Cancelled); + } } From 3b170b0772c5c6b3361384b4f81ba6af248e8e0b Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 13:20:24 +0100 Subject: [PATCH 43/57] Add a ClassData attribute --- .../Services/DialogOptionsFooterAction.cs | 11 +++- .../Components/Dialog/Data/ShortCutsData.cs | 66 +++++++++++++++++++ .../Components/Dialog/FluentDialogTests.razor | 30 +++------ 3 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 tests/Core/Components/Dialog/Data/ShortCutsData.cs diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs index db38d9951e..9dac54b7c2 100644 --- a/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooterAction.cs @@ -2,6 +2,8 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using Microsoft.AspNetCore.Components.Web; + namespace Microsoft.FluentUI.AspNetCore.Components; /// @@ -28,8 +30,15 @@ internal DialogOptionsFooterAction(ButtonAppearance appearance) /// /// Gets or sets the shortcut key for the action button. /// By default, "Enter" for the primary action and "Escape" for the secondary action. - /// Example: "Enter", "Escape", "Ctrl+Enter", "Escape;Enter" /// + /// + /// The shortcut key is a combination of one or more keys separated by a plus sign. + /// You must use the key names defined in the class. + /// You can use the following modifier keys: "Ctrl", "Alt", "Shift", in this order. + /// + /// + /// "Enter", "Escape", "Ctrl+Enter", "Ctrl+Alt+Shift+Enter", "Escape;Enter". + /// public string? ShortCut { get; set; } /// diff --git a/tests/Core/Components/Dialog/Data/ShortCutsData.cs b/tests/Core/Components/Dialog/Data/ShortCutsData.cs new file mode 100644 index 0000000000..0795229e63 --- /dev/null +++ b/tests/Core/Components/Dialog/Data/ShortCutsData.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// MIT License - Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------------------ + +using System.Collections; +using Microsoft.AspNetCore.Components.Web; + +namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Dialog.Data; + +public class ShortcutsData : IEnumerable +{ + public IEnumerator GetEnumerator() + { + // Primary Action + yield return CreateDataItem(expectedCancelled: false, + pressed: ToPress("Enter")); + + yield return CreateDataItem(expectedCancelled: false, + primaryShortcut: "Enter", + pressed: ToPress("Enter")); + + yield return CreateDataItem(expectedCancelled: false, + primaryShortcut: "Enter;Ctrl+Enter", + pressed: ToPress("Enter", ctrlKey: true)); + + yield return CreateDataItem(expectedCancelled: false, + primaryShortcut: "Ctrl+Alt+Shift+Enter", + pressed: ToPress("Enter", ctrlKey: true, shiftKey: true, altKey: true)); + + // Secondary Action + yield return CreateDataItem(expectedCancelled: true, + pressed: ToPress("Escape")); + + yield return CreateDataItem(expectedCancelled: true, + secondaryShortcut: "Escape", + pressed: ToPress("Escape")); + + yield return CreateDataItem(expectedCancelled: true, + secondaryShortcut: "Escape;Ctrl+Escape", + pressed: ToPress("Escape", ctrlKey: true)); + + yield return CreateDataItem(expectedCancelled: true, + secondaryShortcut: "Ctrl+Alt+Shift+Escape", + pressed: ToPress("Escape", ctrlKey: true, shiftKey: true, altKey: true)); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private static object[] CreateDataItem(bool expectedCancelled, string? primaryShortcut = null, string? secondaryShortcut = null, KeyboardEventArgs? pressed = null) + { + return [new ShortcutsDataItem(expectedCancelled, primaryShortcut, secondaryShortcut, pressed ?? new KeyboardEventArgs())]; + } + + private static KeyboardEventArgs ToPress(string key, bool? ctrlKey = null, bool? shiftKey = null, bool? altKey = null) + { + return new KeyboardEventArgs() + { + Key = key, + CtrlKey = ctrlKey ?? false, + ShiftKey = shiftKey ?? false, + AltKey = altKey ?? false + }; + } +} + +public record ShortcutsDataItem(bool ExpectedCancelled, string? PrimaryShortcut, string? SecondaryShortcut, KeyboardEventArgs Pressed); diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 94d1656647..1563cf38d5 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -69,24 +69,12 @@ } [Theory(Timeout = TEST_TIMEOUT)] - [InlineData(true, null, null, "Escape", false, false, false)] - [InlineData(true, null, "Escape", "Escape", false, false, false)] - [InlineData(true, null, "Escape;Ctrl+Escape", "Escape", true, false, false)] - [InlineData(false, null, null, "Enter", false, false, false)] - [InlineData(false, "Enter", null, "Enter", false, false, false)] - [InlineData(false, "Enter;Ctrl+Enter", null, "Enter", true, false, false)] - public async Task FluentDialog_ShortcutToClose(bool expectedCancelled, string? primaryShortcut, string? secondaryShortcut, string key, bool ctrl, bool shift, bool alt) + [ClassData(typeof(Data.ShortcutsData))] + public async Task FluentDialog_ShortcutToClose(Data.ShortcutsDataItem item) { // Arrange var renderOptions = new Templates.DialogRenderOptions(); - var keyEventArg = new KeyboardEventArgs() - { - Key = key, - AltKey = alt, - CtrlKey = ctrl, - ShiftKey = shift, - }; - + // Act var dialogTask = DialogService.ShowDialogAsync(options => { @@ -94,24 +82,24 @@ options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); options.Footer.PrimaryAction.Label = "OK"; - if (!string.IsNullOrEmpty(primaryShortcut)) + if (!string.IsNullOrEmpty(item.PrimaryShortcut)) { - options.Footer.PrimaryAction.ShortCut = primaryShortcut; + options.Footer.PrimaryAction.ShortCut = item.PrimaryShortcut; } options.Footer.SecondaryAction.Label = "Cancel"; - if (!string.IsNullOrEmpty(secondaryShortcut)) + if (!string.IsNullOrEmpty(item.SecondaryShortcut)) { - options.Footer.SecondaryAction.ShortCut = secondaryShortcut; + options.Footer.SecondaryAction.ShortCut = item.SecondaryShortcut; } }); // Send a shortcut to close the dialog - DialogProvider.Find("fluent-dialog").KeyDown(keyEventArg); + DialogProvider.Find("fluent-dialog").KeyDown(item.Pressed); var result = await dialogTask; // Assert - Assert.Equal(expectedCancelled, result.Cancelled); + Assert.Equal(item.ExpectedCancelled, result.Cancelled); } } From dbf123ffdb8bee4c1d74cf702255fcd08376ac64 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 16:03:24 +0100 Subject: [PATCH 44/57] Update Shortcut Unit Tests --- .../Components/Dialog/Data/ShortCutsData.cs | 74 ++++++++++++------- .../Components/Dialog/FluentDialogTests.razor | 43 +++++++++-- .../Dialog/Templates/DialogRender.razor | 23 +++++- .../Dialog/Templates/DialogRenderOptions.cs | 2 + 4 files changed, 109 insertions(+), 33 deletions(-) diff --git a/tests/Core/Components/Dialog/Data/ShortCutsData.cs b/tests/Core/Components/Dialog/Data/ShortCutsData.cs index 0795229e63..c5530bce7e 100644 --- a/tests/Core/Components/Dialog/Data/ShortCutsData.cs +++ b/tests/Core/Components/Dialog/Data/ShortCutsData.cs @@ -4,6 +4,7 @@ using System.Collections; using Microsoft.AspNetCore.Components.Web; +using Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Dialog.Templates; namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Dialog.Data; @@ -12,44 +13,60 @@ public class ShortcutsData : IEnumerable public IEnumerator GetEnumerator() { // Primary Action - yield return CreateDataItem(expectedCancelled: false, - pressed: ToPress("Enter")); + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + Pressed: ToPress("Enter"))]; - yield return CreateDataItem(expectedCancelled: false, - primaryShortcut: "Enter", - pressed: ToPress("Enter")); + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + PrimaryShortcut: "Enter", + Pressed: ToPress("Enter"))]; - yield return CreateDataItem(expectedCancelled: false, - primaryShortcut: "Enter;Ctrl+Enter", - pressed: ToPress("Enter", ctrlKey: true)); + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + PrimaryShortcut: "Enter;Ctrl+Enter", + Pressed: ToPress("Enter", ctrlKey: true))]; - yield return CreateDataItem(expectedCancelled: false, - primaryShortcut: "Ctrl+Alt+Shift+Enter", - pressed: ToPress("Enter", ctrlKey: true, shiftKey: true, altKey: true)); + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + PrimaryShortcut: "Ctrl+Alt+Shift+Enter", + Pressed: ToPress("Enter", ctrlKey: true, shiftKey: true, altKey: true))]; // Secondary Action - yield return CreateDataItem(expectedCancelled: true, - pressed: ToPress("Escape")); + yield return [new ShortcutsDataItem(ExpectedCancelled: true, + Pressed: ToPress("Escape"))]; - yield return CreateDataItem(expectedCancelled: true, - secondaryShortcut: "Escape", - pressed: ToPress("Escape")); + yield return [new ShortcutsDataItem(ExpectedCancelled: true, + SecondaryShortcut: "Escape", + Pressed: ToPress("Escape"))]; - yield return CreateDataItem(expectedCancelled: true, - secondaryShortcut: "Escape;Ctrl+Escape", - pressed: ToPress("Escape", ctrlKey: true)); + yield return [new ShortcutsDataItem(ExpectedCancelled: true, + SecondaryShortcut: "Escape;Ctrl+Escape", + Pressed: ToPress("Escape", ctrlKey: true))]; - yield return CreateDataItem(expectedCancelled: true, - secondaryShortcut: "Ctrl+Alt+Shift+Escape", - pressed: ToPress("Escape", ctrlKey: true, shiftKey: true, altKey: true)); + yield return [new ShortcutsDataItem(ExpectedCancelled: true, + SecondaryShortcut: "Ctrl+Alt+Shift+Escape", + Pressed: ToPress("Escape", ctrlKey: true, shiftKey: true, altKey: true))]; + + // Unknown Shortcut + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + Pressed: ToPress("A"), + RenderOptions: CloseAfter200ms)]; + + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + PrimaryShortcut: "", + Pressed: ToPress("A"), + RenderOptions: CloseAfter200ms)]; + + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + SecondaryShortcut: "", + Pressed: ToPress("A"), + RenderOptions: CloseAfter200ms)]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private static object[] CreateDataItem(bool expectedCancelled, string? primaryShortcut = null, string? secondaryShortcut = null, KeyboardEventArgs? pressed = null) + private static DialogRenderOptions CloseAfter200ms => new() { - return [new ShortcutsDataItem(expectedCancelled, primaryShortcut, secondaryShortcut, pressed ?? new KeyboardEventArgs())]; - } + AutoClose = true, + AutoCloseDelay = 200 + }; private static KeyboardEventArgs ToPress(string key, bool? ctrlKey = null, bool? shiftKey = null, bool? altKey = null) { @@ -63,4 +80,9 @@ private static KeyboardEventArgs ToPress(string key, bool? ctrlKey = null, bool? } } -public record ShortcutsDataItem(bool ExpectedCancelled, string? PrimaryShortcut, string? SecondaryShortcut, KeyboardEventArgs Pressed); +public record ShortcutsDataItem( + bool ExpectedCancelled, + string? PrimaryShortcut = null, + string? SecondaryShortcut = null, + KeyboardEventArgs? Pressed = null, + DialogRenderOptions? RenderOptions = null); diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 1563cf38d5..f0a9da8625 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -73,8 +73,8 @@ public async Task FluentDialog_ShortcutToClose(Data.ShortcutsDataItem item) { // Arrange - var renderOptions = new Templates.DialogRenderOptions(); - + var renderOptions = item.RenderOptions ?? new Templates.DialogRenderOptions(); + // Act var dialogTask = DialogService.ShowDialogAsync(options => { @@ -82,24 +82,57 @@ options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); options.Footer.PrimaryAction.Label = "OK"; - if (!string.IsNullOrEmpty(item.PrimaryShortcut)) + if (item.PrimaryShortcut is not null) { options.Footer.PrimaryAction.ShortCut = item.PrimaryShortcut; } options.Footer.SecondaryAction.Label = "Cancel"; - if (!string.IsNullOrEmpty(item.SecondaryShortcut)) + if (item.SecondaryShortcut is not null) { options.Footer.SecondaryAction.ShortCut = item.SecondaryShortcut; } }); // Send a shortcut to close the dialog - DialogProvider.Find("fluent-dialog").KeyDown(item.Pressed); + if (item.Pressed is not null) + { + DialogProvider.Find("fluent-dialog").KeyDown(item.Pressed); + } var result = await dialogTask; // Assert Assert.Equal(item.ExpectedCancelled, result.Cancelled); } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentDialog_ShortcutInvalid() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions() + { + AutoClose = true, + AutoCloseDelay = 200, + AutoCloseResult = DialogResult.Ok("AUTO_CLOSED"), + }; + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + options.Footer.PrimaryAction.Label = "OK"; + options.Footer.SecondaryAction.Label = "Cancel"; + }); + + // Send a shortcut to close the dialog + DialogProvider.Find("fluent-dialog").KeyDown("A"); + + var result = await dialogTask; + + // Assert + Assert.False(result.Cancelled); + Assert.Equal("AUTO_CLOSED", result.Value); + } } diff --git a/tests/Core/Components/Dialog/Templates/DialogRender.razor b/tests/Core/Components/Dialog/Templates/DialogRender.razor index ddeba9e781..147b9c1fad 100644 --- a/tests/Core/Components/Dialog/Templates/DialogRender.razor +++ b/tests/Core/Components/Dialog/Templates/DialogRender.razor @@ -1,9 +1,12 @@ - +@implements IDisposable + Hello @Name @code { + private System.Threading.Timer? _timer; + [CascadingParameter] public required IDialogInstance Dialog { get; set; } @@ -29,13 +32,29 @@ { if (firstRender) { + if (Options.AutoClose && Options.AutoCloseDelay > 0) + { + _timer = new System.Threading.Timer(async _ => + { + await Dialog.CloseAsync(Options.AutoCloseResult); + }, null, Options.AutoCloseDelay, 5000); + } + return; } - if (Options.AutoClose) + if (Options.AutoClose && Options.AutoCloseDelay <= 0) { await Task.Delay(10); await Dialog.CloseAsync(Options.AutoCloseResult); } } + + public void Dispose() + { + if (_timer is not null) + { + _timer.Dispose(); + } + } } diff --git a/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs b/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs index 8b40f0db7b..206cecdaf8 100644 --- a/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs +++ b/tests/Core/Components/Dialog/Templates/DialogRenderOptions.cs @@ -8,6 +8,8 @@ public class DialogRenderOptions { public bool AutoClose { get; set; } + public int AutoCloseDelay { get; set; } + public DialogResult AutoCloseResult { get; set; } = DialogResult.Ok(true); public int OnInitializedCount { get; set; } From a061ffb83b5235f16570bcdc38fe408af68efd5c Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 17:22:19 +0100 Subject: [PATCH 45/57] Add Unit Tests --- .../Components/Dialog/FluentDialog.razor.cs | 10 +-- .../Components/Dialog/Data/ShortCutsData.cs | 10 +++ .../Components/Dialog/FluentDialogTests.razor | 69 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/Core/Components/Dialog/FluentDialog.razor.cs b/src/Core/Components/Dialog/FluentDialog.razor.cs index 389c77d229..7c0aed97c3 100644 --- a/src/Core/Components/Dialog/FluentDialog.razor.cs +++ b/src/Core/Components/Dialog/FluentDialog.razor.cs @@ -2,6 +2,7 @@ // MIT License - Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Utilities; using Microsoft.JSInterop; @@ -90,11 +91,11 @@ protected override Task OnAfterRenderAsync(bool firstRender) } /// - private async Task OnToggleAsync(DialogToggleEventArgs args) + internal async Task OnToggleAsync(DialogToggleEventArgs args) { // Raise the event received from the Web Component var dialogEventArgs = await RaiseOnStateChangeAsync(args); - + if (LaunchedFromService) { switch (dialogEventArgs.State) @@ -117,7 +118,7 @@ private async Task RaiseOnStateChangeAsync(DialogEventArgs args { if (OnStateChange.HasDelegate) { - await OnStateChange.InvokeAsync(args); + await InvokeAsync(() => OnStateChange.InvokeAsync(args)); } return args; @@ -132,6 +133,7 @@ private async Task RaiseOnStateChangeAsync(DialogEventArgs args /// /// Displays the dialog. /// + [ExcludeFromCodeCoverage] public async Task ShowAsync() { await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Show", Id); @@ -140,6 +142,7 @@ public async Task ShowAsync() /// /// Hide the dialog. /// + [ExcludeFromCodeCoverage] public async Task HideAsync() { await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Components.Dialog.Hide", Id); @@ -198,7 +201,6 @@ async Task ShortCutPressedAsync(DialogOptionsFooterAction button, string s return true; } } - return false; } diff --git a/tests/Core/Components/Dialog/Data/ShortCutsData.cs b/tests/Core/Components/Dialog/Data/ShortCutsData.cs index c5530bce7e..142bcdcafa 100644 --- a/tests/Core/Components/Dialog/Data/ShortCutsData.cs +++ b/tests/Core/Components/Dialog/Data/ShortCutsData.cs @@ -16,6 +16,10 @@ public IEnumerator GetEnumerator() yield return [new ShortcutsDataItem(ExpectedCancelled: false, Pressed: ToPress("Enter"))]; + yield return [new ShortcutsDataItem(ExpectedCancelled: false, + Pressed: ToPress("Enter"), + PrimaryClickAsync: (e) => e.CloseAsync() )]; + yield return [new ShortcutsDataItem(ExpectedCancelled: false, PrimaryShortcut: "Enter", Pressed: ToPress("Enter"))]; @@ -32,6 +36,10 @@ public IEnumerator GetEnumerator() yield return [new ShortcutsDataItem(ExpectedCancelled: true, Pressed: ToPress("Escape"))]; + yield return [new ShortcutsDataItem(ExpectedCancelled: true, + Pressed: ToPress("Escape"), + SecondaryClickAsync: (e) => e.CancelAsync() )]; + yield return [new ShortcutsDataItem(ExpectedCancelled: true, SecondaryShortcut: "Escape", Pressed: ToPress("Escape"))]; @@ -83,6 +91,8 @@ private static KeyboardEventArgs ToPress(string key, bool? ctrlKey = null, bool? public record ShortcutsDataItem( bool ExpectedCancelled, string? PrimaryShortcut = null, + Func? PrimaryClickAsync = null, string? SecondaryShortcut = null, + Func? SecondaryClickAsync = null, KeyboardEventArgs? Pressed = null, DialogRenderOptions? RenderOptions = null); diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index f0a9da8625..4b0da21cbc 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -44,6 +44,73 @@ DialogProvider.Verify(); } + [Theory(Timeout = TEST_TIMEOUT)] + [InlineData("position=\"start\"", DialogAlignment.Start)] + [InlineData("position=\"end\"", DialogAlignment.End)] + public async Task FluentDialog_Panel(string expectedContains, DialogAlignment alignment) + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Alignment = alignment; + + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + Assert.Contains(expectedContains, DialogProvider.Markup); + } + + [Theory(Timeout = TEST_TIMEOUT)] + [InlineData(DialogState.Closed, "unknown", "any-old", "any-new")] + [InlineData(DialogState.Open, "toggle", "any-old", "open")] + [InlineData(DialogState.Closed, "toggle", "any-old", "closed")] + [InlineData(DialogState.Opening, "beforetoggle", "closed", "any-new")] + [InlineData(DialogState.Closing, "beforetoggle", "open", "any-new")] + public async Task FluentDialog_Toggle(DialogState state, string eventType, string oldState, string newState) + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + var toggleArgs = new DialogToggleEventArgs() + { + Id = "my-id", + Type = eventType, + OldState = oldState, + NewState = newState, + }; + + DialogEventArgs? dialogEventArgs = null; + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.OnStateChange = (args) => + { + dialogEventArgs = args; + }; + + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Find the dialog and close it + var dialog = DialogProvider.FindComponent(); + await dialog.Instance.OnToggleAsync(toggleArgs); + + // Assert + Assert.Equal(state, dialogEventArgs?.State); + } + [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentDialog_OpenClose() { @@ -82,12 +149,14 @@ options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); options.Footer.PrimaryAction.Label = "OK"; + options.Footer.PrimaryAction.OnClickAsync = item.PrimaryClickAsync; if (item.PrimaryShortcut is not null) { options.Footer.PrimaryAction.ShortCut = item.PrimaryShortcut; } options.Footer.SecondaryAction.Label = "Cancel"; + options.Footer.SecondaryAction.OnClickAsync = item.SecondaryClickAsync; if (item.SecondaryShortcut is not null) { options.Footer.SecondaryAction.ShortCut = item.SecondaryShortcut; From 4af756e87db545bcc0952034504acd880eba612d Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 21:33:20 +0100 Subject: [PATCH 46/57] Add Unit Tests --- ...uentDialogBody_Default.verified.razor.html | 9 +++ ...nce-Disabled_Invisible.verified.razor.html | 8 +++ ...tance-Disabled_Visible.verified.razor.html | 9 +++ ...stance-Enabled_Visible.verified.razor.html | 9 +++ .../Dialog/FluentDialogBodyTests.razor | 70 +++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Default.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Invisible.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentDialogBodyTests.razor diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Default.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Default.verified.razor.html new file mode 100644 index 0000000000..69522cd4cd --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Default.verified.razor.html @@ -0,0 +1,9 @@ + + +
My title
+
My title actions
+
My content
+
+ My actions +
+
diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Invisible.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Invisible.verified.razor.html new file mode 100644 index 0000000000..c23ae19d25 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Invisible.verified.razor.html @@ -0,0 +1,8 @@ + + +
My title
+
My content
+
+ OK +
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html new file mode 100644 index 0000000000..1ea4b3c2dd --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html @@ -0,0 +1,9 @@ + + +
My title
+
My content
+
+ Cancel + OK +
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html new file mode 100644 index 0000000000..0539a53860 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html @@ -0,0 +1,9 @@ + + +
My title
+
My content
+
+ Cancel + OK +
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.razor b/tests/Core/Components/Dialog/FluentDialogBodyTests.razor new file mode 100644 index 0000000000..abd7de6937 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.razor @@ -0,0 +1,70 @@ +@using Xunit; +@inherits TestContext +@code +{ + public FluentDialogBodyTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddFluentUIComponents(); + + DialogService = Services.GetRequiredService(); + DialogProvider = RenderComponent(); + } + + /// + /// Gets the dialog service. + /// + public IDialogService DialogService { get; } + + /// + /// Gets the dialog provider. + /// + public IRenderedComponent DialogProvider { get; } + + [Fact] + public void FluentDialogBody_Default() + { + var cut = Render( + @ + My title + My title actions + My content + + My actions + + + ); + + // Assert + cut.Verify(); + } + + [Theory] + [InlineData("Enabled_Visible", false, true)] + [InlineData("Disabled_Visible", true, true)] + [InlineData("Disabled_Invisible", true, false)] + public void FluentDialogBody_Instance(string name, bool buttonDisabled, bool buttonVisible) + { + // Arrange + var options = new DialogOptions(); + var instance = new DialogInstance(DialogService, typeof(Templates.DialogRender), options); + + options.Header.Title = "My title"; + + options.Footer.PrimaryAction.Label = "OK"; + options.Footer.PrimaryAction.Disabled = false; + + options.Footer.SecondaryAction.Label = "Cancel"; + options.Footer.SecondaryAction.Disabled = buttonDisabled; + options.Footer.SecondaryAction.Visible = buttonVisible; + + // Act + var cut = Render( + @ + My content + ); + + // Assert + cut.Verify(suffix: name); + } +} From 51b86f0516a8ab4885f04d50dda8027a7b474a5e Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 22:17:04 +0100 Subject: [PATCH 47/57] Add Unit Tests --- .../Dialog/FluentDialogBody.razor.cs | 2 +- .../Dialog/MessageBox/IDialogService.cs | 4 +- .../Dialog/FluentDialogBodyTests.razor | 20 +++- .../Components/Dialog/FluentDialogTests.razor | 26 ++++++ ...essageBox_Confirmation.verified.razor.html | 20 ++++ ...FluentMessageBox_Error.verified.razor.html | 19 ++++ ....FluentMessageBox_Info.verified.razor.html | 19 ++++ ...uentMessageBox_Success.verified.razor.html | 19 ++++ ...uentMessageBox_Warning.verified.razor.html | 19 ++++ .../Dialog/FluentMessageBoxTests.razor | 91 +++++++++++++++++++ 10 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.razor diff --git a/src/Core/Components/Dialog/FluentDialogBody.razor.cs b/src/Core/Components/Dialog/FluentDialogBody.razor.cs index a4ef7f67ab..47e66bd5ab 100644 --- a/src/Core/Components/Dialog/FluentDialogBody.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogBody.razor.cs @@ -50,7 +50,7 @@ public partial class FluentDialogBody : FluentComponentBase public RenderFragment? ActionTemplate { get; set; } /// - private async Task ActionClickHandlerAsync(DialogOptionsFooterAction item) + internal async Task ActionClickHandlerAsync(DialogOptionsFooterAction item) { if (item.Disabled || Instance is null) { diff --git a/src/Core/Components/Dialog/MessageBox/IDialogService.cs b/src/Core/Components/Dialog/MessageBox/IDialogService.cs index 2c9f0f4f50..ea751f5003 100644 --- a/src/Core/Components/Dialog/MessageBox/IDialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/IDialogService.cs @@ -40,9 +40,9 @@ public partial interface IDialogService /// /// Message to display in the dialog. /// Title to display in the dialog header. Default is "Information". - /// Text to display in the primary action button. Default is "OK". + /// Text to display in the primary action button. Default is "OK". /// Result of the dialog. Always `Cancelled = false`. - Task ShowInfoAsync(string message, string? title = null, string? primaryAction = null); + Task ShowInfoAsync(string message, string? title = null, string? button = null); /// /// Shows a dialog with a confirmation icon, a message and a Yes/No buttons. diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.razor b/tests/Core/Components/Dialog/FluentDialogBodyTests.razor index abd7de6937..a254a81812 100644 --- a/tests/Core/Components/Dialog/FluentDialogBodyTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.razor @@ -43,8 +43,10 @@ [InlineData("Enabled_Visible", false, true)] [InlineData("Disabled_Visible", true, true)] [InlineData("Disabled_Invisible", true, false)] - public void FluentDialogBody_Instance(string name, bool buttonDisabled, bool buttonVisible) + public async Task FluentDialogBody_Instance(string name, bool buttonDisabled, bool buttonVisible) { + bool buttonClicked = false; + // Arrange var options = new DialogOptions(); var instance = new DialogInstance(DialogService, typeof(Templates.DialogRender), options); @@ -57,14 +59,28 @@ options.Footer.SecondaryAction.Label = "Cancel"; options.Footer.SecondaryAction.Disabled = buttonDisabled; options.Footer.SecondaryAction.Visible = buttonVisible; + options.Footer.SecondaryAction.OnClickAsync = async (e) => + { + buttonClicked = true; + await Task.CompletedTask; + }; // Act var cut = Render( @ My content - ); + + ); + var body = cut.FindComponent(); + await body.Instance.ActionClickHandlerAsync(options.Footer.SecondaryAction); + // Assert cut.Verify(suffix: name); + + if (buttonVisible && !buttonDisabled) + { + Assert.True(buttonClicked); + } } } diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 4b0da21cbc..dff3f706b1 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -135,6 +135,32 @@ Assert.Equal(1, renderOptions.OnParametersSetCount); } + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentDialog_ActionToClose() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + + options.Footer.PrimaryAction.Label = "OK"; + options.Footer.SecondaryAction.Label = "Cancel"; + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Click the primary action to close the dialog + DialogProvider.Find("fluent-button").Click(); + + // Assert + Assert.DoesNotContain(" + + +
My title
+
+
+ +
My message
+
+
+
+ No + Yes +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html new file mode 100644 index 0000000000..f9de5676af --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html @@ -0,0 +1,19 @@ + +
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html new file mode 100644 index 0000000000..1fc3ed61d7 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html @@ -0,0 +1,19 @@ + +
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html new file mode 100644 index 0000000000..746bbe0163 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html @@ -0,0 +1,19 @@ + +
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html new file mode 100644 index 0000000000..c31a87d094 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html @@ -0,0 +1,19 @@ + +
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.razor b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor new file mode 100644 index 0000000000..d0c7e68d7a --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor @@ -0,0 +1,91 @@ +@using Xunit; +@inherits TestContext +@code +{ + // A timeout can be set when you open a dialog box and do not close it. + private const int TEST_TIMEOUT = 3000; + + public FluentMessageBoxTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddFluentUIComponents(); + + DialogService = Services.GetRequiredService(); + DialogProvider = RenderComponent(); + } + + /// + /// Gets the dialog service. + /// + public IDialogService DialogService { get; } + + /// + /// Gets the dialog provider. + /// + public IRenderedComponent DialogProvider { get; } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_Success() + { + // Act + var dialogTask = DialogService.ShowSuccessAsync("My message", "My title", "OK"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_Info() + { + // Act + var dialogTask = DialogService.ShowInfoAsync("My message", "My title", "OK"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_Warning() + { + // Act + var dialogTask = DialogService.ShowWarningAsync("My message", "My title", "OK"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_Error() + { + // Act + var dialogTask = DialogService.ShowErrorAsync("My message", "My title", "OK"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_Confirmation() + { + // Act + var dialogTask = DialogService.ShowConfirmationAsync("My message", "My title", "Yes", "No"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } +} From 8b3f881b4c553adc7ac692f9bc00d408777345f3 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sat, 28 Dec 2024 22:35:53 +0100 Subject: [PATCH 48/57] Add Unit Tests --- .../Components/Dialog/FluentDialogInstance.cs | 15 +++- ...entDialog_WithInstance.verified.razor.html | 14 +++ .../Components/Dialog/FluentDialogTests.razor | 20 +++++ .../Dialog/Templates/DialogWithInstance.razor | 85 +++++++++++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html create mode 100644 tests/Core/Components/Dialog/Templates/DialogWithInstance.razor diff --git a/src/Core/Components/Dialog/FluentDialogInstance.cs b/src/Core/Components/Dialog/FluentDialogInstance.cs index 9250056ec5..4a993f3a56 100644 --- a/src/Core/Components/Dialog/FluentDialogInstance.cs +++ b/src/Core/Components/Dialog/FluentDialogInstance.cs @@ -12,13 +12,17 @@ namespace Microsoft.FluentUI.AspNetCore.Components; public abstract class FluentDialogInstance : ComponentBase { /// - /// + /// Gets or sets the dialog instance. /// [CascadingParameter] public required IDialogInstance DialogInstance { get; set; } /// - /// + /// Method invoked when the component is ready to start, having received its + /// initial parameters from its parent in the render tree. + /// + /// Override this method if you will perform an asynchronous operation and + /// want the component to refresh when that operation is completed. /// /// /// @@ -27,7 +31,7 @@ protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOpti } /// - /// + /// Configures the dialog header and footer. /// /// protected override Task OnInitializedAsync() @@ -44,7 +48,10 @@ protected override Task OnInitializedAsync() } /// - /// + /// Method invoked when an action is clicked. + /// + /// Override this method if you will perform an asynchronous operation + /// when the user clicks an action button. /// /// protected abstract Task OnActionClickedAsync(bool primary); diff --git a/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html new file mode 100644 index 0000000000..6c4913319e --- /dev/null +++ b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html @@ -0,0 +1,14 @@ + +
+ + +
Dialog Title - John
+
+ Hello John
+
+ Cancel + OK +
+
+
+
diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index dff3f706b1..46623c78f1 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -230,4 +230,24 @@ Assert.False(result.Cancelled); Assert.Equal("AUTO_CLOSED", result.Value); } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentDialog_WithInstance() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } } diff --git a/tests/Core/Components/Dialog/Templates/DialogWithInstance.razor b/tests/Core/Components/Dialog/Templates/DialogWithInstance.razor new file mode 100644 index 0000000000..6a0d39eb62 --- /dev/null +++ b/tests/Core/Components/Dialog/Templates/DialogWithInstance.razor @@ -0,0 +1,85 @@ +@implements IDisposable +@inherits FluentDialogInstance + + + Hello @Name + + +@code { + + private System.Threading.Timer? _timer; + + [CascadingParameter] + public required IDialogInstance Dialog { get; set; } + + [Parameter] + public string? Name { get; set; } + + [Parameter] + public DialogRenderOptions Options { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await Task.Delay(10); + Options.OnInitializedCount++; + } + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + await Task.Delay(10); + Options.OnParametersSetCount++; + } + + // Initialize the dialog + protected override void OnInitializeDialog(DialogOptionsHeader header, DialogOptionsFooter footer) + { + base.OnInitializeDialog(header, footer); + header.Title = $"Dialog Title - {Name}"; + footer.SecondaryAction.Visible = true; + } + + // Handle the action click + protected override async Task OnActionClickedAsync(bool primary) + { + if (primary) + { + await DialogInstance.CloseAsync("Yes"); + } + else + { + await DialogInstance.CancelAsync(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + if (Options.AutoClose && Options.AutoCloseDelay > 0) + { + _timer = new System.Threading.Timer(async _ => + { + await Dialog.CloseAsync(Options.AutoCloseResult); + }, null, Options.AutoCloseDelay, 5000); + } + + return; + } + + if (Options.AutoClose && Options.AutoCloseDelay <= 0) + { + await Task.Delay(10); + await Dialog.CloseAsync(Options.AutoCloseResult); + } + } + + public void Dispose() + { + if (_timer is not null) + { + _timer.Dispose(); + } + } +} From ed44f38fa11d51fca14d17e39e92b94800ff16f2 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 29 Dec 2024 14:52:15 +0100 Subject: [PATCH 49/57] Update Localized items --- Directory.Packages.props | 2 +- .../Components/Dialog/FluentDialogInstance.cs | 13 ++++++++--- .../Dialog/MessageBox/DialogService.cs | 23 ++++++++++--------- .../Dialog/Services/DialogOptionsHeader.cs | 12 ---------- src/Core/Localization/LanguageResource.resx | 3 +++ ...soft.FluentUI.AspNetCore.Components.csproj | 1 + 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 833fa20de1..57d2e7ea1f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/src/Core/Components/Dialog/FluentDialogInstance.cs b/src/Core/Components/Dialog/FluentDialogInstance.cs index 4a993f3a56..733e450b83 100644 --- a/src/Core/Components/Dialog/FluentDialogInstance.cs +++ b/src/Core/Components/Dialog/FluentDialogInstance.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Localization; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -15,7 +16,13 @@ public abstract class FluentDialogInstance : ComponentBase /// Gets or sets the dialog instance. ///
[CascadingParameter] - public required IDialogInstance DialogInstance { get; set; } + public virtual required IDialogInstance DialogInstance { get; set; } + + /// + /// Gets or sets the localizer. + /// + [Inject] + public virtual required IFluentLocalizer Localizer { get; set; } /// /// Method invoked when the component is ready to start, having received its @@ -37,9 +44,9 @@ protected virtual void OnInitializeDialog(DialogOptionsHeader header, DialogOpti protected override Task OnInitializedAsync() { var footer = DialogInstance.Options.Footer; - footer.PrimaryAction.Label ??= "OK"; + footer.PrimaryAction.Label ??= Localizer[LanguageResource.MessageBox_ButtonOk]; footer.PrimaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: true); - footer.SecondaryAction.Label ??= "Cancel"; + footer.SecondaryAction.Label ??= Localizer[LanguageResource.MessageBox_ButtonCancel]; footer.SecondaryAction.OnClickAsync ??= (e) => OnActionClickedAsync(primary: false); OnInitializeDialog(DialogInstance.Options.Header, DialogInstance.Options.Footer); diff --git a/src/Core/Components/Dialog/MessageBox/DialogService.cs b/src/Core/Components/Dialog/MessageBox/DialogService.cs index bc70e9a017..2cc6808347 100644 --- a/src/Core/Components/Dialog/MessageBox/DialogService.cs +++ b/src/Core/Components/Dialog/MessageBox/DialogService.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Dialog.MessageBox; +using Microsoft.FluentUI.AspNetCore.Components.Localization; namespace Microsoft.FluentUI.AspNetCore.Components; @@ -15,9 +16,9 @@ public Task ShowSuccessAsync(string message, string? title = null, { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? Localizer["MessageBox_Success"], + Title = title ?? Localizer[LanguageResource.MessageBox_Success], Message = message, - PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryButton = button ?? Localizer[LanguageResource.MessageBox_ButtonOk], PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.CheckmarkCircle(), IconColor = Color.Success, @@ -29,9 +30,9 @@ public Task ShowWarningAsync(string message, string? title = null, { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? Localizer["MessageBox_Warning"], + Title = title ?? Localizer[LanguageResource.MessageBox_Warning], Message = message, - PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryButton = button ?? Localizer[LanguageResource.MessageBox_ButtonOk], PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.Warning(), IconColor = Color.Warning, @@ -43,9 +44,9 @@ public Task ShowErrorAsync(string message, string? title = null, s { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? Localizer["MessageBox_Error"], + Title = title ?? Localizer[LanguageResource.MessageBox_Error], Message = message, - PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryButton = button ?? Localizer[LanguageResource.MessageBox_ButtonOk], PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.DismissCircle(), IconColor = Color.Error, @@ -57,9 +58,9 @@ public Task ShowInfoAsync(string message, string? title = null, st { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? Localizer["MessageBox_Information"], + Title = title ?? Localizer[LanguageResource.MessageBox_Information], Message = message, - PrimaryButton = button ?? Localizer["MessageBox_ButtonOk"], + PrimaryButton = button ?? Localizer[LanguageResource.MessageBox_ButtonOk], PrimaryShortCut = "Enter;Escape", Icon = new CoreIcons.Filled.Size20.Info(), IconColor = Color.Info, @@ -71,11 +72,11 @@ public Task ShowConfirmationAsync(string message, string? title = { return ShowMessageBoxAsync(new MessageBoxOptions { - Title = title ?? Localizer["MessageBox_Confirmation"], + Title = title ?? Localizer[LanguageResource.MessageBox_Confirmation], Message = message, - PrimaryButton = primaryButton ?? Localizer["MessageBox_ButtonYes"], + PrimaryButton = primaryButton ?? Localizer[LanguageResource.MessageBox_ButtonYes], PrimaryShortCut = "Enter;Y", - SecondaryButton = secondaryButton ?? Localizer["MessageBox_ButtonNo"], + SecondaryButton = secondaryButton ?? Localizer[LanguageResource.MessageBox_ButtonNo], SecondaryShortCut = "Escape;N", Icon = new CoreIcons.Regular.Size20.QuestionCircle(), IconColor = Color.Default, diff --git a/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs b/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs index 853c8a5d8c..b1a36783cd 100644 --- a/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs +++ b/src/Core/Components/Dialog/Services/DialogOptionsHeader.cs @@ -19,16 +19,4 @@ internal DialogOptionsHeader() /// Gets or sets the title of the dialog. /// public string? Title { get; set; } - - /* - /// - /// Gets or sets whether the close button is visible. - /// - public bool DismissVisible { get; set; } = true; - - /// - /// Gets or sets the icon to use for the close button. - /// - public Icon DismissIcon { get; set; } = new CoreIcons.Regular.Size20.Dismiss(); - */ } diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index 251dabbbad..14daae0ecc 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -150,4 +150,7 @@ Confirmation + + Cancel + \ No newline at end of file diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj index 4f5586ddfe..3bf4c01949 100644 --- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj +++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj @@ -109,6 +109,7 @@ false true true + CS1591 From 24b3915606921ecb85053ad29dc89691ee35b165 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 29 Dec 2024 15:29:25 +0100 Subject: [PATCH 50/57] Add Unit Tests --- .../Components/Dialog/FluentDialogTests.razor | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 46623c78f1..3d624b19f0 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -108,6 +108,8 @@ await dialog.Instance.OnToggleAsync(toggleArgs); // Assert + Assert.NotNull(dialogEventArgs?.Instance); + Assert.Equal("my-id", dialogEventArgs?.Id); Assert.Equal(state, dialogEventArgs?.State); } @@ -250,4 +252,71 @@ // Assert DialogProvider.Verify(); } + + [Fact] + public void FluentDialog_DialogResult() + { + // Arrange + var result = new DialogResult(content: "OK", cancelled: false); + + // Assert + Assert.Equal("OK", result.Value); + Assert.Equal("OK", result.GetValue()); + Assert.Equal(0, result.GetValue()); + + Assert.False(result.Cancelled); + } + + [Fact] + public void FluentDialog_DialogResult_Ok() + { + // Arrange + var result = DialogResult.Ok("My content"); + + // Assert + Assert.Equal("My content", result.Value); + Assert.False(result.Cancelled); + } + + [Fact] + public void FluentDialog_DialogResult_Cancel() + { + // Arrange + var result = DialogResult.Cancel("My content"); + + // Assert + Assert.Equal("My content", result.Value); + Assert.True(result.Cancelled); + } + + [Fact] + public async Task FluentDialog_Instance() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var dialogTask = DialogService.ShowDialogAsync(options => + { + options.Id = "my-dialog"; + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + options.AdditionalAttributes = new Dictionary { { "data-test", "my-dialog" } }; + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Find the dialog and close it + var dialog = DialogProvider.FindComponent(); + var instanceId = dialog.Instance.Instance?.Id; + await dialog.Instance.Instance!.CloseAsync(42); + + // Wait for the dialog to be closed (auto-closed) + var result = await dialogTask; + + // Assert + Assert.Equal("my-dialog", instanceId); + Assert.Equal(42, result.Value); + } } From bae3ce4b2385152e783b1db85092a88f05799e2f Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 29 Dec 2024 15:36:02 +0100 Subject: [PATCH 51/57] Add Unit Tests --- ...essageBox_Confirmation.verified.razor.html | 2 +- ...ConfirmationCustomized.verified.razor.html | 20 ++++++ ...FluentMessageBox_Error.verified.razor.html | 2 +- ...ageBox_ErrorCustomized.verified.razor.html | 19 ++++++ ....FluentMessageBox_Info.verified.razor.html | 2 +- ...sageBox_InfoCustomized.verified.razor.html | 19 ++++++ ...uentMessageBox_Success.verified.razor.html | 2 +- ...eBox_SuccessCustomized.verified.razor.html | 19 ++++++ ...uentMessageBox_Warning.verified.razor.html | 2 +- ...eBox_WarningCustomized.verified.razor.html | 19 ++++++ .../Dialog/FluentMessageBoxTests.razor | 65 +++++++++++++++++++ 11 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ConfirmationCustomized.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ErrorCustomized.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_InfoCustomized.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_SuccessCustomized.verified.razor.html create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_WarningCustomized.verified.razor.html diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html index c95c627ccd..0b0965a065 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html @@ -2,7 +2,7 @@
-
My title
+
Confirmation
+ + +
My title
+
+
+ +
My message
+
+
+
+ No + Yes +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html index f9de5676af..1e146d5855 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Error.verified.razor.html @@ -2,7 +2,7 @@
-
My title
+
Error
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html index 1fc3ed61d7..a2877d01de 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Info.verified.razor.html @@ -2,7 +2,7 @@
-
My title
+
Information
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html index 746bbe0163..78261b1b6f 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Success.verified.razor.html @@ -2,7 +2,7 @@
-
My title
+
Success
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html index c31a87d094..d5eb838ba2 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Warning.verified.razor.html @@ -2,7 +2,7 @@
-
My title
+
Warning
+ + +
My title
+
+
+ +
My message
+
+
+
+ OK +
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.razor b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor index d0c7e68d7a..55b79f9a7d 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.razor +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor @@ -26,6 +26,19 @@ [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentMessageBox_Success() + { + // Act + var dialogTask = DialogService.ShowSuccessAsync("My message"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_SuccessCustomized() { // Act var dialogTask = DialogService.ShowSuccessAsync("My message", "My title", "OK"); @@ -39,6 +52,19 @@ [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentMessageBox_Info() + { + // Act + var dialogTask = DialogService.ShowInfoAsync("My message"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_InfoCustomized() { // Act var dialogTask = DialogService.ShowInfoAsync("My message", "My title", "OK"); @@ -52,6 +78,19 @@ [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentMessageBox_Warning() + { + // Act + var dialogTask = DialogService.ShowWarningAsync("My message"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_WarningCustomized() { // Act var dialogTask = DialogService.ShowWarningAsync("My message", "My title", "OK"); @@ -65,6 +104,19 @@ [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentMessageBox_Error() + { + // Act + var dialogTask = DialogService.ShowErrorAsync("My message"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_ErrorCustomized() { // Act var dialogTask = DialogService.ShowErrorAsync("My message", "My title", "OK"); @@ -78,6 +130,19 @@ [Fact(Timeout = TEST_TIMEOUT)] public async Task FluentMessageBox_Confirmation() + { + // Act + var dialogTask = DialogService.ShowConfirmationAsync("My message"); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_ConfirmationCustomized() { // Act var dialogTask = DialogService.ShowConfirmationAsync("My message", "My title", "Yes", "No"); From 5ed5130f3c0add51fcb3d43d5b39ad02e673623c Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 29 Dec 2024 16:07:02 +0100 Subject: [PATCH 52/57] Add Unit Tests --- .../Dialog/FluentDialogProvider.razor.cs | 14 ++++++ .../Components/Dialog/FluentDialogTests.razor | 43 +++++++++++++++++++ ...essageBox_EmptyOptions.verified.razor.html | 12 ++++++ .../Dialog/FluentMessageBoxTests.razor | 13 ++++++ 4 files changed, 82 insertions(+) create mode 100644 tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_EmptyOptions.verified.razor.html diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs index a1c18fe30c..b9482aed2e 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor.cs +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor.cs @@ -56,4 +56,18 @@ protected override void OnInitialized() /// private static Action EmptyOnStateChange => (_) => { }; + + /// + /// Only for Unit Tests + /// + /// + internal void UpdateId(string? id) + { + Id = id; + + if (DialogService is not null) + { + DialogService.ProviderId = id; + } + } } diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 3d624b19f0..e1cf6f36d6 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -253,6 +253,49 @@ DialogProvider.Verify(); } + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentDialog_ComponentRule() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + + // Act + var ex = await Assert.ThrowsAsync(async () => + { + var dialogTask = await DialogService.ShowDialogAsync(typeof(string), new DialogOptions()); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + Assert.Equal("System.String must be a Blazor Component (Parameter 'componentType')", ex.Message); + } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentDialog_ProviderRequired() + { + // Arrange + var renderOptions = new Templates.DialogRenderOptions(); + DialogProvider.Instance.UpdateId(null); + + // Act + var ex = await Assert.ThrowsAsync>(async () => + { + var dialogTask = await DialogService.ShowDialogAsync(options => + { + options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + }); + }); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + Assert.Equal("FluentDialogProvider needs to be added to the main layout of your application/site.", ex.Message); + } + [Fact] public void FluentDialog_DialogResult() { diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_EmptyOptions.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_EmptyOptions.verified.razor.html new file mode 100644 index 0000000000..528ee83bb0 --- /dev/null +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_EmptyOptions.verified.razor.html @@ -0,0 +1,12 @@ + +
+ + +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.razor b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor index 55b79f9a7d..aebc2e0e27 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.razor +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.razor @@ -153,4 +153,17 @@ // Assert DialogProvider.Verify(); } + + [Fact(Timeout = TEST_TIMEOUT)] + public async Task FluentMessageBox_EmptyOptions() + { + // Act + var dialogTask = DialogService.ShowMessageBoxAsync(new()); + + // Don't wait for the dialog to be closed + await Task.CompletedTask; + + // Assert + DialogProvider.Verify(); + } } From 075e98f2d10ee514cf80127ec8cec052de2a98a3 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Sun, 29 Dec 2024 16:10:51 +0100 Subject: [PATCH 53/57] Add Unit Tests --- .../Components/Dialog/FluentDialogTests.razor | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index e1cf6f36d6..df40e0a018 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -143,15 +143,14 @@ // Arrange var renderOptions = new Templates.DialogRenderOptions(); - // Act - var dialogTask = DialogService.ShowDialogAsync(options => - { - options.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); - options.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + var dialogOptions = new DialogOptions(); + dialogOptions.Parameters.Add(nameof(Templates.DialogRender.Options), renderOptions); + dialogOptions.Parameters.Add(nameof(Templates.DialogRender.Name), "John"); + dialogOptions.Footer.PrimaryAction.Label = "OK"; + dialogOptions.Footer.SecondaryAction.Label = "Cancel"; - options.Footer.PrimaryAction.Label = "OK"; - options.Footer.SecondaryAction.Label = "Cancel"; - }); + // Act + var dialogTask = DialogService.ShowDialogAsync(dialogOptions); // Don't wait for the dialog to be closed await Task.CompletedTask; From 5e9ef3a7b6c204c057c8ac4f43f77327a2b3e696 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Mon, 30 Dec 2024 10:38:57 +0100 Subject: [PATCH 54/57] Order the dialog in the Provider --- .../Examples/Dialog/CustomizedDialog.razor | 17 ++++++++++++++--- .../Dialog/Examples/Dialog/PersonDetails.cs | 5 +++-- .../Dialog/FluentDialogProvider.razor | 2 +- .../Dialog/Services/DialogInstance.cs | 5 +++++ .../Dialog/Services/IDialogInstance.cs | 5 +++++ .../Components/Dialog/FluentDialogTests.razor | 2 ++ 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor index c9926a29d7..fcf0617dda 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/CustomizedDialog.razor @@ -1,7 +1,7 @@  - @Dialog?.Options.Header.Title + @Dialog.Options.Header.Title @@ -26,13 +26,16 @@ [CascadingParameter] public required IDialogInstance Dialog { get; set; } + [Inject] + public required IDialogService DialogService { get; set; } + // A simple type is not updatable [Parameter] public string? Name { get; set; } // A class is updatable [Parameter] - public PersonDetails Person { get; set; } = new(); + public PersonDetails Person { get; set; } = new(); // A nullable type is optional [Parameter] @@ -40,7 +43,15 @@ private async Task btnOK_Click() { - await Dialog.CloseAsync(DialogResult.Ok("Yes")); + int.TryParse(Person.Age, out var age); + if (age <= 0) + { + await DialogService.ShowErrorAsync("Age must be a positive number."); + } + else + { + await Dialog.CloseAsync(DialogResult.Ok("Yes")); + } } private async Task btnCancel_Click() diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs index f074f48052..e21c546bc3 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/Dialog/PersonDetails.cs @@ -8,10 +8,11 @@ namespace FluentUI.Demo.Client.Documentation.Components.Dialog.Examples.Dialog; public class PersonDetails { + private static readonly string _longDescription = string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + public string Age { get; set; } = ""; - public MarkupString VeryLongDescription - => (MarkupString)string.Join("", SampleData.Text.LoremIpsum.Select(i => $"

{i}

")); + public MarkupString VeryLongDescription => (MarkupString)_longDescription; public override string ToString() => $"Age: {Age}"; } diff --git a/src/Core/Components/Dialog/FluentDialogProvider.razor b/src/Core/Components/Dialog/FluentDialogProvider.razor index d4ab9a70ba..7182af2252 100644 --- a/src/Core/Components/Dialog/FluentDialogProvider.razor +++ b/src/Core/Components/Dialog/FluentDialogProvider.razor @@ -4,7 +4,7 @@
@if (DialogService != null) { - @foreach (var dialog in DialogService.Items.Values) + @foreach (var dialog in DialogService.Items.Values.OrderBy(i => i.Index)) { public class DialogInstance : IDialogInstance { + private static long _counter; private readonly Type _componentType; internal readonly TaskCompletionSource ResultCompletion = new(); @@ -21,6 +22,7 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog Options = options; DialogService = dialogService; Id = string.IsNullOrEmpty(options.Id) ? Identifier.NewId() : options.Id; + Index = Interlocked.Increment(ref _counter); } /// @@ -41,6 +43,9 @@ internal DialogInstance(IDialogService dialogService, Type componentType, Dialog /// " public string Id { get; } + /// " + public long Index { get; } + /// public Task CancelAsync() { diff --git a/src/Core/Components/Dialog/Services/IDialogInstance.cs b/src/Core/Components/Dialog/Services/IDialogInstance.cs index 4a52eaaab5..e93f01b053 100644 --- a/src/Core/Components/Dialog/Services/IDialogInstance.cs +++ b/src/Core/Components/Dialog/Services/IDialogInstance.cs @@ -20,6 +20,11 @@ public interface IDialogInstance /// string Id { get; } + /// + /// Gets the index of the dialog (sequential number). + /// + long Index { get; } + /// /// Gets the options used to configure the dialog. /// diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index df40e0a018..04618fb519 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -352,6 +352,7 @@ // Find the dialog and close it var dialog = DialogProvider.FindComponent(); var instanceId = dialog.Instance.Instance?.Id; + var instanceIndex = dialog.Instance.Instance?.Index; await dialog.Instance.Instance!.CloseAsync(42); // Wait for the dialog to be closed (auto-closed) @@ -360,5 +361,6 @@ // Assert Assert.Equal("my-dialog", instanceId); Assert.Equal(42, result.Value); + Assert.True(instanceIndex > 0); } } From 5e76fbb8653e85fbfafbb86a22be67c1ace86247 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Tue, 31 Dec 2024 15:37:44 +0100 Subject: [PATCH 55/57] Inverse Primary and Secondary buttons --- src/Core/Components/Dialog/Services/DialogOptionsFooter.cs | 2 +- ...ntDialogBody_Instance-Disabled_Visible.verified.razor.html | 4 ++-- ...entDialogBody_Instance-Enabled_Visible.verified.razor.html | 4 ++-- ...tDialogTests.FluentDialog_WithInstance.verified.razor.html | 2 +- ...BoxTests.FluentMessageBox_Confirmation.verified.razor.html | 4 ++-- ...luentMessageBox_ConfirmationCustomized.verified.razor.html | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs b/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs index 83ca4920a1..fa1651bb2f 100644 --- a/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs +++ b/src/Core/Components/Dialog/Services/DialogOptionsFooter.cs @@ -26,7 +26,7 @@ public DialogOptionsFooter() public DialogOptionsFooterAction SecondaryAction { get; } = new(ButtonAppearance.Default); /// - internal IEnumerable Actions => [SecondaryAction, PrimaryAction]; + internal IEnumerable Actions => [PrimaryAction, SecondaryAction]; /// internal bool HasActions => PrimaryAction.ToDisplay || SecondaryAction.ToDisplay; diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html index 1ea4b3c2dd..6e0a4e63d8 100644 --- a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Disabled_Visible.verified.razor.html @@ -3,7 +3,7 @@
My title
My content
- Cancel OK + Cancel
- \ No newline at end of file + diff --git a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html index 0539a53860..6e6ebb3955 100644 --- a/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentDialogBodyTests.FluentDialogBody_Instance-Enabled_Visible.verified.razor.html @@ -3,7 +3,7 @@
My title
My content
- Cancel OK + Cancel
- \ No newline at end of file + diff --git a/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html index 6c4913319e..73107f1fb1 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentDialogTests.FluentDialog_WithInstance.verified.razor.html @@ -6,8 +6,8 @@
Hello John
- Cancel OK + Cancel
diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html index 0b0965a065..b7eadac5c0 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_Confirmation.verified.razor.html @@ -12,9 +12,9 @@
- No Yes + No
-
\ No newline at end of file +
diff --git a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ConfirmationCustomized.verified.razor.html b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ConfirmationCustomized.verified.razor.html index c95c627ccd..bf57dad8a1 100644 --- a/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ConfirmationCustomized.verified.razor.html +++ b/tests/Core/Components/Dialog/FluentMessageBoxTests.FluentMessageBox_ConfirmationCustomized.verified.razor.html @@ -12,9 +12,9 @@
- No Yes + No
-
\ No newline at end of file +
From 440e7a9ea30615dda6e6b50449d7e0f6d1d67d61 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 7 Jan 2025 11:38:31 +0100 Subject: [PATCH 56/57] Also copy *.cs files to sources --- examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj index 7be038c2f9..e3fd8b944c 100644 --- a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj +++ b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj @@ -43,7 +43,7 @@ - + From d53110227e664ab24bb3cff8aba9ff0d420114e0 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Wed, 8 Jan 2025 10:28:36 +0100 Subject: [PATCH 57/57] Update PR comments --- .../Examples/DialogServiceCustomized.razor | 1 - .../Components/Dialog/FluentDialog.md | 9 +++--- .../src/Components/Dialog/FluentDialog.ts | 4 +-- .../Base/FluentServiceProviderException.cs | 2 +- .../{ShortCutsData.cs => ShortcutData.cs} | 30 +++++++++---------- .../Components/Dialog/FluentDialogTests.razor | 6 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) rename tests/Core/Components/Dialog/Data/{ShortCutsData.cs => ShortcutData.cs} (78%) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor index d8b91cd490..63b646bb46 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/Examples/DialogServiceCustomized.razor @@ -1,6 +1,5 @@ @inject IDialogService DialogService -Open Panel Open Dialog
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md index fc67102403..cec57e2289 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Dialog/FluentDialog.md @@ -5,10 +5,11 @@ route: /Dialog # Dialog -`FluentDialog` is a window overlaid on either the primary window or another dialog window. -Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. -Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, -attempts to interact with the inert content cause the dialog to close. +A **Dialog** is a supplemental surface that can provide helpful interactions or require someone to take an +action before they can continue their task, like confirming a deletion. + +**Dialogs** are often interruptions, so use them for important actions. +If you need to give someone an update on an action they just took but that they don't need to act on, try a toast. ## Best practices diff --git a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts index b435285741..4798f4b6a4 100644 --- a/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts +++ b/src/Core.Scripts/src/Components/Dialog/FluentDialog.ts @@ -10,8 +10,8 @@ export namespace Microsoft.FluentUI.Blazor.Components.Dialog { } /** - * Display the fluent-dialog with the given id - * @param id The id of the fluent-dialog to display + * Hide the fluent-dialog with the given id + * @param id The id of the fluent-dialog to hide */ export function Hide(id: string): void { const dialog = document.getElementById(id) as any; diff --git a/src/Core/Components/Base/FluentServiceProviderException.cs b/src/Core/Components/Base/FluentServiceProviderException.cs index 43dadd97a7..6310f0f151 100644 --- a/src/Core/Components/Base/FluentServiceProviderException.cs +++ b/src/Core/Components/Base/FluentServiceProviderException.cs @@ -13,7 +13,7 @@ public class FluentServiceProviderException : Exception /// Creates a new instance of the class. /// public FluentServiceProviderException() - : base($"{typeof(TProvider).Name} needs to be added to the main layout of your application/site.") + : base($"{typeof(TProvider).Name} needs to be added to the page/component hierarchy of your application/site. Usually this will be 'MainLayout' but depending on your setup it could be at a different location.") { } diff --git a/tests/Core/Components/Dialog/Data/ShortCutsData.cs b/tests/Core/Components/Dialog/Data/ShortcutData.cs similarity index 78% rename from tests/Core/Components/Dialog/Data/ShortCutsData.cs rename to tests/Core/Components/Dialog/Data/ShortcutData.cs index 142bcdcafa..48765bcd32 100644 --- a/tests/Core/Components/Dialog/Data/ShortCutsData.cs +++ b/tests/Core/Components/Dialog/Data/ShortcutData.cs @@ -8,61 +8,61 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Dialog.Data; -public class ShortcutsData : IEnumerable +public class ShortcutData : IEnumerable { public IEnumerator GetEnumerator() { // Primary Action - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, Pressed: ToPress("Enter"))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, Pressed: ToPress("Enter"), PrimaryClickAsync: (e) => e.CloseAsync() )]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, PrimaryShortcut: "Enter", Pressed: ToPress("Enter"))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, PrimaryShortcut: "Enter;Ctrl+Enter", Pressed: ToPress("Enter", ctrlKey: true))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, PrimaryShortcut: "Ctrl+Alt+Shift+Enter", Pressed: ToPress("Enter", ctrlKey: true, shiftKey: true, altKey: true))]; // Secondary Action - yield return [new ShortcutsDataItem(ExpectedCancelled: true, + yield return [new ShortcutDataItem(ExpectedCancelled: true, Pressed: ToPress("Escape"))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: true, + yield return [new ShortcutDataItem(ExpectedCancelled: true, Pressed: ToPress("Escape"), SecondaryClickAsync: (e) => e.CancelAsync() )]; - yield return [new ShortcutsDataItem(ExpectedCancelled: true, + yield return [new ShortcutDataItem(ExpectedCancelled: true, SecondaryShortcut: "Escape", Pressed: ToPress("Escape"))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: true, + yield return [new ShortcutDataItem(ExpectedCancelled: true, SecondaryShortcut: "Escape;Ctrl+Escape", Pressed: ToPress("Escape", ctrlKey: true))]; - yield return [new ShortcutsDataItem(ExpectedCancelled: true, + yield return [new ShortcutDataItem(ExpectedCancelled: true, SecondaryShortcut: "Ctrl+Alt+Shift+Escape", Pressed: ToPress("Escape", ctrlKey: true, shiftKey: true, altKey: true))]; // Unknown Shortcut - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, Pressed: ToPress("A"), RenderOptions: CloseAfter200ms)]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, PrimaryShortcut: "", Pressed: ToPress("A"), RenderOptions: CloseAfter200ms)]; - yield return [new ShortcutsDataItem(ExpectedCancelled: false, + yield return [new ShortcutDataItem(ExpectedCancelled: false, SecondaryShortcut: "", Pressed: ToPress("A"), RenderOptions: CloseAfter200ms)]; @@ -88,7 +88,7 @@ private static KeyboardEventArgs ToPress(string key, bool? ctrlKey = null, bool? } } -public record ShortcutsDataItem( +public record ShortcutDataItem( bool ExpectedCancelled, string? PrimaryShortcut = null, Func? PrimaryClickAsync = null, diff --git a/tests/Core/Components/Dialog/FluentDialogTests.razor b/tests/Core/Components/Dialog/FluentDialogTests.razor index 04618fb519..1037f2b5a7 100644 --- a/tests/Core/Components/Dialog/FluentDialogTests.razor +++ b/tests/Core/Components/Dialog/FluentDialogTests.razor @@ -163,8 +163,8 @@ } [Theory(Timeout = TEST_TIMEOUT)] - [ClassData(typeof(Data.ShortcutsData))] - public async Task FluentDialog_ShortcutToClose(Data.ShortcutsDataItem item) + [ClassData(typeof(Data.ShortcutData))] + public async Task FluentDialog_ShortcutToClose(Data.ShortcutDataItem item) { // Arrange var renderOptions = item.RenderOptions ?? new Templates.DialogRenderOptions(); @@ -292,7 +292,7 @@ await Task.CompletedTask; // Assert - Assert.Equal("FluentDialogProvider needs to be added to the main layout of your application/site.", ex.Message); + Assert.Equal("FluentDialogProvider needs to be added to the page/component hierarchy of your application/site. Usually this will be 'MainLayout' but depending on your setup it could be at a different location.", ex.Message); } [Fact]