Skip to content

Commit 96458bc

Browse files
authored
Rename interaction inputs, improve input dialog UX (#10056)
1 parent 8db4ade commit 96458bc

File tree

9 files changed

+98
-31
lines changed

9 files changed

+98
-31
lines changed

playground/Stress/Stress.AppHost/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@
203203
var interactionService = commandContext.ServiceProvider.GetRequiredService<IInteractionService>();
204204
var dinnerInput = new InteractionInput
205205
{
206-
InputType = InputType.Select,
206+
InputType = InputType.Choice,
207207
Label = "Dinner",
208208
Placeholder = "Select dinner",
209209
Required = true,
@@ -231,10 +231,10 @@
231231
var inputs = new List<InteractionInput>
232232
{
233233
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true },
234-
new InteractionInput { InputType = InputType.Password, Label = "Password", Placeholder = "Enter password", Required = true },
234+
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true },
235235
dinnerInput,
236236
numberOfPeopleInput,
237-
new InteractionInput { InputType = InputType.Checkbox, Label = "Remember me", Placeholder = "What does this do?", Required = true },
237+
new InteractionInput { InputType = InputType.Boolean, Label = "Remember me", Placeholder = "What does this do?", Required = true },
238238
};
239239
var result = await interactionService.PromptInputsAsync(
240240
"Input request",

src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,41 @@
2727
<p>@((MarkupString)Content.Interaction.Message)</p>
2828
}
2929

30-
<EditForm EditContext="@_editContext">
30+
<EditForm EditContext="@_editContext" OnValidSubmit="@SubmitAsync">
3131
<FluentStack Orientation="Orientation.Vertical" VerticalGap="12">
3232
@foreach (var vm in _inputDialogInputViewModels)
3333
{
34+
@*
35+
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
36+
* Immediate value of true on text inputs ensures the value is set to the server token with every key press in textbox.
37+
*@
3438
var localItem = vm;
3539
<div class="interaction-input">
3640
@switch (vm.Input.InputType)
3741
{
3842
case InputType.Text:
39-
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" />
43+
<FluentTextField @ref="@_elementRefs[localItem]"
44+
@bind-Value="localItem.Value"
45+
Label="@localItem.Input.Label"
46+
Placeholder="@localItem.Input.Placeholder"
47+
Required="localItem.Input.Required"
48+
Immediate="true" />
4049
<ValidationMessage For="@(() => localItem.Value)" />
4150
break;
42-
case InputType.Password:
43-
<FluentTextField @bind-Value="localItem.Value" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" TextFieldType="TextFieldType.Password" />
51+
case InputType.SecretText:
52+
<FluentTextField @ref="@_elementRefs[localItem]"
53+
@bind-Value="localItem.Value"
54+
Label="@localItem.Input.Label"
55+
Placeholder="@localItem.Input.Placeholder"
56+
Required="localItem.Input.Required"
57+
TextFieldType="TextFieldType.Password"
58+
AutoComplete="one-time-code"
59+
Immediate="true" />
4460
<ValidationMessage For="@(() => localItem.Value)" />
4561
break;
46-
case InputType.Select:
62+
case InputType.Choice:
4763
<FluentSelect TOption="SelectViewModel<string>"
64+
@ref="@_elementRefs[localItem]"
4865
@bind-Value="localItem.Value"
4966
Label="@localItem.Input.Label"
5067
Placeholder="@localItem.Input.Placeholder"
@@ -56,11 +73,20 @@
5673
Position="SelectPosition.Below" />
5774
<ValidationMessage For="@(() => localItem.Value)" />
5875
break;
59-
case InputType.Checkbox:
60-
<FluentCheckbox @bind-Value="localItem.IsChecked" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" />
76+
case InputType.Boolean:
77+
<FluentCheckbox @ref="@_elementRefs[localItem]"
78+
@bind-Value="localItem.IsChecked"
79+
Label="@localItem.Input.Label"
80+
Placeholder="@localItem.Input.Placeholder" />
6181
break;
6282
case InputType.Number:
63-
<FluentNumberField @bind-Value="localItem.NumberValue" Label="@localItem.Input.Label" Placeholder="@localItem.Input.Placeholder" Required="localItem.Input.Required" />
83+
<FluentNumberField TValue="int?"
84+
@ref="@_elementRefs[localItem]"
85+
@bind-Value="localItem.NumberValue"
86+
Label="@localItem.Input.Label"
87+
Placeholder="@localItem.Input.Placeholder"
88+
Required="localItem.Input.Required"
89+
Immediate="true" />
6490
<ValidationMessage For="@(() => localItem.NumberValue)" />
6591
break;
6692
default:
@@ -70,11 +96,14 @@
7096
</div>
7197
}
7298
</FluentStack>
99+
100+
@* Hidden submit is so the form is submitted when the user presses enter. *@
101+
<button type="submit" style="display:none"></button>
73102
</EditForm>
74103
</FluentDialogBody>
75104

76105
<FluentDialogFooter>
77-
<FluentButton Appearance="Appearance.Accent" OnClick="@OkAsync">
106+
<FluentButton Appearance="Appearance.Accent" OnClick="@SubmitAsync">
78107
@Dialog.Instance.Parameters.PrimaryAction
79108
</FluentButton>
80109
@if (!string.IsNullOrEmpty(Dialog.Instance.Parameters.SecondaryAction))

src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public partial class InteractionsInputDialog
2121
private EditContext _editContext = default!;
2222
private ValidationMessageStore _validationMessages = default!;
2323
private List<InputViewModel> _inputDialogInputViewModels = default!;
24+
private Dictionary<InputViewModel, FluentComponentBase?> _elementRefs = default!;
2425

2526
protected override void OnInitialized()
2627
{
@@ -29,6 +30,8 @@ protected override void OnInitialized()
2930

3031
_editContext.OnValidationRequested += (s, e) => ValidateModel();
3132
_editContext.OnFieldChanged += (s, e) => ValidateField(e.FieldIdentifier);
33+
34+
_elementRefs = new();
3235
}
3336

3437
protected override void OnParametersSet()
@@ -38,6 +41,15 @@ protected override void OnParametersSet()
3841
_content = Content;
3942
_inputDialogInputViewModels = Content.Inputs.Select(input => new InputViewModel(input)).ToList();
4043

44+
// Initialize keys for @ref binding.
45+
// Do this in case Blazor tries to get the element from the dictionary.
46+
// If the input view model isn't in the dictionary then it will throw a KeyNotFoundException.
47+
_elementRefs.Clear();
48+
foreach (var inputVM in _inputDialogInputViewModels)
49+
{
50+
_elementRefs[inputVM] = null;
51+
}
52+
4153
AddValidationErrorsFromModel();
4254

4355
Content.OnInteractionUpdated = async () =>
@@ -49,6 +61,31 @@ protected override void OnParametersSet()
4961
}
5062
}
5163

64+
protected override Task OnAfterRenderAsync(bool firstRender)
65+
{
66+
if (firstRender)
67+
{
68+
// Focus the first input when the dialog loads.
69+
if (_inputDialogInputViewModels.Count > 0 && _elementRefs.TryGetValue(_inputDialogInputViewModels[0], out var firstInputElement))
70+
{
71+
if (firstInputElement is FluentInputBase<string> textInput)
72+
{
73+
textInput.FocusAsync();
74+
}
75+
else if (firstInputElement is FluentInputBase<bool> boolInput)
76+
{
77+
boolInput.FocusAsync();
78+
}
79+
else if (firstInputElement is FluentInputBase<int?> numberInput)
80+
{
81+
numberInput.FocusAsync();
82+
}
83+
}
84+
}
85+
86+
return Task.CompletedTask;
87+
}
88+
5289
private void AddValidationErrorsFromModel()
5390
{
5491
for (var i = 0; i < Content.Inputs.Count; i++)
@@ -101,7 +138,7 @@ private static FieldIdentifier GetFieldIdentifier(InputViewModel inputModel)
101138
{
102139
var fieldName = inputModel.Input.InputType switch
103140
{
104-
InputType.Checkbox => nameof(inputModel.IsChecked),
141+
InputType.Boolean => nameof(inputModel.IsChecked),
105142
InputType.Number => nameof(inputModel.NumberValue),
106143
_ => nameof(inputModel.Value)
107144
};
@@ -111,11 +148,11 @@ private static FieldIdentifier GetFieldIdentifier(InputViewModel inputModel)
111148
private static bool IsMissingRequiredValue(InputViewModel inputModel)
112149
{
113150
return inputModel.Input.Required &&
114-
inputModel.Input.InputType != InputType.Checkbox &&
151+
inputModel.Input.InputType != InputType.Boolean &&
115152
string.IsNullOrWhiteSpace(inputModel.Value);
116153
}
117154

118-
private async Task OkAsync()
155+
private async Task SubmitAsync()
119156
{
120157
// The workflow is:
121158
// 1. Validate the model that required fields are present.

src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,8 @@ public async Task<IDialogReference> ShowMessageBoxAsync(IDialogService dialogSer
459459
Width = parameters.Width,
460460
Height = parameters.Height,
461461
AriaLabel = (content.Title ?? ""),
462-
OnDialogResult = parameters.OnDialogResult
462+
OnDialogResult = parameters.OnDialogResult,
463+
PreventDismissOnOverlayClick = true
463464
};
464465
return await dialogService.ShowDialogAsync(typeof(MessageBox), content, dialogParameters);
465466
}

src/Aspire.Dashboard/Model/InputViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public InputViewModel(InteractionInput input)
1919
public void SetInput(InteractionInput input)
2020
{
2121
Input = input;
22-
if (input.InputType == InputType.Select && input.Options != null)
22+
if (input.InputType == InputType.Choice && input.Options != null)
2323
{
2424
var optionsVM = input.Options
2525
.Select(option => new SelectViewModel<string> { Id = option.Key, Name = option.Value, })

src/Aspire.Hosting/ApplicationModel/IInteractionService.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public sealed class InteractionInput
121121
public bool Required { get; init; }
122122

123123
/// <summary>
124-
/// Gets or sets the options for the input. Only used by <see cref="InputType.Select"/> inputs.
124+
/// Gets or sets the options for the input. Only used by <see cref="InputType.Choice"/> inputs.
125125
/// </summary>
126126
public IReadOnlyList<KeyValuePair<string, string>>? Options { get; init; }
127127

@@ -151,17 +151,17 @@ public enum InputType
151151
/// </summary>
152152
Text,
153153
/// <summary>
154-
/// A password input.
154+
/// A secure text input.
155155
/// </summary>
156-
Password,
156+
SecretText,
157157
/// <summary>
158-
/// A select input.
158+
/// A choice input. Selects from a list of options.
159159
/// </summary>
160-
Select,
160+
Choice,
161161
/// <summary>
162-
/// A checkbox input.
162+
/// A boolean input.
163163
/// </summary>
164-
Checkbox,
164+
Boolean,
165165
/// <summary>
166166
/// A numeric input.
167167
/// </summary>

src/Aspire.Hosting/Dashboard/DashboardService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ private static InputType MapInputType(ApplicationModel.InputType inputType)
196196
return inputType switch
197197
{
198198
ApplicationModel.InputType.Text => InputType.Text,
199-
ApplicationModel.InputType.Password => InputType.Password,
200-
ApplicationModel.InputType.Select => InputType.Select,
201-
ApplicationModel.InputType.Checkbox => InputType.Checkbox,
199+
ApplicationModel.InputType.SecretText => InputType.SecretText,
200+
ApplicationModel.InputType.Choice => InputType.Choice,
201+
ApplicationModel.InputType.Boolean => InputType.Boolean,
202202
ApplicationModel.InputType.Number => InputType.Number,
203203
_ => throw new InvalidOperationException($"Unexpected input type: {inputType}"),
204204
};

src/Aspire.Hosting/Dashboard/DashboardServiceData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ await _interactionService.CompleteInteractionAsync(
174174
var incomingValue = requestInput.Value;
175175

176176
// Ensure checkbox value is either true or false.
177-
if (requestInput.InputType == Aspire.DashboardService.Proto.V1.InputType.Checkbox)
177+
if (requestInput.InputType == Aspire.DashboardService.Proto.V1.InputType.Boolean)
178178
{
179179
incomingValue = (bool.TryParse(incomingValue, out var b) && b) ? "true" : "false";
180180
}

src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,9 @@ enum MessageIntent {
385385
enum InputType {
386386
INPUT_TYPE_UNSPECIFIED = 0;
387387
INPUT_TYPE_TEXT = 1;
388-
INPUT_TYPE_PASSWORD = 2;
389-
INPUT_TYPE_SELECT = 3;
390-
INPUT_TYPE_CHECKBOX = 4;
388+
INPUT_TYPE_SECRET_TEXT = 2;
389+
INPUT_TYPE_CHOICE = 3;
390+
INPUT_TYPE_BOOLEAN = 4;
391391
INPUT_TYPE_NUMBER = 5;
392392
}
393393

0 commit comments

Comments
 (0)