From a8060b9566be166ec0125fd0be9dadf22c86048d Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 1 May 2026 14:42:45 +0800 Subject: [PATCH] Guard aria-describedby and InfoText id against missing/empty input id - Stop using the localized text as the aria-describedby value in AddInfoTextId; reference the actual id directly - Skip rendering the InfoText id and aria-describedby when the input/select has no id (or an empty one) so the form never renders a non-unique "InfoText" id - Cover the no-id case with a new test --- .../Form/AbpInputTagHelperService.cs | 15 ++++---- .../Form/AbpSelectTagHelperService.cs | 15 ++++---- .../Form/AbpSelectTagHelperService_Tests.cs | 36 +++++++++++++++++-- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index 25fb1aff3e..a3adc4eae7 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -259,15 +259,14 @@ public class AbpInputTagHelperService : AbpTagHelperService } var idAttr = inputTagHelperOutput.Attributes.FirstOrDefault(a => a.Name == "id"); + var idValue = idAttr?.Value?.ToString(); - if (idAttr == null) + if (string.IsNullOrEmpty(idValue)) { return; } - var infoText = _tagHelperLocalizer.GetLocalizedText(idAttr.Value + "InfoText", TagHelper.AspFor.ModelExplorer); - - inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", infoText); + inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", idValue + "InfoText"); } protected virtual bool IsInputCheckbox(TagHelperContext context, TagHelperOutput output, TagHelperAttributeList attributes) @@ -356,14 +355,18 @@ public class AbpInputTagHelperService : AbpTagHelperService } var idAttr = inputTag.Attributes.FirstOrDefault(a => a.Name == "id"); + var idValue = idAttr?.Value?.ToString(); var localizedText = _tagHelperLocalizer.GetLocalizedText(text, TagHelper.AspFor.ModelExplorer); var div = new TagBuilder("div"); - div.Attributes.Add("id", idAttr?.Value + "InfoText"); div.AddCssClass("form-text"); div.InnerHtml.Append(localizedText); - inputTag.Attributes.SetAttribute("aria-describedby", idAttr?.Value + "InfoText"); + if (!string.IsNullOrEmpty(idValue)) + { + div.Attributes.Add("id", idValue + "InfoText"); + inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText"); + } return div.ToHtmlString(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index 854415f8c1..928082d6e8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -216,15 +216,14 @@ public class AbpSelectTagHelperService : AbpTagHelperService } var idAttr = inputTagHelperOutput.Attributes.FirstOrDefault(a => a.Name == "id"); + var idValue = idAttr?.Value?.ToString(); - if (idAttr == null) + if (string.IsNullOrEmpty(idValue)) { return; } - var infoText = _tagHelperLocalizer.GetLocalizedText(idAttr.Value + "InfoText", TagHelper.AspFor.ModelExplorer); - - inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", infoText); + inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", idValue + "InfoText"); } protected virtual string GetInfoAsHtml(TagHelperContext context, TagHelperOutput output, TagHelperOutput inputTag) @@ -249,14 +248,18 @@ public class AbpSelectTagHelperService : AbpTagHelperService } var idAttr = inputTag.Attributes.FirstOrDefault(a => a.Name == "id"); + var idValue = idAttr?.Value?.ToString(); var localizedText = _tagHelperLocalizer.GetLocalizedText(text, TagHelper.AspFor.ModelExplorer); var div = new TagBuilder("div"); - div.Attributes.Add("id", idAttr?.Value + "InfoText"); div.AddCssClass("form-text"); div.InnerHtml.Append(localizedText); - inputTag.Attributes.SetAttribute("aria-describedby", idAttr?.Value + "InfoText"); + if (!string.IsNullOrEmpty(idValue)) + { + div.Attributes.Add("id", idValue + "InfoText"); + inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText"); + } return div.ToHtmlString(); } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo/Abp/AspNetCore/Mvc/UI/Bootstrap/TagHelpers/Form/AbpSelectTagHelperService_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo/Abp/AspNetCore/Mvc/UI/Bootstrap/TagHelpers/Form/AbpSelectTagHelperService_Tests.cs index 79d61ff74d..4ec7948846 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo/Abp/AspNetCore/Mvc/UI/Bootstrap/TagHelpers/Form/AbpSelectTagHelperService_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo/Abp/AspNetCore/Mvc/UI/Bootstrap/TagHelpers/Form/AbpSelectTagHelperService_Tests.cs @@ -55,6 +55,29 @@ public class AbpSelectTagHelperService_Tests service.LastGroupHtml.ShouldContain("aria-describedby=\"TestSelectInfoText\""); } + [Fact] + public async Task Info_text_should_skip_id_and_aria_describedby_when_select_has_no_id() + { + var service = new TestAbpSelectTagHelperService(selectId: null); + var tagHelper = new AbpSelectTagHelper(service) + { + AspFor = CreateModelExpression(), + InfoText = "Description" + }; + + var output = CreateOutput(); + + await tagHelper.ProcessAsync(CreateContext(), output); + + service.LastGroupHtml.ShouldContain("
GetSelectTagAsync(TagHelperContext context, TagHelperOutput output, TagHelperContent childContent) { + var attributes = new TagHelperAttributeList(); + if (!string.IsNullOrEmpty(_selectId)) + { + attributes.Add("id", _selectId); + } + LastSelectTag = new TagHelperOutput( "select", - new TagHelperAttributeList { { "id", "TestSelect" } }, + attributes, (_, _) => Task.FromResult(new DefaultTagHelperContent())) { TagMode = TagMode.StartTagAndEndTag