Skip to content

Commit

Permalink
[HxSearchBox] Keyboard navigation #348
Browse files Browse the repository at this point in the history
  • Loading branch information
Harvey1214 committed Sep 2, 2022
1 parent 419e549 commit c206025
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 12 deletions.
Expand Up @@ -28,11 +28,12 @@
private string textQuery;

List<SearchBoxItem> Data { get; set; } = new()
{
new() { Title = "Table", Subtitle = "50 000", Icon = BootstrapIcon.Table },
new() { Title = "Mouse", Subtitle = "400", Icon = BootstrapIcon.Mouse },
new() { Title = "Door", Subtitle = "1000", Icon = BootstrapIcon.DoorClosed }
};
{
new() { Title = "Table", Subtitle = "50 000", Icon = BootstrapIcon.Table },
new() { Title = "Mouse", Subtitle = "400", Icon = BootstrapIcon.Mouse },
new() { Title = "Door", Subtitle = "1000", Icon = BootstrapIcon.DoorClosed },
new() { Title = "Tasks", Subtitle = "720", Icon = BootstrapIcon.ListTask }
};

private void OnItemSelected(SearchBoxItem item)
{
Expand All @@ -49,9 +50,9 @@
await Task.Delay(400); // imitate slower server API
return new()
{
Data = Data.Where(i => i.Title.Contains(request.UserInput, StringComparison.OrdinalIgnoreCase))
};
{
Data = Data.Where(i => i.Title.Contains(request.UserInput, StringComparison.OrdinalIgnoreCase))
};
}

class SearchBoxItem
Expand Down
Expand Up @@ -15,7 +15,7 @@

<div class="position-relative">
<form @onsubmit="HandleTextQueryTriggered" @onfocusout="HandleInputBlur">
<HxInputText Value="@TextQuery"
@* <HxInputText Value="@TextQuery"
ValueChanged="HandleTextQueryValueChanged"
ValueExpression="() => this.TextQuery"
InputMode="InputMode.Search"
Expand All @@ -29,7 +29,15 @@
InputGroupStartText="@InputGroupStartText"
InputGroupEndText="@InputGroupEndText"
InputGroupStartTemplate="@InputGroupStartTemplate"
InputGroupEndTemplate="@InputGroupEndTemplate" />
InputGroupEndTemplate="@InputGroupEndTemplate" />*@
<input
value="@TextQuery"
@oninput="(eventArgs) => HandleTextQueryValueChanged(eventArgs.Value.ToString())"
@onkeydown="UpdateFocusedItem"
inputmode="search"
enabled="@Enabled"
placeholder="@Placeholder"
class="@CssClassHelper.Combine("form-control", InputCssClassEffective)" />

@if (InputGroupEndText is null && InputGroupEndTemplate is null)
{
Expand Down Expand Up @@ -66,7 +74,12 @@
IconBase icon = ItemIconSelector?.Invoke(item) ?? null;

<li class="overflow-hidden">
<button type="button" tabindex="0" class="@CssClassHelper.Combine("dropdown-item", ItemCssClassEffective)" @onclick="() => HandleItemSelected(item)">
<button
type="button"
tabindex="0"
class="@CssClassHelper.Combine("dropdown-item", HasItemFocus(item) ? "hx-dropdown-item-focused" : null, ItemCssClassEffective)"
@onclick="() => HandleItemSelected(item)">

@if (ItemTemplate is null)
{
<HxSearchBoxItem Title="@title" Subtitle="@subtitle" Icon="@icon" />
Expand All @@ -91,7 +104,12 @@
@if (AllowTextQuery && (TextQuery is not null) && (TextQuery.Length >= MinimumLengthEffective))
{
<li class="overflow-hidden">
<button type="button" tabindex="0" class="@CssClassHelper.Combine("dropdown-item", ItemCssClassEffective)" @onclick="HandleTextQueryTriggered">
<button
type="button"
tabindex="0"
class="@CssClassHelper.Combine("dropdown-item", HasFreeTextItemFocus() ? "hx-dropdown-item-focused" : null, ItemCssClassEffective)"
@onclick="HandleTextQueryTriggered">

@if (TextQueryItemTemplate is null)
{
<HxSearchBoxItem Title="@TextQuery" Icon="@SearchIconEffective" />
Expand Down
Expand Up @@ -11,6 +11,12 @@ namespace Havit.Blazor.Components.Web.Bootstrap;
/// <typeparam name="TItem"></typeparam>
public partial class HxSearchBox<TItem> : IAsyncDisposable
{
protected const string ArrowUpKeyCode = "ArrowUp";
protected const string ArrowDownKeyCode = "ArrowDown";

protected const string EnterKeyCode = "Enter";
protected const string NumpadEnterKeyCode = "NumpadEnter";

/// <summary>
/// Returns application-wide defaults for the component.
/// Enables overriding defaults in descandants (use separate set of defaults).
Expand Down Expand Up @@ -204,6 +210,7 @@ public partial class HxSearchBox<TItem> : IAsyncDisposable
private string dropdownToggleElementId = "hx" + Guid.NewGuid().ToString("N");
private string dropdownId = "hx" + Guid.NewGuid().ToString("N");
private List<TItem> searchResults = new();
private int focusedItemIndex = -1;
private HxDropdownToggleElement dropdownToggle;
private bool dropdownMenuActive = false;
private bool initialized = false;
Expand Down Expand Up @@ -274,6 +281,8 @@ protected async Task UpdateSuggestionsAsync()
}

dataProviderInProgress = false;

focusedItemIndex = default;
searchResults = result?.Data.ToList();

textQueryHasBeenBelowMinimumLength = false;
Expand All @@ -282,6 +291,25 @@ protected async Task UpdateSuggestionsAsync()
StateHasChanged();
}

protected bool HasItemFocus(TItem item)
{
TItem focusedItem = GetItemByIndex(focusedItemIndex);

if ((focusedItem is not null) && (!focusedItem.Equals(default)))
{
return item.Equals(focusedItem);
}
else
{
return false;
}
}

protected bool HasFreeTextItemFocus()
{
return focusedItemIndex == GetFreeTextItemIndex();
}

protected async Task HandleTextQueryValueChanged(string newTextQuery)
{
this.TextQuery = newTextQuery;
Expand Down Expand Up @@ -318,6 +346,62 @@ protected async Task HandleTextQueryValueChanged(string newTextQuery)
await InvokeTextQueryChangedAsync(newTextQuery);
}

private async Task UpdateFocusedItem(KeyboardEventArgs keyboardEventArgs)
{
// Confirm selection on the focused item if an item is focused and the enter key is pressed.
TItem focusedItem = GetItemByIndex(focusedItemIndex);
if (keyboardEventArgs.Code == EnterKeyCode || keyboardEventArgs.Code == NumpadEnterKeyCode)
{
if ((focusedItem is not null) && (!focusedItem.Equals(default)))
{
await HandleItemSelected(focusedItem);
}
else if (focusedItemIndex == GetFreeTextItemIndex())
{
await HandleTextQueryTriggered();
}
}

// Move focus up or down.
if (keyboardEventArgs.Code == ArrowUpKeyCode)
{
int previousItemIndex = focusedItemIndex - 1;
if (previousItemIndex >= -1) // If the index is -1, no item is focused.
{
focusedItemIndex = previousItemIndex;
}
}
else if (keyboardEventArgs.Code == ArrowDownKeyCode)
{
int nextItemIndex = focusedItemIndex + 1;
if (nextItemIndex < searchResults.Count)
{
focusedItemIndex = nextItemIndex;
}
else
{
focusedItemIndex = GetFreeTextItemIndex(); // Select the item that's used to confirm the freetext.
}
}
}

private TItem GetItemByIndex(int index)
{
if (index >= 0 && index < searchResults.Count)
{
return searchResults[index];
}
else
{
return default;
}
}

protected int GetFreeTextItemIndex()
{
return searchResults.Count;
}

private async void HandleTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// when a time interval reached, update suggestions
Expand Down
Expand Up @@ -24,3 +24,7 @@
.dropdown-item:not(:active) ::deep .hx-search-box-item-subtitle {
color: var(--hx-search-box-item-subtitle-color);
}

::deep .hx-dropdown-item-focused {
background-color: var(--hx-autosuggest-item-highlighted-background-color);
}

0 comments on commit c206025

Please sign in to comment.