diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardCustomized.razor new file mode 100644 index 0000000000..426380eff6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardCustomized.razor @@ -0,0 +1,113 @@ + + + + + + +
+ Intro +
+
+ +
Introduction
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut nisi eget dolor semper + luctus vitae a nulla. Cras semper eros sed lacinia tincidunt. Mauris dignissim ullamcorper dolor, + ut blandit dui ullamcorper faucibus. Interdum et malesuada fames ac ante ipsum. +
+
+ + +
+ Get Started +
+
+ +
Get Started
+ Maecenas sed justo ac sapien venenatis ullamcorper. Sed maximus nunc non venenatis euismod. + Fusce vel porta ex, imperdiet molestie nisl. Vestibulum eu ultricies mauris, eget aliquam quam. +
+
+ + +
+ Set budget +
+
+ +
Set budget
+ Phasellus quis augue convallis, congue velit ac, aliquam ex. In egestas porttitor massa + aliquet porttitor. Donec bibendum faucibus urna vitae elementum. Phasellus vitae efficitur + turpis, eget molestie ipsum. +
+
+ + +
+ Summary +
+
+ +
Summary
+ Ut iaculis sed magna efficitur tempor. Vestibulum est erat, imperdiet in diam ac, + aliquam tempus sapien. Nam rutrum mi at enim mattis, non mollis diam molestie. + Cras sodales dui libero, sit amet cursus sapien elementum ac. Nulla euismod nisi sem. +
+
+
+ + + @{ + var index = context; + var lastStepIndex = 3; + +
+ @if (index > 0) + { + Go to first page + Previous + } +
+ +
+ @if (index != lastStepIndex) + { + Next + Go to last page + } + else + { + Finish + } +
+ } +
+
+ +@code +{ + FluentWizard MyWizard = default!; + int Value = 0; + + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + } + + async Task OnFinish() + { + await Task.CompletedTask; + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor new file mode 100644 index 0000000000..0c2ceceb99 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor @@ -0,0 +1,56 @@ + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut nisi eget dolor semper + luctus vitae a nulla. Cras semper eros sed lacinia tincidunt. Mauris dignissim ullamcorper dolor, + ut blandit dui ullamcorper faucibus. Interdum et malesuada fames ac ante ipsum. + + + Maecenas sed justo ac sapien venenatis ullamcorper. Sed maximus nunc non venenatis euismod. + Fusce vel porta ex, imperdiet molestie nisl. Vestibulum eu ultricies mauris, eget aliquam quam. + + + Nunc dignissim tortor eget lacus porta tristique. Nunc in posuere dui. Cras ligula ex, + ullamcorper in gravida in, euismod vitae purus. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Aliquam at velit leo. Suspendisse potenti. Cras dictum eu augue in laoreet. + + + Phasellus quis augue convallis, congue velit ac, aliquam ex. In egestas porttitor massa + aliquet porttitor. Donec bibendum faucibus urna vitae elementum. Phasellus vitae efficitur + turpis, eget molestie ipsum. + + + Ut iaculis sed magna efficitur tempor. Vestibulum est erat, imperdiet in diam ac, + aliquam tempus sapien. Nam rutrum mi at enim mattis, non mollis diam molestie. + Cras sodales dui libero, sit amet cursus sapien elementum ac. Nulla euismod nisi sem. + + + + +@code +{ + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + Console.WriteLine("Step changed to {0}", e.TargetIndex); + } + + void OnFinished() + { + Console.WriteLine("Wizard has been finished"); + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardEditForms.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardEditForms.razor new file mode 100644 index 0000000000..e5cd3cedc7 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardEditForms.razor @@ -0,0 +1,140 @@ +@using System.ComponentModel.DataAnnotations + +@inject IDialogService DialogService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Phasellus quis augue convallis, congue velit ac, aliquam ex. In egestas porttitor massa + aliquet porttitor. Donec bibendum faucibus urna vitae elementum. Phasellus vitae efficitur + turpis, eget molestie ipsum. + + + + + + + + + + + + + + +@if (_overlayIsVisible) +{ + + + +} + +@code +{ + private FormData1 _formData1 = new FormData1(); + private FormData2 _formData2 = new FormData2(); + private FinishFormData _finishFormData = new FinishFormData(); + private bool _overlayIsVisible = false; + + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + } + + async Task OnFinishedAsync() + { + await DialogService.ShowInfoAsync("Wizard completed"); + } + + async Task OnValidSubmit() + { + _overlayIsVisible = true; + await Task.Delay(2000); + _overlayIsVisible = false; + } + + void OnInvalidSubmit() + { + } + + private class FormData1 + { + [Required] + [MaxLength(3)] + public string? FirstName { get; set; } + + [Required] + [MinLength(10)] + public string? LastName { get; set; } + } + + private class FormData2 + { + [Required] + public string? AddressLine1 { get; set; } + + public string? AddressLine2 { get; set; } + + [Required] + public string? City { get; set; } + + [Required] + public string? StateOrProvince { get; set; } + + [Required] + public string? Country { get; set; } + + [Required] + public string? PostalCode { get; set; } + } + + private class FinishFormData + { + [Required] + [MinLength(5)] + public string? Signature { get; set; } + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardPosition.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardPosition.razor new file mode 100644 index 0000000000..90de4bd2f3 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardPosition.razor @@ -0,0 +1,57 @@ + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut nisi eget dolor semper + luctus vitae a nulla. Cras semper eros sed lacinia tincidunt. Mauris dignissim ullamcorper dolor, + ut blandit dui ullamcorper faucibus. Interdum et malesuada fames ac ante ipsum. + + + Maecenas sed justo ac sapien venenatis ullamcorper. Sed maximus nunc non venenatis euismod. + Fusce vel porta ex, imperdiet molestie nisl. Vestibulum eu ultricies mauris, eget aliquam quam. + + + Nunc dignissim tortor eget lacus porta tristique. Nunc in posuere dui. Cras ligula ex, + ullamcorper in gravida in, euismod vitae purus. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Aliquam at velit leo. Suspendisse potenti. Cras dictum eu augue in laoreet. + + + Phasellus quis augue convallis, congue velit ac, aliquam ex. In egestas porttitor massa + aliquet porttitor. Donec bibendum faucibus urna vitae elementum. Phasellus vitae efficitur + turpis, eget molestie ipsum. + + + Ut iaculis sed magna efficitur tempor. Vestibulum est erat, imperdiet in diam ac, + aliquam tempus sapien. Nam rutrum mi at enim mattis, non mollis diam molestie. + Cras sodales dui libero, sit amet cursus sapien elementum ac. Nulla euismod nisi sem. + + + + +@code +{ + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + Console.WriteLine("Step changed to {0}", e.TargetIndex); + } + + void OnFinished() + { + Console.WriteLine("Wizard has been finished"); + } +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md new file mode 100644 index 0000000000..725beddba9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md @@ -0,0 +1,67 @@ +--- +title: Wizard +route: /Wizard +icon: Steps +--- + +# Wizard + +**Wizards** are a step-by-step user interface used to break down complex tasks into digestible pieces. +The simplified layout allows the reader to more easily understand the scope of a given task and the actions +needed to complete the task. + +By default, steps are displayed on the left, but you can move them to the top of the component. +They are in the form of circular bubbles, with a check mark indicating whether it has been processed or not. +They are not numbered, but the **DisplayStepNumber** property can be used to add this numbering. +It's also possible to customize these bubbles via the **IconPrevious**, **IconCurrent** +and **IconNext** properties. + +The order of the steps must be defined when designing the Wizard. +However, it is possible to enable or disable a step via the **Disabled** property. + +By default, the contents of all steps are hidden and displayed when the user arrives at that +that step (for display performance reasons). But the **DeferredLoading** property +property reverses this process and generates the contents of the active step only. + +The **Label** and **Summary** properties display the name and a small summary of the step below or next to the bubble. +The **StepTitleHiddenWhen** property is used to hide this title and summary when the screen width +is reduced, for example on mobile devices. By default, the value `XsAndDown` is applied +to hide this data on cell phones (< 600px). + +All these areas (bubbles on the left/top and navigation buttons at the bottom) are fully customizable +using the **StepTemplate** and **ButtonTemplate** properties (see the second example). +You can customize button labels using the **ButtonTemplate** or by modifying +the static properties **FluentWizard.LabelButtonPrevious / LabelButtonNext / LabelButtonDone**. + +> **note**: this FluentWizard is not yet fully compatible with accessibility. + +{{ WizardDefault }} + +## Positioning + +You can choose to display the steps on the left (default) or on the top of the component using the **StepperPosition** parameter. + +{{ WizardPosition }} + +## Customized + +You can customize the wizard with a **ButtonTemplate** to replace the default Previous/Next/Done buttons, +and **StepTemplate** to fully control how each step indicator is rendered. + +{{ WizardCustomized }} + +## EditForms + +The wizard supports **EditForm** validation. When a step contains an `EditForm`, the wizard will +automatically validate the form before navigating to the next step. If validation fails, +the step change is cancelled. + +{{ WizardEditForms }} + +## API FluentWizard + +{{ API Type=FluentWizard }} + +## API FluentWizardStep + +{{ API Type=FluentWizardStep }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentWizard.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentWizard.md index 9469dc5470..5019745d07 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentWizard.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentWizard.md @@ -4,80 +4,32 @@ route: /Migration/Wizard hidden: true --- -- ### Component removed 💥 +- ### Component re-introduced - `FluentWizard` and `FluentWizardStep` have been **removed** in V5. - There is no direct replacement component. + `FluentWizard` and `FluentWizardStep` have been **re-introduced** in V5. + The component preserves the same API and functionality from V4 with the following changes. -- ### V4 FluentWizard parameters (removed) +- ### Breaking changes 💥 - | Parameter | Type | Default | - |-----------|------|---------| - | `Height` | `string` | `"400px"` | - | `Width` | `string` | `"100%"` | - | `OnFinish` | `EventCallback` | — | - | `StepperPosition` | `StepperPosition` | `Left` | - | `StepperSize` | `string?` | — | - | `StepperBulletSpace` | `string?` | — | - | `Border` | `WizardBorder` | `None` | - | `DisplayStepNumber` | `WizardStepStatus` | `None` | - | `Value` / `ValueChanged` | `int` / `EventCallback` | `0` | - | `ButtonTemplate` | `RenderFragment?` | — | - | `Steps` | `RenderFragment?` | — | - | `StepTitleHiddenWhen` | `GridItemHidden?` | `XsAndDown` | - | `StepSequence` | `WizardStepSequence` | `Linear` | + | V4 | V5 | + |----|-----| + | `Appearance.Neutral` (in ButtonTemplate) | `ButtonAppearance.Default` | + | `Appearance.Accent` (in ButtonTemplate) | `ButtonAppearance.Primary` | + | `FluentLabel Typo="Typography.Body"` | `FluentLabel` (no `Typo` parameter; use `Size` / `Weight`) | + | `FluentLabel Typo="Typography.Header"` | `FluentLabel Weight="LabelWeight.Bold"` | + | `Icons.*.Size24.*` (icon defaults) | `CoreIcons.*.Size20.*` | + | `FluentTextField` | `FluentTextInput` | + | `FluentEditForm` | `EditForm` (standard Blazor) | -- ### V4 FluentWizardStep parameters (removed) +- ### Icon defaults changed - | Parameter | Type | Default | - |-----------|------|---------| - | `Label` | `string` | `""` | - | `Summary` | `string` | `""` | - | `Disabled` | `bool` | `false` | - | `DeferredLoading` | `bool` | `false` | - | `OnChange` | `EventCallback` | — | - | `IconPrevious` / `IconCurrent` / `IconNext` | `Icon` | — | - | `StepTemplate` | `RenderFragment?` | — | + The default icons for wizard steps now use **Size20** instead of Size24: + - `IconPrevious` = `CoreIcons.Filled.Size20.CheckmarkCircle()` + - `IconCurrent` = `CoreIcons.Filled.Size20.Circle()` + - `IconNext` = `CoreIcons.Regular.Size20.Circle()` -- ### Removed enums +- ### Re-introduced enums - `WizardBorder` - `WizardStepSequence` - `WizardStepStatus` - `StepperPosition` - -- ### Migration strategy - - Build a custom wizard using `FluentTabs` for step navigation, - or implement step-based logic with conditional rendering: - - ```xml - - - - - @for (int i = 0; i < steps.Length; i++) - { - - @(i + 1). @steps[i] - - } - - - - @switch (currentStep) - { - case 0: break; - case 1: break; - case 2: break; - } - - - - Previous - - @(currentStep == steps.Length - 1 ? "Finish" : "Next") - - - - ``` diff --git a/src/Core/Components/Icons/CoreIcons.cs b/src/Core/Components/Icons/CoreIcons.cs index 0f97af663c..bb0b42a1ee 100644 --- a/src/Core/Components/Icons/CoreIcons.cs +++ b/src/Core/Components/Icons/CoreIcons.cs @@ -95,6 +95,8 @@ public class PresenceTentative : Icon { public PresenceTentative() : base("Prese public class PresenceUnknown : Icon { public PresenceUnknown() : base("PresenceUnknown", IconVariant.Regular, IconSize.Size20, "") { } } + public class Circle : Icon { public Circle() : base("Circle", IconVariant.Regular, IconSize.Size20, "") { } } + public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } }; public class Search : Icon { public Search() : base("Search", IconVariant.Regular, IconSize.Size20, "") { } } @@ -133,6 +135,8 @@ public class PresenceBusy : Icon { public PresenceBusy() : base("PresenceBusy", public class PresenceDnd : Icon { public PresenceDnd() : base("PresenceDnd", IconVariant.Filled, IconSize.Size20, "") { } } + public class Circle : Icon { public Circle() : base("Circle", IconVariant.Filled, IconSize.Size20, "") { } } + public class RadioButton : Icon { public RadioButton() : base("RadioButton", IconVariant.Regular, IconSize.Size20, "") { } }; public class Star : Icon { public Star() : base("Star", IconVariant.Filled, IconSize.Size20, "") { } }; diff --git a/src/Core/Components/Wizard/FluentWizard.razor b/src/Core/Components/Wizard/FluentWizard.razor new file mode 100644 index 0000000000..e591d09f28 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -0,0 +1,64 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@inherits FluentComponentBase + + + +
+
    + @Steps +
+ +
+ @foreach (var step in _steps.Where(i => i.Index == Value || !i.DeferredLoading)) + { +
+ + @(step.ChildContent) + +
+ } +
+ +
+ @if (ButtonTemplate == null) + { + + @if (DisplayPreviousButton) + { + + @Localizer[Localization.LanguageResource.Wizard_LabelButtonPrevious] + + } + + @if (DisplayNextButton) + { + + @Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] + + } + else + { + + @Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] + + } + + } + else + { + @ButtonTemplate(Value) + } +
+
+
diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs new file mode 100644 index 0000000000..121ac75a1f --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -0,0 +1,395 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// A wizard component that provides a step-by-step user interface. +/// +public partial class FluentWizard : FluentComponentBase +{ + private readonly List _steps = []; + internal int _maxStepVisited; + + /// + public FluentWizard(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + /// + protected string? ClassValue => DefaultClassBuilder + .AddClass("fluent-wizard") + .Build(); + + /// + protected string? StyleValue => DefaultStyleBuilder + .AddStyle("width", Width) + .AddStyle("height", Height) + .Build(); + + /// + /// Gets or sets the height of the wizard. + /// + [Parameter] + public string Height { get; set; } = "400px"; + + /// + /// Gets or sets the width of the wizard. + /// + [Parameter] + public string Width { get; set; } = "100%"; + + /// + /// Triggers when the done button is clicked. + /// + [Parameter] + public EventCallback OnFinish { get; set; } + + /// + /// Gets or sets the stepper position in the wizard (Top or Left). + /// + [Parameter] + public StepperPosition StepperPosition { get; set; } = StepperPosition.Left; + + /// + /// Gets or sets the stepper width (if position is Left) + /// or the stepper height (if position is Top). + /// + [Parameter] + public string? StepperSize { get; set; } + + /// + /// Gets or sets the space between two bullets (ex. 120px). + /// + [Parameter] + public string? StepperBulletSpace { get; set; } + + /// + /// Display a border of the Wizard. + /// + [Parameter] + public WizardBorder Border { get; set; } = WizardBorder.None; + + /// + /// Display a number on each step icon. Can be overridden by the step property. + /// + [Parameter] + public WizardStepStatus DisplayStepNumber { get; set; } = WizardStepStatus.None; + + /// + /// Gets or sets the step index of the current step. + /// This value is bindable. + /// + [Parameter] + public int Value { get; set; } + + /// + /// Triggers when the value has changed. + /// + [Parameter] + public EventCallback ValueChanged { get; set; } + + /// + /// Gets or sets the buttons section of the wizard. + /// This configuration overrides the whole rendering of the bottom-right section of the Wizard, + /// including the built-in buttons and thus provides a full control over it. + /// Custom Wizard buttons do not trigger the component OnChange and OnFinish events. + /// The OnChange event can be triggered using the method from your code. + /// + [Parameter] + public RenderFragment? ButtonTemplate { get; set; } + + /// + /// Gets or sets the wizard steps. Add WizardStep tags inside this tag. + /// + [Parameter] + public RenderFragment? Steps { get; set; } + + /// + /// Hide step titles and summaries on specified sizes (you can combine several values: GridItemHidden.Sm | GridItemHidden.Xl). + /// The default value is to adapt to mobile devices. + /// + [Parameter] + public GridItemHidden? StepTitleHiddenWhen { get; set; } = GridItemHidden.XsAndDown; + + /// + /// Gets or sets the way to navigate in the Wizard Steps. + /// Default is . + /// + [Parameter] + public WizardStepSequence StepSequence { get; set; } = WizardStepSequence.Linear; + + /// + protected override void OnParametersSet() + { + SetCurrentValue(Value); + base.OnParametersSet(); + } + + /// + protected virtual async Task OnNextHandlerAsync(MouseEventArgs e) + { + // Target step index + var targetIndex = Value; + do + { + targetIndex++; + } + while (_steps[targetIndex].Disabled && targetIndex < _steps.Count - 1); + + // StepChange event + var stepChangeArgs = await OnStepChangeHandlerAsync(targetIndex, true); + var isCanceled = stepChangeArgs?.IsCancelled ?? false; + + if (!isCanceled) + { + SetCurrentValue(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(Value); + } + + StateHasChanged(); + } + } + + /// + protected virtual async Task OnPreviousHandlerAsync(MouseEventArgs e) + { + // Target step index + var targetIndex = Value; + do + { + targetIndex--; + } + while (_steps[targetIndex].Disabled && targetIndex > 0); + + // StepChange event + var stepChangeArgs = await OnStepChangeHandlerAsync(targetIndex, false); + var isCanceled = stepChangeArgs?.IsCancelled ?? false; + + if (!isCanceled) + { + SetCurrentValue(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(Value); + } + + StateHasChanged(); + } + } + + /// + protected virtual async Task OnStepChangeHandlerAsync(int targetIndex, bool validateEditContexts) + { + var stepChangeArgs = new FluentWizardStepChangeEventArgs(targetIndex, _steps[targetIndex].Label); + + if (validateEditContexts) + { + var allEditContextsAreValid = _steps[Value].ValidateEditContexts(); + stepChangeArgs.IsCancelled = !allEditContextsAreValid; + + if (!allEditContextsAreValid) + { + await _steps[Value].InvokeOnInValidSubmitForEditFormsAsync(); + } + + if (!stepChangeArgs.IsCancelled && allEditContextsAreValid) + { + // Invoke the 'OnValidSubmit' handlers for the Edit Forms + await _steps[Value].InvokeOnValidSubmitForEditFormsAsync(); + } + + await _steps[Value].InvokeOnSubmitForEditFormsAsync(); + } + + return await OnStepChangeHandlerAsync(stepChangeArgs); + } + + /// + protected virtual async Task OnStepChangeHandlerAsync(FluentWizardStepChangeEventArgs args) + { + if (_steps[Value].OnChange.HasDelegate) + { + await _steps[Value].OnChange.InvokeAsync(args); + } + + if (_steps[Value].DeferredLoading && !args.IsCancelled) + { + _steps[Value].ClearEditFormAndContext(); + } + + return args; + } + + /// + protected virtual Task OnFinishHandlerAsync(MouseEventArgs e) + { + return FinishAsync(validateEditContexts: true); + } + + /// + /// Optionally validate and invoke the handler. + /// + /// Validate the EditContext. Default is false. + /// + public async Task FinishAsync(bool validateEditContexts = false) + { + if (validateEditContexts) + { + // Validate any form edit contexts + var allEditContextsAreValid = _steps[Value].ValidateEditContexts(); + if (!allEditContextsAreValid) + { + // Invoke the 'OnInvalidSubmit' handlers for the edit forms. + await _steps[Value].InvokeOnInValidSubmitForEditFormsAsync(); + return; + } + } + + // Invoke the 'OnValidSubmit' handlers for the edit forms. + await _steps[Value].InvokeOnValidSubmitForEditFormsAsync(); + await _steps[Value].InvokeOnSubmitForEditFormsAsync(); + + _steps[Value].Status = WizardStepStatus.Previous; + + if (OnFinish.HasDelegate) + { + await OnFinish.InvokeAsync(); + } + } + + /// + /// Navigate to the specified step, with or without validate the current EditContexts. + /// + /// Index number of the step to display + /// Validate the EditContext. Default is false. + /// + public Task GoToStepAsync(int step, bool validateEditContexts = false) + { + return ValidateAndGoToStepAsync(step, validateEditContexts); + } + + internal async Task ValidateAndGoToStepAsync(int targetIndex, bool validateEditContexts) + { + var stepChangeArgs = await OnStepChangeHandlerAsync(targetIndex, validateEditContexts); + var isCanceled = stepChangeArgs?.IsCancelled ?? false; + + if (!isCanceled) + { + SetCurrentValue(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(Value); + } + + StateHasChanged(); + } + } + + internal int AddStep(FluentWizardStep step) + { + _steps.Add(step); + var index = _steps.Count - 1; + + if (index == Value) + { + SetCurrentStatusToStep(index); + } + + try + { + StateHasChanged(); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("render handle is not yet assigned", StringComparison.OrdinalIgnoreCase)) + { + } + + return index; + } + + internal int StepCount => _steps.Count; + + internal void RemoveStep(FluentWizardStep step) + { + _steps.Remove(step); + } + + private void SetCurrentValue(int value) + { + Value = NormalizeValue(value); + _maxStepVisited = Math.Max(Value, _maxStepVisited); + + SetCurrentStatusToStep(Value); + } + + private int NormalizeValue(int value) + { + if (value < 0 || _steps.Count <= 0) + { + return 0; + } + + if (value > _steps.Count - 1) + { + return _steps.Count - 1; + } + + return value; + } + + private void SetCurrentStatusToStep(int stepIndex) + { + for (var i = 0; i < _steps.Count; i++) + { + // Step disabled + if (_steps[i].Disabled) + { + _steps[i].Status = WizardStepStatus.Next; + } + + // Step enabled + else + { + if (i < stepIndex) + { + _steps[i].Status = WizardStepStatus.Previous; + } + else if (i == stepIndex) + { + _steps[i].Status = WizardStepStatus.Current; + } + else + { + _steps[i].Status = WizardStepStatus.Next; + } + } + } + } + + private string? GetStepperWidthOrHeight() + { + if (string.IsNullOrEmpty(StepperSize)) + { + return null; + } + + return StepperPosition switch + { + StepperPosition.Top => $"height: {StepperSize}", + StepperPosition.Left => $"width: {StepperSize}", + _ => null, + }; + } + + private bool DisplayPreviousButton => Value > 0 && _steps[..Value].Any(i => !i.Disabled); + + private bool DisplayNextButton => Value < _steps.Count - 1 && _steps[(Value + 1)..].Any(i => !i.Disabled); +} diff --git a/src/Core/Components/Wizard/FluentWizard.razor.css b/src/Core/Components/Wizard/FluentWizard.razor.css new file mode 100644 index 0000000000..c948191cc7 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizard.razor.css @@ -0,0 +1,80 @@ +.fluent-wizard { + display: grid; + height: 100%; + --fluent-wizard-circle-size: 24px; + --fluent-wizard-spacing: 4px; +} + + .fluent-wizard[border-outside] { + border: 1px solid var(--colorNeutralStroke1); + } + + .fluent-wizard > ol { + display: flex; + list-style-type: none; + padding-inline-start: 0px; + margin-block-start: 0px; + margin-block-end: 0px; + padding: 10px; + } + + .fluent-wizard .fluent-wizard-buttons { + display: flex; + justify-content: end; + } + + .fluent-wizard .fluent-wizard-buttons[border-inside] { + border-top: 1px solid var(--colorNeutralStroke1); + } + + /* Wizard with steps on Left */ + .fluent-wizard[position="left"] { + grid-template-columns: auto 1fr; + grid-template-rows: 1fr auto; + } + + .fluent-wizard[position="left"] > ol { + flex-direction: column; + grid-column: 1; + grid-row: 1 / span 2; + } + + .fluent-wizard[position="left"] > ol[border-inside] { + border-right: 1px solid var(--colorNeutralStroke1); + } + + .fluent-wizard[position="left"] .fluent-wizard-content { + grid-column: 2; + grid-row: 1; + margin: 5px 10px 0px 15px; + } + + .fluent-wizard[position="left"] .fluent-wizard-buttons { + grid-column: 2; + grid-row: 2; + text-align-last: end; + padding: 10px; + } + + /* Wizard with steps on Top */ + .fluent-wizard[position="top"] { + grid-template-columns: auto; + grid-template-rows: auto 1fr auto; + } + + .fluent-wizard[position="top"] > ol { + flex-direction: row; + justify-content: center; + } + + .fluent-wizard[position="top"] > ol[border-inside] { + border-bottom: 1px solid var(--colorNeutralStroke1); + } + + .fluent-wizard[position="top"] .fluent-wizard-content { + margin: 5px 10px 0px 10px; + } + + .fluent-wizard[position="top"] .fluent-wizard-buttons { + padding: 10px; + } diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor b/src/Core/Components/Wizard/FluentWizardStep.razor new file mode 100644 index 0000000000..a391f585a0 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor @@ -0,0 +1,45 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@inherits FluentComponentBase + +
  • + @if (StepTemplate is null) + { +
    + + + @if (DisplayStepNumber ?? FluentWizard.DisplayStepNumber.HasFlag(Status)) + { +
    + @(Index + 1) +
    + } +
    +
    + @Label + @if (!string.IsNullOrEmpty(Summary)) + { + @Summary + } +
    + } + else + { + @StepTemplate(new FluentWizardStepArgs(Index, FluentWizard.Value)) + } + + @if (!IsLastStep) + { +
    +
    + } +
  • diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs new file mode 100644 index 0000000000..efeeda710f --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -0,0 +1,291 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Represents an individual step within a component. +/// +public partial class FluentWizardStep : FluentComponentBase +{ + private readonly Dictionary _editForms = []; + private readonly List _editContexts = []; + + /// + public FluentWizardStep(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + /// + protected string? ClassValue => DefaultClassBuilder.Build(); + + /// + protected string? StyleValue => DefaultStyleBuilder + .AddStyle("position", "relative") + .AddStyle("display", "flex") + .AddStyle("gap", "10px", when: FluentWizard.StepperPosition == StepperPosition.Left) + .AddStyle("flex-direction", "column", when: FluentWizard.StepperPosition == StepperPosition.Top) + .AddStyle("align-items", "center", when: FluentWizard.StepperPosition == StepperPosition.Top) + .AddStyle("flex", "1", when: FluentWizard.StepperPosition == StepperPosition.Top) + .AddStyle("text-align", "center", when: FluentWizard.StepperPosition == StepperPosition.Top) + .AddStyle("max-width", FluentWizard.StepperBulletSpace ?? "100%", when: FluentWizard.StepperPosition == StepperPosition.Top) + .AddStyle("height", IsLastStep ? "auto" : (FluentWizard.StepperBulletSpace ?? "100%"), when: FluentWizard.StepperPosition == StepperPosition.Left) + .AddStyle("cursor", "pointer", when: IsStepClickable) + .Build(); + + /// + /// Gets or sets the content of the step. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Gets or sets the template of the step icon. + /// + [Parameter] + public RenderFragment? StepTemplate { get; set; } + + /// + /// Gets the step index. + /// + public int Index { get; private set; } + + /// + /// Gets or sets whether the step is disabled. + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Render the Wizard Step content only when the Step is selected. + /// + [Parameter] + public bool DeferredLoading { get; set; } + + /// + /// Gets or sets the label of the step. + /// + [Parameter] + public string Label { get; set; } = string.Empty; + + /// + /// Display a number on the step icon. + /// By default, this is the value. + /// + [Parameter] + public bool? DisplayStepNumber { get; set; } + + /// + /// The OnChange event fires before the current step has changed. + /// The EventArgs contains a field of the targeted new step and a field to cancel the built-in action. + /// + [Parameter] + public EventCallback OnChange { get; set; } + + /// + /// Reference to the parent component. + /// For internal use only. + /// + [CascadingParameter] + internal FluentWizard FluentWizard { get; set; } = default!; + + /// + /// Gets or sets the summary of the step, to display near the label. + /// + [Parameter] + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the icon to display for the past/previous step. + /// By default, it is a checkmark circle. + /// + [Parameter] + public Icon IconPrevious { get; set; } = new CoreIcons.Filled.Size20.CheckmarkCircle(); + + /// + /// Gets or sets the icon to display for the current/active step. + /// By default, it is a filled circle. + /// + [Parameter] + public Icon IconCurrent { get; set; } = new CoreIcons.Filled.Size20.Circle(); + + /// + /// Gets or sets the icon to display for the future/next step. + /// By default, it is a regular circle. + /// + [Parameter] + public Icon IconNext { get; set; } = new CoreIcons.Regular.Size20.Circle(); + + internal WizardStepStatus Status { get; set; } = WizardStepStatus.Next; + + private bool IsLastStep => Index >= FluentWizard.StepCount - 1; + + private string IconStyle => "width: var(--fluent-wizard-circle-size);" + + (Disabled ? " fill-opacity: 0.4;" : string.Empty); + + private Icon StepIcon + { + get + { + return Status switch + { + WizardStepStatus.Previous => IconPrevious, + WizardStepStatus.Current => IconCurrent, + WizardStepStatus.Next => IconNext, + _ => new CoreIcons.Regular.Size20.Circle(), + }; + } + } + + /// + protected override void OnInitialized() + { + if (FluentWizard == null) + { + throw new ArgumentException("The FluentWizardStep must be included in the FluentWizard component."); + } + + Index = FluentWizard.AddStep(this); + base.OnInitialized(); + } + + /// + public override async ValueTask DisposeAsync() + { + FluentWizard?.RemoveStep(this); + await base.DisposeAsync(); + } + + /// + /// Registers an EditForm and its EditContext for validation tracking. + /// + public void RegisterEditFormAndContext(EditForm editForm, EditContext editContext) + { + _editForms.TryAdd(editForm, editContext); + } + + /// + /// Clears all registered EditForm and EditContext pairs. + /// + internal void ClearEditFormAndContext() + { + _editForms.Clear(); + _editContexts.Clear(); + } + + /// + /// Registers an for validation tracking. + /// This is typically called by the component. + /// + internal void RegisterEditContext(EditContext editContext) + { + if (!_editContexts.Contains(editContext)) + { + _editContexts.Add(editContext); + } + } + + /// + /// Unregisters an from validation tracking. + /// + internal void UnregisterEditContext(EditContext editContext) + { + _editContexts.Remove(editContext); + } + + /// + /// Validates all registered EditContexts. + /// + public bool ValidateEditContexts() + { + var isValid = true; + foreach (var editForm in _editForms) + { + var contextIsValid = editForm.Value.Validate(); + if (!contextIsValid) + { + isValid = false; + } + } + + foreach (var editContext in _editContexts) + { + var contextIsValid = editContext.Validate(); + if (!contextIsValid) + { + isValid = false; + } + } + + return isValid; + } + + internal async Task InvokeOnValidSubmitForEditFormsAsync() + { + foreach (var editForm in _editForms) + { + await editForm.Key.OnValidSubmit.InvokeAsync(editForm.Value); + } + } + + internal async Task InvokeOnInValidSubmitForEditFormsAsync() + { + foreach (var editForm in _editForms) + { + await editForm.Key.OnInvalidSubmit.InvokeAsync(editForm.Value); + } + } + + internal async Task InvokeOnSubmitForEditFormsAsync() + { + foreach (var editForm in _editForms) + { + await editForm.Key.OnSubmit.InvokeAsync(editForm.Value); + } + } + + private async Task OnClickHandlerAsync() + { + if (!IsStepClickable) + { + return; + } + + await FluentWizard.ValidateAndGoToStepAsync(Index, validateEditContexts: Index > FluentWizard.Value); + } + + private bool IsStepClickable + { + get + { + if (Disabled) + { + return false; + } + + if (FluentWizard.Value == Index) + { + return false; + } + + if (FluentWizard.StepSequence == WizardStepSequence.Linear) + { + return false; + } + + if (FluentWizard.StepSequence == WizardStepSequence.Visited && + Index > FluentWizard._maxStepVisited) + { + return false; + } + + return true; + } + } +} diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.css b/src/Core/Components/Wizard/FluentWizardStep.razor.css new file mode 100644 index 0000000000..4cb0865901 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.css @@ -0,0 +1,66 @@ +/* Icon */ +.fluent-wizard-icon { + position: relative; + width: var(--fluent-wizard-circle-size); + height: var(--fluent-wizard-circle-size); +} + +/* Icon Number */ +.fluent-wizard-icon-number { + position: absolute; + top: 0; + left: 0; + width: var(--fluent-wizard-circle-size); + height: var(--fluent-wizard-circle-size); + font-size: small; + color: var(--colorBrandForeground1); + text-align: center; + align-content: center; +} + +.fluent-wizard-icon-number[disabled] { + opacity: 0.4; +} + +.fluent-wizard-icon-number[status='previous']:not([disabled]) { + color: var(--colorNeutralForegroundOnBrand); +} + +.fluent-wizard-icon-number[status='current']:not([disabled]) { + color: var(--colorNeutralForegroundOnBrand); +} + +.fluent-wizard-icon-number[status='next']:not([disabled]) { + color: var(--colorBrandForeground1); +} + +/* Connector line between steps */ +.fluent-wizard-step-connector[position="left"] { + position: absolute; + left: 0; + top: calc(var(--fluent-wizard-circle-size) + var(--fluent-wizard-spacing)); + bottom: var(--fluent-wizard-spacing); + transform: translateX(calc(var(--fluent-wizard-circle-size) / 2)); + width: 2px; + background-color: var(--colorNeutralStroke1); +} + +[dir="rtl"] .fluent-wizard-step-connector[position="left"] { + left: revert; + transform: translateX(calc(var(--fluent-wizard-circle-size) / -2)); +} + +.fluent-wizard-step-connector[position="top"] { + position: absolute; + left: calc(50% + calc(var(--fluent-wizard-circle-size) / 2 + var(--fluent-wizard-spacing))); + right: unset; + top: calc(var(--fluent-wizard-circle-size) / 2); + width: calc(100% - var(--fluent-wizard-circle-size) - calc(var(--fluent-wizard-spacing) * 2)); + height: 2px; + background-color: var(--colorNeutralStroke1); +} + +[dir="rtl"] .fluent-wizard-step-connector[position="top"] { + left: unset; + right: calc(50% + calc(var(--fluent-wizard-circle-size) / 2 + var(--fluent-wizard-spacing))); +} diff --git a/src/Core/Components/Wizard/FluentWizardStepArgs.cs b/src/Core/Components/Wizard/FluentWizardStepArgs.cs new file mode 100644 index 0000000000..82472c70fd --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStepArgs.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Arguments passed to the render fragment. +/// +public class FluentWizardStepArgs +{ + internal FluentWizardStepArgs(int index, int active) + { + Index = index; + Active = index == active; + } + + /// + /// Gets the index of the step. + /// + public int Index { get; } + + /// + /// Gets a value indicating whether the step is the currently active step. + /// + public bool Active { get; } +} diff --git a/src/Core/Components/Wizard/FluentWizardStepChangeEventArgs.cs b/src/Core/Components/Wizard/FluentWizardStepChangeEventArgs.cs new file mode 100644 index 0000000000..1cb602bb46 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStepChangeEventArgs.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Event arguments for the event. +/// +public class FluentWizardStepChangeEventArgs +{ + /// + internal FluentWizardStepChangeEventArgs(int targetIndex, string targetLabel) + { + TargetIndex = targetIndex; + TargetLabel = targetLabel; + } + + /// + /// Gets the index of the target step. + /// + public int TargetIndex { get; } + + /// + /// Gets the label of the target step. + /// + public string TargetLabel { get; } + + /// + /// Gets or sets a value indicating whether the step change should be cancelled. + /// + public bool IsCancelled { get; set; } +} diff --git a/src/Core/Components/Wizard/FluentWizardStepValidator.cs b/src/Core/Components/Wizard/FluentWizardStepValidator.cs new file mode 100644 index 0000000000..5d7541a292 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStepValidator.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// A component that automatically registers an with a parent +/// for validation when navigating between wizard steps. +/// Place this component inside an within a . +/// +public class FluentWizardStepValidator : ComponentBase, IDisposable +{ + [CascadingParameter] + private FluentWizardStep? WizardStep { get; set; } + + [CascadingParameter] + private EditContext? EditContext { get; set; } + + /// + protected override void OnInitialized() + { + if (WizardStep is null) + { + throw new InvalidOperationException( + $"{nameof(FluentWizardStepValidator)} must be used inside a {nameof(FluentWizardStep)}."); + } + + if (EditContext is null) + { + throw new InvalidOperationException( + $"{nameof(FluentWizardStepValidator)} must be used inside an {nameof(EditForm)}."); + } + + WizardStep.RegisterEditContext(EditContext); + } + + /// + public void Dispose() + { + if (WizardStep is not null && EditContext is not null) + { + WizardStep.UnregisterEditContext(EditContext); + } + } +} diff --git a/src/Core/Enums/StepperPosition.cs b/src/Core/Enums/StepperPosition.cs new file mode 100644 index 0000000000..c4dd6bea25 --- /dev/null +++ b/src/Core/Enums/StepperPosition.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Defines the position of the stepper in the component. +/// +public enum StepperPosition +{ + /// + /// Steps are displayed at the top of the wizard. + /// + Top, + + /// + /// Steps are displayed on the left side of the wizard. + /// + Left, +} diff --git a/src/Core/Enums/WizardBorder.cs b/src/Core/Enums/WizardBorder.cs new file mode 100644 index 0000000000..75808c18b3 --- /dev/null +++ b/src/Core/Enums/WizardBorder.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Defines the border display options for the component. +/// +[Flags] +public enum WizardBorder +{ + /// + /// No border. + /// + None = 0, + + /// + /// Border inside (between sections). + /// + Inside = 1, + + /// + /// Border outside (around the wizard). + /// + Outside = 2, + + /// + /// Both inside and outside borders. + /// + All = Inside | Outside, +} diff --git a/src/Core/Enums/WizardStepSequence.cs b/src/Core/Enums/WizardStepSequence.cs new file mode 100644 index 0000000000..29de9c30f5 --- /dev/null +++ b/src/Core/Enums/WizardStepSequence.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Defines the navigation mode for wizard steps. +/// +public enum WizardStepSequence +{ + /// + /// The user can go to the next/previous step only, using the Next/Previous button. + /// + Linear, + + /// + /// The user can go to any steps (not disabled) clicking on an item. + /// + Any, + + /// + /// The user can go to the next step using the Next button, + /// or go to any previous step, already visited. + /// + Visited, +} diff --git a/src/Core/Enums/WizardStepStatus.cs b/src/Core/Enums/WizardStepStatus.cs new file mode 100644 index 0000000000..97f25858b4 --- /dev/null +++ b/src/Core/Enums/WizardStepStatus.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; + +namespace Microsoft.FluentUI.AspNetCore.Components; + +/// +/// Defines the status of a wizard step. +/// +[Flags] +public enum WizardStepStatus +{ + /// + /// No status. + /// + [Description("none")] + None = 0, + + /// + /// The step has been completed. + /// + [Description("previous")] + Previous = 1, + + /// + /// The step is the current active step. + /// + [Description("current")] + Current = 2, + + /// + /// The step has not been visited yet. + /// + [Description("next")] + Next = 4, + + /// + /// All statuses. + /// + [Description("all")] + All = Previous | Current | Next, +} diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index ded2d0c36c..8724a3be6d 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -354,4 +354,13 @@ The maximum number of {0} selected items has been reached. - \ No newline at end of file + + Previous + + + Next + + + Done + + diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs index 33de7a6256..5397342dec 100644 --- a/tests/Core/Components/Base/ComponentBaseTests.cs +++ b/tests/Core/Components/Base/ComponentBaseTests.cs @@ -71,6 +71,7 @@ public class ComponentBaseTests : Bunit.BunitContext { typeof(FluentNavSectionHeader), Loader.Default.WithCascadingValue(new FluentNav(new LibraryConfiguration())) }, { typeof(FluentAppBarItem), Loader.Default.WithCascadingValue(new InternalAppBarContext(new FluentAppBar(new LibraryConfiguration()))) }, { typeof(FluentSortableList<>), Loader.MakeGenericType(typeof(string)).WithRequiredParameter("ItemTemplate", (RenderFragment)(p => builder => builder.AddContent(0, "MyItemTemplate")))}, + { typeof(FluentWizardStep), Loader.Default.WithCascadingValue(new FluentWizard(new LibraryConfiguration())) }, }; /// diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html new file mode 100644 index 0000000000..6c32129d2f --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html @@ -0,0 +1,39 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    Content 2
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ButtonTemplate.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ButtonTemplate.verified.razor.html new file mode 100644 index 0000000000..0a6871650b --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ButtonTemplate.verified.razor.html @@ -0,0 +1,56 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    +
    +
    +
    + + Next + + Go to last page +
    +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html new file mode 100644 index 0000000000..87fdf077e1 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html @@ -0,0 +1,39 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    Content 2
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html new file mode 100644 index 0000000000..26aeb84f64 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html @@ -0,0 +1,51 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html new file mode 100644 index 0000000000..3ae935efbd --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html @@ -0,0 +1,38 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html new file mode 100644 index 0000000000..cab85167d1 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html @@ -0,0 +1,51 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Disabled +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Disabled content
    +
    Content 3
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html new file mode 100644 index 0000000000..e3975032b6 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html @@ -0,0 +1,53 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    + + Previous + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html new file mode 100644 index 0000000000..6bceceea20 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html @@ -0,0 +1,41 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    Content 2
    +
    +
    + + Previous + + + Done +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html new file mode 100644 index 0000000000..26aeb84f64 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html @@ -0,0 +1,51 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html new file mode 100644 index 0000000000..c26f52bdf3 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html @@ -0,0 +1,53 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    + + Previous + + + Done +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html new file mode 100644 index 0000000000..b5d5578f7b --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html @@ -0,0 +1,42 @@ + + + +
    +
      +
    1. +
      + Intro +
      +
      +
    2. +
    3. +
      + Details +
      +
      +
    4. +
    5. +
      + Conclusion +
      +
    6. +
    +
    +
    + Content 1 +
    +
    + Content 2 +
    +
    + Content 3 +
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html new file mode 100644 index 0000000000..87fdf077e1 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html @@ -0,0 +1,39 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    Content 2
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html new file mode 100644 index 0000000000..e3975032b6 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html @@ -0,0 +1,53 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
      +
    4. +
    5. +
      + +
      +
      + Step 3 +
      +
    6. +
    +
    +
    Content 1
    +
    Content 2
    +
    Content 3
    +
    +
    + + Previous + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html new file mode 100644 index 0000000000..87fdf077e1 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html @@ -0,0 +1,39 @@ + + + +
    +
      +
    1. +
      + +
      +
      + Step 1 +
      +
      +
    2. +
    3. +
      + +
      +
      + Step 2 +
      +
    4. +
    +
    +
    Content 1
    +
    Content 2
    +
    +
    + + + Next +
    +
    + + \ No newline at end of file diff --git a/tests/Core/Components/Wizard/FluentWizardTests.razor b/tests/Core/Components/Wizard/FluentWizardTests.razor new file mode 100644 index 0000000000..5271a6e7ec --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.razor @@ -0,0 +1,380 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.Tests.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.Utilities +@using Xunit; +@inherits FluentUITestContext + +@code +{ + public FluentWizardTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddFluentUIComponents(); + } + + [Fact] + public void FluentWizard_Default() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_WithSteps() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_NextButton() + { + // Arrange + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + ); + + // Act + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + Assert.Equal(1, value); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_PreviousButton() + { + // Arrange + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + ); + + // Act - click Next then Previous + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + Assert.Equal(1, value); + + var prevButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Previous"); + prevButton.Click(); + Assert.Equal(0, value); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_DisabledStep() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Disabled content + Content 3 + + ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_OnFinish() + { + // Arrange + var finishCalled = false; + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Act - navigate to last step, click Done + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + Assert.Equal(1, value); + + var doneButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Done"); + doneButton.Click(); + Assert.True(finishCalled); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_ValueBinding() + { + // Arrange + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + ); + + // Act + Assert.Equal(0, value); + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + Assert.Equal(1, value); + + // Assert + cut.Verify(); + } + + [Theory] + [InlineData(StepperPosition.Top, "top")] + [InlineData(StepperPosition.Left, "left")] + public void FluentWizard_StepperPosition(StepperPosition position, string expectedValue) + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Assert + var attribute = cut.Find("div").GetAttribute("position") ?? string.Empty; + Assert.Equal(expectedValue, attribute); ; + } + + [Fact] + public void FluentWizard_Border() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_DeferredLoading() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Deferred content + + ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_CancelStepChange() + { + // Arrange + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Act - try to click next + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + Assert.Equal(0, value); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_ButtonTemplate() + { + FluentWizard wizard = default!; + int Value = 0; + + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + + @{ + var index = context; + var lastStepIndex = 3; + +
    + @if (index > 0) + { + Go to first page + Previous + } +
    + +
    + @if (index != lastStepIndex) + { + Next + Go to last page + } + else + { + Finish + } +
    + } +
    +
    ); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_StepSequenceAny() + { + // Arrange + var value = 0; + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + ); + + // Act - click on step 3 directly + var steps = cut.FindAll("li"); + steps[2].Click(); + Assert.Equal(2, value); + + // Assert + cut.Verify(); + } + + [Fact] + public void FluentWizard_ChangeEventArgs() + { + // Arrange + FluentWizardStepChangeEventArgs? eventArgs = null; + + var cut = Render( + @ + + Content 1 + Content 2 + Content 3 + + + ); + + // Act + var nextButton = cut.FindAll("fluent-button").First(b => b.TextContent.Trim() == "Next"); + nextButton.Click(); + + // Assert + Assert.NotNull(eventArgs); + Assert.Equal(1, eventArgs.TargetIndex); + Assert.Equal("Step 2", eventArgs.TargetLabel); + Assert.False(eventArgs.IsCancelled); + } + + + [Fact] + public void FluentWizard_StepTemplate() + { + + // Arrange && Act + var cut = Render( + @ + + + +
    + Intro +
    +
    + + Content 1 + +
    + + +
    + Details +
    +
    + + Content 2 + +
    + + +
    + Conclusion +
    +
    + + Content 3 + +
    +
    +
    ); + + // Assert + cut.Verify(); + } +}