diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml b/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml index 5ce553033..2276f873c 100755 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml @@ -2533,15 +2533,9 @@ Additional attributes to be splatted onto an underlying HTML element. - + - Select the first suggested item when an enter key is pressed. - - - - - - Visually highlights the first suggestion. + Input's index for the keyboard navigation. If this is the current index, then no item is selected. diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs index d2715e6ac..8d07b3558 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs @@ -14,7 +14,7 @@ public partial class HxAutosuggestInput [Parameter] public EventCallback OnInputMouseDown { get; set; } - [Parameter] public EventCallback OnEnter { get; set; } + [Parameter] public EventCallback OnKeyDown { get; set; } [Parameter] public string InputId { get; set; } @@ -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() diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor index cc999dea1..3e7b2a150 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor @@ -17,7 +17,7 @@ OnInputInput="HandleInputInput" OnInputFocus="HandleInputFocus" OnInputBlur="HandleInputBlur" - OnEnter="HandleInputEnterKeyDown" + OnKeyDown="UpdateFocusedItem" Placeholder="@Placeholder" CssClass="@((!HasAnyInputGroupEnd ? "rounded-end " : null) + InputCssClass)" DropdownOffset="@DropdownOffset" @@ -57,7 +57,13 @@ } - + + @if (ItemTemplate != null) { @@ -71,6 +77,7 @@ @EmptyTemplate + } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.cs index 2fd931e38..cae6de3a6 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.cs @@ -223,19 +223,6 @@ private async Task HandleInputInput(string newUserInput) } } - /// - /// Select the first suggested item when an enter key is pressed. - /// - /// - 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 @@ -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) @@ -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"; + + /// + /// Input's index for the keyboard navigation. If this is the current index, then no item is selected. + /// + 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; diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.css index 9c58e1800..bfd36aea4 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.css +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor.css @@ -14,4 +14,8 @@ .hx-autosuggest-input-icon div[role="button"]:not(:hover) ::deep .hx-icon { opacity: var(--hx-autosuggest-input-close-icon-opacity); -} \ No newline at end of file +} + +::deep .hx-dropdown-item-focused { + background-color: var(--hx-autosuggest-item-highlighted-background-color); +} diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor index 3207a1a88..b9048ce9d 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor @@ -9,9 +9,9 @@ TItem currentItem = item;