From a5840501b41a90ff453b7f8fb28e872a659e2813 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:47:36 +0200 Subject: [PATCH] fix(forms): use Alpine state for password visibility toggles Replace shared `changePasswordFieldType` JS with component-local Alpine logic across input, textarea, and env-var-input components. This keeps toggle behavior consistent, resets visibility on `success` events, and preserves `truncate` styling only when showing plaintext on enabled fields. Also adds `PasswordVisibilityComponentTest` to verify Alpine bindings are rendered and legacy handler references are removed. --- .../components/forms/env-var-input.blade.php | 26 ++++++++---- .../views/components/forms/input.blade.php | 13 +++--- .../views/components/forms/textarea.blade.php | 18 +++++--- resources/views/layouts/base.blade.php | 26 +----------- .../PasswordVisibilityComponentTest.php | 41 +++++++++++++++++++ 5 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 tests/Feature/PasswordVisibilityComponentTest.php diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php index 2466a57f9..d26e248c1 100644 --- a/resources/views/components/forms/env-var-input.blade.php +++ b/resources/views/components/forms/env-var-input.blade.php @@ -10,7 +10,7 @@ @endif -
@if ($type === 'password' && $allowToPeak) -
- + -
+ + + + + + + @endif merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) @@ -210,12 +220,10 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif wire:loading.attr="disabled" - @if ($type === 'password') - :type="type" - @else + @disabled($disabled) + @if ($type !== 'password') type="{{ $type }}" @endif - @disabled($disabled) @if ($htmlId !== 'null') id="{{ $htmlId }}" @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index cf72dfbe9..456aa1da8 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -13,10 +13,11 @@ @endif @if ($type === 'password') -
+
@if ($allowToPeak) -
+
+ @endif merge(['class' => $defaultClass]) }} @required($required) @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif wire:loading.attr="disabled" - type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" + @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index 3f8fdb112..22c89fd72 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -30,18 +30,26 @@ function handleKeydown(e) { readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" /> @else @if ($type === 'password') -
+
@if ($allowToPeak) -
- + -
+ + + + + + + @endif merge(['class' => $defaultClassInput]) }} @required($required) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 2b4ca6054..33968ee32 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -203,30 +203,6 @@ function checkTheme() { let checkHealthInterval = null; let checkIfIamDeadInterval = null; - function changePasswordFieldType(event) { - let element = event.target - for (let i = 0; i < 10; i++) { - if (element.className === "relative") { - break; - } - element = element.parentElement; - } - element = element.children[1]; - if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - if (element.type === 'password') { - element.type = 'text'; - if (element.disabled) return; - element.classList.add('truncate'); - this.type = 'text'; - } else { - element.type = 'password'; - if (element.disabled) return; - element.classList.remove('truncate'); - this.type = 'password'; - } - } - } - function copyToClipboard(text) { navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.'); } @@ -326,4 +302,4 @@ function copyToClipboard(text) { @show - \ No newline at end of file + diff --git a/tests/Feature/PasswordVisibilityComponentTest.php b/tests/Feature/PasswordVisibilityComponentTest.php new file mode 100644 index 000000000..efc0e27cf --- /dev/null +++ b/tests/Feature/PasswordVisibilityComponentTest.php @@ -0,0 +1,41 @@ +put('default', new MessageBag); + view()->share('errors', $errors); +}); + +it('renders password input with Alpine-managed visibility state', function () { + $html = Blade::render(''); + + expect($html) + ->toContain('@success.window="type = \'password\'"') + ->toContain("x-data=\"{ type: 'password' }\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->toContain('x-bind:type="type"') + ->toContain("x-bind:class=\"{ 'truncate': type === 'text' && ! \$el.disabled }\"") + ->not->toContain('changePasswordFieldType'); +}); + +it('renders password textarea with Alpine-managed visibility state', function () { + $html = Blade::render(''); + + expect($html) + ->toContain('@success.window="type = \'password\'"') + ->toContain("x-data=\"{ type: 'password' }\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->not->toContain('changePasswordFieldType'); +}); + +it('resets password visibility on success event for env-var-input', function () { + $html = Blade::render(''); + + expect($html) + ->toContain("@success.window=\"type = 'password'\"") + ->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"") + ->toContain('x-bind:type="type"'); +});