mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
feat(forms): add copy button to password inputs by default
This commit is contained in:
1 parent
ab531972d2
commit
59928035b7
9 files changed
+41
-14
No files matched your search
@@ -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') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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('input').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');
|
||||
});
|
||||
Reference in new issue
Block a user