diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Forms/Forms.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Forms/Forms.md
index b190b5a7ca..6da78db158 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Forms/Forms.md
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Forms/Forms.md
@@ -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
@@ -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 }}
-
diff --git a/src/Core/Components/Base/FluentInputBase.cs b/src/Core/Components/Base/FluentInputBase.cs
index e604d21500..37c4cb54b5 100644
--- a/src/Core/Components/Base/FluentInputBase.cs
+++ b/src/Core/Components/Base/FluentInputBase.cs
@@ -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]
@@ -206,6 +214,12 @@ protected FluentInputBase(LibraryConfiguration configuration)
[Parameter]
public virtual bool ReadOnly { get; set; }
+ ///
+ /// Gets or sets whether the control will use the native browser constraint validation UI.
+ ///
+ [Parameter]
+ public bool UseNativeConstraintValidationUI { get; set; }
+
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value", Justification = "TODO")]
protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
@@ -229,6 +243,13 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
///
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;
diff --git a/src/Core/Infrastructure/LibraryConfiguration.cs b/src/Core/Infrastructure/LibraryConfiguration.cs
index a6fc50e519..742349631a 100644
--- a/src/Core/Infrastructure/LibraryConfiguration.cs
+++ b/src/Core/Infrastructure/LibraryConfiguration.cs
@@ -55,6 +55,14 @@ public class LibraryConfiguration
///
public LibraryToastOptions Toast { get; } = new LibraryToastOptions();
+ ///
+ /// 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.
+ ///
+ public bool UseNativeConstraintValidationUI { get; set; }
+
///
/// Gets the sanitized markup string for safe rendering in HTML/Styles contexts.
///
diff --git a/tests/Core/Components/Switch/FluentSwitchTests.razor b/tests/Core/Components/Switch/FluentSwitchTests.razor
index 13e1721fc3..174a2c6082 100644
--- a/tests/Core/Components/Switch/FluentSwitchTests.razor
+++ b/tests/Core/Components/Switch/FluentSwitchTests.razor
@@ -1,5 +1,4 @@
-@using Microsoft.FluentUI.AspNetCore.Components.Utilities
-@using Xunit;
+@using Xunit;
@inherits FluentUITestContext
@code
@@ -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(@);
-
- // 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()
{
diff --git a/tests/Core/Components/TextArea/FluentTextAreaTests.razor b/tests/Core/Components/TextArea/FluentTextAreaTests.razor
index eb1ca80df5..ea1d5539e6 100644
--- a/tests/Core/Components/TextArea/FluentTextAreaTests.razor
+++ b/tests/Core/Components/TextArea/FluentTextAreaTests.razor
@@ -130,25 +130,6 @@
.MarkupMatches("");
}
- [Fact]
- public void FluentTextArea_OnChange_ReportsValidity()
- {
- // Arrange
- using var context = new IdentifierContext(i => "myId");
- var value = "init";
- var cut = Render(@);
-
- // 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
diff --git a/tests/Core/Components/Validation/ReportValidityTests.cs b/tests/Core/Components/Validation/ReportValidityTests.cs
new file mode 100644
index 0000000000..974cdf0a76
--- /dev/null
+++ b/tests/Core/Components/Validation/ReportValidityTests.cs
@@ -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(new HtmlParser());
+ ctx.Services.AddFluentUIComponents();
+
+ // Arrange
+ var cut = ctx.Render(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(new HtmlParser());
+ ctx.Services.AddFluentUIComponents(config => config.UseNativeConstraintValidationUI = true);
+
+ // Arrange
+ var cut = ctx.Render(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);
+ }
+}