From b9cee4c8b09e9fd30242922935cc6227916358c7 Mon Sep 17 00:00:00 2001 From: KitKeen Date: Sun, 19 Apr 2026 11:35:57 +0300 Subject: [PATCH 1/2] Combine ValidationSummary class attribute with custom classes When a class attribute is supplied to via attribute splatting, append it to the built-in validation-errors class instead of replacing it. This matches the pattern used in InputBase, InputRadio, and QuickGrid via AttributeUtilities.CombineClassNames. Fixes #43860 --- .../Web/src/Forms/ValidationSummary.cs | 5 +- .../Web/test/Forms/ValidationSummaryTest.cs | 144 ++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/Components/Web/test/Forms/ValidationSummaryTest.cs 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(); + } + } +} From cb9b513907bd79acb7a4948ecb63898482b7590b Mon Sep 17 00:00:00 2001 From: KitKeen Date: Mon, 20 Apr 2026 13:54:22 +0300 Subject: [PATCH 2/2] Retrigger CI