Skip to content

Wizard component#4640

Open
agriffard wants to merge 7 commits intomicrosoft:dev-v5from
agriffard:WizardMigrateV5
Open

Wizard component#4640
agriffard wants to merge 7 commits intomicrosoft:dev-v5from
agriffard:WizardMigrateV5

Conversation

@agriffard
Copy link
Contributor

Fixes #4191

Default
image

Top Stepper
image

Customized
image

EditForms
image

Copilot AI review requested due to automatic review settings March 20, 2026 12:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FluentWizard and FluentWizardStep components 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.

Comment on lines +183 to +209
/// <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);
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +246 to +252
internal async Task InvokeOnInValidSubmitForEditFormsAsync()
{
foreach (var editForm in _editForms)
{
await editForm.Key.OnInvalidSubmit.InvokeAsync(editForm.Value);
}
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name InvokeOnInValidSubmitForEditFormsAsync uses inconsistent casing (“InValid”). For readability and consistency with EditForm.OnInvalidSubmit, rename to InvokeOnInvalidSubmitForEditFormsAsync (and update call sites).

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
string buttonWidth = "80px;";

@if (DisplayPreviousButton)
{
<FluentButton Appearance="ButtonAppearance.Default"
Style="@($"width: {buttonWidth};")"
OnClick="@OnPreviousHandlerAsync">
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +24
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.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +140
[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();
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary>
/// Event arguments for the <see cref="FluentWizardStep.OnChange"/> event.
/// </summary>
public class FluentWizardStepChangeEventArgs
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +222
var stepChangeArgs = new FluentWizardStepChangeEventArgs(targetIndex, _steps[targetIndex].Label);

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
<li id="@Id" status="@Status.ToAttributeValue()"
@onclick="@OnClickHandlerAsync"
disabled="@Disabled"
class="@ClassValue" style="@StyleValue" @attributes="@AdditionalAttributes">
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>.

Copilot uses AI. Check for mistakes.
Comment on lines +282 to +287
return;
}
}

// Invoke the 'OnValidSubmit' handlers for the edit forms.
await _steps[Value].InvokeOnValidSubmitForEditFormsAsync();
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +189
/// <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();
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
/// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, try to respect this rule (if possible... if not, why?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, we have a "Localization" engine... can you use it?
https://fluentui-blazor-v5.azurewebsites.net/localization

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, use ILocalizer and add resources for Label properties default

if (!isCanceled)
{
Value = targetIndex;
await ValueChanged.InvokeAsync(targetIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if ValueChanged.HasDelegate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, Checked if ValueChanged.HasDelegate

if (!isCanceled)
{
Value = targetIndex;
await ValueChanged.InvokeAsync(targetIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if ValueChanged.HasDelegate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, Checked if ValueChanged.HasDelegate

if (!isCanceled)
{
Value = targetIndex;
await ValueChanged.InvokeAsync(targetIndex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, Checked if ValueChanged.HasDelegate

/// For internal use only.
/// </summary>
[CascadingParameter]
public FluentWizard FluentWizard { get; set; } = default!;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to change to internal ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, changed FluentWizard to internal

}

/* Hidden (responsive) */
@media (max-width: 599.98px) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you added these styles? See FluentGrid.razor.css file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to retrieve the same styles than v4

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add [Description("none")]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added [Description("none")] to None in WizardStepStatus

/// <summary>
/// All statuses.
/// </summary>
All = Previous | Current | Next
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add [Description("all")]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add [Description("all")] to All in WizardStepStatus


@code
{
public FluentWizardTests()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'")

@MarvinKlein1508 MarvinKlein1508 modified the milestone: v5.0-RC2 Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants