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:
@@ -12,6 +12,7 @@ interface FieldRendererProps {
|
|||||||
|
|
||||||
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const [hoverTooltip, setHoverTooltip] = useState(false);
|
||||||
|
|
||||||
// provider field: single-option select — hide from UI, auto-register with default value
|
// provider field: single-option select — hide from UI, auto-register with default value
|
||||||
if (field.key === 'provider' && field.options?.length === 1) {
|
if (field.key === 'provider' && field.options?.length === 1) {
|
||||||
@@ -48,7 +49,9 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
{field.tooltipText && (
|
{field.tooltipText && (
|
||||||
<button
|
<button
|
||||||
type="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}`}
|
aria-label={`More info about ${field.label}`}
|
||||||
className="text-primary hover:text-primary text-xs leading-none"
|
className="text-primary hover:text-primary text-xs leading-none"
|
||||||
>
|
>
|
||||||
@@ -56,7 +59,7 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5 mt-1">
|
||||||
{field.tooltipText}
|
{field.tooltipText}
|
||||||
</p>
|
</p>
|
||||||
@@ -80,23 +83,23 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// text (default) — uses TextFieldMD3 for floating label
|
// text (default) — uses TextFieldMD3 for floating label
|
||||||
|
const tooltipIcon = field.tooltipText ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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/80 text-xs leading-none mt-px shrink-0"
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</button>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
const tooltipVisible = showTooltip || hoverTooltip;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{field.tooltipText && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowTooltip(v => !v)}
|
|
||||||
aria-label={`More info about ${field.label}`}
|
|
||||||
className="text-primary hover:text-primary text-xs leading-none self-start"
|
|
||||||
>
|
|
||||||
ⓘ
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
<TextFieldMD3
|
<TextFieldMD3
|
||||||
id={field.key}
|
id={field.key}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
@@ -104,7 +107,13 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
registration={register(field.key)}
|
registration={register(field.key)}
|
||||||
helpText={field.helpText}
|
helpText={field.helpText}
|
||||||
required={field.required}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface PasswordFieldProps {
|
|||||||
export function PasswordField({ id, label, error, registration, 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 [hoverTooltip, setHoverTooltip] = useState(false);
|
||||||
|
|
||||||
const toggleButton = (
|
const toggleButton = (
|
||||||
<button
|
<button
|
||||||
@@ -26,23 +27,23 @@ export function PasswordField({ id, label, error, registration, helpText, toolti
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tooltipIcon = tooltipText ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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/80 text-xs leading-none mt-px shrink-0"
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</button>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
const tooltipVisible = showTooltip || hoverTooltip;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{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">
|
|
||||||
{tooltipText}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<TextFieldMD3
|
<TextFieldMD3
|
||||||
id={id}
|
id={id}
|
||||||
label={label}
|
label={label}
|
||||||
@@ -50,8 +51,14 @@ export function PasswordField({ id, label, error, registration, helpText, toolti
|
|||||||
registration={registration}
|
registration={registration}
|
||||||
type={show ? 'text' : 'password'}
|
type={show ? 'text' : 'password'}
|
||||||
helpText={helpText}
|
helpText={helpText}
|
||||||
|
helpTextPrefix={tooltipIcon}
|
||||||
suffix={toggleButton}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface TextFieldMD3Props {
|
|||||||
registration: UseFormRegisterReturn;
|
registration: UseFormRegisterReturn;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
|
helpTextPrefix?: React.ReactNode;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
suffix?: React.ReactNode;
|
suffix?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -18,6 +19,7 @@ export function TextFieldMD3({
|
|||||||
registration,
|
registration,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
helpText,
|
helpText,
|
||||||
|
helpTextPrefix,
|
||||||
required,
|
required,
|
||||||
suffix,
|
suffix,
|
||||||
}: TextFieldMD3Props) {
|
}: TextFieldMD3Props) {
|
||||||
@@ -62,8 +64,13 @@ export function TextFieldMD3({
|
|||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">{suffix}</div>
|
<div className="absolute right-2 top-1/2 -translate-y-1/2">{suffix}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{helpText && !error && (
|
{(helpText || helpTextPrefix) && !error && (
|
||||||
<p className="text-xs text-on-surface-container/70">{helpText}</p>
|
<div className="flex items-start gap-1">
|
||||||
|
{helpTextPrefix}
|
||||||
|
{helpText && (
|
||||||
|
<p className="text-xs text-on-surface-container/70">{helpText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-error" role="alert">
|
<p className="text-xs text-error" role="alert">
|
||||||
|
|||||||
Reference in New Issue
Block a user