From b9a109dfc14bcd92b96053910df4f41bb51a214a Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 13:37:55 +0100 Subject: [PATCH 01/27] Wizard component --- .../Wizard/Examples/WizardCustomized.razor | 113 +++++ .../Wizard/Examples/WizardDefault.razor | 73 ++++ .../Wizard/Examples/WizardEditForms.razor | 140 +++++++ .../Components/Wizard/FluentWizard.md | 61 +++ .../Migration/MigrationFluentWizard.md | 86 +--- src/Core/Components/Icons/CoreIcons.cs | 4 + src/Core/Components/Wizard/FluentWizard.razor | 69 +++ .../Components/Wizard/FluentWizard.razor.cs | 395 ++++++++++++++++++ .../Components/Wizard/FluentWizard.razor.css | 80 ++++ .../Components/Wizard/FluentWizardStep.razor | 42 ++ .../Wizard/FluentWizardStep.razor.cs | 300 +++++++++++++ .../Wizard/FluentWizardStep.razor.css | 103 +++++ .../Components/Wizard/FluentWizardStepArgs.cs | 27 ++ .../Wizard/FluentWizardStepChangeEventArgs.cs | 33 ++ .../Wizard/FluentWizardStepValidator.cs | 49 +++ src/Core/Enums/StepperPosition.cs | 21 + src/Core/Enums/WizardBorder.cs | 32 ++ src/Core/Enums/WizardStepSequence.cs | 27 ++ src/Core/Enums/WizardStepStatus.cs | 42 ++ ...ts.FluentWizard_Border.verified.razor.html | 39 ++ ...izard_CancelStepChange.verified.razor.html | 39 ++ ...s.FluentWizard_Default.verified.razor.html | 51 +++ ...Wizard_DeferredLoading.verified.razor.html | 38 ++ ...entWizard_DisabledStep.verified.razor.html | 51 +++ ...luentWizard_NextButton.verified.razor.html | 53 +++ ....FluentWizard_OnFinish.verified.razor.html | 41 ++ ...tWizard_PreviousButton.verified.razor.html | 51 +++ ...Wizard_StepSequenceAny.verified.razor.html | 53 +++ ...ard_StepperPositionTop.verified.razor.html | 39 ++ ...entWizard_ValueBinding.verified.razor.html | 53 +++ ...FluentWizard_WithSteps.verified.razor.html | 39 ++ .../Components/Wizard/FluentWizardTests.razor | 258 ++++++++++++ 32 files changed, 2435 insertions(+), 67 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardCustomized.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardEditForms.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md create mode 100644 src/Core/Components/Wizard/FluentWizard.razor create mode 100644 src/Core/Components/Wizard/FluentWizard.razor.cs create mode 100644 src/Core/Components/Wizard/FluentWizard.razor.css create mode 100644 src/Core/Components/Wizard/FluentWizardStep.razor create mode 100644 src/Core/Components/Wizard/FluentWizardStep.razor.cs create mode 100644 src/Core/Components/Wizard/FluentWizardStep.razor.css create mode 100644 src/Core/Components/Wizard/FluentWizardStepArgs.cs create mode 100644 src/Core/Components/Wizard/FluentWizardStepChangeEventArgs.cs create mode 100644 src/Core/Components/Wizard/FluentWizardStepValidator.cs create mode 100644 src/Core/Enums/StepperPosition.cs create mode 100644 src/Core/Enums/WizardBorder.cs create mode 100644 src/Core/Enums/WizardStepSequence.cs create mode 100644 src/Core/Enums/WizardStepStatus.cs create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.razor 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..57dad5302e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor @@ -0,0 +1,73 @@ + + + + WizardStepSequence: + + + + + + + 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 +{ + bool IsTop = false; + WizardStepSequence StepSequence = WizardStepSequence.Linear; + + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + } + + async Task OnFinishedAsync() + { + await Task.CompletedTask; + } +} 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/FluentWizard.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md new file mode 100644 index 0000000000..f682d31550 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md @@ -0,0 +1,61 @@ +--- +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 }} + +## 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..4aeab0a69e --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -0,0 +1,69 @@ +@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) + { + string buttonWidth = "80px;"; + + @if (DisplayPreviousButton) + { + + @LabelButtonPrevious + + } + + + + @if (DisplayNextButton) + { + + @LabelButtonNext + + } + else + { + + @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..9b0c05c31f --- /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 +{ + /// + /// Gets or sets the label for the Previous button. + /// + public static string LabelButtonPrevious { get; set; } = "Previous"; + + /// + /// Gets or sets the label for the Next button. + /// + public static string LabelButtonNext { get; set; } = "Next"; + + /// + /// Gets or sets the label for the Done button. + /// + public static string LabelButtonDone { get; set; } = "Done"; + + private readonly List _steps = new(); + private int _value; + 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. + /// +#pragma warning disable BL0007 // Component parameters should be auto properties + [Parameter] + public int Value +#pragma warning restore BL0007 + { + get + { + return _value; + } + + set + { + if (value < 0 || _steps.Count <= 0) + { + _value = 0; + } + else if (value > _steps.Count - 1) + { + _value = _steps.Count - 1; + } + else + { + _value = value; + } + + _maxStepVisited = Math.Max(_value, _maxStepVisited); + + SetCurrentStatusToStep(_value); + } + } + + /// + /// 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 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) + { + Value = targetIndex; + await ValueChanged.InvokeAsync(targetIndex); + 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) + { + Value = targetIndex; + await ValueChanged.InvokeAsync(targetIndex); + 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 async Task OnFinishHandlerAsync(MouseEventArgs e) + { + await FinishAsync(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) + { + Value = targetIndex; + await ValueChanged.InvokeAsync(targetIndex); + StateHasChanged(); + } + } + + internal int AddStep(FluentWizardStep step) + { + _steps.Add(step); + var index = _steps.Count - 1; + + if (index == Value) + { + SetCurrentStatusToStep(index); + } + + StateHasChanged(); + + return index; + } + + internal int StepCount => _steps.Count; + + internal void RemoveStep(FluentWizardStep step) + { + _steps.Remove(step); + } + + 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; + } + + switch (StepperPosition) + { + case StepperPosition.Top: + return $"height: {StepperSize}"; + + case StepperPosition.Left: + return $"width: {StepperSize}"; + } + + return 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..4177ef0bb3 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor @@ -0,0 +1,42 @@ +@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..85da886171 --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -0,0 +1,300 @@ +// ------------------------------------------------------------------------ +// 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 = new Dictionary(); + private readonly List _editContexts = new List(); + + /// + 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; } = false; + + /// + /// Render the Wizard Step content only when the Step is selected. + /// + [Parameter] + public bool DeferredLoading { get; set; } = false; + + /// + /// 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] + public 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 + { + switch (Status) + { + case WizardStepStatus.Previous: + return IconPrevious; + + case WizardStepStatus.Current: + return IconCurrent; + + case WizardStepStatus.Next: + return IconNext; + + default: + return 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) + { + if (!_editForms.ContainsKey(editForm)) + { + _editForms.Add(editForm, editContext); + } + } + + /// + /// Clears all registered EditForm and EditContext pairs. + /// + public void ClearEditFormAndContext() + { + _editForms.Clear(); + } + + /// + /// Registers an for validation tracking. + /// This is typically called by the component. + /// + public void RegisterEditContext(EditContext editContext) + { + if (!_editContexts.Contains(editContext)) + { + _editContexts.Add(editContext); + } + } + + /// + /// Unregisters an from validation tracking. + /// + public 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..4df4551bbc --- /dev/null +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.css @@ -0,0 +1,103 @@ +/* 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))); +} + +/* Hidden (responsive) */ +@media (max-width: 599.98px) { + div[hidden-when~="xs"] { + display: none; + } +} + +@media (min-width: 600px) and (max-width: 959.98px) { + div[hidden-when~="sm"] { + display: none; + } +} + +@media (min-width: 960px) and (max-width: 1279.98px) { + div[hidden-when~="md"] { + display: none; + } +} + +@media (min-width: 1280px) and (max-width: 1919.98px) { + div[hidden-when~="lg"] { + display: none; + } +} + +@media (min-width: 1920px) and (max-width: 2559.98px) { + div[hidden-when~="xl"] { + display: none; + } +} + +@media (min-width: 2560px) { + div[hidden-when~="xxl"] { + display: none; + } +} 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..ace05ba92a --- /dev/null +++ b/src/Core/Enums/WizardStepStatus.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------ +// 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. + /// + 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. + /// + All = Previous | Current | Next +} 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_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_StepperPositionTop.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html new file mode 100644 index 0000000000..b86a2ed5c4 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.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..8bda081241 --- /dev/null +++ b/tests/Core/Components/Wizard/FluentWizardTests.razor @@ -0,0 +1,258 @@ +@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(); + } + + [Fact] + public void FluentWizard_StepperPositionTop() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Assert + cut.Verify(); + } + + [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_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(); + } +} From c4b6e9b45e742841ef4b3b23682eb9769ebcb8bb Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 15:46:09 +0100 Subject: [PATCH 02/27] Fix ComponentBaseTests. Check if ValueChanged.HasDelegate. internal FluentWizard in FluentWizardStep --- .../Components/Wizard/FluentWizard.razor.cs | 26 ++++++++++++++++--- .../Wizard/FluentWizardStep.razor.cs | 2 +- .../Components/Base/ComponentBaseTests.cs | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 9b0c05c31f..a096290b93 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -187,7 +187,11 @@ protected virtual async Task OnNextHandlerAsync(MouseEventArgs e) if (!isCanceled) { Value = targetIndex; - await ValueChanged.InvokeAsync(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(targetIndex); + } + StateHasChanged(); } } @@ -210,7 +214,11 @@ protected virtual async Task OnPreviousHandlerAsync(MouseEventArgs e) if (!isCanceled) { Value = targetIndex; - await ValueChanged.InvokeAsync(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(targetIndex); + } + StateHasChanged(); } } @@ -314,7 +322,11 @@ internal async Task ValidateAndGoToStepAsync(int targetIndex, bool validateEditC if (!isCanceled) { Value = targetIndex; - await ValueChanged.InvokeAsync(targetIndex); + if (ValueChanged.HasDelegate) + { + await ValueChanged.InvokeAsync(targetIndex); + } + StateHasChanged(); } } @@ -329,7 +341,13 @@ internal int AddStep(FluentWizardStep step) SetCurrentStatusToStep(index); } - StateHasChanged(); + try + { + StateHasChanged(); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("render handle is not yet assigned", StringComparison.OrdinalIgnoreCase)) + { + } return index; } diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index 85da886171..09e601f991 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -93,7 +93,7 @@ public FluentWizardStep(LibraryConfiguration configuration) : base(configuration /// For internal use only. ///
    [CascadingParameter] - public FluentWizard FluentWizard { get; set; } = default!; + internal FluentWizard FluentWizard { get; set; } = default!; /// /// Gets or sets the summary of the step, to display near the label. diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs index 7a8e760f28..237ed44bb1 100644 --- a/tests/Core/Components/Base/ComponentBaseTests.cs +++ b/tests/Core/Components/Base/ComponentBaseTests.cs @@ -70,6 +70,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())) }, }; /// From cbd2561b5be418e0338083a85fb079b411be4d34 Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 15:48:55 +0100 Subject: [PATCH 03/27] WizardStepStatus : Add [Description("all")] to All --- src/Core/Enums/WizardStepStatus.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Enums/WizardStepStatus.cs b/src/Core/Enums/WizardStepStatus.cs index ace05ba92a..ce5c336fec 100644 --- a/src/Core/Enums/WizardStepStatus.cs +++ b/src/Core/Enums/WizardStepStatus.cs @@ -38,5 +38,6 @@ public enum WizardStepStatus /// /// All statuses. /// + [Description("all")] All = Previous | Current | Next } From ccab369a7bb53e568554553629e0cf9c1d1e399a Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 16:01:20 +0100 Subject: [PATCH 04/27] Retrieve @media styles in FluentWizardStep.razor.css from v4 --- .../Wizard/FluentWizardStep.razor.css | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.css b/src/Core/Components/Wizard/FluentWizardStep.razor.css index 4df4551bbc..a8c1f867d6 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.css +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.css @@ -65,39 +65,39 @@ right: calc(50% + calc(var(--fluent-wizard-circle-size) / 2 + var(--fluent-wizard-spacing))); } -/* Hidden (responsive) */ +/* Hidden */ @media (max-width: 599.98px) { - div[hidden-when~="xs"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="xs"] { + display: none; + } } @media (min-width: 600px) and (max-width: 959.98px) { - div[hidden-when~="sm"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="sm"] { + display: none; + } } @media (min-width: 960px) and (max-width: 1279.98px) { - div[hidden-when~="md"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="md"] { + display: none; + } } @media (min-width: 1280px) and (max-width: 1919.98px) { - div[hidden-when~="lg"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="lg"] { + display: none; + } } @media (min-width: 1920px) and (max-width: 2559.98px) { - div[hidden-when~="xl"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="xl"] { + display: none; + } } @media (min-width: 2560px) { - div[hidden-when~="xxl"] { - display: none; - } + .fluent-wizard > ol > li ::deep > div[hidden-when~="xxl"] { + display: none; + } } From 34c85ca034423457fb9d60946eba895b96550fee Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 16:11:11 +0100 Subject: [PATCH 05/27] Use Localizer for default label properties values --- src/Core/Components/Wizard/FluentWizard.razor | 8 +++---- .../Components/Wizard/FluentWizard.razor.cs | 21 ++++++++++++++++--- src/Core/Localization/LanguageResource.resx | 11 +++++++++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor b/src/Core/Components/Wizard/FluentWizard.razor index 4aeab0a69e..82a0ff44d0 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -1,4 +1,4 @@ -@namespace Microsoft.FluentUI.AspNetCore.Components +@namespace Microsoft.FluentUI.AspNetCore.Components @inherits FluentComponentBase @@ -37,7 +37,7 @@ - @LabelButtonPrevious + @LabelButtonPreviousValue } @@ -48,7 +48,7 @@ - @LabelButtonNext + @LabelButtonNextValue } else @@ -56,7 +56,7 @@ - @LabelButtonDone + @LabelButtonDoneValue } } diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index a096290b93..160fe4808b 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -15,18 +15,33 @@ public partial class FluentWizard : FluentComponentBase { /// /// Gets or sets the label for the Previous button. + /// If null or empty, the localized value is used. /// - public static string LabelButtonPrevious { get; set; } = "Previous"; + public static string? LabelButtonPrevious { get; set; } /// /// Gets or sets the label for the Next button. + /// If null or empty, the localized value is used. /// - public static string LabelButtonNext { get; set; } = "Next"; + public static string? LabelButtonNext { get; set; } /// /// Gets or sets the label for the Done button. + /// If null or empty, the localized value is used. /// - public static string LabelButtonDone { get; set; } = "Done"; + public static string? LabelButtonDone { get; set; } + + private string LabelButtonPreviousValue => string.IsNullOrWhiteSpace(LabelButtonPrevious) + ? Localizer[Localization.LanguageResource.Wizard_LabelButtonPrevious] + : LabelButtonPrevious; + + private string LabelButtonNextValue => string.IsNullOrWhiteSpace(LabelButtonNext) + ? Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] + : LabelButtonNext; + + private string LabelButtonDoneValue => string.IsNullOrWhiteSpace(LabelButtonDone) + ? Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] + : LabelButtonDone; private readonly List _steps = new(); private int _value; diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index 7eb4af6f18..95fab87e96 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -342,4 +342,13 @@ Toggle nesting - \ No newline at end of file + + Previous + + + Next + + + Done + + From 1db349c21fc6f50c8fb7ef2066376226c850b8f6 Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 16:22:50 +0100 Subject: [PATCH 06/27] Change Value to public int Value { get; set; } --- .../Components/Wizard/FluentWizard.razor.cs | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 160fe4808b..91e2a65271 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -44,7 +44,6 @@ public partial class FluentWizard : FluentComponentBase : LabelButtonDone; private readonly List _steps = new(); - private int _value; internal int _maxStepVisited; /// @@ -117,36 +116,8 @@ public FluentWizard(LibraryConfiguration configuration) : base(configuration) /// Gets or sets the step index of the current step. /// This value is bindable. /// -#pragma warning disable BL0007 // Component parameters should be auto properties [Parameter] - public int Value -#pragma warning restore BL0007 - { - get - { - return _value; - } - - set - { - if (value < 0 || _steps.Count <= 0) - { - _value = 0; - } - else if (value > _steps.Count - 1) - { - _value = _steps.Count - 1; - } - else - { - _value = value; - } - - _maxStepVisited = Math.Max(_value, _maxStepVisited); - - SetCurrentStatusToStep(_value); - } - } + public int Value { get; set; } /// /// Triggers when the value has changed. @@ -184,6 +155,13 @@ public int Value [Parameter] public WizardStepSequence StepSequence { get; set; } = WizardStepSequence.Linear; + /// + protected override void OnParametersSet() + { + SetCurrentValue(Value); + base.OnParametersSet(); + } + /// protected virtual async Task OnNextHandlerAsync(MouseEventArgs e) { @@ -201,10 +179,10 @@ protected virtual async Task OnNextHandlerAsync(MouseEventArgs e) if (!isCanceled) { - Value = targetIndex; + SetCurrentValue(targetIndex); if (ValueChanged.HasDelegate) { - await ValueChanged.InvokeAsync(targetIndex); + await ValueChanged.InvokeAsync(Value); } StateHasChanged(); @@ -228,10 +206,10 @@ protected virtual async Task OnPreviousHandlerAsync(MouseEventArgs e) if (!isCanceled) { - Value = targetIndex; + SetCurrentValue(targetIndex); if (ValueChanged.HasDelegate) { - await ValueChanged.InvokeAsync(targetIndex); + await ValueChanged.InvokeAsync(Value); } StateHasChanged(); @@ -336,10 +314,10 @@ internal async Task ValidateAndGoToStepAsync(int targetIndex, bool validateEditC if (!isCanceled) { - Value = targetIndex; + SetCurrentValue(targetIndex); if (ValueChanged.HasDelegate) { - await ValueChanged.InvokeAsync(targetIndex); + await ValueChanged.InvokeAsync(Value); } StateHasChanged(); @@ -374,6 +352,29 @@ 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++) From 3e52b8550df15d7426d3835fa70b302b8a7dcefe Mon Sep 17 00:00:00 2001 From: agriffard Date: Fri, 20 Mar 2026 16:26:11 +0100 Subject: [PATCH 07/27] Add [Description("none")] to None in WizardStepStatus --- src/Core/Enums/WizardStepStatus.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Enums/WizardStepStatus.cs b/src/Core/Enums/WizardStepStatus.cs index ce5c336fec..2485f0af4b 100644 --- a/src/Core/Enums/WizardStepStatus.cs +++ b/src/Core/Enums/WizardStepStatus.cs @@ -15,6 +15,7 @@ public enum WizardStepStatus /// /// No status. /// + [Description("none")] None = 0, /// From 7da6b3ec2f37d6899d61d8b4394cd480ed3d8101 Mon Sep 17 00:00:00 2001 From: Marvin Klein <32510006+MarvinKlein1508@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:43:48 +0200 Subject: [PATCH 08/27] Code review fixes --- .../Components/Wizard/FluentWizard.razor.cs | 4 +-- .../Wizard/FluentWizardStep.razor.cs | 32 +++++++------------ src/Core/Enums/WizardStepStatus.cs | 2 +- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 91e2a65271..66a4042d9e 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -260,9 +260,9 @@ protected virtual async Task OnStepChangeHandle } /// - protected virtual async Task OnFinishHandlerAsync(MouseEventArgs e) + protected virtual Task OnFinishHandlerAsync(MouseEventArgs e) { - await FinishAsync(true); + return FinishAsync(validateEditContexts: true); } /// diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index 09e601f991..6a0801b637 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -13,8 +13,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial class FluentWizardStep : FluentComponentBase { - private readonly Dictionary _editForms = new Dictionary(); - private readonly List _editContexts = new List(); + private readonly Dictionary _editForms = []; + private readonly List _editContexts = []; /// public FluentWizardStep(LibraryConfiguration configuration) : base(configuration) @@ -60,13 +60,13 @@ public FluentWizardStep(LibraryConfiguration configuration) : base(configuration /// Gets or sets whether the step is disabled. /// [Parameter] - public bool Disabled { get; set; } = false; + public bool Disabled { get; set; } /// /// Render the Wizard Step content only when the Step is selected. /// [Parameter] - public bool DeferredLoading { get; set; } = false; + public bool DeferredLoading { get; set; } /// /// Gets or sets the label of the step. @@ -133,20 +133,13 @@ private Icon StepIcon { get { - switch (Status) + return Status switch { - case WizardStepStatus.Previous: - return IconPrevious; - - case WizardStepStatus.Current: - return IconCurrent; - - case WizardStepStatus.Next: - return IconNext; - - default: - return new CoreIcons.Regular.Size20.Circle(); - } + WizardStepStatus.Previous => IconPrevious, + WizardStepStatus.Current => IconCurrent, + WizardStepStatus.Next => IconNext, + _ => new CoreIcons.Regular.Size20.Circle(), + }; } } @@ -174,10 +167,7 @@ public override async ValueTask DisposeAsync() /// public void RegisterEditFormAndContext(EditForm editForm, EditContext editContext) { - if (!_editForms.ContainsKey(editForm)) - { - _editForms.Add(editForm, editContext); - } + _editForms.TryAdd(editForm, editContext); } /// diff --git a/src/Core/Enums/WizardStepStatus.cs b/src/Core/Enums/WizardStepStatus.cs index 2485f0af4b..97f25858b4 100644 --- a/src/Core/Enums/WizardStepStatus.cs +++ b/src/Core/Enums/WizardStepStatus.cs @@ -40,5 +40,5 @@ public enum WizardStepStatus /// All statuses. /// [Description("all")] - All = Previous | Current | Next + All = Previous | Current | Next, } From 7ab0ccfc36a2cc522c1afd8f1a26b763291cc0fb Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 21:52:54 +0200 Subject: [PATCH 09/27] Remove hidden styles --- .../Wizard/FluentWizardStep.razor.css | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.css b/src/Core/Components/Wizard/FluentWizardStep.razor.css index a8c1f867d6..4cb0865901 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.css +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.css @@ -64,40 +64,3 @@ left: unset; right: calc(50% + calc(var(--fluent-wizard-circle-size) / 2 + var(--fluent-wizard-spacing))); } - -/* Hidden */ -@media (max-width: 599.98px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="xs"] { - display: none; - } -} - -@media (min-width: 600px) and (max-width: 959.98px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="sm"] { - display: none; - } -} - -@media (min-width: 960px) and (max-width: 1279.98px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="md"] { - display: none; - } -} - -@media (min-width: 1280px) and (max-width: 1919.98px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="lg"] { - display: none; - } -} - -@media (min-width: 1920px) and (max-width: 2559.98px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="xl"] { - display: none; - } -} - -@media (min-width: 2560px) { - .fluent-wizard > ol > li ::deep > div[hidden-when~="xxl"] { - display: none; - } -} From 3b644b47ef68f290888fe2e1eca45fd5ab0b0546 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 22:10:15 +0200 Subject: [PATCH 10/27] Add console log --- .../Components/Wizard/Examples/WizardDefault.razor | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 index 57dad5302e..85b8d790cb 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor @@ -1,4 +1,4 @@ - + @@ -59,12 +59,13 @@ @code { - bool IsTop = false; - WizardStepSequence StepSequence = WizardStepSequence.Linear; + bool IsTop = false; + WizardStepSequence StepSequence = WizardStepSequence.Linear; - void OnStepChange(FluentWizardStepChangeEventArgs e) - { - } + void OnStepChange(FluentWizardStepChangeEventArgs e) + { + Console.WriteLine("Step changed to {0}", e.TargetIndex); + } async Task OnFinishedAsync() { From 708828b94df26e3552a83dea9cac850312051cbf Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 22:11:44 +0200 Subject: [PATCH 11/27] Use switch expression --- src/Core/Components/Wizard/FluentWizard.razor.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 66a4042d9e..3a145afb4f 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -411,16 +411,12 @@ private void SetCurrentStatusToStep(int stepIndex) return null; } - switch (StepperPosition) + return StepperPosition switch { - case StepperPosition.Top: - return $"height: {StepperSize}"; - - case StepperPosition.Left: - return $"width: {StepperSize}"; - } - - return null; + StepperPosition.Top => $"height: {StepperSize}", + StepperPosition.Left => $"width: {StepperSize}", + _ => null, + }; } private bool DisplayPreviousButton => Value > 0 && _steps[..Value].Any(i => !i.Disabled); From 7a3bc4b767662c938229a6c855806ec3be5a4495 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 22:16:27 +0200 Subject: [PATCH 12/27] Update default sample --- .../Wizard/Examples/WizardDefault.razor | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) 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 index 85b8d790cb..0c2ceceb99 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor @@ -1,21 +1,6 @@ - - - - WizardStepSequence: - - - - + OnFinish="@OnFinished"> @@ -59,16 +44,13 @@ @code { - bool IsTop = false; - WizardStepSequence StepSequence = WizardStepSequence.Linear; - void OnStepChange(FluentWizardStepChangeEventArgs e) { Console.WriteLine("Step changed to {0}", e.TargetIndex); } - async Task OnFinishedAsync() - { - await Task.CompletedTask; - } + void OnFinished() + { + Console.WriteLine("Wizard has been finished"); + } } From e71067ce27bbd72330dbdc444b2609aea4aa8632 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 22:23:01 +0200 Subject: [PATCH 13/27] Add test for FluentWizardStepChangeEventArgs --- .../Components/Wizard/FluentWizardTests.razor | 527 +++++++++--------- 1 file changed, 277 insertions(+), 250 deletions(-) diff --git a/tests/Core/Components/Wizard/FluentWizardTests.razor b/tests/Core/Components/Wizard/FluentWizardTests.razor index 8bda081241..c289fef7e6 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.razor +++ b/tests/Core/Components/Wizard/FluentWizardTests.razor @@ -1,4 +1,4 @@ -@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.Extensions @using Microsoft.FluentUI.AspNetCore.Components.Tests.Extensions @using Microsoft.FluentUI.AspNetCore.Components.Utilities @using Xunit; @@ -6,253 +6,280 @@ @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(); - } - - [Fact] - public void FluentWizard_StepperPositionTop() - { - // Arrange && Act - var cut = Render( - @ - - Content 1 - Content 2 - - ); - - // Assert - cut.Verify(); - } - - [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_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(); - } + 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(); + } + + [Fact] + public void FluentWizard_StepperPositionTop() + { + // Arrange && Act + var cut = Render( + @ + + Content 1 + Content 2 + + ); + + // Assert + cut.Verify(); + } + + [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_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); + } } From bd177af4c269eb2972ee2c611facd4ff66cbaaac Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 3 Apr 2026 22:25:43 +0200 Subject: [PATCH 14/27] On new line --- src/Core/Components/Wizard/FluentWizardStep.razor | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor b/src/Core/Components/Wizard/FluentWizardStep.razor index 4177ef0bb3..a391f585a0 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor +++ b/src/Core/Components/Wizard/FluentWizardStep.razor @@ -1,11 +1,14 @@ -@namespace Microsoft.FluentUI.AspNetCore.Components +@namespace Microsoft.FluentUI.AspNetCore.Components @using Microsoft.FluentUI.AspNetCore.Components.Extensions @inherits FluentComponentBase -
  • + class="@ClassValue" + style="@StyleValue" + @attributes="@AdditionalAttributes"> @if (StepTemplate is null) {
    From 91e9d18e3d1b61df9c339c5311132a61c57afa7f Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:25:58 +0200 Subject: [PATCH 15/27] Add position demo --- .../Wizard/Examples/WizardPosition.razor | 57 +++++++++++++++++++ .../Components/Wizard/FluentWizard.md | 6 ++ 2 files changed, 63 insertions(+) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardPosition.razor 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 index f682d31550..725beddba9 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md @@ -37,6 +37,12 @@ the static properties **FluentWizard.LabelButtonPrevious / LabelButtonNext / Lab {{ 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, From 1f93f2cb927e929f4159189ee3f65cfb8efc9716 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:27:00 +0200 Subject: [PATCH 16/27] Clear also _editContext --- src/Core/Components/Wizard/FluentWizardStep.razor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index 6a0801b637..186db90ab1 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -173,9 +173,10 @@ public void RegisterEditFormAndContext(EditForm editForm, EditContext editContex /// /// Clears all registered EditForm and EditContext pairs. /// - public void ClearEditFormAndContext() + internal void ClearEditFormAndContext() { _editForms.Clear(); + _editContexts.Clear(); } /// From 1f210777ce856390c1703eb2883bc78e0cea1f11 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:27:19 +0200 Subject: [PATCH 17/27] Make internal --- src/Core/Components/Wizard/FluentWizardStep.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index 186db90ab1..d8a0821f77 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -183,7 +183,7 @@ internal void ClearEditFormAndContext() /// Registers an for validation tracking. /// This is typically called by the component. /// - public void RegisterEditContext(EditContext editContext) + internal void RegisterEditContext(EditContext editContext) { if (!_editContexts.Contains(editContext)) { From e678c81c99899a93368c35699884f06d330f5825 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:27:32 +0200 Subject: [PATCH 18/27] make internal --- src/Core/Components/Wizard/FluentWizardStep.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Components/Wizard/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index d8a0821f77..efeeda710f 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -194,7 +194,7 @@ internal void RegisterEditContext(EditContext editContext) /// /// Unregisters an from validation tracking. /// - public void UnregisterEditContext(EditContext editContext) + internal void UnregisterEditContext(EditContext editContext) { _editContexts.Remove(editContext); } From 33d78089f914ff42c69dc971c71fbfcb61a0c48a Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:42:37 +0200 Subject: [PATCH 19/27] Add ButtonTemplate test --- ...tWizard_ButtonTemplate.verified.razor.html | 56 +++++++++++++++++++ .../Components/Wizard/FluentWizardTests.razor | 54 +++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ButtonTemplate.verified.razor.html 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.razor b/tests/Core/Components/Wizard/FluentWizardTests.razor index c289fef7e6..56c40c995c 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.razor +++ b/tests/Core/Components/Wizard/FluentWizardTests.razor @@ -163,12 +163,14 @@ cut.Verify(); } - [Fact] - public void FluentWizard_StepperPositionTop() + [Theory] + [InlineData(StepperPosition.Top)] + [InlineData(StepperPosition.Left)] + public void FluentWizard_StepperPosition(StepperPosition position) { // Arrange && Act var cut = Render( - @ + @ Content 1 Content 2 @@ -233,6 +235,52 @@ 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() { From 1598a8ecc5793ebf2ed9918c058d9496e146dcf4 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Mon, 6 Apr 2026 12:48:15 +0200 Subject: [PATCH 20/27] Update tests --- ...sts.FluentWizard_StepperPosition.verified.razor.html} | 8 ++++---- tests/Core/Components/Wizard/FluentWizardTests.razor | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) rename tests/Core/Components/Wizard/{FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html => FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html} (83%) diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html similarity index 83% rename from tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html rename to tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html index b86a2ed5c4..87fdf077e1 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html @@ -1,9 +1,9 @@ -
    +
      -
    1. +
    2. Step 1
      -
      +
    3. -
    4. +
    5. + +
      +
        +
      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.razor b/tests/Core/Components/Wizard/FluentWizardTests.razor index fc88ab6c2b..5271a6e7ec 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.razor +++ b/tests/Core/Components/Wizard/FluentWizardTests.razor @@ -331,4 +331,50 @@ 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(); + } } From a39367191f1aa9590be5c1891b591a1ee6c400fc Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Sat, 11 Apr 2026 13:57:53 +0200 Subject: [PATCH 22/27] Remove static properties --- src/Core/Components/Wizard/FluentWizard.razor | 6 ++-- .../Components/Wizard/FluentWizard.razor.cs | 32 +------------------ src/Core/Localization/LanguageResource.resx | 1 - 3 files changed, 4 insertions(+), 35 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor b/src/Core/Components/Wizard/FluentWizard.razor index 82a0ff44d0..fefc6d85c3 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -37,7 +37,7 @@ - @LabelButtonPreviousValue + @Localizer[Localization.LanguageResource.Wizard_LabelButtonPrevious] } @@ -48,7 +48,7 @@ - @LabelButtonNextValue + @Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] } else @@ -56,7 +56,7 @@ - @LabelButtonDoneValue + @Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] } } diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 3a145afb4f..121ac75a1f 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -13,37 +13,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components; ///
  • public partial class FluentWizard : FluentComponentBase { - /// - /// Gets or sets the label for the Previous button. - /// If null or empty, the localized value is used. - /// - public static string? LabelButtonPrevious { get; set; } - - /// - /// Gets or sets the label for the Next button. - /// If null or empty, the localized value is used. - /// - public static string? LabelButtonNext { get; set; } - - /// - /// Gets or sets the label for the Done button. - /// If null or empty, the localized value is used. - /// - public static string? LabelButtonDone { get; set; } - - private string LabelButtonPreviousValue => string.IsNullOrWhiteSpace(LabelButtonPrevious) - ? Localizer[Localization.LanguageResource.Wizard_LabelButtonPrevious] - : LabelButtonPrevious; - - private string LabelButtonNextValue => string.IsNullOrWhiteSpace(LabelButtonNext) - ? Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] - : LabelButtonNext; - - private string LabelButtonDoneValue => string.IsNullOrWhiteSpace(LabelButtonDone) - ? Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] - : LabelButtonDone; - - private readonly List _steps = new(); + private readonly List _steps = []; internal int _maxStepVisited; /// diff --git a/src/Core/Localization/LanguageResource.resx b/src/Core/Localization/LanguageResource.resx index af65c48388..8724a3be6d 100644 --- a/src/Core/Localization/LanguageResource.resx +++ b/src/Core/Localization/LanguageResource.resx @@ -354,7 +354,6 @@ The maximum number of {0} selected items has been reached. - Previous From d75cd91d594a32c16810fe5aa427d7f6edcce893 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Sat, 11 Apr 2026 13:58:35 +0200 Subject: [PATCH 23/27] Remove button width because it can overflow when the user overwrite the translations --- src/Core/Components/Wizard/FluentWizard.razor | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor b/src/Core/Components/Wizard/FluentWizard.razor index fefc6d85c3..e500bd0978 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -30,12 +30,9 @@ border-inside=@Border.HasFlag(WizardBorder.Inside)> @if (ButtonTemplate == null) { - string buttonWidth = "80px;"; - @if (DisplayPreviousButton) { @Localizer[Localization.LanguageResource.Wizard_LabelButtonPrevious] @@ -46,7 +43,6 @@ @if (DisplayNextButton) { @Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] @@ -54,7 +50,6 @@ else { @Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] From 8991e906fb5552c8e208918daa06e6d269fcd112 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Sat, 11 Apr 2026 14:01:12 +0200 Subject: [PATCH 24/27] Use FluentStack --- src/Core/Components/Wizard/FluentWizard.razor | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor b/src/Core/Components/Wizard/FluentWizard.razor index e500bd0978..e591d09f28 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor +++ b/src/Core/Components/Wizard/FluentWizard.razor @@ -3,62 +3,62 @@ -
    -
      - @Steps -
    +
    +
      + @Steps +
    -
    - @foreach (var step in _steps.Where(i => i.Index == Value || !i.DeferredLoading)) - { -
    - - @(step.ChildContent) - -
    - } -
    +
    + @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 (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) - } -
    -
    + @if (DisplayNextButton) + { + + @Localizer[Localization.LanguageResource.Wizard_LabelButtonNext] + + } + else + { + + @Localizer[Localization.LanguageResource.Wizard_LabelButtonDone] + + } + + } + else + { + @ButtonTemplate(Value) + } +
    +
    From 471e76a4f2ed68eb6d8b39f3610fdb62ff55fcdd Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 17 Apr 2026 22:57:38 +0200 Subject: [PATCH 25/27] Use SetParametersAsync instead on OnParameterSet --- src/Core/Components/Wizard/FluentWizard.razor.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 121ac75a1f..11f402b02e 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -126,10 +126,15 @@ public FluentWizard(LibraryConfiguration configuration) : base(configuration) public WizardStepSequence StepSequence { get; set; } = WizardStepSequence.Linear; /// - protected override void OnParametersSet() + public override Task SetParametersAsync(ParameterView parameters) { - SetCurrentValue(Value); - base.OnParametersSet(); + // If Value parameter changes, we need to switch to the new step. + if (parameters.TryGetValue(nameof(Value), out var newValue) && !Equals(newValue, Value)) + { + SetCurrentValue(newValue); + } + + return base.SetParametersAsync(parameters); } /// From 683d9dc158716b15850a8ba4d13850d032755473 Mon Sep 17 00:00:00 2001 From: Marvin Klein Date: Fri, 17 Apr 2026 23:07:22 +0200 Subject: [PATCH 26/27] Order code according to blazor-code.md --- .../Components/Wizard/FluentWizard.razor.cs | 118 +++++------ .../Wizard/FluentWizardStep.razor.cs | 185 +++++++++--------- 2 files changed, 151 insertions(+), 152 deletions(-) diff --git a/src/Core/Components/Wizard/FluentWizard.razor.cs b/src/Core/Components/Wizard/FluentWizard.razor.cs index 11f402b02e..f4c7f12f63 100644 --- a/src/Core/Components/Wizard/FluentWizard.razor.cs +++ b/src/Core/Components/Wizard/FluentWizard.razor.cs @@ -22,17 +22,6 @@ 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. /// @@ -125,6 +114,23 @@ public FluentWizard(LibraryConfiguration configuration) : base(configuration) [Parameter] public WizardStepSequence StepSequence { get; set; } = WizardStepSequence.Linear; + /// + protected string? ClassValue => DefaultClassBuilder + .AddClass("fluent-wizard") + .Build(); + + /// + protected string? StyleValue => DefaultStyleBuilder + .AddStyle("width", Width) + .AddStyle("height", Height) + .Build(); + + internal int StepCount => _steps.Count; + + 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); + /// public override Task SetParametersAsync(ParameterView parameters) { @@ -137,6 +143,48 @@ public override Task SetParametersAsync(ParameterView parameters) return base.SetParametersAsync(parameters); } + /// + /// 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); + } + /// protected virtual async Task OnNextHandlerAsync(MouseEventArgs e) { @@ -240,48 +288,6 @@ 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); @@ -320,8 +326,6 @@ internal int AddStep(FluentWizardStep step) return index; } - internal int StepCount => _steps.Count; - internal void RemoveStep(FluentWizardStep step) { _steps.Remove(step); @@ -393,8 +397,4 @@ private void SetCurrentStatusToStep(int stepIndex) _ => 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/FluentWizardStep.razor.cs b/src/Core/Components/Wizard/FluentWizardStep.razor.cs index efeeda710f..d45426ab61 100644 --- a/src/Core/Components/Wizard/FluentWizardStep.razor.cs +++ b/src/Core/Components/Wizard/FluentWizardStep.razor.cs @@ -22,23 +22,6 @@ 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. /// @@ -51,11 +34,6 @@ public FluentWizardStep(LibraryConfiguration configuration) : base(configuration [Parameter] public RenderFragment? StepTemplate { get; set; } - /// - /// Gets the step index. - /// - public int Index { get; private set; } - /// /// Gets or sets whether the step is disabled. /// @@ -88,13 +66,6 @@ public FluentWizardStep(LibraryConfiguration configuration) : base(configuration [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. /// @@ -122,13 +93,41 @@ public FluentWizardStep(LibraryConfiguration configuration) : base(configuration [Parameter] public Icon IconNext { get; set; } = new CoreIcons.Regular.Size20.Circle(); + /// + /// Reference to the parent component. + /// For internal use only. + /// + [CascadingParameter] + internal FluentWizard FluentWizard { get; set; } = default!; + + /// + /// Gets the step index. + /// + public int Index { get; private set; } + + /// + 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(); + 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 @@ -143,6 +142,35 @@ private Icon StepIcon } } + 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; + } + } + /// protected override void OnInitialized() { @@ -155,13 +183,6 @@ protected override void OnInitialized() base.OnInitialized(); } - /// - public override async ValueTask DisposeAsync() - { - FluentWizard?.RemoveStep(this); - await base.DisposeAsync(); - } - /// /// Registers an EditForm and its EditContext for validation tracking. /// @@ -170,6 +191,40 @@ public void RegisterEditFormAndContext(EditForm editForm, EditContext editContex _editForms.TryAdd(editForm, 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; + } + + /// + public override async ValueTask DisposeAsync() + { + FluentWizard?.RemoveStep(this); + await base.DisposeAsync(); + } + /// /// Clears all registered EditForm and EditContext pairs. /// @@ -199,33 +254,6 @@ 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) @@ -259,33 +287,4 @@ private async Task OnClickHandlerAsync() 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; - } - } } From 22b78814482dae0e6a4410a064660d3d5ada452d Mon Sep 17 00:00:00 2001 From: Marvin Klein <32510006+MarvinKlein1508@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:37:04 +0200 Subject: [PATCH 27/27] Fix verified files --- ...ts.FluentWizard_Border.verified.razor.html | 7 ++-- ...izard_CancelStepChange.verified.razor.html | 7 ++-- ...s.FluentWizard_Default.verified.razor.html | 7 ++-- ...Wizard_DeferredLoading.verified.razor.html | 7 ++-- ...entWizard_DisabledStep.verified.razor.html | 7 ++-- ...luentWizard_NextButton.verified.razor.html | 11 +++--- ....FluentWizard_OnFinish.verified.razor.html | 11 +++--- ...tWizard_PreviousButton.verified.razor.html | 7 ++-- ...Wizard_StepSequenceAny.verified.razor.html | 11 +++--- ...entWizard_StepTemplate.verified.razor.html | 7 ++-- ...Wizard_StepperPosition.verified.razor.html | 39 ------------------- ...entWizard_ValueBinding.verified.razor.html | 11 +++--- ...FluentWizard_WithSteps.verified.razor.html | 7 ++-- 13 files changed, 56 insertions(+), 83 deletions(-) delete mode 100644 tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html index 6c32129d2f..1a9403712f 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html @@ -30,9 +30,10 @@
    Content 2
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html index 87fdf077e1..f60b138e7d 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html @@ -30,9 +30,10 @@
    Content 2
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html index 26aeb84f64..d8e5501463 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html @@ -42,9 +42,10 @@
    Content 3
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html index 3ae935efbd..f845d65e6e 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html @@ -29,9 +29,10 @@
    Content 1
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html index cab85167d1..acc3d86351 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html @@ -42,9 +42,10 @@
    Content 3
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html index e3975032b6..728ce98e36 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html @@ -42,11 +42,12 @@
    Content 3
    - - Previous - - - Next +
    + + Previous + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html index 6bceceea20..a6fa02d2ae 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html @@ -30,11 +30,12 @@
    Content 2
    - - Previous - - - Done +
    + + Previous + + Done +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html index 26aeb84f64..d8e5501463 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html @@ -42,9 +42,10 @@
    Content 3
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html index c26f52bdf3..781a551f00 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html @@ -42,11 +42,12 @@
    Content 3
    - - Previous - - - Done +
    + + Previous + + Done +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html index b5d5578f7b..7bb22767f8 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepTemplate.verified.razor.html @@ -33,9 +33,10 @@
    - - - Next +
    + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html deleted file mode 100644 index 87fdf077e1..0000000000 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPosition.verified.razor.html +++ /dev/null @@ -1,39 +0,0 @@ - - - -
    -
      -
    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 index e3975032b6..728ce98e36 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html @@ -42,11 +42,12 @@
    Content 3
    - - Previous - - - Next +
    + + Previous + + Next +
    diff --git a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html index 87fdf077e1..f60b138e7d 100644 --- a/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html +++ b/tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html @@ -30,9 +30,10 @@
    Content 2
    - - - Next +
    + + Next +