Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Migrate Demo Search to use FluentAutocomplete #1599

Merged
merged 15 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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"
Hona marked this conversation as resolved.
Show resolved Hide resolved
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]
Hona marked this conversation as resolved.
Show resolved Hide resolved
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);
dvoituron marked this conversation as resolved.
Show resolved Hide resolved
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
Loading