feat(FE-Storyless): add FieldMessage component for consistent field feedback across inputs

This commit is contained in:
rstubryan
2025-10-20 18:54:02 +07:00
parent ba9ae07455
commit 1bcfd9bbb4
8 changed files with 195 additions and 96 deletions
+65
View File
@@ -0,0 +1,65 @@
'use client';
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
type FieldMessageTone = 'error' | 'info' | 'success';
export interface FieldMessageProps {
message?: ReactNode;
tone?: FieldMessageTone;
isVisible?: boolean;
persistent?: boolean;
className?: string;
ariaLive?: 'off' | 'polite' | 'assertive';
}
const toneClassName: Record<FieldMessageTone, string> = {
error: 'text-error',
info: 'text-base-content/60',
success: 'text-success',
};
/**
* Shared helper to render bottom field feedback without causing layout shift.
* Keeps a minimal slot height, but expands when the content wraps onto multiple lines.
*/
export const FieldMessage = ({
message,
tone = 'info',
isVisible,
persistent = true,
className,
ariaLive,
}: FieldMessageProps) => {
const hasMessage = Boolean(message);
const visible = isVisible ?? hasMessage;
const liveRegion = ariaLive ?? (tone === 'error' ? 'assertive' : 'polite');
return (
<div
aria-live={liveRegion}
aria-hidden={!visible && !hasMessage}
className={cn(
'relative w-full text-sm leading-5 transition-[opacity,transform] duration-150 ease-out',
persistent && 'min-h-[1.25rem]',
className
)}
>
<span
className={cn(
'block whitespace-pre-line',
toneClassName[tone],
visible
? 'opacity-100 translate-y-0'
: 'opacity-0 -translate-y-1 pointer-events-none'
)}
>
{visible || persistent ? (message ?? '\u00A0') : message}
</span>
</div>
);
};
export default FieldMessage;