diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Demo_Basic.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Demo_Basic.razor index 0f6df51fc..b82916326 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Demo_Basic.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Demo_Basic.razor @@ -1,14 +1,16 @@ @using System.Globalization - @item.EnglishName @item.LCID + + @item.EnglishName @item.LCID + Couldn't find any matching locale @@ -18,28 +20,28 @@ @code { - private int? autosuggestValue = 1033; + private int? autosuggestValue = 1033; - private async Task> ProvideSuggestions(AutosuggestDataProviderRequest request) - { - await Task.Delay(400); // backend API speed simulation - return new AutosuggestDataProviderResult - { - Data = CultureInfo.GetCultures(CultureTypes.AllCultures) - .Where(c => c.LCID != 4096) // see Remarks: https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.lcid?view=net-5.0#System_Globalization_CultureInfo_LCID - .Where(c => c.EnglishName?.Contains(request.UserInput, StringComparison.CurrentCultureIgnoreCase) ?? false) - .OrderBy(c => c.EnglishName) - .ToList() - }; - } + private async Task> ProvideSuggestions(AutosuggestDataProviderRequest request) + { + await Task.Delay(400); // backend API speed simulation + return new AutosuggestDataProviderResult + { + Data = CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(c => c.LCID != 4096) // see Remarks: https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.lcid?view=net-5.0#System_Globalization_CultureInfo_LCID + .Where(c => c.EnglishName?.Contains(request.UserInput, StringComparison.CurrentCultureIgnoreCase) ?? false) + .OrderBy(c => c.EnglishName) + .ToList() + }; + } - private async Task ResolveAutosuggestItemFromValue(int? value) - { - if (value is null) - { - return null; - } - await Task.Delay(400); // backend API speed simulation - return CultureInfo.GetCultureInfo(value.Value); - } + private async Task ResolveAutosuggestItemFromValue(int? value) + { + if (value is null) + { + return null; + } + await Task.Delay(400); // backend API speed simulation + return CultureInfo.GetCultureInfo(value.Value); + } } \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Documentation.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Documentation.razor index 879c0403e..64b69e688 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Documentation.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxAutosuggestDoc/HxAutosuggest_Documentation.razor @@ -2,17 +2,15 @@ - - Due to breaking change in Bootstrap 5.2 the keyboard navigation does not work in HxAutosuggest and HxSearchBox (the Up and Down keys). - We are trying to fix this with Bootstrap team and will try to come up with a temporary solution on our side until the final solution will be available.
- You can check the progress here: https://github.com/havit/Havit.Blazor/issues/348. -
- + - Although HxAutosuggest supports input groups, ending input groups conflict with the search icon, therefore, when InputGroupEndText or InputGroupEndTemplate are set, the search icon is not displayed. + Due to breaking change in Bootstrap 5.2, the keyboard navigation stopped working for dropdowns triggered from input (the Up and Down keys). + As an experimental feature we added our own keyboard navigation routines to the affected components. This might be subject to future change. + You can check the progress here: https://github.com/havit/Havit.Blazor/issues/348. +
diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxInputTagsDoc/HxInputTags_Documentation.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxInputTagsDoc/HxInputTags_Documentation.razor index 66f375d41..a56444fe1 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxInputTagsDoc/HxInputTags_Documentation.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxInputTagsDoc/HxInputTags_Documentation.razor @@ -2,7 +2,13 @@ - + + Due to breaking change in Bootstrap 5.2, the keyboard navigation stopped working for dropdowns triggered from input (the Up and Down keys). + As an experimental feature we added our own keyboard navigation routines to the affected components. This might be subject to future change. + You can check the progress here: https://github.com/havit/Havit.Blazor/issues/348. + + + diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo.razor index c8f9433f9..581b9a0e8 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo.razor @@ -1,12 +1,10 @@ 
Search for Mouse, Table or Door...
@@ -18,21 +16,21 @@

Last selected item: @selectedItem?.Title
- Triggered text-query: @triggeredTextQuery
- Bound text-query: @textQuery
+ Last triggered text-query: @triggeredTextQuery

@code { private SearchBoxItem selectedItem; private string triggeredTextQuery; - private string textQuery; List 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 = "Big table", Subtitle = "9 000", Icon = BootstrapIcon.Table }, + new() { Title = "Small table", Subtitle = "7 200", Icon = BootstrapIcon.Table } + }; private void OnItemSelected(SearchBoxItem item) { @@ -49,12 +47,12 @@ 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 + internal class SearchBoxItem { public string Title { get; set; } public string Subtitle { get; set; } diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo_DisableTextQuery.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo_DisableTextQuery.razor new file mode 100644 index 000000000..f71b875bd --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Demo_DisableTextQuery.razor @@ -0,0 +1,54 @@ + + +
Search for Mouse, Table or Door...
+
+ +
Sorry, I did not find that...
+
+
+ +

+ Last selected item: @selectedItem?.Title
+

+ +@code { + private SearchBoxItem selectedItem; + + List 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 = "Big table", Subtitle = "9 000", Icon = BootstrapIcon.Table }, + new() { Title = "Small table", Subtitle = "7 200", Icon = BootstrapIcon.Table } + }; + + private void OnItemSelected(SearchBoxItem item) + { + selectedItem = item; + } + + private async Task> ProvideSearchResults(SearchBoxDataProviderRequest request) + { + await Task.Delay(400); // imitate slower server API + + return new() + { + Data = Data.Where(i => i.Title.Contains(request.UserInput, StringComparison.OrdinalIgnoreCase)) + }; + } + + class SearchBoxItem + { + public string Title { get; set; } + public string Subtitle { get; set; } + public BootstrapIcon Icon { get; set; } + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Documentation.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Documentation.razor index c9f94480d..c3c1881cc 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Documentation.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxSearchBoxDoc/HxSearchBox_Documentation.razor @@ -2,17 +2,18 @@ - - Due to breaking change in Bootstrap 5.2 the keyboard navigation does not work in HxAutosuggest and HxSearchBox (the Up and Down keys). - We are trying to fix this with Bootstrap team and will try to come up with a temporary solution on our side until the final solution will be available.
+ + Due to breaking change in Bootstrap 5.2, the keyboard navigation stopped working for dropdowns triggered from input (the Up and Down keys). + As an experimental feature we added our own keyboard navigation routines to the affected components. This might be subject to future change. You can check the progress here: https://github.com/havit/Havit.Blazor/issues/348. Basic usage - - Although HxSearchBox supports input groups, ending input groups conflict with the search icon, therefore, when InputGroupEndText or InputGroupEndTemplate are set, the search icon is not displayed. - + + Suggestions only +

You can disable free-text query by setting AllowTextQuery="false". Only suggestions will be allowed.

+
diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Migrations/MigratingToV3.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Migrations/MigratingToV3.razor index a580e9e89..823ab565f 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Migrations/MigratingToV3.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Migrations/MigratingToV3.razor @@ -43,7 +43,13 @@

If you were using PagerCssClass in v2, replace it with new PagerSettings parameter:

-
4. [OPTIONAL] Replace obsolete components with new ones
+
3.4 HxAutosuggest first item highlighting
+

+ The HighlightFirstSuggestion parameter was removed. The component now highlights the first suggestion by default (cannot be disabled). + Remove the parameter from your code (incl. HxAutosuggest.Settings and HxAutosuggest.Defaults). +

+ +

4. [OPTIONAL] Replace obsolete components with new ones

We replaced HxInputCheckbox with new HxCheckbox and HxInputSwitch with new HxSwitch.
The original Label parameter is now Text (the Label parameter of new components has a different purpose, see HxCheckbox documentation). 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 6f7789164..ba77abacb 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 @@ -2282,11 +2282,6 @@ Input size. - -

- If true, the first suggestion is highlighted until another is chosen by the user. - - Component for single item selection with dynamic suggestions (based on typed characters).
@@ -2376,11 +2371,6 @@ - - - If true, the first suggestion is highlighted until another is chosen by the user. - - Offset between the dropdown and the input. @@ -2439,13 +2429,13 @@ Application-wide defaults for the and derived components. - + Offset between the dropdown and the input. - + Additional attributes to be splatted onto an underlying HTML element. @@ -2523,25 +2513,14 @@ Input-group at the end of the input.
- - - If true, the first suggestion is highlighted until another is chosen by the user. - - 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. @@ -2858,7 +2837,7 @@ Raised when the item is clicked. - + Offset between dropdown input and dropdown menu @@ -2960,6 +2939,11 @@ Additional attributes to be splatted onto an underlying HTML input. + + + Input's index for the keyboard navigation. If this is the current index, then no item is selected. + + Common implementation for and . @@ -5031,6 +5015,11 @@ Additional CSS classes for the search box input. + + + Custom CSS class to render with input-group span. + + Icon of the input, when no text is written. @@ -5074,7 +5063,8 @@ - Indicates whether text-query mode is enabled (accepts free text in addition to suggested items). + Indicates whether text-query mode is enabled (accepts free text in addition to suggested items).
+ Default is true.
@@ -5104,6 +5094,11 @@ Shows whether the has been below minimum required length recently (before data provider loading is completed).
+ + + Input's index for the keyboard navigation. If this is the current index, then no item is selected. + + If the is empty, we don't want to display anything when nothing (or below the minimum amount of characters) is typed into the input. @@ -5185,6 +5180,11 @@ Additional CSS classes for the search box input. + + + Custom CSS class to render with input-group span. + + Settings for the component. diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/AutosuggestSettings.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/AutosuggestSettings.cs index 616c46ddc..47f4c4cf3 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/AutosuggestSettings.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/AutosuggestSettings.cs @@ -31,10 +31,5 @@ public record AutosuggestSettings : InputsSettings, IInputSettingsWithSize /// Input size. /// public InputSize? InputSize { get; set; } - - /// - /// If true, the first suggestion is highlighted until another is chosen by the user. - /// - public bool? HighlightFirstSuggestion { get; set; } } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs index f97e55884..935069281 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.cs @@ -99,12 +99,6 @@ public class HxAutosuggest : HxInputBase, IInputWithSize, /// [Parameter] public LabelType? LabelType { get; set; } - /// - /// If true, the first suggestion is highlighted until another is chosen by the user. - /// - [Parameter] public bool? HighlightFirstSuggestion { get; set; } - protected bool HighlightFirstSuggestionEffective => this.HighlightFirstSuggestion ?? GetSettings()?.HighlightFirstSuggestion ?? GetDefaults()?.HighlightFirstSuggestion ?? throw new InvalidOperationException(nameof(HighlightFirstSuggestion) + " default for " + nameof(HxAutosuggest) + " has to be set."); - /// /// Offset between the dropdown and the input. /// @@ -175,7 +169,6 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) builder.AddAttribute(1023, nameof(HxAutosuggestInternal.InputGroupStartTemplate), this.InputGroupStartTemplate); builder.AddAttribute(1024, nameof(HxAutosuggestInternal.InputGroupEndText), this.InputGroupEndText); builder.AddAttribute(1025, nameof(HxAutosuggestInternal.InputGroupEndTemplate), this.InputGroupEndTemplate); - builder.AddAttribute(1026, nameof(HxAutosuggestInternal.HighlightFirstSuggestionEffective), this.HighlightFirstSuggestionEffective); builder.AddMultipleAttributes(2000, this.AdditionalAttributes); diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.nongeneric.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.nongeneric.cs index f678ca60d..beb9a2e68 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.nongeneric.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/HxAutosuggest.nongeneric.cs @@ -19,7 +19,6 @@ static HxAutosuggest() ClearIcon = BootstrapIcon.XLg, MinimumLength = 2, Delay = 300, - HighlightFirstSuggestion = true }; } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor similarity index 74% rename from Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor rename to Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor index f98f6e10e..2d1bd1433 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor @@ -10,10 +10,10 @@ data-bs-offset="@($"{DropdownOffset.Skidding},{DropdownOffset.Distance}")" value="@Value" @oninput="HandleInput" - @onkeydown="HandleKeyDown" - @onfocus="OnInputFocus" - @onblur="OnInputBlur" - @onmousedown="OnInputMouseDown" + @onkeydown="OnKeyDown" + @onfocus="OnFocus" + @onblur="OnBlur" + @onmousedown="OnMouseDown" @onclick:stopPropagation placeholder="@Placeholder" onfocusin="this.select()" diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor.cs similarity index 61% rename from Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs rename to Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor.cs index d2715e6ac..76c9f3a1f 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor.cs @@ -1,20 +1,20 @@ namespace Havit.Blazor.Components.Web.Bootstrap.Internal { - public partial class HxAutosuggestInput + public partial class HxAutosuggestInputInternal { [Parameter] public string Value { get; set; } [Parameter] public string Placeholder { get; set; } - [Parameter] public EventCallback OnInputInput { get; set; } + [Parameter] public EventCallback OnInput { get; set; } - [Parameter] public EventCallback OnInputFocus { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } - [Parameter] public EventCallback OnInputBlur { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } - [Parameter] public EventCallback OnInputMouseDown { get; set; } + [Parameter] public EventCallback OnMouseDown { get; set; } - [Parameter] public EventCallback OnEnter { get; set; } + [Parameter] public EventCallback OnKeyDown { get; set; } [Parameter] public string InputId { get; set; } @@ -37,15 +37,7 @@ public partial class HxAutosuggestInput private async Task HandleInput(ChangeEventArgs changeEventArgs) { - await OnInputInput.InvokeAsync((string)changeEventArgs.Value); - } - - private async Task HandleKeyDown(KeyboardEventArgs keyboardEventArgs) - { - if ((keyboardEventArgs.Code == "Enter") || (keyboardEventArgs.Code == "NumpadEnter")) - { - await OnEnter.InvokeAsync(); - } + await OnInput.InvokeAsync((string)changeEventArgs.Value); } public async ValueTask FocusAsync() diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor.css similarity index 100% rename from Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInput.razor.css rename to Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInputInternal.razor.css 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..3d9ed9f65 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestInternal.razor @@ -3,74 +3,81 @@ @typeparam TValue
- @if (InputGroupStartText is not null) - { - @InputGroupStartText - } - - @InputGroupStartTemplate - - + @if (InputGroupStartText is not null) + { + @InputGroupStartText + } - @InputGroupEndTemplate + @InputGroupStartTemplate - @if (InputGroupEndText is not null) - { - @InputGroupEndText - } + - @if (LabelTypeEffective == Havit.Blazor.Components.Web.Bootstrap.LabelType.Floating) - { - - } - @if (EnabledEffective) - { -
- @if (dataProviderInProgress) - { -
- -
- } - else if (!EqualityComparer.Default.Equals(Value, default)) - { - if (this.ClearIconEffective is not null) - { -
- -
- } - } - else if (this.SearchIconEffective is not null) - { - - } -
- - - @if (ItemTemplate != null) - { - @ItemTemplate(context) - } - else - { - @TextSelectorEffective(context) - } - - - @EmptyTemplate - - - } + @InputGroupEndTemplate + + @if (InputGroupEndText is not null) + { + @InputGroupEndText + } + + @if (LabelTypeEffective == Havit.Blazor.Components.Web.Bootstrap.LabelType.Floating) + { + + } + @if (EnabledEffective) + { +
+ @if (dataProviderInProgress) + { +
+ +
+ } + else if (!EqualityComparer.Default.Equals(Value, default)) + { + if (this.ClearIconEffective is not null) + { +
+ +
+ } + } + else if (this.SearchIconEffective is not null) + { + + } +
+ + + + @if (ItemTemplate != null) + { + @ItemTemplate(context) + } + else + { + @TextSelectorEffective(context) + } + + + @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..0765447cf 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 @@ -1,6 +1,4 @@ -using System.Threading; -using Havit.Diagnostics.Contracts; -using Microsoft.JSInterop; +using Microsoft.JSInterop; namespace Havit.Blazor.Components.Web.Bootstrap.Internal { @@ -98,11 +96,6 @@ public partial class HxAutosuggestInternal : IAsyncDisposable ///
[Parameter] public RenderFragment InputGroupEndTemplate { get; set; } - /// - /// If true, the first suggestion is highlighted until another is chosen by the user. - /// - [Parameter] public bool HighlightFirstSuggestionEffective { get; set; } - /// /// Additional attributes to be splatted onto an underlying HTML element. /// @@ -125,7 +118,7 @@ public partial class HxAutosuggestInternal : IAsyncDisposable private bool currentlyFocused; private bool disposed; private IJSObjectReference jsModule; - private HxAutosuggestInput autosuggestInput; + private HxAutosuggestInputInternal autosuggestInput; private TValue lastKnownValue; private bool dataProviderInProgress; private DotNetObjectReference> dotnetObjectReference; @@ -223,19 +216,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 +290,10 @@ private async Task UpdateSuggestionsAsync() } dataProviderInProgress = false; + + // KeyboardNavigation + focusedItemIndex = 0; // First item in the searchResults collection. + suggestions = result.Data?.ToList(); if ((suggestions?.Any() ?? false) || EmptyTemplate != null) @@ -324,9 +308,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 HandleInputKeyDown(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..849138a91 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-autosuggest-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 deleted file mode 100644 index 3207a1a88..000000000 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor +++ /dev/null @@ -1,29 +0,0 @@ -@namespace Havit.Blazor.Components.Web.Bootstrap.Internal -@typeparam TItem - -
- @if (Items != null && Items.Any()) - { - foreach (TItem item in Items) - { - TItem currentItem = item; - - - } - } - else if (EmptyTemplate != null) - { - @EmptyTemplate - } -
\ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor.cs deleted file mode 100644 index ecd5f1c19..000000000 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Havit.Blazor.Components.Web.Bootstrap.Internal -{ - public partial class HxAutosuggestItems - { - [Parameter] public List Items { get; set; } - - [Parameter] public EventCallback OnItemClick { get; set; } - - [Parameter] public RenderFragment ItemTemplate { get; set; } - - /// - /// Visually highlights the first suggestion. - /// - [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(); - } - - private async Task HandleItemClick(TItem value) - { - await OnItemClick.InvokeAsync(value); - } - } -} diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor new file mode 100644 index 000000000..f88c81f26 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor @@ -0,0 +1,29 @@ +@namespace Havit.Blazor.Components.Web.Bootstrap.Internal +@typeparam TItem + +
+ @if (Items != null && Items.Any()) + { + for (int i = 0; i < Items.Count; i++) + { + TItem currentItem = Items[i]; + + + } + } + else if (EmptyTemplate != null) + { + @EmptyTemplate + } +
\ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor.cs new file mode 100644 index 000000000..7b3723309 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor.cs @@ -0,0 +1,22 @@ +namespace Havit.Blazor.Components.Web.Bootstrap.Internal +{ + public partial class HxAutosuggestItemsInternal + { + [Parameter] public List Items { get; set; } + + [Parameter] public EventCallback OnItemClick { get; set; } + + [Parameter] public RenderFragment ItemTemplate { get; set; } + + [Parameter] public RenderFragment EmptyTemplate { get; set; } + [Parameter] public string CssClass { get; set; } + + [Parameter] public int FocusedItemIndex { get; set; } + [Parameter] public string FocusedItemCssClass { get; set; } + + private async Task HandleItemClick(TItem value) + { + await OnItemClick.InvokeAsync(value); + } + } +} diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor.css similarity index 100% rename from Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItems.razor.css rename to Havit.Blazor.Components.Web.Bootstrap/Forms/Autosuggests/Internal/HxAutosuggestItemsInternal.razor.css diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor index fb1060c84..c7d949d73 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/SearchBox/HxSearchBox.razor @@ -15,21 +15,39 @@
- + @if (!string.IsNullOrEmpty(Label)) + { + + } + + + @if (InputGroupStartText is not null) + { + @InputGroupStartText + } + + @InputGroupStartTemplate + + + + @InputGroupEndTemplate + + @if (InputGroupEndText is not null) + { + @InputGroupEndText + } + @if (InputGroupEndText is null && InputGroupEndTemplate is null) { @@ -59,17 +77,24 @@ @if ((searchResults.Count > 0) && (TextQuery.Length >= MinimumLengthEffective)) { - @foreach (var item in searchResults) + @for (int i = 0; i < searchResults.Count; i++) { + var item = searchResults[i]; + string title = ItemTitleSelector?.Invoke(item) ?? null; string subtitle = ItemSubtitleSelector?.Invoke(item) ?? null; IconBase icon = ItemIconSelector?.Invoke(item) ?? null;
  • -
  • diff --git a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.cs b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.cs index 4d47d4480..be9002042 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.cs @@ -1,5 +1,4 @@ -using System.Threading; -using Microsoft.JSInterop; +using Microsoft.JSInterop; namespace Havit.Blazor.Components.Web.Bootstrap.Internal { @@ -132,7 +131,7 @@ public partial class HxInputTagsInternal private bool mouseDownFocus; private bool disposed; private IJSObjectReference jsModule; - private HxInputTagsAutosuggestInput autosuggestInput; + private HxInputTagsAutosuggestInputInternal autosuggestInput; private bool dataProviderInProgress; private DotNetObjectReference dotnetObjectReference; @@ -186,6 +185,8 @@ protected async Task HandleInputKeyDown(KeyboardEventArgs args) { await RemoveTagWithEventCallbackAsync(ValueEffective.Last()); } + + await UpdateFocusedItemAsync(args); } private async Task HandleInputInput(string newUserInput) @@ -338,6 +339,10 @@ private async Task UpdateSuggestionsAsync(bool bypassShow = false) } dataProviderInProgress = false; + + // KeyboardNavigation + focusedItemIndex = 0; // First item in the searchResults collection. + suggestions = result.Data.ToList(); if (suggestions?.Any() ?? false) @@ -352,7 +357,66 @@ private async Task UpdateSuggestionsAsync(bool bypassShow = false) StateHasChanged(); } - private async Task HandleItemClick(string tag) + #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 async Task UpdateFocusedItemAsync(KeyboardEventArgs keyboardEventArgs) + { + // Confirm selection on the focused item if an item is focused and the enter key is pressed. + string focusedItem = GetItemByIndex(focusedItemIndex); + if (keyboardEventArgs.Code == EnterKeyCode || keyboardEventArgs.Code == NumpadEnterKeyCode) + { + if ((focusedItem is not null) && (!focusedItem.Equals(default))) + { + await TryDestroyDropdownAsync(); + 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 string GetItemByIndex(int index) + { + if (index >= 0 && index < suggestions?.Count) + { + return suggestions[index]; + } + else + { + return default; + } + } + #endregion KeyboardNavigation + + private async Task HandleItemSelected(string tag) { // user clicked on an item in the "dropdown". userInput = String.Empty; diff --git a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.css b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.css index 5ef39ee36..83f266842 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.css +++ b/Havit.Blazor.Components.Web.Bootstrap/Tags/Internal/HxInputTagsInternal.razor.css @@ -59,3 +59,7 @@ input:focus { .hx-tag-add-button-text { margin: var(--hx-input-tags-add-button-text-margin); } + +::deep .hx-input-tags-dropdown-item-focused { + background-color: var(--hx-input-tags-dropdown-item-highlighted-background-color); +} diff --git a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css index c2faec971..1482e420c 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css +++ b/Havit.Blazor.Components.Web.Bootstrap/wwwroot/defaults.css @@ -118,15 +118,16 @@ --hx-multi-select-background-color: var(--bs-white); --hx-multi-select-dropdown-menu-height: 300px; /* TagInput */ - --hx-input-tags-tag-margin: 0 .25rem 0 0; - --hx-input-tags-input-width: 3em; - --hx-input-tags-input-placeholder-color: var(--bs-gray-600); - --hx-input-tags-naked-font-size-lg: 1.25em; - --hx-input-tags-naked-font-size-sm: .875em; - --hx-input-tags-control-focused-box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); - --hx-input-tags-control-focused-border-color: rgba(var(--bs-primary-rgb), .3); - --hx-input-tags-add-button-text-margin: 0 0 0 .25rem; - --hx-input-tags-remove-button-margin: 0 0 0 .25rem; + --hx-input-tags-tag-margin: 0 .25rem 0 0; + --hx-input-tags-input-width: 3em; + --hx-input-tags-input-placeholder-color: var(--bs-gray-600); + --hx-input-tags-naked-font-size-lg: 1.25em; + --hx-input-tags-naked-font-size-sm: .875em; + --hx-input-tags-control-focused-box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); + --hx-input-tags-control-focused-border-color: rgba(var(--bs-primary-rgb), .3); + --hx-input-tags-add-button-text-margin: 0 0 0 .25rem; + --hx-input-tags-remove-button-margin: 0 0 0 .25rem; + --hx-input-tags-dropdown-item-highlighted-background-color: var(--bs-gray-200); /* TreeView */ --hx-tree-view-item-border-radius: .25rem; --hx-tree-view-item-border-width: 0; @@ -157,12 +158,13 @@ /* HxToastContainer */ --hx-toast-container-margin: .5rem; /* HxSearchBox */ - --hx-search-box-item-icon-margin: 0 .5rem 0 0; - --hx-search-box-item-icon-font-size: inherit; - --hx-search-box-item-title-font-size: inherit; - --hx-search-box-item-title-color: inherit; - --hx-search-box-item-subtitle-color: var(--bs-secondary); - --hx-search-box-item-subtitle-font-size: .75rem; + --hx-search-box-item-icon-margin: 0 .5rem 0 0; + --hx-search-box-item-icon-font-size: inherit; + --hx-search-box-item-title-font-size: inherit; + --hx-search-box-item-title-color: inherit; + --hx-search-box-item-subtitle-color: var(--bs-secondary); + --hx-search-box-item-subtitle-font-size: .75rem; + --hx-search-box-item-highlighted-background-color: var(--bs-gray-200); } form {