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:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
||||||
import type { FieldDef } from '../../schemas/registry';
|
import type { FieldDef } from '../../schemas/registry';
|
||||||
import { PasswordField } from './PasswordField';
|
import { PasswordField } from './PasswordField';
|
||||||
|
import { TextFieldMD3 } from './TextFieldMD3';
|
||||||
|
|
||||||
interface FieldRendererProps {
|
interface FieldRendererProps {
|
||||||
field: FieldDef;
|
field: FieldDef;
|
||||||
@@ -30,7 +31,6 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
label={field.label}
|
label={field.label}
|
||||||
error={error}
|
error={error}
|
||||||
registration={register(field.key)}
|
registration={register(field.key)}
|
||||||
placeholder={field.placeholder}
|
|
||||||
helpText={field.helpText}
|
helpText={field.helpText}
|
||||||
tooltipText={field.tooltipText}
|
tooltipText={field.tooltipText}
|
||||||
/>
|
/>
|
||||||
@@ -79,42 +79,32 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// text (default)
|
// text (default) — uses TextFieldMD3 for floating label
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
{field.tooltipText && (
|
||||||
<label htmlFor={field.key} className="text-sm font-medium text-on-surface-container">
|
<button
|
||||||
{field.label}
|
type="button"
|
||||||
{field.required && <span className="ml-1 text-error">*</span>}
|
onClick={() => setShowTooltip(v => !v)}
|
||||||
</label>
|
aria-label={`More info about ${field.label}`}
|
||||||
{field.tooltipText && (
|
className="text-primary hover:text-primary text-xs leading-none self-start"
|
||||||
<button
|
>
|
||||||
type="button"
|
ⓘ
|
||||||
onClick={() => setShowTooltip(v => !v)}
|
</button>
|
||||||
aria-label={`More info about ${field.label}`}
|
)}
|
||||||
className="text-primary hover:text-primary text-xs leading-none"
|
|
||||||
>
|
|
||||||
ⓘ
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{field.tooltipText && showTooltip && (
|
{field.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">
|
||||||
{field.tooltipText}
|
{field.tooltipText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<input
|
<TextFieldMD3
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type="text"
|
label={field.label}
|
||||||
placeholder={field.placeholder}
|
error={error}
|
||||||
className={[
|
registration={register(field.key)}
|
||||||
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
|
helpText={field.helpText}
|
||||||
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
|
required={field.required}
|
||||||
].join(' ')}
|
|
||||||
{...register(field.key)}
|
|
||||||
/>
|
/>
|
||||||
{field.helpText && !error && <p className="text-xs text-on-surface-container/70">{field.helpText}</p>}
|
|
||||||
{error && <p className="text-xs text-error">{error.message}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,57 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
|
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
|
||||||
|
import { TextFieldMD3 } from './TextFieldMD3';
|
||||||
|
|
||||||
interface PasswordFieldProps {
|
interface PasswordFieldProps {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
error?: FieldError;
|
error?: FieldError;
|
||||||
registration: UseFormRegisterReturn;
|
registration: UseFormRegisterReturn;
|
||||||
placeholder?: string;
|
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
tooltipText?: 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 [show, setShow] = useState(false);
|
||||||
const [showTooltip, setShowTooltip] = 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 (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
{tooltipText && (
|
||||||
<label htmlFor={id} className="text-sm font-medium text-on-surface-container">
|
<button
|
||||||
{label}
|
type="button"
|
||||||
</label>
|
onClick={() => setShowTooltip(v => !v)}
|
||||||
{tooltipText && (
|
aria-label={`More info about ${label}`}
|
||||||
<button
|
className="text-primary hover:text-primary text-xs leading-none self-start"
|
||||||
type="button"
|
>
|
||||||
onClick={() => setShowTooltip(v => !v)}
|
ⓘ
|
||||||
aria-label={`More info about ${label}`}
|
</button>
|
||||||
className="text-primary hover:text-primary text-xs leading-none"
|
)}
|
||||||
>
|
|
||||||
ⓘ
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{tooltipText && showTooltip && (
|
{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}
|
{tooltipText}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<TextFieldMD3
|
||||||
<input
|
id={id}
|
||||||
id={id}
|
label={label}
|
||||||
type={show ? 'text' : 'password'}
|
error={error}
|
||||||
placeholder={placeholder}
|
registration={registration}
|
||||||
className={[
|
type={show ? 'text' : 'password'}
|
||||||
'w-full rounded-md border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2',
|
helpText={helpText}
|
||||||
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
|
suffix={toggleButton}
|
||||||
].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>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user