fix(forms): focus password fields before visibility toggles (#10519)
This commit is contained in:
commit
50c07eb8e4
4 changed files with 63 additions and 45 deletions
|
|
@ -196,26 +196,6 @@
|
||||||
}"
|
}"
|
||||||
@click.outside="showDropdown = false">
|
@click.outside="showDropdown = false">
|
||||||
|
|
||||||
@if ($type === 'password' && $allowToPeak)
|
|
||||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
|
||||||
class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dark:hover:text-white"
|
|
||||||
aria-label="Toggle password visibility">
|
|
||||||
<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"
|
|
||||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
<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"
|
|
||||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
|
||||||
<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" />
|
|
||||||
<path d="M3 3l18 18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
x-ref="input"
|
x-ref="input"
|
||||||
@input="handleInput()"
|
@input="handleInput()"
|
||||||
|
|
@ -241,6 +221,26 @@ class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dar
|
||||||
placeholder="{{ $attributes->get('placeholder') }}"
|
placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
@if ($autofocus) autofocus @endif>
|
@if ($autofocus) autofocus @endif>
|
||||||
|
|
||||||
|
@if ($type === 'password' && $allowToPeak)
|
||||||
|
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||||
|
class="flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||||
|
aria-label="Toggle password visibility">
|
||||||
|
<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"
|
||||||
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<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"
|
||||||
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||||
|
<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" />
|
||||||
|
<path d="M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
{{-- Dropdown for suggestions --}}
|
{{-- Dropdown for suggestions --}}
|
||||||
<div x-show="showDropdown"
|
<div x-show="showDropdown"
|
||||||
x-transition
|
x-transition
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@
|
||||||
@endif
|
@endif
|
||||||
@if ($type === 'password')
|
@if ($type === 'password')
|
||||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||||
|
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||||
|
x-bind:type="type"
|
||||||
|
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||||
|
{{ $attributes->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"
|
||||||
|
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||||
|
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
|
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
|
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||||
@if ($allowToPeak)
|
@if ($allowToPeak)
|
||||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||||
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white"
|
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white"
|
||||||
|
|
@ -35,16 +45,6 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
|
||||||
x-bind:type="type"
|
|
||||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
|
||||||
{{ $attributes->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"
|
|
||||||
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
|
||||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
|
||||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@ function handleKeydown(e) {
|
||||||
@else
|
@else
|
||||||
@if ($type === 'password')
|
@if ($type === 'password')
|
||||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||||
|
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||||
|
{{ $attributes->merge(['class' => $defaultClassInput]) }} @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 }}"
|
||||||
|
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||||
|
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||||
|
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||||
|
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||||
|
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $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]"
|
||||||
|
@else
|
||||||
|
wire:model={{ $value ?? $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
|
||||||
|
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||||
|
name="{{ $name }}" name={{ $modelBinding }}
|
||||||
|
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||||
@if ($allowToPeak)
|
@if ($allowToPeak)
|
||||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
|
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
|
||||||
|
|
@ -51,21 +66,6 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
|
||||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @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 }}"
|
|
||||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
|
||||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
|
||||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
|
||||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
|
||||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $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]"
|
|
||||||
@else
|
|
||||||
wire:model={{ $value ?? $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
|
|
||||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
|
||||||
name="{{ $name }}" name={{ $modelBinding }}
|
|
||||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@
|
||||||
->not->toContain('changePasswordFieldType');
|
->not->toContain('changePasswordFieldType');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders password input before visibility toggle in tab order', function () {
|
||||||
|
$html = Blade::render('<x-forms.input type="password" id="secret" />');
|
||||||
|
|
||||||
|
expect(strpos($html, '<input'))->toBeLessThan(strpos($html, 'aria-label="Toggle password visibility"'));
|
||||||
|
});
|
||||||
|
|
||||||
it('renders password textarea with Alpine-managed visibility state', function () {
|
it('renders password textarea with Alpine-managed visibility state', function () {
|
||||||
$html = Blade::render('<x-forms.textarea type="password" id="secret" />');
|
$html = Blade::render('<x-forms.textarea type="password" id="secret" />');
|
||||||
|
|
||||||
|
|
@ -31,6 +37,12 @@
|
||||||
->not->toContain('changePasswordFieldType');
|
->not->toContain('changePasswordFieldType');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders password textarea input before visibility toggle in tab order', function () {
|
||||||
|
$html = Blade::render('<x-forms.textarea type="password" id="secret" />');
|
||||||
|
|
||||||
|
expect(strpos($html, '<input'))->toBeLessThan(strpos($html, 'aria-label="Toggle password visibility"'));
|
||||||
|
});
|
||||||
|
|
||||||
it('renders textarea without monospace classes by default', function () {
|
it('renders textarea without monospace classes by default', function () {
|
||||||
$html = Blade::render('<x-forms.textarea id="notes" />');
|
$html = Blade::render('<x-forms.textarea id="notes" />');
|
||||||
|
|
||||||
|
|
@ -53,3 +65,9 @@
|
||||||
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
|
->toContain("x-on:click=\"type = type === 'password' ? 'text' : 'password'\"")
|
||||||
->toContain('x-bind:type="type"');
|
->toContain('x-bind:type="type"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders env var password input before visibility toggle in tab order', function () {
|
||||||
|
$html = Blade::render('<x-forms.env-var-input type="password" id="secret" />');
|
||||||
|
|
||||||
|
expect(strpos($html, '<input'))->toBeLessThan(strpos($html, 'aria-label="Toggle password visibility"'));
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue