Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@ icon: Form
---

## Validation

The Fluent UI Razor components work with a validation summary in the same way the standard Blazor (input) components do. An extra component is provided to make it possible to show a validation summary that follows the Fluent Design guidelines:

- FluentValidationSummary

### Native constraint validation UI

By default the Fluent UI components render validation feedback using the library's UI. If you prefer the browser's native HTML5 constraint validation UI (the built-in validation bubbles/messages driven by the Constraint Validation API), you can opt in at library registration time.

```csharp
// Program.cs
builder.Services.AddFluentUIComponents(config =>
{
// Default is false. Set to true to enable the browser's native constraint validation UI.
config.UseNativeConstraintValidationUi = true;
});
```

The name "constraint" refers to the HTML5 Constraint Validation API (attributes like `required`, `pattern`, `min`, `max`, etc.) and the browser's native UI for reporting those violations. Use this option when you want parity with the browser's built-in validation experience; otherwise keep the library defaults for a consistent Fluent UI look-and-feel.

See the [documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components) on the Learn site for more information on the standard components. As the Fluent component is based on the standard component, the same documentation applies

## Example form with validation
Expand All @@ -20,8 +36,6 @@ Not all of the library's input components are used in this form. No data is actu

{{ BasicForm }}


## API FluentValidationSummary

{{ API Type=FluentValidationSummary }}

21 changes: 21 additions & 0 deletions src/Core/Components/Base/FluentInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ protected FluentInputBase(LibraryConfiguration configuration)
{
ValueExpression = () => CurrentValueOrDefault;
configuration?.DefaultValues.ApplyDefaults(this);

// Apply the library configured default for using the native browser
// constraint validation UI. This value acts as the component default
// and can be overridden by setting the component parameter in markup.
if (configuration is not null)
{
UseNativeConstraintValidationUI = configuration.UseNativeConstraintValidationUI;
}
}

[Inject]
Expand Down Expand Up @@ -206,6 +214,12 @@ protected FluentInputBase(LibraryConfiguration configuration)
[Parameter]
public virtual bool ReadOnly { get; set; }

/// <summary>
/// Gets or sets whether the control will use the native browser constraint validation UI.
/// </summary>
[Parameter]
public bool UseNativeConstraintValidationUI { get; set; }

/// <summary />
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value", Justification = "TODO")]
protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
Expand All @@ -229,6 +243,13 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
/// </summary>
protected virtual async Task ReportValidityAsync()
{
// Only call the browser native constraint validation UI when enabled.
// This behavior is opt-in via UseNativeConstraintValidationUi (default is false).
if (!UseNativeConstraintValidationUI)
{
return;
}

if (string.IsNullOrWhiteSpace(Id))
{
return;
Expand Down
8 changes: 8 additions & 0 deletions src/Core/Infrastructure/LibraryConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ public class LibraryConfiguration
/// </summary>
public LibraryToastOptions Toast { get; } = new LibraryToastOptions();

/// <summary>
/// Gets or sets a value indicating whether Fluent input components should use the
/// browser's native constraint validation UI by default (e.g., showing built-in
/// validation bubbles). Default is false. Consumers can opt-in to enable the
/// native validation UI for parity with existing browser behavior.
/// </summary>
public bool UseNativeConstraintValidationUI { get; set; }

/// <summary>
/// Gets the sanitized markup string for safe rendering in HTML/Styles contexts.
/// </summary>
Expand Down
22 changes: 1 addition & 21 deletions tests/Core/Components/Switch/FluentSwitchTests.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@using Microsoft.FluentUI.AspNetCore.Components.Utilities
@using Xunit;
@using Xunit;
@inherits FluentUITestContext

@code
Expand Down Expand Up @@ -84,25 +83,6 @@
Assert.Equal(expectedValue, value);
}

[Fact]
public void FluentSwitch_OnChange_ReportsValidity()
{
// Arrange
using var context = new IdentifierContext(i => "myId");
var value = false;
var cut = Render(@<FluentSwitch Id="myId" @bind-Value="@value" />);

// Act
cut.Find("fluent-switch").Change("");

// Assert
var reportValidityInvocations = JSInterop.Invocations
.Where(invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.Utilities.Attributes.reportValidity")
.ToList();

Assert.Single(reportValidityInvocations);
}

[Fact]
public void FluentSwitch_TryParseValueFromString()
{
Expand Down
19 changes: 0 additions & 19 deletions tests/Core/Components/TextArea/FluentTextAreaTests.razor
Original file line number Diff line number Diff line change
Expand Up @@ -130,25 +130,6 @@
.MarkupMatches("<fluent-textarea id=\"myId\" slot=\"input\" value=\"new value\" />");
}

[Fact]
public void FluentTextArea_OnChange_ReportsValidity()
{
// Arrange
using var context = new IdentifierContext(i => "myId");
var value = "init";
var cut = Render(@<FluentTextArea @bind-Value="@value" />);

// Act
cut.Find("fluent-textarea").Change("new value");

// Assert
var reportValidityInvocations = JSInterop.Invocations
.Where(invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.Utilities.Attributes.reportValidity")
.ToList();

Assert.Single(reportValidityInvocations);
}

[Theory]
[InlineData(false, 0, "init", "init")] // No Immediate
[InlineData(true, 0, "new value", "new value")] // With Immediate and Delay = 0 ms
Expand Down
58 changes: 58 additions & 0 deletions tests/Core/Components/Validation/ReportValidityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

using AngleSharp.Html.Parser;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;

namespace Microsoft.FluentUI.AspNetCore.Components.Tests.Components.Validation;

public class ReportValidityTests
{
[Fact]
public void Default_NoOptIn_ReportValidityNotCalled()
{
using var ctx = new Bunit.BunitContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddSingleton<IHtmlParser>(new HtmlParser());
ctx.Services.AddFluentUIComponents();

// Arrange
var cut = ctx.Render<FluentTextArea>(parameters => parameters.Add(p => p.Id, "myId").Add(p => p.Value, "init"));

// Act
cut.Find("fluent-textarea").Change("new value");

// Assert
var reportValidityInvocations = ctx.JSInterop.Invocations
.Where(invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.Utilities.Attributes.reportValidity")
.ToList();

Assert.Empty(reportValidityInvocations);
}

[Fact]
public void OptIn_ReportsValidity()
{
using var ctx = new Bunit.BunitContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddSingleton<IHtmlParser>(new HtmlParser());
ctx.Services.AddFluentUIComponents(config => config.UseNativeConstraintValidationUI = true);

// Arrange
var cut = ctx.Render<FluentTextArea>(parameters => parameters.Add(p => p.Id, "myId").Add(p => p.Value, "init"));

// Act
cut.Find("fluent-textarea").Change("new value");

// Assert
var reportValidityInvocations = ctx.JSInterop.Invocations
.Where(invocation => invocation.Identifier == "Microsoft.FluentUI.Blazor.Utilities.Attributes.reportValidity")
.ToList();

Assert.Single(reportValidityInvocations);
}
}
Loading