diff --git a/src/Components/Web/src/Forms/ValidationSummary.cs b/src/Components/Web/src/Forms/ValidationSummary.cs
index e2580fd0cf97..c889483e96ff 100644
--- a/src/Components/Web/src/Forms/ValidationSummary.cs
+++ b/src/Components/Web/src/Forms/ValidationSummary.cs
@@ -25,6 +25,7 @@ public class ValidationSummary : ComponentBase, IDisposable
///
/// Gets or sets a collection of additional attributes that will be applied to the created ul element.
+ /// A class attribute supplied here is appended to the built-in validation-errors class rather than replacing it.
///
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? AdditionalAttributes { get; set; }
@@ -73,8 +74,8 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
first = false;
builder.OpenElement(0, "ul");
- builder.AddAttribute(1, "class", "validation-errors");
- builder.AddMultipleAttributes(2, AdditionalAttributes);
+ builder.AddMultipleAttributes(1, AdditionalAttributes);
+ builder.AddAttribute(2, "class", AttributeUtilities.CombineClassNames(AdditionalAttributes, "validation-errors"));
}
builder.OpenElement(3, "li");
diff --git a/src/Components/Web/test/Forms/ValidationSummaryTest.cs b/src/Components/Web/test/Forms/ValidationSummaryTest.cs
new file mode 100644
index 000000000000..8e87265839a6
--- /dev/null
+++ b/src/Components/Web/test/Forms/ValidationSummaryTest.cs
@@ -0,0 +1,144 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
+using Microsoft.AspNetCore.Components.Test.Helpers;
+
+namespace Microsoft.AspNetCore.Components.Forms;
+
+public class ValidationSummaryTest
+{
+ [Fact]
+ public async Task RendersValidationErrorsClass_WhenNoAdditionalAttributes()
+ {
+ var classAttribute = await RenderAndGetUlClassAttribute(additionalAttributes: null);
+
+ Assert.Equal("validation-errors", classAttribute);
+ }
+
+ [Fact]
+ public async Task CombinesCustomClassWithValidationErrors()
+ {
+ var classAttribute = await RenderAndGetUlClassAttribute(new Dictionary
+ {
+ { "class", "pt-2" },
+ });
+
+ Assert.Equal("pt-2 validation-errors", classAttribute);
+ }
+
+ [Fact]
+ public async Task SplatsAdditionalAttributesToUl()
+ {
+ var frames = await RenderAndGetValidationSummaryFrames(new Dictionary
+ {
+ { "data-test", "validation-errors" },
+ });
+
+ var ulAttributes = GetUlAttributes(frames);
+ Assert.Contains(ulAttributes, a => a.AttributeName == "data-test" && (string)a.AttributeValue == "validation-errors");
+ Assert.Equal("validation-errors", GetClassValue(ulAttributes));
+ }
+
+ [Fact]
+ public async Task DoesNotRenderUl_WhenNoValidationMessages()
+ {
+ var model = new TestModel();
+ var editContext = new EditContext(model);
+ var rootComponent = new TestValidationSummaryHostComponent
+ {
+ EditContext = editContext,
+ };
+ var testRenderer = new TestRenderer();
+ var componentId = testRenderer.AssignRootComponentId(rootComponent);
+ await testRenderer.RenderRootComponentAsync(componentId);
+
+ var frames = GetValidationSummaryFrames(testRenderer);
+ Assert.DoesNotContain(frames.AsEnumerable(), f => f.FrameType == RenderTreeFrameType.Element && f.ElementName == "ul");
+ }
+
+ private static async Task RenderAndGetUlClassAttribute(Dictionary additionalAttributes)
+ {
+ var frames = await RenderAndGetValidationSummaryFrames(additionalAttributes);
+ return GetClassValue(GetUlAttributes(frames));
+ }
+
+ private static async Task> RenderAndGetValidationSummaryFrames(Dictionary additionalAttributes)
+ {
+ var model = new TestModel();
+ var editContext = new EditContext(model);
+ var messages = new ValidationMessageStore(editContext);
+ messages.Add(new FieldIdentifier(model, nameof(TestModel.StringProperty)), "An error");
+
+ var rootComponent = new TestValidationSummaryHostComponent
+ {
+ EditContext = editContext,
+ AdditionalAttributes = additionalAttributes,
+ };
+ var testRenderer = new TestRenderer();
+ var componentId = testRenderer.AssignRootComponentId(rootComponent);
+ await testRenderer.RenderRootComponentAsync(componentId);
+
+ return GetValidationSummaryFrames(testRenderer);
+ }
+
+ private static ArrayRange GetValidationSummaryFrames(TestRenderer testRenderer)
+ {
+ var batch = testRenderer.Batches.Single();
+ var componentId = batch.GetComponentFrames().Single().ComponentId;
+ return testRenderer.GetCurrentRenderTreeFrames(componentId);
+ }
+
+ private static List GetUlAttributes(ArrayRange frames)
+ {
+ var result = new List();
+ var seenUl = false;
+ foreach (var frame in frames.AsEnumerable())
+ {
+ if (!seenUl)
+ {
+ if (frame.FrameType == RenderTreeFrameType.Element && frame.ElementName == "ul")
+ {
+ seenUl = true;
+ }
+ continue;
+ }
+
+ if (frame.FrameType != RenderTreeFrameType.Attribute)
+ {
+ break;
+ }
+ result.Add(frame);
+ }
+ return result;
+ }
+
+ private static string GetClassValue(List ulAttributes)
+ => ulAttributes.Single(a => a.AttributeName == "class").AttributeValue as string;
+
+ private class TestModel
+ {
+ public string StringProperty { get; set; }
+ }
+
+ private class TestValidationSummaryHostComponent : AutoRenderComponent
+ {
+ public EditContext EditContext { get; set; }
+
+ public Dictionary AdditionalAttributes { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent>(0);
+ builder.AddComponentParameter(1, "Value", EditContext);
+ builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
+ {
+ childBuilder.OpenComponent(0);
+ childBuilder.AddMultipleAttributes(1, AdditionalAttributes);
+ childBuilder.CloseComponent();
+ }));
+ builder.CloseComponent();
+ }
+ }
+}