Skip to content

Commit d944d0b

Browse files
Fix enum selection issues
1 parent 58a8871 commit d944d0b

File tree

2 files changed

+193
-23
lines changed

2 files changed

+193
-23
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using AngleSharp.Html.Dom;
2+
using Bunit;
3+
using FluentAssertions;
4+
using Microsoft.AspNetCore.Components;
5+
using Xunit;
6+
7+
namespace SimpleBlazorMultiselect.Tests;
8+
9+
public class EnumTests : BaseTest
10+
{
11+
[Fact]
12+
public void Component_WithEmptySet_ShouldShowNoChecks()
13+
{
14+
var options = Enum.GetValues<TestEnum>().ToList();
15+
var selectedItems = new HashSet<TestEnum>();
16+
17+
var component = RenderComponent<SimpleMultiselect<TestEnum>>(parameters => parameters
18+
.Add(p => p.Options, options)
19+
.Add(p => p.SelectedOptions, selectedItems)
20+
.Add(p => p.StringSelector, item => item.ToString())
21+
.Add(p => p.DefaultText, "empty")
22+
.Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create<HashSet<TestEnum>>(this, newSelection => { selectedItems = newSelection; })));
23+
24+
var button = component.Find("button");
25+
button.TextContent.Should().Be("empty");
26+
button.Click();
27+
28+
// Check if all options are unchecked
29+
var dropdownItems = component.FindAll(".dropdown-item");
30+
foreach (var item in dropdownItems)
31+
{
32+
var checkbox = item.QuerySelector("input[type='checkbox']") as IHtmlInputElement;
33+
checkbox.Should().NotBeNull();
34+
checkbox.IsChecked.Should().BeFalse();
35+
}
36+
}
37+
38+
[Fact]
39+
public void Component_WithPrefilledOptions_ShouldShowCorrectCheck()
40+
{
41+
var options = Enum.GetValues<TestEnum>().ToList();
42+
var selectedItems = new HashSet<TestEnum>
43+
{
44+
TestEnum.OptionB
45+
};
46+
47+
var component = RenderComponent<SimpleMultiselect<TestEnum>>(parameters => parameters
48+
.Add(p => p.Options, options)
49+
.Add(p => p.SelectedOptions, selectedItems)
50+
.Add(p => p.StringSelector, item => item.ToString())
51+
.Add(p => p.DefaultText, "empty")
52+
.Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create<HashSet<TestEnum>>(this, newSelection => { selectedItems = newSelection; })));
53+
54+
var button = component.Find("button");
55+
button.TextContent.Should().Be(nameof(TestEnum.OptionB));
56+
button.Click();
57+
58+
// Check if all options are unchecked except OptionB
59+
var dropdownItems = component.FindAll(".dropdown-item");
60+
foreach (var item in dropdownItems)
61+
{
62+
var checkbox = item.QuerySelector("input[type='checkbox']") as IHtmlInputElement;
63+
checkbox.Should().NotBeNull();
64+
65+
var optionText = item.TextContent.Trim();
66+
if (optionText == nameof(TestEnum.OptionB))
67+
{
68+
checkbox.Should().NotBeNull();
69+
checkbox.IsChecked.Should().BeTrue();
70+
}
71+
else
72+
{
73+
checkbox.IsChecked.Should().BeFalse();
74+
}
75+
}
76+
}
77+
78+
[Fact]
79+
public void Component_CanUncheckPrefilled()
80+
{
81+
var options = Enum.GetValues<TestEnum>().ToList();
82+
var selectedItems = new HashSet<TestEnum>
83+
{
84+
TestEnum.OptionB
85+
};
86+
87+
var component = RenderComponent<SimpleMultiselect<TestEnum>>(parameters => parameters
88+
.Add(p => p.Options, options)
89+
.Add(p => p.SelectedOptions, selectedItems)
90+
.Add(p => p.StringSelector, item => item.ToString())
91+
.Add(p => p.DefaultText, "empty")
92+
.Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create<HashSet<TestEnum>>(this, newSelection => { selectedItems = newSelection; })));
93+
94+
var button = component.Find("button");
95+
button.TextContent.Should().Be(nameof(TestEnum.OptionB));
96+
button.Click();
97+
98+
// Check if option B is checked
99+
var dropdownItems = component.FindAll(".dropdown-item");
100+
var optionBItem = dropdownItems.First(item => item.TextContent.Trim() == nameof(TestEnum.OptionB));
101+
var optionBCheckbox = optionBItem.QuerySelector("input[type='checkbox']") as IHtmlInputElement;
102+
optionBCheckbox.Should().NotBeNull();
103+
optionBCheckbox.IsChecked.Should().BeTrue();
104+
105+
// Click to uncheck OptionB
106+
optionBItem.Click();
107+
108+
// After clicking, OptionB should be deselected
109+
selectedItems.Should().BeEmpty();
110+
button = component.Find("button");
111+
button.TextContent.Should().Be("empty");
112+
}
113+
114+
[Fact]
115+
public void Component_CanCheckUncheckedOption()
116+
{
117+
var options = Enum.GetValues<TestEnum>().ToList();
118+
var selectedItems = new HashSet<TestEnum>();
119+
120+
var component = RenderComponent<SimpleMultiselect<TestEnum>>(parameters => parameters
121+
.Add(p => p.Options, options)
122+
.Add(p => p.SelectedOptions, selectedItems)
123+
.Add(p => p.StringSelector, item => item.ToString())
124+
.Add(p => p.DefaultText, "empty")
125+
.Add(p => p.SelectedOptionsChanged, EventCallback.Factory.Create<HashSet<TestEnum>>(this, newSelection => { selectedItems = newSelection; })));
126+
127+
var button = component.Find("button");
128+
button.TextContent.Should().Be("empty");
129+
button.Click();
130+
131+
// Check if all options are unchecked
132+
var dropdownItems = component.FindAll(".dropdown-item");
133+
var optionCItem = dropdownItems.First(item => item.TextContent.Trim() == nameof(TestEnum.OptionC));
134+
var optionCCheckbox = optionCItem.QuerySelector("input[type='checkbox']") as IHtmlInputElement;
135+
optionCCheckbox.Should().NotBeNull();
136+
optionCCheckbox.IsChecked.Should().BeFalse();
137+
138+
// Click to check OptionC
139+
optionCItem.Click();
140+
141+
// After clicking, OptionC should be selected
142+
selectedItems.Should().Contain(TestEnum.OptionC);
143+
button = component.Find("button");
144+
button.TextContent.Should().Be(nameof(TestEnum.OptionC));
145+
}
146+
147+
private enum TestEnum
148+
{
149+
OptionA,
150+
OptionB,
151+
OptionC
152+
}
153+
}

src/SimpleBlazorMultiselect/SimpleMultiselect.razor.cs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.AspNetCore.Components;
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.AspNetCore.Components;
23

34
namespace SimpleBlazorMultiselect;
45

@@ -101,25 +102,30 @@ private bool DefaultFilterPredicate(TItem item, string filterString)
101102
/// </summary>
102103
[CascadingParameter(Name = "Standalone")]
103104
public bool Standalone { get; set; }
104-
105+
105106
/// <summary>
106107
/// If true, the selection of options will be matched by reference instead of by string representation.
107108
/// </summary>
108109
[Parameter]
109110
public bool MatchByReference { get; set; }
110-
111+
111112
private async Task ToggleOption(TItem option)
112113
{
113114
var newSelected = new HashSet<TItem>(SelectedOptions);
114-
115-
var existingSelected = FindSelected(option);
116-
var wasSelected = existingSelected != null && newSelected.Remove(existingSelected);
115+
116+
var wasSelected = false;
117+
if (TryFindSelected(option, out var existing))
118+
{
119+
wasSelected = newSelected.Remove(existing);
120+
}
121+
117122
if (!wasSelected)
118123
{
119124
if (!IsMultiSelect)
120125
{
121126
newSelected.Clear();
122127
}
128+
123129
newSelected.Add(option);
124130
}
125131

@@ -134,45 +140,56 @@ private async Task ToggleOption(TItem option)
134140

135141
private bool IsOptionSelected(TItem option)
136142
{
137-
return FindSelected(option) != null;
143+
return TryFindSelected(option, out _);
138144
}
139-
140-
/// <summary>
141-
/// Helper function to find the selected option that matches the given options.
142-
/// Options might not be the same reference.
143-
/// </summary>
144-
private TItem? FindSelected(TItem option)
145+
146+
private bool TryFindSelected(TItem option, [NotNullWhen(true)] out TItem existing)
145147
{
148+
if (SelectedOptions.Count == 0)
149+
{
150+
existing = default!;
151+
return false;
152+
}
153+
146154
if (MatchByReference)
147155
{
148-
SelectedOptions.TryGetValue(option, out var existing);
149-
return existing;
156+
return SelectedOptions.TryGetValue(option, out existing!);
150157
}
151-
158+
152159
var optionString = StringSelector(option);
153-
return SelectedOptions.FirstOrDefault(selected => StringSelector(selected) == optionString);
160+
foreach (var selected in SelectedOptions)
161+
{
162+
if (StringSelector(selected) == optionString)
163+
{
164+
existing = selected!;
165+
return true;
166+
}
167+
}
168+
169+
existing = default!;
170+
return false;
154171
}
155172

156173
private List<TItem>? _filteredOptionsCache;
157174
private List<TItem>? _prevOptions;
158175
private Func<TItem, string, bool>? _prevFilterPredicate;
159176
private string? _prevFilterText;
160177
private bool _prevCanFilter;
161-
178+
162179
private List<TItem> FilteredOptions()
163180
{
164-
if(_prevCanFilter == CanFilter && _prevFilterPredicate == FilterPredicate && _prevFilterText == _filterText && _prevOptions == Options)
181+
if (_prevCanFilter == CanFilter && _prevFilterPredicate == FilterPredicate && _prevFilterText == _filterText && _prevOptions == Options)
165182
{
166183
return _filteredOptionsCache ?? Options;
167184
}
168-
185+
169186
_prevOptions = Options;
170187
_prevCanFilter = CanFilter;
171188
_prevFilterPredicate = FilterPredicate;
172189
_prevFilterText = _filterText;
173-
190+
174191
_filteredOptionsCache = [];
175-
if(!CanFilter || string.IsNullOrWhiteSpace(_filterText))
192+
if (!CanFilter || string.IsNullOrWhiteSpace(_filterText))
176193
{
177194
_filteredOptionsCache.AddRange(Options);
178195
}
@@ -181,7 +198,7 @@ private List<TItem> FilteredOptions()
181198
var predicate = FilterPredicate ?? DefaultFilterPredicate;
182199
_filteredOptionsCache.AddRange(Options.Where(option => predicate(option, _filterText)));
183200
}
184-
201+
185202
return _filteredOptionsCache;
186203
}
187204
}

0 commit comments

Comments
 (0)