From 89c7d11773163049227b057c2312e10caac34521 Mon Sep 17 00:00:00 2001 From: pigwing <35950090@qq.com> Date: Sat, 7 Nov 2026 12:20:31 +0800 Subject: [PATCH] Fix nested list selected-value synchronization --- .../ListExtended/MudListExtended.razor.cs | 4 +- .../SelectNestedMultiSelectionTest.razor | 37 ++++++ .../SelectExtendedNestedSelectionTests.cs | 116 ++++++++++++++++++ 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectNestedMultiSelectionTest.razor create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedNestedSelectionTests.cs diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListExtended.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListExtended.razor.cs index 1bd3d70f..f0065b75 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListExtended.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListExtended.razor.cs @@ -513,7 +513,7 @@ protected internal void UpdateSelectedValue() [Category(CategoryTypes.List.Selecting)] public T? SelectedValue { - get => _selectedValue; + get => ParentList != null ? ParentList.SelectedValue : _selectedValue; set { if (Converter.Convert(_selectedValue) != Converter.Convert(default(T)) && !_firstRendered) @@ -549,7 +549,7 @@ public IEnumerable? SelectedValues { get { - return _selectedValues; + return ParentList != null ? ParentList.SelectedValues : _selectedValues; } set diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectNestedMultiSelectionTest.razor b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectNestedMultiSelectionTest.razor new file mode 100644 index 00000000..e9268335 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests.Viewer/TestComponents/SelectExtended/SelectNestedMultiSelectionTest.razor @@ -0,0 +1,37 @@ +@namespace MudExtensions.UnitTests.TestComponents + + + + + + + + + + + + + + +@code { + [Parameter] + public bool InitiallyExpanded { get; set; } = true; + + public IEnumerable SelectedValues { get; private set; } = ["electric"]; + + public List SelectionChanges { get; } = new(); + + public void SetSelectedValues(params string[] values) + { + SelectedValues = values; + StateHasChanged(); + } + + private void OnSelectedValuesChanged(IEnumerable? values) + { + var snapshot = values?.ToArray() ?? []; + SelectedValues = snapshot; + SelectionChanges.Add(snapshot); + } +} diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedNestedSelectionTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedNestedSelectionTests.cs new file mode 100644 index 00000000..de6a7f71 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedNestedSelectionTests.cs @@ -0,0 +1,116 @@ +using AwesomeAssertions; +using Bunit; +using MudExtensions.UnitTests.TestComponents; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class SelectExtendedNestedSelectionTests : BunitTest + { + [TestCase(false)] + [TestCase(true)] + public void NestedMultiSelection_SelectingItem_PreservesExistingSelection(bool initiallyExpanded) + { + var cut = Context.Render(parameters => parameters + .Add(x => x.InitiallyExpanded, initiallyExpanded)); + + cut.Find("div.mud-input-control").Click(); + if (!initiallyExpanded) + cut.Find("[role='option'][aria-label='Station A']").Click(); + + var callbacks = cut.Instance.SelectionChanges.Count; + cut.Find("[role='option'][aria-label='Pressure']").Click(); + + cut.WaitForAssertion(() => + { + cut.Instance.SelectedValues.Should().BeEquivalentTo(new[] { "electric", "pressure" }); + cut.Instance.SelectionChanges.Should().HaveCount(callbacks + 1); + cut.Find("[role='option'][aria-label='Electricity']").GetAttribute("aria-selected").Should().Be("true"); + cut.Find("[role='option'][aria-label='Pressure']").GetAttribute("aria-selected").Should().Be("true"); + }); + } + + [Test] + public async Task NestedMultiSelection_ParentUpdates_ArePreservedWhenSelectingInAnotherGroup() + { + var cut = Context.Render(); + cut.Find("div.mud-input-control").Click(); + + await cut.InvokeAsync(() => cut.Instance.SetSelectedValues("flow", "pressure")); + var callbacks = cut.Instance.SelectionChanges.Count; + cut.Find("[role='option'][aria-label='Temperature']").Click(); + + cut.WaitForAssertion(() => + { + cut.Instance.SelectedValues.Should().BeEquivalentTo(new[] { "flow", "pressure", "temperature" }); + cut.Instance.SelectionChanges.Should().HaveCount(callbacks + 1); + cut.Find("[role='option'][aria-label='Electricity']").GetAttribute("aria-selected").Should().Be("false"); + cut.Find("[role='option'][aria-label='Flow']").GetAttribute("aria-selected").Should().Be("true"); + cut.Find("[role='option'][aria-label='Pressure']").GetAttribute("aria-selected").Should().Be("true"); + cut.Find("[role='option'][aria-label='Temperature']").GetAttribute("aria-selected").Should().Be("true"); + }); + } + + [Test] + public async Task NestedMultiSelection_DeselectingItem_PreservesOtherSelections() + { + var cut = Context.Render(); + cut.Find("div.mud-input-control").Click(); + + await cut.InvokeAsync(() => cut.Instance.SetSelectedValues("electric", "pressure", "temperature")); + var callbacks = cut.Instance.SelectionChanges.Count; + cut.Find("[role='option'][aria-label='Pressure']").Click(); + + cut.WaitForAssertion(() => + { + cut.Instance.SelectedValues.Should().BeEquivalentTo(new[] { "electric", "temperature" }); + cut.Instance.SelectionChanges.Should().HaveCount(callbacks + 1); + cut.Find("[role='option'][aria-label='Pressure']").GetAttribute("aria-selected").Should().Be("false"); + cut.Find("[role='option'][aria-label='Temperature']").GetAttribute("aria-selected").Should().Be("true"); + }); + } + + [Test] + public void NestedMultiSelection_ClickingSelectedItemAgain_DeselectsItOnce() + { + var cut = Context.Render(); + cut.Find("div.mud-input-control").Click(); + cut.Find("[role='option'][aria-label='Pressure']").Click(); + var callbacks = cut.Instance.SelectionChanges.Count; + + cut.Find("[role='option'][aria-label='Pressure']").Click(); + + cut.WaitForAssertion(() => + { + cut.Instance.SelectedValues.Should().BeEquivalentTo(new[] { "electric" }); + cut.Instance.SelectionChanges.Should().HaveCount(callbacks + 1); + cut.Find("[role='option'][aria-label='Pressure']").GetAttribute("aria-selected").Should().Be("false"); + }); + } + + [Test] + public async Task NestedSingleSelection_ChildListsReadRootValueAfterChangesAndClear() + { + var cut = Context.Render(); + var root = cut.FindComponent>(); + var lists = cut.FindComponents>(); + lists.Should().HaveCount(3); + + await cut.InvokeAsync(() => cut.Instance.SetSelectedValue(4)); + cut.WaitForAssertion(() => + { + root.Instance.SelectedValue.Should().Be(4); + foreach (var list in lists) + list.Instance.SelectedValue.Should().Be(4); + }); + + await cut.InvokeAsync(() => cut.Instance.SetSelectedValue(null)); + cut.WaitForAssertion(() => + { + root.Instance.SelectedValue.Should().BeNull(); + foreach (var list in lists) + list.Instance.SelectedValue.Should().BeNull(); + }); + } + } +}