feat(forms): add copy button to password inputs by default

This commit is contained in:
Andras Bacsai committed 2026-10-22 13:43:44 +02:00
1 parent ab531972d2
commit 59928035b7
9 files changed
+41 -14

No files matched your search

+4 -1
View File
@@ -25,7 +25,7 @@ class Input extends Component
public bool $readonly = false,
public ?string $helper = null,
public bool $allowToPeak = true,
public bool $copyable = false,
public ?bool $copyable = null,
public bool $isMultiline = false,
public string $defaultClass = 'input',
public string $autocomplete = 'off',
@@ -71,6 +71,9 @@ class Input extends Component
if (is_null($this->name)) {
$this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id;
}
// Password fields get a copy button unless the caller opts out (account password forms).
$this->copyable ??= $this->type === 'password';
// Durable class (not type-attr based): Alpine may toggle type to "text" when revealing,
// and settings-workspace CSS otherwise overrides utility padding-right.
$hasPeek = $this->type === 'password' && $this->allowToPeak;
@@ -22,7 +22,7 @@
<form action="/user/confirm-password" method="POST" class="flex flex-col gap-4">
@csrf
<x-forms.input required type="password" name="password" autocomplete="current-password" autofocus
<x-forms.input required type="password" :copyable="false" name="password" autocomplete="current-password" autofocus
label="{{ __('input.password') }}" />
<x-forms.button class="w-full justify-center" type="submit" isHighlighted>
{{ __('auth.confirm_password') }}
+2 -2
View File
@@ -25,13 +25,13 @@
<x-forms.input value="test@example.com" type="email"
autocomplete="{{ $passkeys_available ? 'email webauthn' : 'email' }}" name="email" required
autofocus label="{{ __('input.email') }}" />
<x-forms.input value="password" type="password" autocomplete="current-password" name="password"
<x-forms.input value="password" type="password" :copyable="false" autocomplete="current-password" name="password"
required label="{{ __('input.password') }}" />
@else
<x-forms.input type="email" name="email"
autocomplete="{{ $passkeys_available ? 'email webauthn' : 'email' }}" required autofocus
label="{{ __('input.email') }}" />
<x-forms.input type="password" name="password" autocomplete="current-password" required
<x-forms.input type="password" :copyable="false" name="password" autocomplete="current-password" required
label="{{ __('input.password') }}" />
@endenv
+2 -2
View File
@@ -37,9 +37,9 @@ $email = getOldOrLocal('email', 'test3@example.com');
autocomplete="name" label="{{ __('input.name') }}" />
<x-forms.input id="email" required type="email" name="email" value="{{ $email }}"
autocomplete="email" label="{{ __('input.email') }}" />
<x-forms.input id="password" required type="password" name="password" autocomplete="new-password"
<x-forms.input id="password" required type="password" :copyable="false" name="password" autocomplete="new-password"
label="{{ __('input.password') }}" />
<x-forms.input id="password_confirmation" required type="password" name="password_confirmation"
<x-forms.input id="password_confirmation" required type="password" :copyable="false" name="password_confirmation"
autocomplete="new-password" label="{{ __('input.password.again') }}" />
<div class="auth-guidance">
@@ -20,9 +20,9 @@
@csrf
<input hidden id="token" name="token" value="{{ request()->route('token') }}">
<input hidden value="{{ request()->query('email') }}" type="email" name="email" />
<x-forms.input required type="password" id="password" name="password" autocomplete="new-password"
<x-forms.input required type="password" :copyable="false" id="password" name="password" autocomplete="new-password"
autofocus label="{{ __('input.password') }}" />
<x-forms.input required type="password" id="password_confirmation" name="password_confirmation"
<x-forms.input required type="password" :copyable="false" id="password_confirmation" name="password_confirmation"
autocomplete="new-password" label="{{ __('input.password.again') }}" />
<div class="auth-guidance">
@@ -5,7 +5,10 @@
// single JS expression here — a directive inside the <x-copy-button> tag would
// break Blade's component-tag compiler.
$copyResolve = null;
if ($copyable) {
if ($copyable && $type === 'password') {
// Copy the field as shown: it includes unsaved edits and works without Livewire.
$copyResolve = "\$el.parentElement.querySelector('input').value";
} elseif ($copyable) {
$copyModel = $modelBinding !== 'null' ? $modelBinding : $attributes->get('wire:model');
$copyResolve = $copyModel
? "\$wire.get('".$copyModel."')"
@@ -2,9 +2,9 @@
description="Your password must be updated before you can continue to the dashboard.">
<form class="flex flex-col gap-4" wire:submit="submit">
<x-forms.input id="email" type="email" readonly label="Email" />
<x-forms.input id="password" type="password" autocomplete="new-password" autofocus label="New password"
<x-forms.input id="password" type="password" :copyable="false" autocomplete="new-password" autofocus label="New password"
required />
<x-forms.input id="password_confirmation" type="password" autocomplete="new-password"
<x-forms.input id="password_confirmation" type="password" :copyable="false" autocomplete="new-password"
label="Confirm new password" required />
<div class="auth-guidance">
@@ -208,10 +208,10 @@
</div>
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
<x-forms.input class="sm:col-span-2" id="current_password" label="Current password"
required type="password" />
<x-forms.input id="new_password" label="New password" required type="password" />
required type="password" :copyable="false" />
<x-forms.input id="new_password" label="New password" required type="password" :copyable="false" />
<x-forms.input id="new_password_confirmation" label="Confirm new password" required
type="password" />
type="password" :copyable="false" />
</div>
</section>
</form>
@@ -74,3 +74,24 @@ it('renders env var password input before visibility toggle in tab order', funct
expect(strpos($html, '<input'))->toBeLessThan(strpos($html, 'aria-label="Toggle password visibility"'));
});
it('adds a copy button that reads the live field value to password inputs by default', function () {
$html = Blade::render('<x-forms.input type="password" name="secret" value="s3cret" />');
expect($html)
->toContain('Copy to clipboard')
->toContain('input-with-copy-and-peek')
->toContain('$el.parentElement.querySelector(&#039;input&#039;).value');
});
it('omits the copy button when a password input opts out', function () {
$html = Blade::render('<x-forms.input type="password" name="password" :copyable="false" />');
expect($html)
->toContain('Toggle password visibility')
->not->toContain('Copy to clipboard');
});
it('keeps text inputs without a copy button unless they opt in', function () {
expect(Blade::render('<x-forms.input name="plain" value="x" />'))->not->toContain('Copy to clipboard');
});