Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3288027
Autocomplete working with single item selected.
StevenRasmussen Mar 24, 2025
2ad0c86
Added a unit test and updated the results of the failing unit tests d…
StevenRasmussen Mar 24, 2025
f3c4199
Fixed whitespace issues.
StevenRasmussen Mar 24, 2025
5b0f2f2
Updates to demo page.
StevenRasmussen Mar 25, 2025
f9fdad0
Revert changes to class names so that it won't break anyone that is d…
StevenRasmussen Mar 25, 2025
5b2dfa4
Revert change to css.
StevenRasmussen Mar 25, 2025
f398a40
Accessibility changes.
StevenRasmussen Mar 25, 2025
1d01ba0
Updated to use a new attribute instead of a new class.
StevenRasmussen Mar 25, 2025
b3df6a2
Fix unit tests.
StevenRasmussen Mar 25, 2025
aee8b20
Fix whitespace change.
StevenRasmussen Mar 25, 2025
2e2c523
Reverted test files to remove noise from PR.
StevenRasmussen Mar 25, 2025
3645b30
Fixed an issue with really long text
StevenRasmussen Mar 27, 2025
2144040
Merge branch 'dev' into auto-complete-single-item-enhancements
StevenRasmussen Apr 3, 2025
cf780ac
Improve performance of line of code.
StevenRasmussen Apr 3, 2025
fa6034c
Removed the usage of addiing to the 'AdditionalAttributes' in favor o…
StevenRasmussen Apr 3, 2025
0b8509d
Added the tab stop for accessibility.
StevenRasmussen Apr 3, 2025
4b0d6c0
Fixed broken unit test after adding in the tabindex property.
StevenRasmussen Apr 3, 2025
629aafd
Update the subgrid to handle N:N relationships.
StevenRasmussen Apr 7, 2025
6fe88c1
Merge branch 'dev' into auto-complete-single-item-enhancements
StevenRasmussen Apr 7, 2025
9a92440
Merge branch 'dev' into auto-complete-single-item-enhancements
dvoituron Apr 13, 2025
d4d6b24
Merge branch 'auto-complete-single-item-enhancements' of https://gith…
StevenRasmussen Apr 19, 2025
0016c0f
Merge branch 'dev' into auto-complete-single-item-enhancements
StevenRasmussen Apr 19, 2025
8026280
Merge dev into auto-complete-single-item-enhancements
StevenRasmussen Apr 19, 2025
499affa
Multiple = false working.
StevenRasmussen Apr 19, 2025
6b5d808
Fixed unit tests. Removed unnecessary unit test.
StevenRasmussen Apr 19, 2025
3e3c8c0
Fixed code standards. Guarded against a null property.
StevenRasmussen Apr 20, 2025
ee83fc9
Merge branch 'dev' into auto-complete-single-item-enhancements
vnbaaij Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5918,12 +5918,6 @@
For the FluentAutocomplete component, use the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.ValueText"/> property instead.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.Multiple">
<summary>
For <see cref="T:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1"/>, this property must be True.
Set the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.MaximumSelectedOptions"/> property to 1 to select just one item.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.Appearance">
<summary>
Gets or sets the visual appearance. See <seealso cref="T:Microsoft.FluentUI.AspNetCore.Components.Appearance"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

<DemoSection Title="Customized options" Component="@typeof(AutocompleteCustomized)" />

<DemoSection Title="Multiple == false" Component="@typeof(AutoCompleteMaxSingleItem)" />

<DemoSection Title="Many Items" Component="@typeof(AutocompleteManyItems)">
<Description>
<p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@inject DataSource Data

<FluentAutocomplete TOption="Country"
AutoComplete="off"
Autofocus="true"
Label="Select a country"
Width="250px"
Placeholder="Select a country"
OnOptionsSearch="@OnSearchAsync"
OptionDisabled="@(e => e.Code == "au")"
Multiple=false
OptionText="@(item => item.Name)"
@bind-SelectedOption=SelectedItem />

<p>
<b>Selected</b>: @(SelectedItem?.Name)
</p>

@code
{
Country? SelectedItem = null;

private async Task OnSearchAsync(OptionsSearchEventArgs<Country> e)
{
var allCountries = await Data.GetCountriesAsync();
e.Items = allCountries.Where(i => i.Name.StartsWith(e.Text, StringComparison.OrdinalIgnoreCase))
.OrderBy(i => i.Name);
}
}
120 changes: 67 additions & 53 deletions src/Core/Components/List/FluentAutocomplete.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<div class="@ClassValue fluent-autocomplete-multiselect"
style="@StyleValue"
@attributes="AdditionalAttributes"
single-select="@GetSingleSelect()"
auto-height="@(!string.IsNullOrEmpty(MaxAutoHeight))">
<FluentKeyCode Anchor="@Id" OnKeyDown="@KeyDownHandlerAsync" Only="@CatchOnly" PreventDefaultOnly="@PreventOnly" />

Expand All @@ -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))
Expand Down Expand Up @@ -67,36 +68,35 @@
@RenderSelectedOptions
</div>
}

}
@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)
{
<FluentIcon Value="@IconDismiss"
Width="12px"
Style="cursor: pointer;"
Slot="end"
Title="@AccessibilityIconDismiss"
Focusable="true"
@onfocus="@(e => { IsReachedMaxItems = false; IsMultiSelectOpened = false; })"
OnClick="@OnClearAsync" />
<FluentIcon Value="@IconDismiss"
Width="12px"
Style="cursor: pointer;"
Slot="end"
Title="@(Multiple == false ? string.Format(AccessibilityRemoveItem, GetOptionText(SelectedOption)) : AccessibilityIconDismiss)"
Focusable="true"
@onfocus="@(e => { IsReachedMaxItems = false; IsMultiSelectOpened = false; })"
OnClick="@OnClearAsync" />
}
}
else
{
if (IconSearch != null)
{
<FluentIcon Value="@IconSearch"
Width="16px"
Style="cursor: pointer;"
Slot="end"
Title="@AccessibilityIconSearch"
Focusable="true"
@onfocus="@(e => { IsReachedMaxItems = false; IsMultiSelectOpened = false; })"
OnClick="@OnDropDownExpandedAsync" />
<FluentIcon Value="@IconSearch"
Width="16px"
Style="cursor: pointer;"
Slot="end"
Title="@AccessibilityIconSearch"
Focusable="true"
@onfocus="@(e => { IsReachedMaxItems = false; IsMultiSelectOpened = false; })"
OnClick="@OnDropDownExpandedAsync" />
}
}
}
Expand All @@ -107,10 +107,10 @@
{
@if (SelectValueOnTab)
{
<FluentKeyCode Anchor="@Id"
OnKeyDown="@KeyDownHandlerAsync"
Only="@SelectValueOnTabOnly"
PreventDefaultOnly="@SelectValueOnTabOnly" />
<FluentKeyCode Anchor="@Id"
OnKeyDown="@KeyDownHandlerAsync"
Only="@SelectValueOnTabOnly"
PreventDefaultOnly="@SelectValueOnTabOnly" />
}

<FluentOverlay OnClose="@(e => IsMultiSelectOpened = false)" Visible="true" Transparent="true" FullScreen="true" />
Expand All @@ -122,33 +122,33 @@
Shadow="ElevationShadow.Flyout">
@if (HeaderContent != null)
{
@HeaderContent(Items ?? Array.Empty<TOption>())
@HeaderContent(Items ?? Array.Empty<TOption>())
}

<div id="@IdPopup" role="listbox" style="@ListStyleValue" tabindex="0">
@if (Items != null)
{
var selectableItem = GetOptionValue(SelectableItem);
var selectableItem = GetOptionValue(SelectableItem);

@if (Virtualize)
{
<Virtualize ItemsProvider="LoadFilteredItemsAsync" @ref="VirtualizationContainer" ItemSize="ItemSize">
@RenderOption((context, selectableItem))
</Virtualize>
}
else
{
foreach (TOption item in Items)
@if (Virtualize)
{
<Virtualize ItemsProvider="LoadFilteredItemsAsync" @ref="VirtualizationContainer" ItemSize="ItemSize">
@RenderOption((context, selectableItem))
</Virtualize>
}
else
{
@RenderOption((item, selectableItem))
foreach (TOption item in Items)
{
@RenderOption((item, selectableItem))
}
}
}
}
</div>

@if (FooterContent != null)
{
@FooterContent(Items ?? Array.Empty<TOption>())
@FooterContent(Items ?? Array.Empty<TOption>())
}
</FluentAnchoredRegion>
}
Expand All @@ -174,15 +174,15 @@
{
var optionValue = GetOptionValue(context.Item);
<FluentOption TOption="TOption"
@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)">
@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)
Expand All @@ -196,27 +196,41 @@

private RenderFragment RenderSelectedOptions => __builder =>
{
if (SelectedOptions != null)
var selectedOptions = new List<TOption>();
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)
{
<FluentLabel tabindex="0" aria-label="@GetOptionText(item)" role="checkbox" aria-checked="true">@text</FluentLabel>
}
else if (ReadOnly || Disabled)
{
<FluentBadge Appearance="@AspNetCore.Components.Appearance.Neutral"
aria-label="@GetOptionText(item)">
aria-label="@GetOptionText(item)">
@text
</FluentBadge>
}
else
{
<FluentBadge Appearance="@AspNetCore.Components.Appearance.Neutral"
OnDismissClick="@(e => RemoveSelectedItemAsync(item))"
DismissTitle="@(string.Format(AccessibilityRemoveItem, text))"
aria-label="@GetOptionText(item)">
OnDismissClick="@(e => RemoveSelectedItemAsync(item))"
DismissTitle="@(string.Format(AccessibilityRemoveItem, text))"
aria-label="@GetOptionText(item)">
@text
</FluentBadge>
}
Expand Down
25 changes: 3 additions & 22 deletions src/Core/Components/List/FluentAutocomplete.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,6 @@ public override string? Value
set => base.Value = ValueText;
}

/// <summary>
/// For <see cref="FluentAutocomplete{TOption}"/>, this property must be True.
/// Set the <see cref="MaximumSelectedOptions"/> property to 1 to select just one item.
/// </summary>
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;
}
}

/// <summary>
/// Gets or sets the visual appearance. See <seealso cref="AspNetCore.Components.Appearance"/>
/// </summary>
Expand Down Expand Up @@ -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;

/// <summary />
private string ComponentWidth
{
Expand Down Expand Up @@ -552,6 +532,7 @@ protected async Task OnClearAsync()
{
RemoveAllSelectedItems();
ValueText = string.Empty;
SelectedOption = default;
await RaiseValueTextChangedAsync(ValueText);
await RaiseChangedEventsAsync();

Expand Down
21 changes: 16 additions & 5 deletions src/Core/Components/List/FluentAutocomplete.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/Core/Components/List/ListComponentBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

<div class=" fluent-autocomplete-multiselect" style="width: 100%;" single-select="" b-hg72r5b4ox="">
<fluent-text-field style="width: 100%; min-width: 100%;" placeholder="" id="xxx" value="" current-value="" appearance="outline" blazor:onchange="1" role="combobox" aria-expanded="false" aria-controls="" blazor:onclick="2" blazor:oninput="3" blazor:elementreference="xxx">
<fluent-horizontal-scroll id="xxx" style="width: 100%;" slot="start" b-hg72r5b4ox="">
<fluent-flipper onclick="event.stopPropagation(); document.getElementById('myComponent-scroll').scrollToPrevious();" slot="previous-flipper" aria-hidden="false" aria-label="Previous" title="Previous" role="button" tabindex="0" class="previous fluent-autocomplete-previous" direction="previous" b-hg72r5b4ox=""></fluent-flipper>
<fluent-flipper onclick="event.stopPropagation(); document.getElementById('myComponent-scroll').scrollToNext();" slot="next-flipper" aria-hidden="false" aria-label="Next" title="Next" role="button" tabindex="0" class="next fluent-autocomplete-next" direction="next" b-hg72r5b4ox=""></fluent-flipper>
<p tabindex="0" aria-label="1-Denis Voituron" role="checkbox" aria-checked="true" typo="body" class="fluent-typography" b-1nnnfjehkp="">1-Denis Voituron</p>
</fluent-horizontal-scroll>
<svg slot="end" style="width: 12px; fill: var(--accent-fill-rest); cursor: pointer;" focusable="true" tabindex="0" role="button" viewBox="0 0 16 16" blazor:onkeydown="4" blazor:onclick="5" blazor:onfocus="6">
<title>Remove 1-Denis Voituron</title>
<path d="m2.59 2.72.06-.07a.5.5 0 0 1 .63-.06l.07.06L8 7.29l4.65-4.64a.5.5 0 0 1 .7.7L8.71 8l4.64 4.65c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L8 8.71l-4.65 4.64a.5.5 0 0 1-.7-.7L7.29 8 2.65 3.35a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z"></path>
</svg>
</fluent-text-field>
</div>
Loading
Loading