Browse Source

Append aria-describedby tokens instead of overwriting

- Add TagHelperOutputExtensions.AppendAriaDescribedby helper that preserves caller-supplied tokens (space-separated id list) and dedupes
- Replace SetAttribute calls in AddInfoTextId/GetInfoAsHtml of abp-input/abp-select with the helper
- Cover the consumer-provided aria-describedby case with a new test
pull/25352/head
maliming 3 weeks ago
parent
commit
19c4e2989a
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 32
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperOutputExtensions.cs
  2. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
  3. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
  4. 27
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo/Abp/AspNetCore/Mvc/UI/Bootstrap/TagHelpers/Form/AbpSelectTagHelperService_Tests.cs

32
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();
}
}
/// <summary>
/// Appends an id token to the space-separated <c>aria-describedby</c> 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.
/// </summary>
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);
}
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs

@ -266,7 +266,7 @@ public class AbpInputTagHelperService : AbpTagHelperService<AbpInputTagHelper>
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<AbpInputTagHelper>
if (!string.IsNullOrEmpty(idValue))
{
div.Attributes.Add("id", idValue + "InfoText");
inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
inputTag.AppendAriaDescribedby(idValue + "InfoText");
}
return div.ToHtmlString();

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs

@ -223,7 +223,7 @@ public class AbpSelectTagHelperService : AbpTagHelperService<AbpSelectTagHelper>
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<AbpSelectTagHelper>
if (!string.IsNullOrEmpty(idValue))
{
div.Attributes.Add("id", idValue + "InfoText");
inputTag.Attributes.SetAttribute("aria-describedby", idValue + "InfoText");
inputTag.AppendAriaDescribedby(idValue + "InfoText");
}
return div.ToHtmlString();

27
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<TagHelperOutput> 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",

Loading…
Cancel
Save