Skip to content

Commit

Permalink
refactor: Migrate Demo Search to use FluentAutocomplete (#1599)
Browse files Browse the repository at this point in the history
* refactor: Migrate Demo Search to use FluentAutocomplete

* refactor: Code review comments

* fix: Revert breaking change for now

* test: ValueText & ValueTextChanged

* fix: regenerate

* test: Overlay on clear behaviours

* fix: random IDs being generated in test

* fix: valuetext as well

---------

Co-authored-by: Vincent Baaij <[email protected]>
  • Loading branch information
Hona and vnbaaij authored Mar 6, 2024
1 parent a99bd38 commit 69fe49a
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 75 deletions.
15 changes: 15 additions & 0 deletions examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4256,6 +4256,16 @@
Gets or sets the placeholder value of the element, generally used to provide a hint to the user.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.ValueText">
<summary>
Gets or sets the text field value.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.ValueTextChanged">
<summary>
Gets or sets the callback that is invoked when the text field value changes.
</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.
Expand Down Expand Up @@ -4341,6 +4351,11 @@
Gets or sets the icon used for the Search button. By default: Search icon.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.ShowOverlayOnEmptyResults">
<summary>
Gets or sets whether the dropdown is shown when there are no items.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentAutocomplete`1.ListStyleValue">
<summary />
</member>
Expand Down
42 changes: 18 additions & 24 deletions examples/Demo/Shared/Shared/DemoSearch.razor
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
<FluentSearch @bind-Value="@_searchValue"
Immediate
@bind-Value:after="HandleSearchInput"
Id="demo-search"
Placeholder="Search everything..."/>

@if (_searchResults is not null)
{
<FluentMenu Anchor="demo-search"
HorizontalPosition="HorizontalPosition.Right"
@bind-Open="_visible">
@foreach (var searchResult in _searchResults)
{
var item = NavProvider.FlattenedMenuItems.First(x => x.Title == searchResult);

<FluentMenuItem OnClick="@(() => HandleSearchClicked(item))">
<span slot="start">
<FluentIcon Value="@(item.Icon)" Class="search-result-icon" Color="Color.Neutral" Slot="start"/>
</span>
@item.Title
</FluentMenuItem>
}
</FluentMenu>
}
<FluentAutocomplete TOption="NavItem"
Width="200px"
AutoComplete="off"
Placeholder="Search everything..."
MaximumSelectedOptions="1"
OptionText="@(item => item.Title)"
@bind-ValueText="@_searchTerm"
@bind-SelectedOptions="_selectedOptions"
@bind-SelectedOptions:after="HandleSearchClicked"
OnOptionsSearch="@HandleSearchInput"
ShowOverlayOnEmptyResults="false">
<OptionTemplate>
<span slot="start">
<FluentIcon Value="@(context.Icon)" Class="search-result-icon" Color="Color.Neutral" Slot="start"/>
</span>
@context.Title
</OptionTemplate>
</FluentAutocomplete>
56 changes: 23 additions & 33 deletions examples/Demo/Shared/Shared/DemoSearch.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,50 @@

using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;

namespace FluentUI.Demo.Shared.Shared;

public partial class DemoSearch
{
[Inject]
protected DemoNavProvider NavProvider { get; set; } = default!;

[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;

private string? _searchValue = string.Empty;

private bool _visible;
private string? _searchTerm = "";
private IEnumerable<NavItem>? _selectedOptions = [];

private List<string>? _searchResults = DefaultResults();
private static List<string>? DefaultResults() => null;

private void HandleSearchInput()
private void HandleSearchInput(OptionsSearchEventArgs<NavItem> e)
{
if (string.IsNullOrWhiteSpace(_searchValue))
var searchTerm = e.Text;

if (string.IsNullOrWhiteSpace(searchTerm))
{
_searchResults = DefaultResults();
_searchValue = string.Empty;
e.Items = null;
}
else
{
var searchTerm = _searchValue.ToLower();

if (searchTerm.Length > 0)
{
_searchResults = NavProvider.FlattenedMenuItems
.Where(x => x.Href != null) // Ignore Group headers
.Where(x => x.Title.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Title)
.ToList();

if (_searchResults.Count == 0)
{
_searchResults = DefaultResults();
}
}
e.Items = NavProvider.FlattenedMenuItems
.Where(x => x.Href != null) // Ignore Group headers
.Where(x => x.Title.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
}

_visible = _searchResults is not null;
}

private void HandleSearchClicked(NavItem item)
private void HandleSearchClicked()
{
_searchValue = string.Empty;
_searchResults = DefaultResults();
_visible = false;
_searchTerm = null;
var targetHref = _selectedOptions?.SingleOrDefault()?.Href;
_selectedOptions = [];
InvokeAsync(StateHasChanged);

NavigationManager.NavigateTo(item.Href ?? throw new UnreachableException("Item has no href"));
// Ignore clearing the search bar
if (targetHref is null)
{
return;
}

NavigationManager.NavigateTo(targetHref ?? throw new UnreachableException("Item has no href"));
}
}
8 changes: 4 additions & 4 deletions src/Core/Components/List/FluentAutocomplete.razor
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
AutoComplete="@AutoComplete"
Appearance="@Appearance"
Disabled="@Disabled"
Placeholder="@(this.SelectedOptions?.Any() == false ? Placeholder : string.Empty)"
Placeholder="@(SelectedOptions?.Any() is false ? Placeholder : string.Empty)"
aria-expanded="@(IsMultiSelectOpened ? "true" : "false")"
aria-controls="@(IsMultiSelectOpened ? IdPopup : string.Empty)"
aria-label="@GetAutocompleteAriaLabel()"
Value="@_valueText"
Value="@ValueText"
Required="@Required"
@onclick="@OnDropDownExpandedAsync"
@oninput="@InputHandlerAsync"
Expand Down Expand Up @@ -66,7 +66,7 @@
}
@if (!Disabled)
{
if (this.SelectedOptions?.Any() == true || !string.IsNullOrEmpty(_valueText))
if (this.SelectedOptions?.Any() == true || !string.IsNullOrEmpty(ValueText))
{
if (IconDismiss != null)
{
Expand Down Expand Up @@ -94,7 +94,7 @@
</FluentTextField>

@* List of available items *@
@if (IsMultiSelectOpened)
@if (IsMultiSelectOpened && (ShowOverlayOnEmptyResults || Items?.Any() == true))
{
<FluentOverlay OnClose="@(e => IsMultiSelectOpened = false)" Visible="true" Transparent="true" FullScreen="true" />
<FluentAnchoredRegion Anchor="@Id"
Expand Down
50 changes: 36 additions & 14 deletions src/Core/Components/List/FluentAutocomplete.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ public partial class FluentAutocomplete<TOption> : ListComponentBase<TOption> wh
public static string AccessibilityReachedMaxItems = "The maximum number of selected items has been reached.";
internal const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/List/FluentAutocomplete.razor.js";

private string _valueText = string.Empty;

public new FluentTextField? Element { get; set; } = default!;

/// <summary>
Expand All @@ -39,6 +37,18 @@ public FluentAutocomplete()
[Parameter]
public string? Placeholder { get; set; }

/// <summary>
/// Gets or sets the text field value.
/// </summary>
[Parameter]
public string ValueText { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the callback that is invoked when the text field value changes.
/// </summary>
[Parameter]
public EventCallback<string> ValueTextChanged { get; set; }

/// <summary>
/// For <see cref="FluentAutocomplete{TOption}"/>, this property must be True.
/// Set the <see cref="MaximumSelectedOptions"/> property to 1 to select just one item.
Expand Down Expand Up @@ -155,6 +165,12 @@ public override bool Multiple
[Parameter]
public Icon? IconSearch { get; set; } = new CoreIcons.Regular.Size16.Search();

/// <summary>
/// Gets or sets whether the dropdown is shown when there are no items.
/// </summary>
[Parameter]
public bool ShowOverlayOnEmptyResults { get; set; } = true;

/// <summary />
private string? ListStyleValue => new StyleBuilder()
.AddStyle("width", Width, when: !string.IsNullOrEmpty(Width))
Expand Down Expand Up @@ -202,7 +218,8 @@ private string ComponentWidth
/// <summary />
protected async Task InputHandlerAsync(ChangeEventArgs e)
{
_valueText = e.Value?.ToString() ?? string.Empty;
ValueText = e.Value?.ToString() ?? string.Empty;
await ValueTextChanged.InvokeAsync(ValueText);

if (MaximumSelectedOptions > 0 && SelectedOptions?.Count() >= MaximumSelectedOptions)
{
Expand All @@ -216,13 +233,15 @@ protected async Task InputHandlerAsync(ChangeEventArgs e)
var args = new OptionsSearchEventArgs<TOption>()
{
Items = Items ?? Array.Empty<TOption>(),
Text = _valueText,
Text = ValueText,
};

await OnOptionsSearch.InvokeAsync(args);

Items = args.Items.Take(MaximumOptionsSearch);
SelectableItem = Items.FirstOrDefault();
Items = args.Items?.Take(MaximumOptionsSearch);
SelectableItem = Items != null
? Items.FirstOrDefault()
: default;
}

private static readonly KeyCode[] CatchOnly = new[] { KeyCode.Escape, KeyCode.Enter, KeyCode.Backspace, KeyCode.Down, KeyCode.Up };
Expand Down Expand Up @@ -289,7 +308,7 @@ Task KeyDown_Escape()
async Task KeyDown_Backspace()
{
// Remove last selected item
if (string.IsNullOrEmpty(_valueText) &&
if (string.IsNullOrEmpty(ValueText) &&
SelectedOptions != null && SelectedOptions.Any())
{
await RemoveSelectedItemAsync(SelectedOptions.LastOrDefault());
Expand All @@ -298,11 +317,11 @@ async Task KeyDown_Backspace()
}

// Remove last char
if (!string.IsNullOrEmpty(_valueText))
if (!string.IsNullOrEmpty(ValueText))
{
await InputHandlerAsync(new ChangeEventArgs()
{
Value = _valueText.Substring(0, _valueText.Length - 1),
Value = ValueText[..^1],
});
return;
}
Expand Down Expand Up @@ -388,22 +407,25 @@ protected Task OnDropDownExpandedAsync()
{
return InputHandlerAsync(new ChangeEventArgs()
{
Value = _valueText,
Value = ValueText,
});
}

/// <summary />
protected Task OnClearAsync()
protected async Task OnClearAsync()
{
RemoveAllSelectedItems();
_valueText = string.Empty;
return RaiseChangedEventsAsync();
ValueText = string.Empty;
await ValueTextChanged.InvokeAsync(ValueText);
await RaiseChangedEventsAsync();
}

/// <summary />
protected override async Task OnSelectedItemChangedHandlerAsync(TOption? item)
{
_valueText = string.Empty;
ValueText = string.Empty;
await ValueTextChanged.InvokeAsync(ValueText);

IsMultiSelectOpened = false;
await base.OnSelectedItemChangedHandlerAsync(item);
await DisplayLastSelectedItemAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

<div class=" fluent-autocomplete-multiselect" style="width: 100%;" 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="true" aria-controls="myComponent-popup" aria-label="No items found" blazor:onclick="2" blazor:oninput="3" blazor:onfocusout="4" blazor:elementreference="">
<svg slot="end" style="width: 16px; fill: var(--accent-fill-rest); cursor: pointer;" focusable="false" viewBox="0 0 16 16" aria-hidden="true" blazor:onclick="6">
<title>Search</title>
<path d="M9.1 10.17a4.5 4.5 0 1 1 1.06-1.06l3.62 3.61a.75.75 0 1 1-1.06 1.06l-3.61-3.61Zm.4-3.67a3 3 0 1 0-6 0 3 3 0 0 0 6 0Z"></path>
</svg>
</fluent-text-field>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

<div class=" fluent-autocomplete-multiselect" style="width: 100%;" 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="true" aria-controls="myComponent-popup" aria-label="No items found" blazor:onclick="2" blazor:oninput="3" blazor:onfocusout="4" blazor:elementreference="">
<svg slot="end" style="width: 16px; fill: var(--accent-fill-rest); cursor: pointer;" focusable="false" viewBox="0 0 16 16" aria-hidden="true" blazor:onclick="6">
<title>Search</title>
<path d="M9.1 10.17a4.5 4.5 0 1 1 1.06-1.06l3.62 3.61a.75.75 0 1 1-1.06 1.06l-3.61-3.61Zm.4-3.67a3 3 0 1 0-6 0 3 3 0 0 0 6 0Z"></path>
</svg>
</fluent-text-field>
<div class="fluent-overlay" style="cursor: auto; position: fixed; z-index: 9900;" blazor:onclick="7" blazor:oncontextmenu="8" blazor:oncontextmenu:preventdefault="" b-xkrr7evqik="">
<div style="display: flex; align-items:center; justify-content: center; width: 100%; height: 100%" b-xkrr7evqik=""></div>
</div>
<fluent-anchored-region anchor="xxx" horizontal-positioning-mode="dynamic" horizontal-default-position="right" horizontal-inset="" horizontal-threshold="0" horizontal-scaling="content" vertical-positioning-mode="dynamic" vertical-default-position="unset" vertical-threshold="0" vertical-scaling="content" auto-update-mode="auto" style="z-index: 9999;" b-2ov9fhztky="" blazor:elementreference="xxx">
<div style="z-index: 9999; background-color: var(--neutral-layer-floating); box-shadow: var(--elevation-shadow-flyout); margin-top: 10px; border-radius: calc(var(--control-corner-radius) * 2px); background-color: var(--neutral-layer-floating);" b-2ov9fhztky="">
<div id="xxx" role="listbox" style="width: 100%;" tabindex="0" b-hg72r5b4ox=""></div>
</div>
</fluent-anchored-region>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

<div class=" fluent-autocomplete-multiselect" style="width: 100%;" b-hg72r5b4ox="">
<fluent-text-field style="width: 100%; min-width: 100%;" placeholder="" id="xxx" value="Preselected value" current-value="Preselected value" appearance="outline" blazor:onchange="1" role="combobox" aria-expanded="false" aria-controls="" blazor:onclick="2" blazor:oninput="3" blazor:onfocusout="4" blazor:elementreference="xxx">
<svg slot="end" style="width: 12px; fill: var(--accent-fill-rest); cursor: pointer;" focusable="false" viewBox="0 0 16 16" aria-hidden="true" blazor:onclick="5">
<title>Clear</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

<div class=" fluent-autocomplete-multiselect" style="width: 100%;" 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="true" aria-controls="myComponent-popup" aria-label="No items found" blazor:onclick="2" blazor:oninput="3" blazor:onfocusout="4" blazor:elementreference="">
<svg slot="end" style="width: 16px; fill: var(--accent-fill-rest); cursor: pointer;" focusable="false" viewBox="0 0 16 16" aria-hidden="true" blazor:onclick="6">
<title>Search</title>
<path d="M9.1 10.17a4.5 4.5 0 1 1 1.06-1.06l3.62 3.61a.75.75 0 1 1-1.06 1.06l-3.61-3.61Zm.4-3.67a3 3 0 1 0-6 0 3 3 0 0 0 6 0Z"></path>
</svg>
</fluent-text-field>
<div class="fluent-overlay" style="cursor: auto; position: fixed; z-index: 9900;" blazor:onclick="7" blazor:oncontextmenu="8" blazor:oncontextmenu:preventdefault="" b-xkrr7evqik="">
<div style="display: flex; align-items:center; justify-content: center; width: 100%; height: 100%" b-xkrr7evqik=""></div>
</div>
<fluent-anchored-region anchor="xxx" horizontal-positioning-mode="dynamic" horizontal-default-position="right" horizontal-inset="" horizontal-threshold="0" horizontal-scaling="content" vertical-positioning-mode="dynamic" vertical-default-position="unset" vertical-threshold="0" vertical-scaling="content" auto-update-mode="auto" style="z-index: 9999;" b-2ov9fhztky="" blazor:elementreference="xxx">
<div style="z-index: 9999; background-color: var(--neutral-layer-floating); box-shadow: var(--elevation-shadow-flyout); margin-top: 10px; border-radius: calc(var(--control-corner-radius) * 2px); background-color: var(--neutral-layer-floating);" b-2ov9fhztky="">
<div id="xxx" role="listbox" style="width: 100%;" tabindex="0" b-hg72r5b4ox=""></div>
</div>
</fluent-anchored-region>
</div>
Loading

0 comments on commit 69fe49a

Please sign in to comment.