Add password visibility toggles (#773)

This commit is contained in:
Leendert de Borst
2026-03-16 21:44:11 +01:00
parent 115325d9f5
commit 4cc5728c27
7 changed files with 142 additions and 107 deletions

View File

@@ -2,16 +2,15 @@
@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" />
<input @attributes="AdditionalAttributes"
@ref="inputElement"
id="@Id"
value="@Value"
@oninput="OnInputChanged"
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"
@onclick="TogglePasswordVisibility"
@@ -35,7 +34,7 @@
@code {
private bool _showPassword = false;
private InputText? inputComponent;
private ElementReference inputElement;
/// <summary>
/// Gets or sets the ID of the input field.
@@ -59,7 +58,7 @@
/// Gets or sets the expression that identifies the value property.
/// </summary>
[Parameter]
public required Expression<Func<string>> ValueExpression { get; set; }
public Expression<Func<string>>? ValueExpression { get; set; }
/// <summary>
/// Gets or sets the placeholder text for the input field.
@@ -73,6 +72,15 @@
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
/// <summary>
/// Handles input changes and triggers the ValueChanged callback immediately.
/// </summary>
private async Task OnInputChanged(ChangeEventArgs e)
{
var newValue = e.Value?.ToString();
await ValueChanged.InvokeAsync(newValue);
}
/// <summary>
/// Toggles the password visibility.
/// </summary>
@@ -89,19 +97,11 @@
{
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()");
}
await inputElement.FocusAsync();
}
catch (Exception)
{
// Final fallback to JS focus if ElementReference focus fails
// Fallback to JS focus if ElementReference focus fails
await JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('{Id}')?.focus()");
}
}

View File

@@ -2,7 +2,9 @@
@using System.Timers
@using Microsoft.Extensions.Localization
@using AliasVault.Client.Main.Components.Shared
@using AliasVault.Client.Main.Components.Loading
@using AliasVault.Client.Main.Constants
@using AliasVault.Client.Auth.Components
<div class="w-full mx-auto">
@if (_isLoading)
@@ -39,13 +41,15 @@
<div class="space-y-4">
<div>
<div class="">
<EditFormRow Id="password" Label="@Localizer["MasterPasswordLabel"]" @bind-Value="Password" Type="password" Placeholder="@Localizer["MasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus"/>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["MasterPasswordLabel"]</label>
<PasswordInputField Id="password" @bind-Value="Password" Placeholder="@Localizer["MasterPasswordPlaceholder"]" @onfocus="@OnPasswordInputFocus" />
</div>
<PasswordStrengthIndicator Password="@Password" />
<div class="mt-4">
<EditFormRow Id="confirmPassword" Label="@Localizer["ConfirmMasterPasswordLabel"]" @bind-Value="ConfirmPassword" Type="password" Placeholder="@Localizer["ConfirmMasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus" />
<label for="confirmPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmMasterPasswordLabel"]</label>
<PasswordInputField Id="confirmPassword" @bind-Value="ConfirmPassword" Placeholder="@Localizer["ConfirmMasterPasswordPlaceholder"]" @onfocus="@OnPasswordInputFocus" />
</div>
@if (!string.IsNullOrEmpty(_errorMessage))
{
@@ -73,7 +77,7 @@
if (_password != value)
{
_password = value;
ValidatePassword();
ValidatePasswordWithDebounce();
}
}
}
@@ -86,7 +90,7 @@
if (_confirmPassword != value)
{
_confirmPassword = value;
ValidatePassword();
ValidatePasswordImmediate();
}
}
}
@@ -115,7 +119,7 @@
_loadingTimer.AutoReset = false;
_loadingTimer.Start();
_debounceTimer = new Timer(300);
_debounceTimer = new Timer(800);
_debounceTimer.Elapsed += async (sender, e) => await ValidatePasswordDebounced();
_debounceTimer.AutoReset = false;
}
@@ -141,9 +145,9 @@
}
/// <summary>
/// Validates the password immediately.
/// Validates the password with debounce (for main password field).
/// </summary>
private void ValidatePassword()
private void ValidatePasswordWithDebounce()
{
_errorMessage = string.Empty;
StateHasChanged();
@@ -152,6 +156,14 @@
_debounceTimer?.Start();
}
/// <summary>
/// Validates the password immediately (for confirm password field).
/// </summary>
private async void ValidatePasswordImmediate()
{
await ValidatePasswordDebounced();
}
/// <summary>
/// Validates the password after input has stopped.
/// </summary>

View File

@@ -142,7 +142,7 @@
/// Gets or sets the CSS class for the confirm button.
/// </summary>
[Parameter]
public string ConfirmButtonClass { get; set; } = "bg-orange-600 hover:bg-orange-500 dark:bg-orange-700 dark:hover:bg-orange-600";
public string ConfirmButtonClass { get; set; } = "bg-primary-600 hover:bg-primary-700";
/// <summary>
/// Gets or sets whether the modal is in a loading state.

View File

@@ -1,4 +1,5 @@
@using Microsoft.Extensions.Localization
@using AliasVault.Client.Auth.Components
<FormModal
IsOpen="@IsOpen"
@@ -30,13 +31,10 @@
<label for="password-confirm" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
@SharedLocalizer["Password"]
</label>
<input
type="password"
id="password-confirm"
@bind="_password"
@bind:event="oninput"
placeholder="@Localizer["EnterPasswordPlaceholder"]"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 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" />
<PasswordInputField
Id="password-confirm"
@bind-Value="_password"
Placeholder="@Localizer["EnterPasswordPlaceholder"]" />
</div>
</ChildContent>
</FormModal>

View File

@@ -1,73 +1,78 @@
@using Microsoft.Extensions.Localization
@using AliasVault.RazorComponents.Services
@using AliasVault.Client.Main.Components.Shared
@using AliasVault.Client.Main.Components.Layout
@using AliasVault.Client.Auth.Components
@inject IStringLocalizerFactory LocalizerFactory
@if (IsOpen)
{
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 w-full max-w-md">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">@Title</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">@Description</p>
<FormModal
IsOpen="@IsOpen"
Title="@Title"
MaxWidth="md"
ShowDefaultFooter="false"
OnClose="@OnClose"
SubmitOnEnter="false">
<Icon>
<svg class="h-6 w-6 text-orange-600 dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
</Icon>
<ChildContent>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
@Description
</p>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 rounded-lg text-sm">
@ErrorMessage
</div>
}
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<AlertMessageError Message="@ErrorMessage" HasTopMargin="false" />
}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@Localizer["ExportPasswordLabel"]
</label>
<input
type="password"
@bind="_exportPassword"
@bind:event="oninput"
@onkeydown="HandleKeyDown"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
autofocus />
<div class="mb-4">
<label for="exportPassword" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
@Localizer["ExportPasswordLabel"]
</label>
<PasswordInputField
Id="exportPassword"
@bind-Value="_exportPassword"
Placeholder=""
@onkeydown="HandleKeyDown"
autofocus="true" />
<PasswordStrengthIndicator Password="@_exportPassword" OnStrengthChanged="@HandleStrengthChanged" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@Localizer["ConfirmExportPasswordLabel"]
</label>
<input
type="password"
@bind="_confirmPassword"
@bind:event="oninput"
@onkeydown="HandleKeyDown"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
@if (!string.IsNullOrEmpty(_confirmPassword) && _exportPassword != _confirmPassword)
{
<p class="mt-1 text-xs text-red-600 dark:text-red-400">@Localizer["PasswordsDoNotMatch"]</p>
}
</div>
<div class="flex justify-end space-x-3">
<button
type="button"
@onclick="OnClose"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg">
@SharedLocalizer["Cancel"]
</button>
<button
type="button"
@onclick="HandleSubmit"
disabled="@(!IsPasswordValid())"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-lg">
@Localizer["CreateEncryptedExportButton"]
</button>
</div>
<PasswordStrengthIndicator Password="@_exportPassword" OnStrengthChanged="@HandleStrengthChanged" />
</div>
</div>
}
<div class="mb-4">
<label for="confirmExportPassword" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
@Localizer["ConfirmExportPasswordLabel"]
</label>
<PasswordInputField
Id="confirmExportPassword"
@bind-Value="_confirmPassword"
Placeholder=""
@onkeydown="HandleKeyDown" />
@if (!string.IsNullOrEmpty(_confirmPassword) && _exportPassword != _confirmPassword)
{
<p class="mt-1 text-xs text-red-600 dark:text-red-400">@Localizer["PasswordsDoNotMatch"]</p>
}
</div>
</ChildContent>
<FooterContent>
<button
type="button"
@onclick="HandleSubmit"
disabled="@(!IsPasswordValid())"
class="inline-flex w-full justify-center rounded-md bg-primary-600 hover:bg-primary-700 px-3 py-2 text-sm font-semibold text-white shadow-sm sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed">
@Localizer["CreateEncryptedExportButton"]
</button>
<button
type="button"
@onclick="OnClose"
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-gray-700 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-600">
@SharedLocalizer["Cancel"]
</button>
</FooterContent>
</FormModal>
@code {
[Parameter]

View File

@@ -15,6 +15,7 @@
@using AliasVault.Shared.Models.WebApi.Favicon
@using AliasClientDb
@using AliasClientDb.Models
@using AliasVault.Client.Auth.Components
<div @onclick="OpenImportModal" data-import-service="@ServiceName" class="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
<div class="flex items-center">
@@ -105,16 +106,15 @@
{
<div class="mb-4">
<p class="mb-4 text-gray-700 dark:text-gray-300">@Localizer["EncryptedFilePasswordPrompt"]</p>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label for="decryptionPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@Localizer["DecryptionPasswordLabel"]
</label>
<input
type="password"
@bind="_decryptionPassword"
@bind:event="oninput"
<PasswordInputField
Id="decryptionPassword"
@bind-Value="_decryptionPassword"
Placeholder=""
@onkeydown="HandlePasswordKeyDown"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
autofocus />
autofocus="true" />
</div>
<div class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
@@ -846,7 +846,7 @@
else
{
// Create new logo from embedded bytes
var logo = new Logo
var logo = new AliasClientDb.Logo
{
Id = Guid.NewGuid(),
Source = domain,
@@ -896,7 +896,7 @@
else if (ExtractedFavicons.TryGetValue(domain, out var favicon))
{
// Create new logo from extracted favicon
var logo = new Logo
var logo = new AliasClientDb.Logo
{
Id = Guid.NewGuid(),
Source = domain,

View File

@@ -2029,6 +2029,11 @@ video {
background-color: rgb(251 146 60 / var(--tw-bg-opacity));
}
.bg-orange-700 {
--tw-bg-opacity: 1;
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@@ -2932,6 +2937,16 @@ video {
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:bg-orange-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
}
.hover\:bg-orange-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(154 52 18 / var(--tw-bg-opacity));
}
.hover\:from-primary-600:hover {
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
@@ -3842,6 +3857,11 @@ video {
background-color: rgb(127 29 29 / 0.5);
}
.dark\:hover\:bg-orange-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
}
.dark\:hover\:from-primary-500:hover:is(.dark *) {
--tw-gradient-from: #f49541 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);