feat(09-03): integrate TextFieldMD3 into FieldRenderer and PasswordField

- FieldRenderer text-branch now renders TextFieldMD3 with floating label
- PasswordField replaced with TextFieldMD3 + show/hide suffix toggle
- Tooltip button kept outside TextFieldMD3, rendered above it
- Removed placeholder prop from PasswordField (TextFieldMD3 uses placeholder=" " internally)
- All 179 tests pass (getByLabelText queries preserved via htmlFor/id pairing)
This commit is contained in:
2026-04-01 09:27:44 +02:00
parent 7a02d483b3
commit e560a6af84
2 changed files with 53 additions and 69 deletions
+34 -40
View File
@@ -1,63 +1,57 @@
import { useState } from 'react';
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
import { TextFieldMD3 } from './TextFieldMD3';
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
tooltipText?: string;
}
export function PasswordField({ id, label, error, registration, placeholder, helpText, tooltipText }: PasswordFieldProps) {
export function PasswordField({ id, label, error, registration, helpText, tooltipText }: PasswordFieldProps) {
const [show, setShow] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const toggleButton = (
<button
type="button"
onClick={() => setShow(v => !v)}
aria-label={show ? 'Hide' : 'Show'}
className="text-on-surface-container/50 hover:text-on-surface-container text-sm"
>
{show ? 'Hide' : 'Show'}
</button>
);
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<label htmlFor={id} className="text-sm font-medium text-on-surface-container">
{label}
</label>
{tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary text-xs leading-none"
>
</button>
)}
</div>
{tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary text-xs leading-none self-start"
>
</button>
)}
{tooltipText && showTooltip && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5 mt-1">
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{tooltipText}
</p>
)}
<div className="relative">
<input
id={id}
type={show ? 'text' : 'password'}
placeholder={placeholder}
className={[
'w-full rounded-md border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2',
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
].join(' ')}
{...registration}
/>
<button
type="button"
onClick={() => setShow(v => !v)}
aria-label={show ? 'Hide' : 'Show'}
className="absolute right-2 top-1/2 -translate-y-1/2 text-on-surface-container/50 hover:text-on-surface-container"
>
{show ? 'Hide' : 'Show'}
</button>
</div>
{helpText && !error && <p className="text-xs text-on-surface-container/70">{helpText}</p>}
{error && <p className="text-xs text-error">{error.message}</p>}
<TextFieldMD3
id={id}
label={label}
error={error}
registration={registration}
type={show ? 'text' : 'password'}
helpText={helpText}
suffix={toggleButton}
/>
</div>
);
}