Conversation
There was a problem hiding this comment.
Pull request overview
Re-introduces the FluentWizard/FluentWizardStep component set into the v5 codebase (per #4191), including core component implementation, supporting enums/icons, demo documentation/examples, and bUnit snapshot tests.
Changes:
- Added
FluentWizardandFluentWizardStepcomponents with step sequencing, borders, and optional deferred rendering. - Added EditForm validation hook component (
FluentWizardStepValidator) plus supporting enums and icons. - Added demo docs/examples and bUnit snapshot-based tests (with verified HTML baselines).
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Core/Components/Wizard/FluentWizardTests.razor | Adds bUnit tests covering common wizard scenarios and interactions. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Default.verified.razor.html | Snapshot baseline for default wizard rendering. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_WithSteps.verified.razor.html | Snapshot baseline for two-step wizard rendering. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_NextButton.verified.razor.html | Snapshot baseline after clicking Next. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_PreviousButton.verified.razor.html | Snapshot baseline for Previous/Next button flow. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DisabledStep.verified.razor.html | Snapshot baseline for disabled step visuals. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_OnFinish.verified.razor.html | Snapshot baseline for Done/finish flow. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_ValueBinding.verified.razor.html | Snapshot baseline for @bind-Value behavior. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepperPositionTop.verified.razor.html | Snapshot baseline for top stepper layout. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_Border.verified.razor.html | Snapshot baseline for border rendering. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_DeferredLoading.verified.razor.html | Snapshot baseline for deferred-loading rendering behavior. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_CancelStepChange.verified.razor.html | Snapshot baseline for cancelling step changes. |
| tests/Core/Components/Wizard/FluentWizardTests.FluentWizard_StepSequenceAny.verified.razor.html | Snapshot baseline for “Any” step navigation via clicking steps. |
| src/Core/Enums/WizardStepStatus.cs | Introduces step status enum (with description values used as attributes). |
| src/Core/Enums/WizardStepSequence.cs | Introduces step navigation sequence enum. |
| src/Core/Enums/WizardBorder.cs | Introduces border flags enum for inside/outside borders. |
| src/Core/Enums/StepperPosition.cs | Introduces top/left stepper positioning enum. |
| src/Core/Components/Wizard/FluentWizardStepValidator.cs | Adds helper component to register an EditContext with a wizard step. |
| src/Core/Components/Wizard/FluentWizardStepChangeEventArgs.cs | Adds step-change args type for FluentWizardStep.OnChange. |
| src/Core/Components/Wizard/FluentWizardStepArgs.cs | Adds template context args for custom step indicator rendering. |
| src/Core/Components/Wizard/FluentWizardStep.razor.css | Adds styling for step indicators, connectors, and responsive hidden labels. |
| src/Core/Components/Wizard/FluentWizardStep.razor.cs | Implements wizard step state, click handling, and validation plumbing. |
| src/Core/Components/Wizard/FluentWizardStep.razor | Implements step indicator markup and default icon/label rendering. |
| src/Core/Components/Wizard/FluentWizard.razor.css | Adds layout styling for left/top stepper wizard variants and borders. |
| src/Core/Components/Wizard/FluentWizard.razor.cs | Implements wizard navigation, step state updates, and finish behavior. |
| src/Core/Components/Wizard/FluentWizard.razor | Implements wizard layout, step content rendering, and default buttons. |
| src/Core/Components/Icons/CoreIcons.cs | Adds Circle icons (filled/regular size20) used by wizard defaults. |
| examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentWizard.md | Updates migration guidance to reflect wizard re-introduction and API changes. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/FluentWizard.md | Adds public documentation page for the Wizard component. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardDefault.razor | Adds default demo example for wizard usage and configuration. |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardCustomized.razor | Adds advanced customization example (templates + programmatic navigation). |
| examples/Demo/FluentUI.Demo.Client/Documentation/Components/Wizard/Examples/WizardEditForms.razor | Adds EditForm validation demo example for wizard navigation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// <summary> | ||
| /// Clears all registered EditForm and EditContext pairs. | ||
| /// </summary> | ||
| public void ClearEditFormAndContext() | ||
| { | ||
| _editForms.Clear(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Registers an <see cref="EditContext"/> for validation tracking. | ||
| /// This is typically called by the <see cref="FluentWizardStepValidator"/> component. | ||
| /// </summary> | ||
| public void RegisterEditContext(EditContext editContext) | ||
| { | ||
| if (!_editContexts.Contains(editContext)) | ||
| { | ||
| _editContexts.Add(editContext); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Unregisters an <see cref="EditContext"/> from validation tracking. | ||
| /// </summary> | ||
| public void UnregisterEditContext(EditContext editContext) | ||
| { | ||
| _editContexts.Remove(editContext); | ||
| } |
There was a problem hiding this comment.
ClearEditFormAndContext() only clears _editForms, but the name/doc comment imply it clears both EditForm/EditContext registrations. Either clear _editContexts here as well, or rename the method/docs so callers don’t assume it resets all validation registrations.
| internal async Task InvokeOnInValidSubmitForEditFormsAsync() | ||
| { | ||
| foreach (var editForm in _editForms) | ||
| { | ||
| await editForm.Key.OnInvalidSubmit.InvokeAsync(editForm.Value); | ||
| } | ||
| } |
There was a problem hiding this comment.
Method name InvokeOnInValidSubmitForEditFormsAsync uses inconsistent casing (“InValid”). For readability and consistency with EditForm.OnInvalidSubmit, rename to InvokeOnInvalidSubmitForEditFormsAsync (and update call sites).
| string buttonWidth = "80px;"; | ||
|
|
||
| @if (DisplayPreviousButton) | ||
| { | ||
| <FluentButton Appearance="ButtonAppearance.Default" | ||
| Style="@($"width: {buttonWidth};")" | ||
| OnClick="@OnPreviousHandlerAsync"> |
There was a problem hiding this comment.
buttonWidth includes a trailing semicolon ("80px;") and is then interpolated into a style string that adds another semicolon, which can produce width: 80px;;. Set buttonWidth to just "80px" (or inline the constant) and keep the semicolon only in the final style string.
| 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. |
There was a problem hiding this comment.
This section has duplicated words (“that … that step”, “property property”) and the DeferredLoading behavior description appears inverted relative to the implementation (in FluentWizard.razor, steps with DeferredLoading=true are rendered only when active). Please fix the typos and clarify that enabling DeferredLoading defers rendering to the active step only (or adjust the code if the current behavior is not intended).
| 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. | |
| By default, the contents of all steps are rendered, but only the active step is visible; other steps are shown | |
| when the user navigates to them. When the **DeferredLoading** property is enabled, rendering is deferred so that | |
| only the active step's content is generated. |
| [Fact] | ||
| public void FluentWizard_OnFinish() | ||
| { | ||
| // Arrange | ||
| var finishCalled = false; | ||
| var value = 0; | ||
| var cut = Render( | ||
| @<FluentWizard @bind-Value="@value" OnFinish="@(() => { finishCalled = true; })"> | ||
| <Steps> | ||
| <FluentWizardStep Label="Step 1">Content 1</FluentWizardStep> | ||
| <FluentWizardStep Label="Step 2">Content 2</FluentWizardStep> | ||
| </Steps> | ||
| </FluentWizard>); | ||
|
|
||
| // 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(); | ||
| } |
There was a problem hiding this comment.
The Wizard tests cover navigation and step sequencing, but there’s no test exercising the EditForm validation integration (cancelling navigation when invalid, allowing when valid, and FinishAsync(true) behavior). Adding at least one bUnit test with an EditForm + DataAnnotationsValidator + FluentWizardStepValidator would help prevent regressions in this area.
| namespace Microsoft.FluentUI.AspNetCore.Components; | ||
|
|
||
| /// <summary> | ||
| /// Event arguments for the <see cref="FluentWizardStep.OnChange"/> event. | ||
| /// </summary> | ||
| public class FluentWizardStepChangeEventArgs |
There was a problem hiding this comment.
FluentWizardStepChangeEventArgs is a public event-args type but does not derive from EventArgs. This repo’s convention for event argument classes (and the guidance in src/Core/Events/EventHandlers.cs) is to inherit from EventArgs; please update this type accordingly to keep the public API consistent.
| namespace Microsoft.FluentUI.AspNetCore.Components; | |
| /// <summary> | |
| /// Event arguments for the <see cref="FluentWizardStep.OnChange"/> event. | |
| /// </summary> | |
| public class FluentWizardStepChangeEventArgs | |
| using System; | |
| namespace Microsoft.FluentUI.AspNetCore.Components; | |
| /// <summary> | |
| /// Event arguments for the <see cref="FluentWizardStep.OnChange"/> event. | |
| /// </summary> | |
| public class FluentWizardStepChangeEventArgs : EventArgs |
| var stepChangeArgs = new FluentWizardStepChangeEventArgs(targetIndex, _steps[targetIndex].Label); | ||
|
|
There was a problem hiding this comment.
GoToStepAsync/ValidateAndGoToStepAsync allow callers to pass any targetIndex, but OnStepChangeHandlerAsync immediately indexes _steps[targetIndex] without validating bounds. This can throw ArgumentOutOfRangeException for out-of-range values (e.g., Value + 1 on the last step) and can also allow navigation attempts to disabled steps; add explicit range/disabled checks (and either clamp, no-op, or throw a clear ArgumentOutOfRangeException).
| var stepChangeArgs = new FluentWizardStepChangeEventArgs(targetIndex, _steps[targetIndex].Label); | |
| if (targetIndex < 0 || targetIndex >= _steps.Count) | |
| { | |
| throw new System.ArgumentOutOfRangeException( | |
| nameof(targetIndex), | |
| $"The targetIndex must be between 0 and {_steps.Count - 1}."); | |
| } | |
| var targetStep = _steps[targetIndex]; | |
| if (targetStep.Disabled) | |
| { | |
| throw new System.ArgumentOutOfRangeException( | |
| nameof(targetIndex), | |
| "The target wizard step is disabled and cannot be navigated to."); | |
| } | |
| var stepChangeArgs = new FluentWizardStepChangeEventArgs(targetIndex, targetStep.Label); |
| <li id="@Id" status="@Status.ToAttributeValue()" | ||
| @onclick="@OnClickHandlerAsync" | ||
| disabled="@Disabled" | ||
| class="@ClassValue" style="@StyleValue" @attributes="@AdditionalAttributes"> |
There was a problem hiding this comment.
The step indicator is clickable (has @onclick and sometimes cursor: pointer), but the <li> isn’t keyboard-focusable and doesn’t provide keyboard activation semantics. For accessibility, consider adding appropriate role/tabindex (when clickable), handling Enter/Space key activation, and using aria-disabled when disabled instead of (or in addition to) a non-standard disabled attribute on <li>.
| return; | ||
| } | ||
| } | ||
|
|
||
| // Invoke the 'OnValidSubmit' handlers for the edit forms. | ||
| await _steps[Value].InvokeOnValidSubmitForEditFormsAsync(); |
There was a problem hiding this comment.
FinishAsync(validateEditContexts: false) currently still invokes InvokeOnValidSubmitForEditFormsAsync(), which doesn’t match EditForm semantics (OnValidSubmit should only run after a successful validation). Also, when validateEditContexts is true and validation fails, the method returns without invoking OnSubmit; consider aligning behavior with EditForm by always invoking OnSubmit, invoking OnValidSubmit only when valid, and OnInvalidSubmit only when invalid.
| return; | |
| } | |
| } | |
| // Invoke the 'OnValidSubmit' handlers for the edit forms. | |
| await _steps[Value].InvokeOnValidSubmitForEditFormsAsync(); | |
| } | |
| else | |
| { | |
| // Invoke the 'OnValidSubmit' handlers for the edit forms. | |
| await _steps[Value].InvokeOnValidSubmitForEditFormsAsync(); | |
| } | |
| } | |
| // Always invoke the 'OnSubmit' handlers for the edit forms. |
| /// <summary> | ||
| /// Registers an EditForm and its EditContext for validation tracking. | ||
| /// </summary> | ||
| public void RegisterEditFormAndContext(EditForm editForm, EditContext editContext) | ||
| { | ||
| if (!_editForms.ContainsKey(editForm)) | ||
| { | ||
| _editForms.Add(editForm, editContext); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Clears all registered EditForm and EditContext pairs. | ||
| /// </summary> | ||
| public void ClearEditFormAndContext() | ||
| { | ||
| _editForms.Clear(); | ||
| } |
There was a problem hiding this comment.
FluentWizardStep contains logic to invoke EditForm.OnValidSubmit/OnInvalidSubmit/OnSubmit via _editForms, but nothing in this PR ever populates _editForms (the provided FluentWizardStepValidator only registers EditContext). As a result, those submit callbacks will never fire (including in the demo where EditForm handlers are provided). Either remove the unused EditForm-callback invocation path, or add a supported way to register an EditForm instance (e.g., via a dedicated helper component/parameter) so these callbacks can actually be invoked.
| /// Gets or sets the step index of the current step. | ||
| /// This value is bindable. | ||
| /// </summary> | ||
| #pragma warning disable BL0007 // Component parameters should be auto properties |
There was a problem hiding this comment.
Please, try to respect this rule (if possible... if not, why?)
There was a problem hiding this comment.
Changed Value to public int Value { get; set; }
| /// <summary> | ||
| /// Gets or sets the label for the Previous button. | ||
| /// </summary> | ||
| public static string LabelButtonPrevious { get; set; } = "Previous"; |
There was a problem hiding this comment.
Now, we have a "Localization" engine... can you use it?
https://fluentui-blazor-v5.azurewebsites.net/localization
There was a problem hiding this comment.
ok, use ILocalizer and add resources for Label properties default
| if (!isCanceled) | ||
| { | ||
| Value = targetIndex; | ||
| await ValueChanged.InvokeAsync(targetIndex); |
There was a problem hiding this comment.
Check if ValueChanged.HasDelegate
There was a problem hiding this comment.
ok, Checked if ValueChanged.HasDelegate
| if (!isCanceled) | ||
| { | ||
| Value = targetIndex; | ||
| await ValueChanged.InvokeAsync(targetIndex); |
There was a problem hiding this comment.
Check if ValueChanged.HasDelegate
There was a problem hiding this comment.
ok, Checked if ValueChanged.HasDelegate
| if (!isCanceled) | ||
| { | ||
| Value = targetIndex; | ||
| await ValueChanged.InvokeAsync(targetIndex); |
There was a problem hiding this comment.
ok, Checked if ValueChanged.HasDelegate
| /// For internal use only. | ||
| /// </summary> | ||
| [CascadingParameter] | ||
| public FluentWizard FluentWizard { get; set; } = default!; |
There was a problem hiding this comment.
Is it possible to change to internal ?
There was a problem hiding this comment.
Ok, changed FluentWizard to internal
| } | ||
|
|
||
| /* Hidden (responsive) */ | ||
| @media (max-width: 599.98px) { |
There was a problem hiding this comment.
Why do you added these styles? See FluentGrid.razor.css file
There was a problem hiding this comment.
Changed to retrieve the same styles than v4
There was a problem hiding this comment.
I don't understand: the @media (max-width: 599.98px) styles are already included in the FluentWizardStep.razor.css` file and works with all components. There's no need to add them.
When building the Scripts project, all CSS files are compiled into a single file (no need for ::deep either)
| /// <summary> | ||
| /// No status. | ||
| /// </summary> | ||
| None = 0, |
There was a problem hiding this comment.
Add [Description("none")]
There was a problem hiding this comment.
Added [Description("none")] to None in WizardStepStatus
| /// <summary> | ||
| /// All statuses. | ||
| /// </summary> | ||
| All = Previous | Current | Next |
There was a problem hiding this comment.
Add [Description("all")]
There was a problem hiding this comment.
Add [Description("all")] to All in WizardStepStatus
|
|
||
| @code | ||
| { | ||
| public FluentWizardTests() |
There was a problem hiding this comment.
Unit tests are failing. You need to implement all "defqult" FluentComponentBase properties (Style, Class, Margin, ...)
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Margin='my-margin'", html: "class='my-margin'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Margin='10px'", html: "style='margin: 10px;*'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Padding='my-padding'", html: "class='my-padding'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Padding='10px'", html: "style='padding: 10px;*'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Id='id='My-Specific-ID'", html: "id='My-Specific-ID'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Style='My-Specific-Style'", html: "style='My-Specific-Style'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "extra-attribute='My-Specific-Attribute'", html: "extra-attribute='My-Specific-Attribute'")
- ❌[FAILED] Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Base.ComponentBaseTests.ComponentBase_DefaultProperties(blazor: "Class='My-Specific-Item'", html: "class='My-Specific-Item'")
Check if ValueChanged.HasDelegate. internal FluentWizard in FluentWizardStep
Fixes #4191
Default

Top Stepper

Customized

EditForms
