From 1645e8fa41e065c65a0d99b5251ee4a9783b1cfb Mon Sep 17 00:00:00 2001 From: Matthew Batchelor <65035377+PlayerModu@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:05:13 +0100 Subject: [PATCH] fix(MudSelectExtended): place aria-label on visible control instead of hidden input --- .../Examples/SelectExtendedExample6.razor | 1 + .../SelectExtended/MudSelectExtended.razor | 5 +- .../SelectExtended/MudSelectExtended.razor.cs | 42 ++++++++ .../SelectWithAriaLabelTest.razor | 16 +++ .../Components/SelectExtendedTests.cs | 98 ++++++++++++++++++- 5 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectWithAriaLabelTest.razor diff --git a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample6.razor b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample6.razor index b71d39d8..d59ccaa1 100644 --- a/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample6.razor +++ b/docs/CodeBeam.MudBlazor.Extensions.Docs/Pages/Components/SelectExtended/Examples/SelectExtendedExample6.razor @@ -35,6 +35,7 @@ ToStringFunc="@((TestHouse house) => $"{house.Number} - {house.Name}")" AnchorOrigin="Origin.BottomCenter" Variant="Variant.Outlined" + aria-label="House Selection" HelperText="Search by number or name (e.g., '1 -' or 'Test3')" SearchBoxClearable="true" /> diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor index 8424574e..18d6f7a5 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor @@ -21,7 +21,8 @@ Disabled="@Disabled" @onclick="@ToggleMenu" Required="@Required" - ForId="@InputElementId"> + ForId="@InputElementId" + @attributes="GetAriaAttributes()"> diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor.cs index a83114fe..838e5710 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor.cs @@ -1332,5 +1332,47 @@ public bool GetOpenState() var n = ToStringFunc(input); return ToStringFunc(input); } + + /// + /// Filters UserAttributes to exclude ARIA attributes so they don't get applied to the hidden input. + /// ARIA attributes should only be on the visible MudInputControl, not the hidden MudInputExtended. + /// + /// A dictionary of user attributes with ARIA attributes removed + protected Dictionary GetUserAttributesForHiddenInput() + { + if (UserAttributes == null || UserAttributes.Count == 0) + return []; + + var filtered = new Dictionary(UserAttributes); + filtered.Remove("aria-label"); + filtered.Remove("aria-labelledby"); + return filtered; + } + + /// + /// Extracts ARIA attributes from UserAttributes that should be applied to the visible control. + /// These attributes belong on the visible MudInputControl, not the hidden MudInputExtended. + /// + /// A dictionary containing only ARIA attributes + protected Dictionary GetAriaAttributes() + { + if (UserAttributes == null || UserAttributes.Count == 0) + return []; + + var aria = new Dictionary(); + + // Only include aria-* attributes that should be on the visible element + var ariaKeys = new[] { "aria-label", "aria-labelledby" }; + + foreach (var key in ariaKeys) + { + if (UserAttributes.TryGetValue(key, out var value)) + { + aria[key] = value; + } + } + + return aria; + } } } \ No newline at end of file diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectWithAriaLabelTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectWithAriaLabelTest.razor new file mode 100644 index 00000000..861e2065 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectWithAriaLabelTest.razor @@ -0,0 +1,16 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + + + + + + +@code { + [Parameter] + public Dictionary UserAttributes { get; set; } = new(); + + string? value = null; +} diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedTests.cs index 41f900f3..42aeabed 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedTests.cs @@ -1354,5 +1354,101 @@ public void Select_Should_NotOpen_WhenParentDisabled() // The menu should not open comp.Find("div.mud-popover").ClassList.Should().NotContain("mud-popover-open"); } + + [Test] + public void Select_AriaLabel_ShouldBeOnVisibleControl_NotHiddenInput() + { + var comp = Context.Render(parameters => + { + parameters.Add(p => p.UserAttributes, new Dictionary { { "aria-label", "Test Dropdown" } }); + }); + + var visibleControl = comp.Find("div.mud-input-control"); + var hiddenInput = comp.Find("input[type='hidden']"); + + // aria-label should be on the visible MudInputControl + visibleControl.GetAttribute("aria-label").Should().Be("Test Dropdown"); + + // aria-label should NOT be on the hidden input + hiddenInput.GetAttribute("aria-label").Should().BeNull(); + } + + [Test] + public void Select_AriaLabelledBy_ShouldBeOnVisibleControl_NotHiddenInput() + { + var comp = Context.Render(parameters => + { + parameters.Add(p => p.UserAttributes, new Dictionary { { "aria-labelledby", "my-label-id" } }); + }); + + var visibleControl = comp.Find("div.mud-input-control"); + var hiddenInput = comp.Find("input[type='hidden']"); + + // aria-labelledby should be on the visible MudInputControl + visibleControl.GetAttribute("aria-labelledby").Should().Be("my-label-id"); + + // aria-labelledby should NOT be on the hidden input + hiddenInput.GetAttribute("aria-labelledby").Should().BeNull(); + } + + [Test] + public void Select_OtherUserAttributes_ShouldBePreservedOnHiddenInput() + { + var comp = Context.Render(parameters => + { + parameters.Add(p => p.UserAttributes, new Dictionary + { + { "aria-label", "Test Dropdown" }, + { "data-testid", "my-select" }, + { "custom-attr", "custom-value" } + }); + }); + + var hiddenInput = comp.Find("input[type='hidden']"); + + // aria-label should be filtered out + hiddenInput.GetAttribute("aria-label").Should().BeNull(); + + // Other attributes should be preserved + hiddenInput.GetAttribute("data-testid").Should().Be("my-select"); + hiddenInput.GetAttribute("custom-attr").Should().Be("custom-value"); + } + + [Test] + public void Select_BothAriaAttributes_ShouldBeFiltered() + { + var comp = Context.Render(parameters => + { + parameters.Add(p => p.UserAttributes, new Dictionary + { + { "aria-label", "Test Dropdown" }, + { "aria-labelledby", "my-label-id" } + }); + }); + + var visibleControl = comp.Find("div.mud-input-control"); + var hiddenInput = comp.Find("input[type='hidden']"); + + // Both aria attributes should be on visible control + visibleControl.GetAttribute("aria-label").Should().Be("Test Dropdown"); + visibleControl.GetAttribute("aria-labelledby").Should().Be("my-label-id"); + + // Neither should be on hidden input + hiddenInput.GetAttribute("aria-label").Should().BeNull(); + hiddenInput.GetAttribute("aria-labelledby").Should().BeNull(); + } + + [Test] + public void Select_NoUserAttributes_ShouldWorkCorrectly() + { + var comp = Context.Render(); + + var visibleControl = comp.Find("div.mud-input-control"); + var hiddenInput = comp.Find("input[type='hidden']"); + + // Neither element should have aria attributes + visibleControl.GetAttribute("aria-label").Should().BeNull(); + hiddenInput.GetAttribute("aria-label").Should().BeNull(); + } } -} +} \ No newline at end of file