Skip to content

Commit

Permalink
[HxAutosuggest] Keyboard navigation #348
Browse files Browse the repository at this point in the history
  • Loading branch information
Harvey1214 committed Sep 5, 2022
1 parent 8f5179a commit b30e64f
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 59 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -14,7 +14,7 @@ public partial class HxAutosuggestInput

[Parameter] public EventCallback OnInputMouseDown { get; set; }

[Parameter] public EventCallback OnEnter { get; set; }
[Parameter] public EventCallback<KeyboardEventArgs> OnKeyDown { get; set; }

[Parameter] public string InputId { get; set; }

Expand Down Expand Up @@ -42,10 +42,7 @@ private async Task HandleInput(ChangeEventArgs changeEventArgs)

private async Task HandleKeyDown(KeyboardEventArgs keyboardEventArgs)
{
if ((keyboardEventArgs.Code == "Enter") || (keyboardEventArgs.Code == "NumpadEnter"))
{
await OnEnter.InvokeAsync();
}
await OnKeyDown.InvokeAsync(keyboardEventArgs);
}

public async ValueTask FocusAsync()
Expand Down
Expand Up @@ -17,7 +17,7 @@
OnInputInput="HandleInputInput"
OnInputFocus="HandleInputFocus"
OnInputBlur="HandleInputBlur"
OnEnter="HandleInputEnterKeyDown"
OnKeyDown="UpdateFocusedItem"
Placeholder="@Placeholder"
CssClass="@((!HasAnyInputGroupEnd ? "rounded-end " : null) + InputCssClass)"
DropdownOffset="@DropdownOffset"
Expand Down Expand Up @@ -57,7 +57,13 @@
<HxIcon Icon="@SearchIconEffective" />
}
</div>
<HxAutosuggestItems CssClass="w-100" TItem="TItem" Items="@suggestions" OnItemClick="HandleItemClick" HighlightFirstSuggestionEffective="HighlightFirstSuggestionEffective">
<HxAutosuggestItems
CssClass="w-100"
TItem="TItem"
Items="@suggestions"
OnItemClick="HandleItemSelected"
FocusedItem="GetFocusedItem()">

<ItemTemplate>
@if (ItemTemplate != null)
{
Expand All @@ -71,6 +77,7 @@
<EmptyTemplate>
@EmptyTemplate
</EmptyTemplate>

</HxAutosuggestItems>
}
</div>
Expand Up @@ -223,19 +223,6 @@ private async Task HandleInputInput(string newUserInput)
}
}

/// <summary>
/// Select the first suggested item when an enter key is pressed.
/// </summary>
/// <returns></returns>
private async Task HandleInputEnterKeyDown()
{
if (HighlightFirstSuggestionEffective)
{
await DestroyDropdownAsync();
await HandleItemClick(suggestions.FirstOrDefault());
}
}

private async void HandleTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// when a time interval reached, update suggestions
Expand Down Expand Up @@ -310,6 +297,17 @@ private async Task UpdateSuggestionsAsync()
}

dataProviderInProgress = false;

// KeyboardNavigation
if (HighlightFirstSuggestionEffective)
{
focusedItemIndex = 0; // First item in the searchResults collection.
}
else
{
focusedItemIndex = InputKeyboardNavigationIndex;
}

suggestions = result.Data?.ToList();

if ((suggestions?.Any() ?? false) || EmptyTemplate != null)
Expand All @@ -324,9 +322,82 @@ private async Task UpdateSuggestionsAsync()
StateHasChanged();
}

private async Task HandleItemClick(TItem item)
#region KeyboardNavigation
private int focusedItemIndex = -1;

private const string ArrowUpKeyCode = "ArrowUp";
private const string ArrowDownKeyCode = "ArrowDown";

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

/// <summary>
/// Input's index for the keyboard navigation. If this is the current index, then no item is selected.
/// </summary>
private const int InputKeyboardNavigationIndex = -1;

private TItem GetFocusedItem()
{
if (focusedItemIndex > InputKeyboardNavigationIndex)
{
TItem focusedItem = GetItemByIndex(focusedItemIndex);
if ((focusedItem is not null) && (!focusedItem.Equals(default)))
{
return focusedItem;
}
}

return default;
}

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 DestroyDropdownAsync();
await HandleItemSelected(focusedItem);
}
}

// Move focus up or down.
if (keyboardEventArgs.Code == ArrowUpKeyCode)
{
int previousItemIndex = focusedItemIndex - 1;
if (previousItemIndex >= InputKeyboardNavigationIndex) // If the index equals InputKeyboardNavigationIndex, no item is focused.
{
focusedItemIndex = previousItemIndex;
}
}
else if (keyboardEventArgs.Code == ArrowDownKeyCode)
{
int nextItemIndex = focusedItemIndex + 1;
if (nextItemIndex < suggestions.Count)
{
focusedItemIndex = nextItemIndex;
}
}
}

private TItem GetItemByIndex(int index)
{
if (index >= 0 && index < suggestions?.Count)
{
return suggestions[index];
}
else
{
return default;
}
}
#endregion KeyboardNavigation

private async Task HandleItemSelected(TItem item)
{
// user clicked on an item in the "dropdown".
// user selected an item in the "dropdown".
await SetValueItemWithEventCallback(item);
userInput = TextSelectorEffective(item);
userInputModified = false;
Expand Down
Expand Up @@ -14,4 +14,8 @@

.hx-autosuggest-input-icon div[role="button"]:not(:hover) ::deep .hx-icon {
opacity: var(--hx-autosuggest-input-close-icon-opacity);
}
}

::deep .hx-dropdown-item-focused {
background-color: var(--hx-autosuggest-item-highlighted-background-color);
}
Expand Up @@ -9,9 +9,9 @@
TItem currentItem = item;

<button
@onfocusin="@(HighlightFirstSuggestionEffective ? (() => hasFocus = true) : null)"
@onfocusout="@(HighlightFirstSuggestionEffective ? (() => hasFocus = false) : null)"
class="@CssClassHelper.Combine("dropdown-item text-truncate", (currentItem.Equals(Items.FirstOrDefault()) && HighlightFirstSuggestionEffective && !hasFocus) ? "hx-autosuggest-items-item-highlighted" : null)"
class="@CssClassHelper.Combine(
"dropdown-item text-truncate",
item.Equals(FocusedItem) ? "hx-dropdown-item-focused" : null)"
type="button"
@onclick="async () => await HandleItemClick(currentItem)"
@onclick:stopPropagation="true"
Expand Down
Expand Up @@ -8,33 +8,10 @@ public partial class HxAutosuggestItems<TItem>

[Parameter] public RenderFragment<TItem> ItemTemplate { get; set; }

/// <summary>
/// Visually highlights the first suggestion.
/// </summary>
[Parameter] public bool HighlightFirstSuggestionEffective { get; set; }

[Parameter] public RenderFragment EmptyTemplate { get; set; }
[Parameter] public string CssClass { get; set; }

private bool hasFocus = false;

private ElementReference FirstItemReference
{
get
{
return firstItemReference;
}
set
{
firstItemReference = firstItemReference.Equals(default(ElementReference)) ? value : firstItemReference;
}
}
private ElementReference firstItemReference;

public async Task FocusFirstItemAsync()
{
await FirstItemReference.FocusAsync();
}
[Parameter] public TItem FocusedItem { get; set; }

private async Task HandleItemClick(TItem value)
{
Expand Down
Expand Up @@ -210,7 +210,6 @@ 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 @@ -337,6 +336,8 @@ protected async Task HandleTextQueryValueChanged(string newTextQuery)
}

#region KeyboardNavigation
private int focusedItemIndex = -1;

private const string ArrowUpKeyCode = "ArrowUp";
private const string ArrowDownKeyCode = "ArrowDown";

Expand Down

0 comments on commit b30e64f

Please sign in to comment.