Skip to content

Commit a584050

Browse files
committed
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.
1 parent 9e96a20 commit a584050

5 files changed

Lines changed: 80 additions & 44 deletions

File tree

resources/views/components/forms/env-var-input.blade.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</label>
1111
@endif
1212

13-
<div class="relative" x-data="{
13+
<div class="relative" @success.window="type = '{{ $type }}'" x-data="{
1414
type: '{{ $type }}',
1515
showDropdown: false,
1616
suggestions: [],
@@ -185,15 +185,23 @@
185185
@click.outside="showDropdown = false">
186186

187187
@if ($type === 'password' && $allowToPeak)
188-
<div x-on:click="changePasswordFieldType"
189-
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white z-10">
190-
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
188+
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
189+
class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dark:hover:text-white"
190+
aria-label="Toggle password visibility">
191+
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
191192
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
192193
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
193194
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
194195
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
195196
</svg>
196-
</div>
197+
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
198+
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
199+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
200+
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
201+
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
202+
<path d="M3 3l18 18" />
203+
</svg>
204+
</button>
197205
@endif
198206

199207
<input
@@ -202,6 +210,8 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
202210
@keydown="handleKeydown($event)"
203211
@click="handleInput()"
204212
autocomplete="{{ $autocomplete }}"
213+
x-bind:type="type"
214+
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
205215
{{ $attributes->merge(['class' => $defaultClass]) }}
206216
@required($required)
207217
@readonly($readonly)
@@ -210,12 +220,10 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
210220
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
211221
@endif
212222
wire:loading.attr="disabled"
213-
@if ($type === 'password')
214-
:type="type"
215-
@else
223+
@disabled($disabled)
224+
@if ($type !== 'password')
216225
type="{{ $type }}"
217226
@endif
218-
@disabled($disabled)
219227
@if ($htmlId !== 'null') id="{{ $htmlId }}" @endif
220228
name="{{ $name }}"
221229
placeholder="{{ $attributes->get('placeholder') }}"

resources/views/components/forms/input.blade.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
</label>
1414
@endif
1515
@if ($type === 'password')
16-
<div class="relative" x-data="{ type: 'password' }">
16+
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
1717
@if ($allowToPeak)
18-
<div x-on:click="changePasswordFieldType; type = type === 'password' ? 'text' : 'password'"
19-
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white">
18+
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
19+
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white"
20+
aria-label="Toggle password visibility">
2021
{{-- Eye icon (shown when password is hidden) --}}
2122
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
2223
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@@ -32,13 +33,15 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
3233
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
3334
<path d="M3 3l18 18" />
3435
</svg>
35-
</div>
36+
</button>
3637
@endif
3738
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
39+
x-bind:type="type"
40+
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
3841
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
3942
@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
4043
wire:loading.attr="disabled"
41-
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
44+
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
4245
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
4346
aria-placeholder="{{ $attributes->get('placeholder') }}"
4447
@if ($autofocus) x-ref="autofocusInput" @endif>

resources/views/components/forms/textarea.blade.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,26 @@ function handleKeydown(e) {
3030
readonly="{{ $readonly }}" label="dockerfile" autofocus="{{ $autofocus }}" />
3131
@else
3232
@if ($type === 'password')
33-
<div class="relative" x-data="{ type: 'password' }">
33+
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
3434
@if ($allowToPeak)
35-
<div x-on:click="changePasswordFieldType"
36-
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white">
37-
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
35+
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
36+
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
37+
aria-label="Toggle password visibility">
38+
<svg x-show="type === 'password'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
3839
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
3940
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
4041
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
4142
<path
4243
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
4344
</svg>
44-
</div>
45+
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
46+
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
47+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
48+
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
49+
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
50+
<path d="M3 3l18 18" />
51+
</svg>
52+
</button>
4553
@endif
4654
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
4755
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)

resources/views/layouts/base.blade.php

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -203,30 +203,6 @@ function checkTheme() {
203203
let checkHealthInterval = null;
204204
let checkIfIamDeadInterval = null;
205205
206-
function changePasswordFieldType(event) {
207-
let element = event.target
208-
for (let i = 0; i < 10; i++) {
209-
if (element.className === "relative") {
210-
break;
211-
}
212-
element = element.parentElement;
213-
}
214-
element = element.children[1];
215-
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
216-
if (element.type === 'password') {
217-
element.type = 'text';
218-
if (element.disabled) return;
219-
element.classList.add('truncate');
220-
this.type = 'text';
221-
} else {
222-
element.type = 'password';
223-
if (element.disabled) return;
224-
element.classList.remove('truncate');
225-
this.type = 'password';
226-
}
227-
}
228-
}
229-
230206
function copyToClipboard(text) {
231207
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
232208
}
@@ -326,4 +302,4 @@ function copyToClipboard(text) {
326302
</body>
327303
@show
328304
329-
</html>
305+
</html>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
use Illuminate\Support\MessageBag;
4+
use Illuminate\Support\ViewErrorBag;
5+
6+
beforeEach(function () {
7+
$errors = new ViewErrorBag;
8+
$errors->put('default', new MessageBag);
9+
view()->share('errors', $errors);
10+
});
11+
12+
it('renders password input with Alpine-managed visibility state', function () {
13+
$html = Blade::render('<x-forms.input type="password" id="secret" />');
14+
15+
expect($html)
16+
->toContain('@success.window="type = \'password\'"')
17+
->toContain("x-data=\"{ type: 'password' }\"")
18+
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
19+
->toContain('x-bind:type="type"')
20+
->toContain("x-bind:class=\"{ 'truncate': type === 'text' && ! \$el.disabled }\"")
21+
->not->toContain('changePasswordFieldType');
22+
});
23+
24+
it('renders password textarea with Alpine-managed visibility state', function () {
25+
$html = Blade::render('<x-forms.textarea type="password" id="secret" />');
26+
27+
expect($html)
28+
->toContain('@success.window="type = \'password\'"')
29+
->toContain("x-data=\"{ type: 'password' }\"")
30+
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
31+
->not->toContain('changePasswordFieldType');
32+
});
33+
34+
it('resets password visibility on success event for env-var-input', function () {
35+
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
36+
37+
expect($html)
38+
->toContain("@success.window=\"type = 'password'\"")
39+
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
40+
->toContain('x-bind:type="type"');
41+
});

0 commit comments

Comments
 (0)