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.

Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.