diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 046d7ea6c0..9728b8c3ca 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -5918,12 +5918,6 @@ For the FluentAutocomplete component, use the property instead. - - - For , this property must be True. - Set the property to 1 to select just one item. - - Gets or sets the visual appearance. See diff --git a/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor b/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor index 140da6e07f..98388ab814 100644 --- a/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor +++ b/examples/Demo/Shared/Pages/List/Autocomplete/AutocompletePage.razor @@ -18,6 +18,8 @@ + +

diff --git a/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutoCompleteMaxSingleItem.razor b/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutoCompleteMaxSingleItem.razor new file mode 100644 index 0000000000..4db11c271a --- /dev/null +++ b/examples/Demo/Shared/Pages/List/Autocomplete/Examples/AutoCompleteMaxSingleItem.razor @@ -0,0 +1,29 @@ +@inject DataSource Data + + + +

+ Selected: @(SelectedItem?.Name) +

+ +@code +{ + Country? SelectedItem = null; + + private async Task OnSearchAsync(OptionsSearchEventArgs e) + { + var allCountries = await Data.GetCountriesAsync(); + e.Items = allCountries.Where(i => i.Name.StartsWith(e.Text, StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => i.Name); + } +} diff --git a/src/Core/Components/List/FluentAutocomplete.razor b/src/Core/Components/List/FluentAutocomplete.razor index e7311d457d..3e52105360 100644 --- a/src/Core/Components/List/FluentAutocomplete.razor +++ b/src/Core/Components/List/FluentAutocomplete.razor @@ -6,6 +6,7 @@
@@ -31,7 +32,7 @@ autofocus="@Autofocus" Style="@ComponentWidth"> @* Selected Items *@ - @if (this.SelectedOptions?.Any() == true) + @if (this.SelectedOptions?.Any() == true || this.SelectedOption is not null) { @* Normal (single) line height *@ if (string.IsNullOrEmpty(MaxAutoHeight)) @@ -67,36 +68,35 @@ @RenderSelectedOptions
} - } @if (!Disabled && !ReadOnly) { - if (this.SelectedOptions?.Any() == true || !string.IsNullOrEmpty(ValueText)) + if (this.SelectedOptions?.Any() == true || !string.IsNullOrEmpty(ValueText) || this.SelectedOption is not null) { if (IconDismiss != null) { - + } } else { if (IconSearch != null) { - + } } } @@ -107,10 +107,10 @@ { @if (SelectValueOnTab) { - + } @@ -122,33 +122,33 @@ Shadow="ElevationShadow.Flyout"> @if (HeaderContent != null) { - @HeaderContent(Items ?? Array.Empty()) + @HeaderContent(Items ?? Array.Empty()) }
@if (Items != null) { - var selectableItem = GetOptionValue(SelectableItem); + var selectableItem = GetOptionValue(SelectableItem); - @if (Virtualize) - { - - @RenderOption((context, selectableItem)) - - } - else - { - foreach (TOption item in Items) + @if (Virtualize) + { + + @RenderOption((context, selectableItem)) + + } + else { - @RenderOption((item, selectableItem)) + foreach (TOption item in Items) + { + @RenderOption((item, selectableItem)) + } } - } }
@if (FooterContent != null) { - @FooterContent(Items ?? Array.Empty()) + @FooterContent(Items ?? Array.Empty()) } } @@ -174,15 +174,15 @@ { var optionValue = GetOptionValue(context.Item); + @key="@context.Item" + Value="@optionValue" + Style="@OptionStyle" + Class="@OptionClass" + Selected="@GetOptionSelected(context.Item)" + Disabled="@(GetOptionDisabled(context.Item) ?? false)" + OnSelect="@OnSelectCallback(context.Item)" + aria-selected="@(GetOptionSelected(context.Item) || optionValue == context.SelectableItem ? "true" : "false")" + selectable="@(optionValue == context.SelectableItem)"> @if (OptionTemplate == null) { @GetOptionText(context.Item) @@ -196,27 +196,41 @@ private RenderFragment RenderSelectedOptions => __builder => { - if (SelectedOptions != null) + var selectedOptions = new List(); + if (Multiple && (SelectedOptions?.Any() ?? false)) + { + selectedOptions.AddRange(SelectedOptions); + } + if (!Multiple && SelectedOption is not null) { - foreach (var item in SelectedOptions) + selectedOptions.Add(SelectedOption); + } + + if (selectedOptions.Any()) + { + foreach (var item in selectedOptions) { if (SelectedOptionTemplate == null) { var text = @GetOptionText(item); - if (ReadOnly || Disabled) + if (Multiple == false) + { + @text + } + else if (ReadOnly || Disabled) { + aria-label="@GetOptionText(item)"> @text } else { + OnDismissClick="@(e => RemoveSelectedItemAsync(item))" + DismissTitle="@(string.Format(AccessibilityRemoveItem, text))" + aria-label="@GetOptionText(item)"> @text } diff --git a/src/Core/Components/List/FluentAutocomplete.razor.cs b/src/Core/Components/List/FluentAutocomplete.razor.cs index 8361510426..12f4cd03f6 100644 --- a/src/Core/Components/List/FluentAutocomplete.razor.cs +++ b/src/Core/Components/List/FluentAutocomplete.razor.cs @@ -76,28 +76,6 @@ public override string? Value set => base.Value = ValueText; } - /// - /// For , this property must be True. - /// Set the property to 1 to select just one item. - /// - public override bool Multiple - { - get - { - return base.Multiple; - } - - set - { - if (value == false) - { - throw new ArgumentException("For FluentAutocomplete, this property must be True. Set the MaximumSelectedOptions property to 1 to select just one item."); - } - - base.Multiple = true; - } - } - /// /// Gets or sets the visual appearance. See /// @@ -245,6 +223,8 @@ public override bool Multiple .AddStyle("display", "none", when: (Items == null || !Items.Any()) && (HeaderContent != null || FooterContent != null)) .Build(); + private bool GetSingleSelect() => Multiple == false && SelectedOption is not null; + /// private string ComponentWidth { @@ -552,6 +532,7 @@ protected async Task OnClearAsync() { RemoveAllSelectedItems(); ValueText = string.Empty; + SelectedOption = default; await RaiseValueTextChangedAsync(ValueText); await RaiseChangedEventsAsync(); diff --git a/src/Core/Components/List/FluentAutocomplete.razor.css b/src/Core/Components/List/FluentAutocomplete.razor.css index 55b6c4c17f..e9d653b1f2 100644 --- a/src/Core/Components/List/FluentAutocomplete.razor.css +++ b/src/Core/Components/List/FluentAutocomplete.razor.css @@ -52,15 +52,26 @@ margin-bottom: 2px; } +.fluent-autocomplete-multiselect[single-select] ::deep fluent-text-field::part(control) { + display: none; +} + +.fluent-autocomplete-multiselect[single-select] ::deep fluent-text-field::part(start) { + max-width: calc(100% - 40px); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + @media (forced-colors: active) { .fluent-autocomplete-multiselect div[role=listbox] { border: calc(var(--stroke-width)* 1px) solid transparent; } - .fluent-autocomplete-multiselect div[role=listbox] ::deep fluent-option:not([disabled]):not([selected])[selectable] { - forced-color-adjust: none; - background: highlight; - color: highlighttext; - } + .fluent-autocomplete-multiselect div[role=listbox] ::deep fluent-option:not([disabled]):not([selected])[selectable] { + forced-color-adjust: none; + background: highlight; + color: highlighttext; + } } diff --git a/src/Core/Components/List/ListComponentBase.razor.cs b/src/Core/Components/List/ListComponentBase.razor.cs index 9e5fb3d364..b0f780d486 100644 --- a/src/Core/Components/List/ListComponentBase.razor.cs +++ b/src/Core/Components/List/ListComponentBase.razor.cs @@ -490,7 +490,7 @@ protected virtual bool GetOptionSelected(TOption item) { if (item != null) { - return OptionValue.Invoke(item) ?? OptionText.Invoke(item) ?? item.ToString(); + return OptionValue?.Invoke(item) ?? OptionText?.Invoke(item) ?? item?.ToString(); } else { diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MultipleEqualsFalse.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MultipleEqualsFalse.verified.razor.html new file mode 100644 index 0000000000..bf55c5f1da --- /dev/null +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MultipleEqualsFalse.verified.razor.html @@ -0,0 +1,14 @@ + +
+ +
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.razor b/tests/Core/List/FluentAutocompleteTests.razor index 58c10a2c83..f1e63cd587 100644 --- a/tests/Core/List/FluentAutocompleteTests.razor +++ b/tests/Core/List/FluentAutocompleteTests.razor @@ -21,10 +21,10 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Items, Customers.Get()); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.Items, Customers.Get()); + }); // Assert cut.Verify(); @@ -35,11 +35,11 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Items, Customers.Get()); - parameters.Add(p => p.Width, string.Empty); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.Items, Customers.Get()); + parameters.Add(p => p.Width, string.Empty); + }); // Assert Assert.Contains("width: 250px", cut.Markup); @@ -51,10 +51,10 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Items, Customers.Get()); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.Items, Customers.Get()); + }); // Act cut.Find("fluent-text-field").Click(); @@ -81,11 +81,11 @@ // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Items, Customers.Get()); - parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.Items, Customers.Get()); + parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); + }); // Act var input = cut.Find("fluent-text-field"); @@ -109,7 +109,7 @@ SelectValueOnTab="true" @bind-SelectedOptions="@SelectedItems" OnOptionsSearch="@OnSearchAsync" /> - ); + ); // Act: click to open -> Tab to select var input = cut.Find("fluent-text-field"); @@ -122,52 +122,35 @@ Assert.Single(SelectedItems); } - [Fact] - public void FluentAutocomplete_MultipleFalse_Exception() - { - // Arrange & Act - var ex = Assert.Throws(() => - { - var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Multiple, false); - }); - }); - - // Assert - Assert.Equal("For FluentAutocomplete, this property must be True. Set the MaximumSelectedOptions property to 1 to select just one item.", ex.InnerException?.Message); - } - [Fact] public void FluentAutocomplete_Templates() { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.OptionValue, context => context.Id.ToString()); - - // Add a HeaderContent template - parameters.Add(p => p.HeaderContent, context => - { - return $"
Please, select an item
"; - }); - - // Add an Item template - parameters.Add(p => p.OptionTemplate, context => - { - return $"
{context?.Id} {context?.Name}
"; - }); - - // Add a FooterContent template - parameters.Add(p => p.FooterContent, context => - { - return $"
{context.Count()} items found
"; - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.OptionValue, context => context.Id.ToString()); + + // Add a HeaderContent template + parameters.Add(p => p.HeaderContent, context => + { + return $"
Please, select an item
"; + }); + + // Add an Item template + parameters.Add(p => p.OptionTemplate, context => + { + return $"
{context?.Id} {context?.Name}
"; + }); + + // Add a FooterContent template + parameters.Add(p => p.FooterContent, context => + { + return $"
{context.Count()} items found
"; + }); - parameters.Add(p => p.Items, Customers.Get()); - }); + parameters.Add(p => p.Items, Customers.Get()); + }); // Act cut.Find("fluent-text-field").Click(); @@ -181,13 +164,13 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.OptionValue, context => context.Id.ToString()); - parameters.Add(p => p.OptionText, context => context.Name); - parameters.Add(p => p.Items, Customers.Get()); - parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.OptionValue, context => context.Id.ToString()); + parameters.Add(p => p.OptionText, context => context.Name); + parameters.Add(p => p.Items, Customers.Get()); + parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); + }); // Act cut.Find("fluent-text-field").Click(); @@ -201,19 +184,19 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.OptionValue, context => context.Id.ToString()); - parameters.Add(p => p.OptionText, context => context.Name); - parameters.Add(p => p.Items, Customers.Get()); - parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); - - // Add an Item template - parameters.Add(p => p.OptionTemplate, context => - { - return $"
{context?.Id} {context?.Name}
"; - }); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.OptionValue, context => context.Id.ToString()); + parameters.Add(p => p.OptionText, context => context.Name); + parameters.Add(p => p.Items, Customers.Get()); + parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); + + // Add an Item template + parameters.Add(p => p.OptionTemplate, context => + { + return $"
{context?.Id} {context?.Name}
"; + }); + }); // Act cut.Find("fluent-text-field").Click(); @@ -227,13 +210,13 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.OptionValue, context => context.Id.ToString()); - parameters.Add(p => p.OptionText, context => context.Name); - parameters.Add(p => p.Items, Customers.Get()); - parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.OptionValue, context => context.Id.ToString()); + parameters.Add(p => p.OptionText, context => context.Name); + parameters.Add(p => p.Items, Customers.Get()); + parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); + }); // Act (click on the Dismiss button) // The first SelectedOption is removed @@ -248,10 +231,10 @@ { // Arrange & Act var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.ValueText, "Preselected value"); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.ValueText, "Preselected value"); + }); // Assert var textField = cut.Find("fluent-text-field"); @@ -274,10 +257,10 @@ // Arrange var valueText = "Preselected value"; var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Bind(p => p.ValueText, valueText, x => valueText = x); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Bind(p => p.ValueText, valueText, x => valueText = x); + }); Assert.False(string.IsNullOrEmpty(valueText)); @@ -295,10 +278,10 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.ValueText, "Some text here"); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.ValueText, "Some text here"); + }); // Act cut.Find("svg").Click(); // Clear button @@ -313,11 +296,11 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.ValueText, "Some text here"); - parameters.Add(p => p.ShowOverlayOnEmptyResults, false); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.ValueText, "Some text here"); + parameters.Add(p => p.ShowOverlayOnEmptyResults, false); + }); // Act cut.Find("svg").Click(); // Clear button @@ -332,10 +315,10 @@ { // Arrange && Act var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.Autofocus, true); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.Autofocus, true); + }); // Assert cut.Verify(); @@ -346,11 +329,11 @@ { // Arrange var cut = RenderComponent>(parameters => - { - parameters.Add(p => p.Id, "myComponent"); - parameters.Add(p => p.MaxAutoHeight, "200px"); - parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); - }); + { + parameters.Add(p => p.Id, "myComponent"); + parameters.Add(p => p.MaxAutoHeight, "200px"); + parameters.Add(p => p.SelectedOptions, Customers.Get().Take(2)); + }); // Act cut.Find("fluent-text-field").Click(); @@ -459,6 +442,29 @@ cut.Verify(); } + [Fact] + public async Task FluentAutocomplete_MultipleEqualsFalse() + { + Customer? SelectedItem = Customers.Get().First(); + + // Arrange + var cut = Render>( + @ + ); + + // Assert (One item selected) + Assert.NotNull(SelectedItem); + Assert.Equal(1, SelectedItem.Id); + + // Assert + cut.Verify(); + } + // Send a key code private async Task PressKeyAsync(IRenderedComponent> cut, KeyCode key, bool popoverOpened = false) { @@ -475,8 +481,4 @@ .OrderBy(i => i.Name); return Task.CompletedTask; } - - - - }