Skip to content

Commit 40c804b

Browse files
committed
Add password()/passwordWithToggle() DynamicFields
Support for password() and passwordWithToggle() DynamicFields styled and sized like text() with password inputs
1 parent 56f2c60 commit 40c804b

File tree

4 files changed

+107
-1
lines changed

4 files changed

+107
-1
lines changed

app/DTOs/DynamicField.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ public function text(): self
3636
return $this;
3737
}
3838

39+
public function password(): self
40+
{
41+
$this->type = 'password';
42+
43+
return $this;
44+
}
45+
46+
public function passwordWithToggle(): self
47+
{
48+
$this->type = 'password-with-toggle';
49+
50+
return $this;
51+
}
52+
3953
public function textarea(): self
4054
{
4155
$this->type = 'textarea';

resources/js/components/ui/dynamic-field.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InputHTMLAttributes, useEffect, useState } from 'react';
22
import { Label } from '@/components/ui/label';
33
import { Input } from '@/components/ui/input';
4+
import { PasswordInput } from '@/components/ui/password-input';
45
import { Switch } from '@/components/ui/switch';
56
import { Textarea } from '@/components/ui/textarea';
67
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -121,6 +122,51 @@ export default function DynamicField({ value, onChange, config, error }: Dynamic
121122
);
122123
}
123124

125+
// Handle password
126+
if (config?.type === 'password') {
127+
return (
128+
<FormField>
129+
<Label htmlFor={`field-${config.name}`} className="capitalize">
130+
{label}
131+
</Label>
132+
<Input
133+
type="password"
134+
name={config.name}
135+
id={`field-${config.name}`}
136+
defaultValue={(value as string) || ''}
137+
placeholder={config.placeholder}
138+
onChange={(e) => onChange(e.target.value)}
139+
autoComplete="off"
140+
spellCheck={false}
141+
/>
142+
{config.description && <p className="text-muted-foreground text-xs">{config.description}</p>}
143+
<InputError message={error} />
144+
</FormField>
145+
);
146+
}
147+
148+
// Handle password with visibility toggle
149+
if (config?.type === 'password-with-toggle') {
150+
return (
151+
<FormField>
152+
<Label htmlFor={`field-${config.name}`} className="capitalize">
153+
{label}
154+
</Label>
155+
<PasswordInput
156+
name={config.name}
157+
id={`field-${config.name}`}
158+
defaultValue={(value as string) || ''}
159+
placeholder={config.placeholder}
160+
onChange={(e) => onChange(e.target.value)}
161+
autoComplete="off"
162+
spellCheck={false}
163+
/>
164+
{config.description && <p className="text-muted-foreground text-xs">{config.description}</p>}
165+
<InputError message={error} />
166+
</FormField>
167+
);
168+
}
169+
124170
// Handle server provider select
125171
if (config?.type === 'component' && config?.name === 'server_provider') {
126172
return (
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as React from 'react';
2+
import { EyeIcon, EyeOffIcon } from 'lucide-react';
3+
4+
import { cn } from '@/lib/utils';
5+
import { useInputFocus } from '@/stores/useInputFocus';
6+
7+
type PasswordInputProps = Omit<React.ComponentProps<'input'>, 'type'>;
8+
9+
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(({ className, ...props }, ref) => {
10+
const [showPassword, setShowPassword] = React.useState(false);
11+
const setFocused = useInputFocus((state) => state.setFocused);
12+
13+
return (
14+
<div className="relative">
15+
<input
16+
type={showPassword ? 'text' : 'password'}
17+
data-slot="input"
18+
className={cn(
19+
'file:text-foreground placeholder:text-muted-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
20+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
21+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
22+
'pr-10',
23+
className,
24+
)}
25+
ref={ref}
26+
onFocus={() => setFocused(true)}
27+
onBlur={() => setFocused(false)}
28+
{...props}
29+
/>
30+
<button
31+
type="button"
32+
className="absolute right-0 top-0 flex h-9 w-9 items-center justify-center text-muted-foreground hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
33+
onClick={() => setShowPassword((prev) => !prev)}
34+
disabled={props.disabled}
35+
aria-label={showPassword ? 'Hide password' : 'Show password'}
36+
aria-pressed={showPassword}
37+
>
38+
{showPassword ? <EyeOffIcon className="size-4" aria-hidden="true" /> : <EyeIcon className="size-4" aria-hidden="true" />}
39+
</button>
40+
</div>
41+
);
42+
});
43+
44+
PasswordInput.displayName = 'PasswordInput';
45+
46+
export { PasswordInput };

resources/js/types/dynamic-field-config.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface DynamicFieldConfig {
2-
type: 'text' | 'textarea' | 'select' | 'checkbox' | 'component' | 'alert';
2+
type: 'text' | 'password' | 'password-with-toggle' | 'textarea' | 'select' | 'checkbox' | 'component' | 'alert';
33
name: string;
44
options?: string[] | { [key: string]: string };
55
component?: string;

0 commit comments

Comments
 (0)