diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs
index 40ca23a8f1..838d662af8 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
+using System;
using System.IO;
+using System.Linq;
using System.Text.Encodings.Web;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions;
@@ -14,4 +16,34 @@ public static class TagHelperOutputExtensions
return writer.ToString();
}
}
+
+ ///
+ /// Appends an id token to the space-separated aria-describedby attribute,
+ /// preserving any tokens that were already present (e.g. provided by the consumer)
+ /// and skipping the token when it is already in the list.
+ ///
+ public static void AppendAriaDescribedby(this TagHelperOutput output, string token)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ return;
+ }
+
+ var existing = output.Attributes
+ .FirstOrDefault(a => a.Name == "aria-describedby")?.Value?.ToString();
+
+ if (string.IsNullOrEmpty(existing))
+ {
+ output.Attributes.SetAttribute("aria-describedby", token);
+ return;
+ }
+
+ var tokens = existing.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Any(t => t == token))
+ {
+ return;
+ }
+
+ output.Attributes.SetAttribute("aria-describedby", existing + " " + token);
+ }
}
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 a3adc4eae7..f8994a462c 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
@@ -266,7 +266,7 @@ public class AbpInputTagHelperService : AbpTagHelperService
return;
}
- inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
+ inputTagHelperOutput.AppendAriaDescribedby(idValue + "InfoText");
}
protected virtual bool IsInputCheckbox(TagHelperContext context, TagHelperOutput output, TagHelperAttributeList attributes)
@@ -365,7 +365,7 @@ public class AbpInputTagHelperService : AbpTagHelperService
if (!string.IsNullOrEmpty(idValue))
{
div.Attributes.Add("id", idValue + "InfoText");
- inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
+ inputTag.AppendAriaDescribedby(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 928082d6e8..42d85cfb74 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
@@ -223,7 +223,7 @@ public class AbpSelectTagHelperService : AbpTagHelperService
return;
}
- inputTagHelperOutput.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
+ inputTagHelperOutput.AppendAriaDescribedby(idValue + "InfoText");
}
protected virtual string GetInfoAsHtml(TagHelperContext context, TagHelperOutput output, TagHelperOutput inputTag)
@@ -258,7 +258,7 @@ public class AbpSelectTagHelperService : AbpTagHelperService
if (!string.IsNullOrEmpty(idValue))
{
div.Attributes.Add("id", idValue + "InfoText");
- inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
+ inputTag.AppendAriaDescribedby(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 4ec7948846..3ec4e12a2c 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
@@ -78,6 +78,25 @@ public class AbpSelectTagHelperService_Tests
service.LastSelectTag!.Attributes.ContainsName("aria-describedby").ShouldBeFalse();
}
+ [Fact]
+ public async Task Aria_describedby_should_preserve_existing_value_set_by_caller()
+ {
+ var service = new TestAbpSelectTagHelperService(existingAriaDescribedby: "custom-id");
+ var tagHelper = new AbpSelectTagHelper(service)
+ {
+ AspFor = CreateModelExpression(),
+ InfoText = "Description"
+ };
+
+ var output = CreateOutput();
+
+ await tagHelper.ProcessAsync(CreateContext(), output);
+
+ service.LastSelectTag.ShouldNotBeNull();
+ service.LastSelectTag!.Attributes["aria-describedby"].Value.ToString().ShouldBe("custom-id TestSelectInfoText");
+ service.LastGroupHtml.ShouldContain("aria-describedby=\"custom-id TestSelectInfoText\"");
+ }
+
[Fact]
public async Task InputInfoText_attribute_should_render_info_text_with_single_aria_describedby()
{
@@ -143,15 +162,17 @@ public class AbpSelectTagHelperService_Tests
private sealed class TestAbpSelectTagHelperService : AbpSelectTagHelperService
{
private readonly string? _selectId;
+ private readonly string? _existingAriaDescribedby;
public string LastGroupHtml { get; private set; } = string.Empty;
public TagHelperOutput? LastSelectTag { get; private set; }
- public TestAbpSelectTagHelperService(string? selectId = "TestSelect")
+ public TestAbpSelectTagHelperService(string? selectId = "TestSelect", string? existingAriaDescribedby = null)
: base(null!, HtmlEncoder.Default, new FakeTagHelperLocalizer(), null!, null!)
{
_selectId = selectId;
+ _existingAriaDescribedby = existingAriaDescribedby;
}
protected override Task GetSelectTagAsync(TagHelperContext context, TagHelperOutput output, TagHelperContent childContent)
@@ -161,6 +182,10 @@ public class AbpSelectTagHelperService_Tests
{
attributes.Add("id", _selectId);
}
+ if (!string.IsNullOrEmpty(_existingAriaDescribedby))
+ {
+ attributes.Add("aria-describedby", _existingAriaDescribedby);
+ }
LastSelectTag = new TagHelperOutput(
"select",