feat(09-05): add helpTextPrefix prop and fix tooltip placement + hover behavior

- TextFieldMD3: add helpTextPrefix prop rendered inline left of helpText
- Guard changed to (helpText || helpTextPrefix) so icon renders without helpText
- FieldRenderer: move tooltip icon from above field to inline with helpText via helpTextPrefix
- FieldRenderer: add hoverTooltip state; tooltip visible on mouseEnter, dismisses on mouseLeave
- FieldRenderer: click-to-unpin clears hoverTooltip to prevent sticky tooltip after dismiss
- PasswordField: same tooltip fix as FieldRenderer text-branch
- Select-branch also gains hover support
This commit is contained in:
2026-04-01 11:19:17 +02:00
parent 29aa6b9e2f
commit 23617c0422
3 changed files with 57 additions and 34 deletions
+22 -13
View File
@@ -12,6 +12,7 @@ interface FieldRendererProps {
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
const [showTooltip, setShowTooltip] = useState(false);
const [hoverTooltip, setHoverTooltip] = useState(false);
// provider field: single-option select — hide from UI, auto-register with default value
if (field.key === 'provider' && field.options?.length === 1) {
@@ -48,7 +49,9 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
{field.tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary text-xs leading-none"
>
@@ -56,7 +59,7 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
</button>
)}
</div>
{field.tooltipText && showTooltip && (
{field.tooltipText && (showTooltip || hoverTooltip) && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5 mt-1">
{field.tooltipText}
</p>
@@ -80,23 +83,23 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
}
// text (default) — uses TextFieldMD3 for floating label
return (
<div className="flex flex-col gap-1">
{field.tooltipText && (
const tooltipIcon = field.tooltipText ? (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary text-xs leading-none self-start"
className="text-primary hover:text-primary/80 text-xs leading-none mt-px shrink-0"
>
</button>
)}
{field.tooltipText && showTooltip && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
) : undefined;
const tooltipVisible = showTooltip || hoverTooltip;
return (
<div className="flex flex-col gap-1">
<TextFieldMD3
id={field.key}
label={field.label}
@@ -104,7 +107,13 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
registration={register(field.key)}
helpText={field.helpText}
required={field.required}
helpTextPrefix={tooltipIcon}
/>
{field.tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
</div>
);
}
+18 -11
View File
@@ -14,6 +14,7 @@ interface PasswordFieldProps {
export function PasswordField({ id, label, error, registration, helpText, tooltipText }: PasswordFieldProps) {
const [show, setShow] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [hoverTooltip, setHoverTooltip] = useState(false);
const toggleButton = (
<button
@@ -26,23 +27,23 @@ export function PasswordField({ id, label, error, registration, helpText, toolti
</button>
);
return (
<div className="flex flex-col gap-1">
{tooltipText && (
const tooltipIcon = tooltipText ? (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary text-xs leading-none self-start"
className="text-primary hover:text-primary/80 text-xs leading-none mt-px shrink-0"
>
</button>
)}
{tooltipText && showTooltip && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{tooltipText}
</p>
)}
) : undefined;
const tooltipVisible = showTooltip || hoverTooltip;
return (
<div className="flex flex-col gap-1">
<TextFieldMD3
id={id}
label={label}
@@ -50,8 +51,14 @@ export function PasswordField({ id, label, error, registration, helpText, toolti
registration={registration}
type={show ? 'text' : 'password'}
helpText={helpText}
helpTextPrefix={tooltipIcon}
suffix={toggleButton}
/>
{tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{tooltipText}
</p>
)}
</div>
);
}
+8 -1
View File
@@ -7,6 +7,7 @@ interface TextFieldMD3Props {
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
helpText?: string;
helpTextPrefix?: React.ReactNode;
required?: boolean;
suffix?: React.ReactNode;
}
@@ -18,6 +19,7 @@ export function TextFieldMD3({
registration,
type = 'text',
helpText,
helpTextPrefix,
required,
suffix,
}: TextFieldMD3Props) {
@@ -62,9 +64,14 @@ export function TextFieldMD3({
<div className="absolute right-2 top-1/2 -translate-y-1/2">{suffix}</div>
)}
</div>
{helpText && !error && (
{(helpText || helpTextPrefix) && !error && (
<div className="flex items-start gap-1">
{helpTextPrefix}
{helpText && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
</div>
)}
{error && (
<p className="text-xs text-error" role="alert">
{error.message}