Skip to content

Commit 3db6582

Browse files
committed
Merge branch 'rel-2.0'
# Conflicts: # Documentation/Blazorise.Docs/Resources/docs-api-index.json # Documentation/Blazorise.Docs/Resources/docs-index.json
2 parents 039b01f + f76f763 commit 3db6582

9 files changed

Lines changed: 174 additions & 10 deletions

File tree

Documentation/Blazorise.Docs/Pages/Docs/Extensions/Autocomplete/AutocompletePage.razor

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,17 @@
9696

9797
<DocsPageSection>
9898
<DocsPageSectionHeader Title="Validation (data annotations)">
99-
Use <Code>ValidationMode.Auto</Code> with a model decorated with data annotations to validate single and multiple selections.
99+
<Paragraph>
100+
Use <Code>ValidationMode.Auto</Code> with a model decorated with data annotations to validate single and multiple selections.
101+
</Paragraph>
102+
103+
<Paragraph>
104+
<Alert Color="Color.Info" Visible>
105+
<AlertDescription>
106+
<Strong>Note:</Strong> When both <Code>Value</Code> and <Code>Text</Code> are bound then validation will work on the <Code>Value</Code> binding.
107+
</AlertDescription>
108+
</Alert>
109+
</Paragraph>
100110
</DocsPageSectionHeader>
101111
<DocsPageSectionContent Outlined FullWidth>
102112
<AutocompleteDataAnnotationValidationExample />
@@ -189,4 +199,4 @@
189199
</DocsPageSection>
190200

191201

192-
<ComponentApiDocs ComponentTypes="[typeof(Autocomplete<,>)]" />
202+
<ComponentApiDocs ComponentTypes="[typeof(Autocomplete<,>)]" />

Documentation/Blazorise.Docs/Pages/Docs/Specifications/AutocompletePage.razor

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,27 @@
209209
<TableRowCell></TableRowCell>
210210
</TableRow>
211211
</TableBody>
212-
</Table>
212+
</Table>
213+
214+
<DocsPageSubtitle>
215+
Validation
216+
</DocsPageSubtitle>
217+
218+
<DocsPageParagraph>
219+
The <Code>Autocomplete</Code> component supports <Code>Validation</Code> like any other regular input field. But because you can
220+
bind both the <Code>Value(s)</Code> and <Code>Text(s)</Code> properties the validation logic will work with a prioritized system
221+
for which binding expression field will be validated.
222+
223+
<OrderedList>
224+
<OrderedListItem><Code>@@bind-Value</Code> expression</OrderedListItem>
225+
<OrderedListItem><Code>@@bind-SelectedValue</Code> expression</OrderedListItem>
226+
<OrderedListItem><Code>@@bind-SelectedText</Code> expression</OrderedListItem>
227+
</OrderedList>
228+
229+
</DocsPageParagraph>
230+
231+
<DocsPageParagraph>
232+
<Strong>Note:</Strong> If you want to validate both the <Code>Value</Code> and the <Code>Text</Code> bindings then you will need
233+
to validate the text as part of the <Code>Value</Code> validation rule. Then the validation feedback will be correctly displayed
234+
on the <Code>Autocomplete</Code> component.
235+
</DocsPageParagraph>

Documentation/Blazorise.Docs/Resources/docs-api-index.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generatedUtc": "2026-03-22T12:27:01.1998578Z",
2+
"generatedUtc": "2026-03-24T09:42:27.6885828Z",
33
"components": [
44
{
55
"type": "global::Blazorise.Abbreviation",
@@ -5036,6 +5036,14 @@
50365036
"summary": "Gets or sets the currently selected item text.",
50375037
"isBlazoriseEnum": false
50385038
},
5039+
{
5040+
"name": "SelectedTextExpression",
5041+
"type": "Expression<>",
5042+
"typeName": "Expression<Func<string>>",
5043+
"defaultValue": "null",
5044+
"summary": "Gets or sets an expression that identifies the selected text.",
5045+
"isBlazoriseEnum": false
5046+
},
50395047
{
50405048
"name": "SelectedTexts",
50415049
"type": "List<>",
@@ -5044,6 +5052,14 @@
50445052
"summary": "Currently selected items texts. Used when multiple selection is set.",
50455053
"isBlazoriseEnum": false
50465054
},
5055+
{
5056+
"name": "SelectedTextsExpression",
5057+
"type": "Expression<>",
5058+
"typeName": "Expression<Func<List<string>>>",
5059+
"defaultValue": "null",
5060+
"summary": "Gets or sets an expression that identifies the selected texts. Used when multiple selection is set.",
5061+
"isBlazoriseEnum": false
5062+
},
50475063
{
50485064
"name": "SelectedValue",
50495065
"type": "object",
@@ -40005,6 +40021,14 @@
4000540021
"summary": "Custom inline styles to apply to the component.",
4000640022
"isBlazoriseEnum": false
4000740023
},
40024+
{
40025+
"name": "TabIndex",
40026+
"type": "int?",
40027+
"typeName": "int?",
40028+
"defaultValue": "null",
40029+
"summary": "Gets or sets the tabindex value for keyboard navigation.",
40030+
"isBlazoriseEnum": false
40031+
},
4000840032
{
4000940033
"name": "TextAlignment",
4001040034
"type": "global::Blazorise.TextAlignment",

Documentation/Blazorise.Docs/Resources/docs-index.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generatedUtc": "2026-03-22T12:27:01.5526320Z",
2+
"generatedUtc": "2026-03-24T09:42:47.9552921Z",
33
"pages": [
44
{
55
"route": "/docs",
@@ -4017,7 +4017,7 @@
40174017
"sourcePath": "Pages/Docs/Extensions/Autocomplete/Examples/AutocompleteDataAnnotationValidationExample.razor",
40184018
"content": "@using System.ComponentModel.DataAnnotations\r\n\r\n<Validations Mode=\"ValidationMode.Auto\" Model=\"@model\">\r\n <Validation>\r\n <Field>\r\n <FieldLabel>Country</FieldLabel>\r\n <FieldBody>\r\n <Autocomplete TItem=\"Country\"\r\n TValue=\"string\"\r\n Data=\"@Countries\"\r\n TextField=\"@(( item ) => item.Name)\"\r\n ValueField=\"@(( item ) => item.Iso)\"\r\n Placeholder=\"Select a country\"\r\n @bind-SelectedValue=\"@model.CountryIso\">\r\n <Feedback>\r\n <ValidationError />\r\n </Feedback>\r\n </Autocomplete>\r\n </FieldBody>\r\n </Field>\r\n </Validation>\r\n <Validation>\r\n <Field>\r\n <FieldLabel>Countries</FieldLabel>\r\n <FieldBody>\r\n <Autocomplete TItem=\"Country\"\r\n TValue=\"string\"\r\n Data=\"@Countries\"\r\n TextField=\"@(( item ) => item.Name)\"\r\n ValueField=\"@(( item ) => item.Iso)\"\r\n SelectionMode=\"AutocompleteSelectionMode.Multiple\"\r\n Placeholder=\"Select countries\"\r\n @bind-SelectedValues=\"@model.CountryIsos\">\r\n <Feedback>\r\n <ValidationError />\r\n </Feedback>\r\n </Autocomplete>\r\n </FieldBody>\r\n </Field>\r\n </Validation>\r\n</Validations>\r\n\r\n@code {\r\n [Inject]\r\n public CountryData CountryData { get; set; }\r\n\r\n public IEnumerable<Country> Countries;\r\n\r\n AutocompleteValidationModel model = new AutocompleteValidationModel();\r\n\r\n protected override async Task OnInitializedAsync()\r\n {\r\n Countries = await CountryData.GetDataAsync();\r\n await base.OnInitializedAsync();\r\n }\r\n\r\n public class AutocompleteValidationModel\r\n {\r\n [Required( ErrorMessage = \"Please select a country.\" )]\r\n public string CountryIso { get; set; }\r\n\r\n [MinLength( 1, ErrorMessage = \"Please select at least one country.\" )]\r\n public List<string> CountryIsos { get; set; } = new List<string>();\r\n }\r\n}",
40194019
"title": "Validation (data annotations)",
4020-
"description": "Use ValidationMode.Auto with a model decorated with data annotations to validate single and multiple selections."
4020+
"description": "Use ValidationMode.Auto with a model decorated with data annotations to validate single and multiple selections. Note: When both Value and Text are bound then validation will work on the Value binding."
40214021
},
40224022
{
40234023
"code": "AutocompleteReadDataExample",

Source/Blazorise/Components/Dropdown/DropdownItem.razor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,18 @@ protected async Task CheckedChangedHandler( bool isChecked )
133133
/// <summary>
134134
/// Gets the tabindex value for keyboard navigation.
135135
/// </summary>
136-
protected int ComputedTabIndex => Disabled ? -1 : 0;
136+
protected int ComputedTabIndex => Disabled ? -1 : TabIndex ?? 0;
137137

138138
/// <summary>
139139
/// Holds the item value.
140140
/// </summary>
141141
[Parameter] public object Value { get; set; }
142142

143+
/// <summary>
144+
/// Gets or sets the tabindex value for keyboard navigation.
145+
/// </summary>
146+
[Parameter] public int? TabIndex { get; set; }
147+
143148
/// <summary>
144149
/// Indicate the currently active item.
145150
/// </summary>

Source/Extensions/Blazorise.Components/Autocomplete.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@
7979
{
8080
var notFoundContext = GetNotFoundItemContext();
8181

82-
<DropdownItem Disabled CloseParentDropdowns="@CloseParentDropdowns" Class="@DropdownItemClassNames( notFoundContext )" Style="@DropdownItemStyleNames( notFoundContext )">
82+
<DropdownItem Disabled TabIndex="-1" CloseParentDropdowns="@CloseParentDropdowns" Class="@DropdownItemClassNames( notFoundContext )" Style="@DropdownItemStyleNames( notFoundContext )">
8383
@NotFoundTemplate( Search )
8484
</DropdownItem>
8585
}
8686
else if ( FreeTypingNotFoundVisible )
8787
{
8888
var notFoundContext = GetNotFoundItemContext();
8989

90-
<DropdownItem Disabled CloseParentDropdowns="@CloseParentDropdowns" Class="@DropdownItemClassNames( notFoundContext )" Style="@DropdownItemStyleNames( notFoundContext )">
90+
<DropdownItem Disabled TabIndex="-1" CloseParentDropdowns="@CloseParentDropdowns" Class="@DropdownItemClassNames( notFoundContext )" Style="@DropdownItemStyleNames( notFoundContext )">
9191
@FreeTypingNotFoundTemplate( Search )
9292
</DropdownItem>
9393
}
@@ -108,7 +108,7 @@
108108
var value = GetItemValue( item );
109109
var itemContext = CreateItemContext( item, value, text, itemIndex, isActiveItem, isFocusedItem, disabled );
110110

111-
<DropdownItem @key="@item" ElementId="@DropdownItemId( itemIndex )" Active="@isActiveItem" Checked=@isActiveItem ShowCheckbox=@isCheckbox Class="@DropdownItemClassNames( itemContext )" Style="@DropdownItemStyleNames( itemContext )" Value="@value"
111+
<DropdownItem @key="@item" ElementId="@DropdownItemId( itemIndex )" Active="@isActiveItem" Checked=@isActiveItem ShowCheckbox=@isCheckbox TabIndex="-1" Class="@DropdownItemClassNames( itemContext )" Style="@DropdownItemStyleNames( itemContext )" Value="@value"
112112
Disabled="@disabled"
113113
CloseParentDropdowns="@CloseParentDropdowns"
114114
@onmousedown="@(() => clickFromCheck = true)"

Source/Extensions/Blazorise.Components/Autocomplete.razor.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ public partial class Autocomplete<TItem, TValue>
101101
/// </summary>
102102
protected ComponentParameterInfo<string> paramSelectedText;
103103

104+
/// <summary>
105+
/// Captured SelectedTextExpression parameter snapshot.
106+
/// </summary>
107+
protected ComponentParameterInfo<Expression<Func<string>>> paramSelectedTextExpression;
108+
104109
/// <summary>
105110
/// Captured SelectedValues parameter snapshot.
106111
/// </summary>
@@ -111,6 +116,11 @@ public partial class Autocomplete<TItem, TValue>
111116
/// </summary>
112117
protected ComponentParameterInfo<IEnumerable<string>> paramSelectedTexts;
113118

119+
/// <summary>
120+
/// Captured SelectedTextExpression parameter snapshot.
121+
/// </summary>
122+
protected ComponentParameterInfo<Expression<Func<List<string>>>> paramSelectedTextsExpression;
123+
114124
/// <summary>
115125
/// Captured Data parameter snapshot.
116126
/// </summary>
@@ -153,6 +163,8 @@ protected override void CaptureParameters( ParameterView parameters )
153163
parameters.TryGetParameter( Data, value => data.IsEqual( value ), out paramData );
154164
parameters.TryGetParameter( SelectedValueExpression, out paramSelectedValueExpression );
155165
parameters.TryGetParameter( SelectedValuesExpression, out paramSelectedValuesExpression );
166+
parameters.TryGetParameter( SelectedTextExpression, out paramSelectedTextExpression );
167+
parameters.TryGetParameter( SelectedTextsExpression, out paramSelectedTextsExpression );
156168
}
157169

158170
/// <inheritdoc/>
@@ -230,6 +242,10 @@ protected override async Task OnAfterSetParametersAsync( ParameterView parameter
230242
{
231243
await ParentValidation.InitializeInputExpression( paramSelectedValuesExpression.Value );
232244
}
245+
else if ( paramSelectedTextsExpression.Defined )
246+
{
247+
await ParentValidation.InitializeInputExpression( paramSelectedTextsExpression.Value );
248+
}
233249
}
234250
else
235251
{
@@ -241,6 +257,10 @@ protected override async Task OnAfterSetParametersAsync( ParameterView parameter
241257
{
242258
await ParentValidation.InitializeInputExpression( paramSelectedValueExpression.Value );
243259
}
260+
else if ( paramSelectedTextExpression.Defined )
261+
{
262+
await ParentValidation.InitializeInputExpression( paramSelectedTextExpression.Value );
263+
}
244264
}
245265

246266
await InitializeValidation();
@@ -510,8 +530,35 @@ public async Task ScrollItemIntoView( int index )
510530
/// <returns>Returns awaitable task</returns>
511531
protected async Task OnTextKeyDownHandler( KeyboardEventArgs eventArgs )
512532
{
533+
bool isPlainTabNavigation = eventArgs.Code == "Tab" && !eventArgs.AltKey && !eventArgs.CtrlKey && !eventArgs.MetaKey;
534+
513535
await OnKeyDownHandler( eventArgs );
514536

537+
if ( isPlainTabNavigation )
538+
{
539+
if ( IsConfirmKey( eventArgs ) )
540+
{
541+
if ( IsMultiple && FreeTyping && !string.IsNullOrEmpty( Search ) && ActiveItemIndex < 0 )
542+
{
543+
await AddMultipleText( Search );
544+
545+
if ( CloseOnSelection )
546+
{
547+
await ResetCurrentSearch();
548+
}
549+
}
550+
else if ( SelectionMode != AutocompleteSelectionMode.Checkbox )
551+
{
552+
await SelectedOrResetOnCommit();
553+
}
554+
}
555+
556+
TextFocused = false;
557+
await Close();
558+
await SearchKeyDown.InvokeAsync( eventArgs );
559+
return;
560+
}
561+
515562
if ( eventArgs.Code == "Escape" )
516563
{
517564
await Close();
@@ -1784,6 +1831,11 @@ public Expression<Func<TValue>> SelectedValueExpression
17841831
[Parameter]
17851832
public string SelectedText { get; set; }
17861833

1834+
/// <summary>
1835+
/// Gets or sets an expression that identifies the selected text.
1836+
/// </summary>
1837+
[Parameter] public Expression<Func<string>> SelectedTextExpression { get; set; }
1838+
17871839
/// <summary>
17881840
/// Gets or sets the currently selected item text.
17891841
/// </summary>
@@ -1918,6 +1970,12 @@ public List<string> SelectedTexts
19181970
set => selectedTextsParam = ( value == null ? null : new( value ) );
19191971
}
19201972

1973+
/// <summary>
1974+
/// Gets or sets an expression that identifies the selected texts.
1975+
/// Used when multiple selection is set.
1976+
/// </summary>
1977+
[Parameter] public Expression<Func<List<string>>> SelectedTextsExpression { get; set; }
1978+
19211979
/// <summary>
19221980
/// Occurs after the selected texts have changed.
19231981
/// Used when multiple selection is set.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Autocomplete TItem="string"
2+
TValue="string"
3+
Data="@items"
4+
MinSearchLength="0"
5+
TextField="@(( item ) => item)"
6+
ValueField="@(( item ) => item)" />
7+
8+
<Button ElementId="nextButton" Outline>Next</Button>
9+
10+
@code {
11+
private readonly List<string> items = new()
12+
{
13+
"Alpha",
14+
"Beta",
15+
"Gamma"
16+
};
17+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Blazorise.E2E.Tests.Tests.Extensions.Autocomplete;
2+
3+
[Parallelizable( ParallelScope.Self )]
4+
[TestFixture]
5+
public class Autocomplete_5452_Tests : BlazorisePageTest
6+
{
7+
[SetUp]
8+
public async Task Init()
9+
{
10+
await SelectTestComponent<Autocomplete_5452Component>();
11+
}
12+
13+
[Test]
14+
public async Task TabbingOutWithOpenListFocusesNextElement()
15+
{
16+
var autocomplete = Page.Locator( ".b-is-autocomplete input[type=search]" );
17+
var dropdownMenu = Page.Locator( ".dropdown-menu.show" );
18+
var nextButton = Page.Locator( "#nextButton" );
19+
20+
await autocomplete.ClickAsync();
21+
await dropdownMenu.WaitForAsync();
22+
23+
await autocomplete.PressAsync( "Tab" );
24+
25+
await Expect( nextButton ).ToBeFocusedAsync();
26+
}
27+
}

0 commit comments

Comments
 (0)