diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index deb5366479..0f0d029977 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -6405,6 +6405,12 @@ Gets or sets the function used to determine if an option is initially selected. + + + Gets or sets the used to determine if an option is already added to the internal list. + ⚠️ Only available when Multiple = true. + + Gets or sets the content source of all items to display in this list. diff --git a/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor b/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor index 98388ab814..6ac269b8f6 100644 --- a/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor +++ b/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor @@ -36,6 +36,14 @@ + + +

+ By default the FluentAutocomplete component compares the search results by instance with it's internal selected items. You can control that behaviour by providing the OptionComparer parameter. +

+
+
+

Documentation

diff --git a/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutocompleteDifferentObjectInstances.razor b/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutocompleteDifferentObjectInstances.razor new file mode 100644 index 0000000000..963afd0977 --- /dev/null +++ b/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutocompleteDifferentObjectInstances.razor @@ -0,0 +1,67 @@ + +
+ Without OptionComparer: + +
+
+ With OptionComparer: + +
+
+@code { + + public IEnumerable Users1 { get; set; } = [new SimplePerson { Firstname = "Marvin", Lastname = "Klein", Age = 28 }]; + public IEnumerable Users2 { get; set; } = [new SimplePerson { Firstname = "Marvin", Lastname = "Klein", Age = 28 }]; + + private Task OnSearchUserAsync(OptionsSearchEventArgs e) + { + // Simulate new instances for every search. Typically you would retrieve these from a database or an API. + var results = new List + { + new SimplePerson { Firstname = "Alice", Lastname = "Wonder", Age = 31 }, + new SimplePerson { Firstname = "Marvin", Lastname = "Klein", Age = 28 }, + new SimplePerson { Firstname = "Vincent", Lastname = "Baaji", Age = 38 }, + }; + + e.Items = results; + + return Task.CompletedTask; + } + + public class MyComparer : IEqualityComparer + { + public static readonly MyComparer Instance = new(); + + public bool Equals(SimplePerson? x, SimplePerson? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.Firstname == y.Firstname && + x.Lastname == y.Lastname && + x.Age == y.Age; + } + + public int GetHashCode(SimplePerson obj) => HashCode.Combine(obj.Firstname, obj.Lastname, obj.Age); + } +} diff --git a/src/Core/Components/List/ListComponentBase.razor.cs b/src/Core/Components/List/ListComponentBase.razor.cs index 90d0e0652a..46ef70d092 100644 --- a/src/Core/Components/List/ListComponentBase.razor.cs +++ b/src/Core/Components/List/ListComponentBase.razor.cs @@ -129,6 +129,13 @@ protected string? InternalValue [Parameter] public virtual Func? OptionSelected { get; set; } + /// + /// Gets or sets the used to determine if an option is already added to the internal list. + /// ⚠️ Only available when Multiple = true. + /// + [Parameter] + public virtual IEqualityComparer? OptionComparer { get; set; } + /// /// Gets or sets the content source of all items to display in this list. /// Each item must be instantiated (cannot be null). @@ -533,11 +540,16 @@ protected virtual async Task OnSelectedItemChangedHandlerAsync(TOption? item) if (Multiple) { - if (_selectedOptions.Contains(item)) + if (OptionComparer is null && _selectedOptions.Contains(item)) { RemoveSelectedItem(item); await RaiseChangedEventsAsync(); } + else if (OptionComparer is not null && _selectedOptions.Find(x => OptionComparer.Equals(x, item)) is TOption addedItem) + { + RemoveSelectedItem(addedItem); + await RaiseChangedEventsAsync(); + } else { AddSelectedItem(item); diff --git a/tests/Core/Extensions/Customer.cs b/tests/Core/Extensions/Customer.cs index be64fe8d47..85ed228d1c 100644 --- a/tests/Core/Extensions/Customer.cs +++ b/tests/Core/Extensions/Customer.cs @@ -21,3 +21,25 @@ public static IEnumerable Get() } } +public class CustomerComparer : IEqualityComparer +{ + public static readonly CustomerComparer Instance = new(); + + public bool Equals(Customer? x, Customer? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.Id == y.Id && + x.Name == y.Name; + } + + public int GetHashCode(Customer obj) => HashCode.Combine(obj.Id, obj.Name); +} diff --git a/tests/Core/List/FluentAutocompleteTests.razor b/tests/Core/List/FluentAutocompleteTests.razor index b8a2312707..a1b5d21f19 100644 --- a/tests/Core/List/FluentAutocompleteTests.razor +++ b/tests/Core/List/FluentAutocompleteTests.razor @@ -495,6 +495,32 @@ cut.Verify(); } + [Fact] + public void FluentAutocomplete_SelectValueFromDifferentObjectInstances() + { + IEnumerable SelectedItems = [new Customer(1, "Marvin Klein")]; + + // Arrange + var cut = Render>( + @ + ); + + // Act: click to open -> KeyDow + Enter to select + var input = cut.Find("fluent-text-field"); + input.Click(); + + // Click on the second FluentOption + var marvin = SelectedItems.First(i => i.Id == 1); + cut.Find($"fluent-option[value='{marvin}']").Click(); + + // Assert (no item selected) + Assert.Empty(SelectedItems); + } + // Send a key code private async Task PressKeyAsync(IRenderedComponent> cut, KeyCode key, bool popoverOpened = false) { @@ -511,4 +537,17 @@ .OrderBy(i => i.Name); return Task.CompletedTask; } + + private Task OnSearchNewInstance(OptionsSearchEventArgs e) + { + var results = new List + { + new Customer(1, "Marvin Klein"), + new Customer(2, "Alice Wonder"), + new Customer(3, "Vincent Baaji") + }; + + e.Items = results; + return Task.CompletedTask; + } } diff --git a/tests/Core/List/FluentListboxTests.razor b/tests/Core/List/FluentListboxTests.razor index f7b17ed648..cc641f9dc8 100644 --- a/tests/Core/List/FluentListboxTests.razor +++ b/tests/Core/List/FluentListboxTests.razor @@ -108,4 +108,32 @@ // Assert cut.Verify(); } + + [Fact] + public void FluentListbox_SelectValueFromDifferentObjectInstances() + { + IEnumerable SelectedItems = [new Customer(1, "Marvin Klein")]; + + List Items = new List + { + new Customer(1, "Marvin Klein"), + new Customer(2, "Alice Wonder"), + new Customer(3, "Vincent Baaji") + }; + + // Arrange + var cut = Render>( + @ + ); + + // Click on the second FluentOption + var marvin = SelectedItems.First(i => i.Id == 1); + cut.Find($"fluent-option[value='{marvin}']").Click(); + + // Assert (no item selected) + Assert.Empty(SelectedItems); + } } diff --git a/tests/Core/List/FluentSelectTests.razor b/tests/Core/List/FluentSelectTests.razor index 24f7a60ca8..7de9b8c08b 100644 --- a/tests/Core/List/FluentSelectTests.razor +++ b/tests/Core/List/FluentSelectTests.razor @@ -114,19 +114,19 @@ { // Arrange && Act var cut = Render(@ - - Search - - - - Show - - - - Generate - - - ); + + Search + + + + Show + + + + Generate + + + ); // Assert cut.Verify(); @@ -138,9 +138,9 @@ { // Arrange && Act var cut = Render(@ - Position forced above - Option Two - ); + Position forced above + Option Two + ); // Assert cut.Verify(); @@ -151,9 +151,9 @@ { // Arrange && Act var cut = Render(@ - Position forced above - Option Two - ); + Position forced above + Option Two + ); // Assert cut.Verify(); @@ -165,11 +165,11 @@ { // Arrange && Act var cut = Render(@ - - - @(context.Name) - - + + + @(context.Name) + + ); // Assert @@ -191,4 +191,32 @@ Assert.Equal("Make a selection...", cut.Find("fluent-option").InnerHtml); cut.Verify(); } + + [Fact] + public void FluentSelect_SelectValueFromDifferentObjectInstances() + { + IEnumerable SelectedItems = [new Customer(1, "Marvin Klein")]; + + List Items = new List + { + new Customer(1, "Marvin Klein"), + new Customer(2, "Alice Wonder"), + new Customer(3, "Vincent Baaji") + }; + + // Arrange + var cut = Render>( + @ + ); + + // Click on the second FluentOption + var marvin = SelectedItems.First(i => i.Id == 1); + cut.Find($"fluent-option[value='{marvin}']").Click(); + + // Assert (no item selected) + Assert.Empty(SelectedItems); + } }