Autofocus password field on web app unlock screen (#1269)

This commit is contained in:
Leendert de Borst
2025-09-24 22:05:33 +02:00
committed by Leendert de Borst
parent 938e8869f2
commit 9a367acbdc
3 changed files with 66 additions and 11 deletions

View File

@@ -1,13 +1,16 @@
@using System.Linq.Expressions
@inject IJSRuntime JSRuntime
<div class="relative">
<InputText @attributes="AdditionalAttributes"
@ref="inputComponent"
id="@Id"
Value="@Value"
ValueChanged="ValueChanged"
ValueExpression="ValueExpression"
type="@(_showPassword ? "text" : "password")"
placeholder="@Placeholder"
autocomplete="off"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
<button type="button"
@@ -32,6 +35,7 @@
@code {
private bool _showPassword = false;
private InputText? inputComponent;
/// <summary>
/// Gets or sets the ID of the input field.
@@ -76,4 +80,29 @@
{
_showPassword = !_showPassword;
}
/// <summary>
/// Focuses the input field.
/// </summary>
/// <returns>A task that represents the asynchronous focus operation.</returns>
public async Task FocusAsync()
{
try
{
if (inputComponent?.Element != null)
{
await inputComponent.Element.Value.FocusAsync();
}
else
{
// Fallback to JS focus if component element is not available
await JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('{Id}')?.focus()");
}
}
catch (Exception)
{
// Final fallback to JS focus if ElementReference focus fails
await JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('{Id}')?.focus()");
}
}
}

View File

@@ -2,6 +2,7 @@
@page "/unlock/{SkipWebAuthn:bool}"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@inject ILogger<Unlock> Logger
@inject IJSRuntime JSRuntime
@layout Auth.Layout.MainLayout
@using System.Text.Json
@using AliasVault.Client.Auth.Components
@@ -49,7 +50,7 @@ else
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg>
@Localizer["UnlockWithWebAuthn"]
</button>
<button type="button" @onclick="ShowPasswordLogin" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-gray-900 rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
<button type="button" @onclick="async () => await ShowPasswordLogin()" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-gray-900 rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
@Localizer["UnlockWithPassword"]
</button>
</div>
@@ -67,7 +68,7 @@ else
<DataAnnotationsValidator/>
<div>
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["YourPasswordLabel"]</label>
<PasswordInputField id="password" @bind-Value="_unlockModel.Password" placeholder="••••••••"/>
<PasswordInputField @ref="passwordField" id="password" @bind-Value="_unlockModel.Password" placeholder="••••••••"/>
<ValidationMessage For="() => _unlockModel.Password"/>
</div>
@@ -96,9 +97,11 @@ else
private bool IsLoading { get; set; } = true;
private bool IsWebAuthnLoading { get; set; }
private bool ShowWebAuthnButton { get; set; }
private bool IsPasswordFocused { get; set; }
private readonly UnlockModel _unlockModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();
private PasswordInputField? passwordField;
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Auth.Unlock", "AliasVault.Client");
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
@@ -123,6 +126,12 @@ else
IsLoading = false;
StateHasChanged();
}
if (!IsLoading && !IsWebAuthnLoading && !ShowWebAuthnButton && !IsPasswordFocused)
{
IsPasswordFocused = true;
await FocusPasswordField();
}
}
/// <summary>
@@ -314,9 +323,34 @@ else
/// <summary>
/// Show the password login form.
/// </summary>
private void ShowPasswordLogin()
private async Task ShowPasswordLogin()
{
ShowWebAuthnButton = false;
StateHasChanged();
// Focus the password field after the form is rendered
if (!IsPasswordFocused)
{
IsPasswordFocused = true;
await FocusPasswordField();
}
}
/// <summary>
/// Focus the password input field using component reference.
/// </summary>
private async Task FocusPasswordField()
{
try
{
if (passwordField != null)
{
await passwordField.FocusAsync();
}
}
catch
{
// Do nothing
}
}
}

View File

@@ -3472,10 +3472,6 @@ video {
}
@media (min-width: 1024px) {
.lg\:fixed {
position: fixed;
}
.lg\:relative {
position: relative;
}
@@ -3536,10 +3532,6 @@ video {
display: none;
}
.lg\:min-h-screen {
min-height: 100vh;
}
.lg\:w-1\/2 {
width: 50%;
}