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
+43 -14
View File
@@ -11,6 +11,7 @@ import {
import { Icon } from '@iconify/react';
import { cn } from '@/lib/helper';
import FieldMessage from '@/components/helper/FieldMessage';
// Utility Functions
const formatNumber = (
@@ -25,7 +26,10 @@ const formatNumber = (
if (isNaN(numValue)) return '';
const parts = numValue.toFixed(decimals).split('.');
const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
const integerPart = parts[0].replace(
/\B(?=(\d{3})+(?!\d))/g,
thousandSeparator
);
const decimalPart = parts[1];
return decimals > 0 && decimalPart
@@ -180,11 +184,13 @@ const NumberInput = ({
const [displayValue, setDisplayValue] = useState<string>('');
// Determine if decimals are allowed based on maskType
const allowDecimal = maskType === 'decimal' || maskType === 'weight' || decimals > 0;
const allowDecimal =
maskType === 'decimal' || maskType === 'weight' || decimals > 0;
// Format value for display based on maskType
const getFormattedValue = (rawValue: number | string): string => {
if (rawValue === '' || rawValue === null || rawValue === undefined) return '';
if (rawValue === '' || rawValue === null || rawValue === undefined)
return '';
switch (maskType) {
case 'currency':
@@ -193,7 +199,12 @@ const NumberInput = ({
return formatWeight(rawValue, weightUnit, decimals);
case 'decimal':
case 'number':
return formatNumber(rawValue, decimals, thousandSeparator, decimalSeparator);
return formatNumber(
rawValue,
decimals,
thousandSeparator,
decimalSeparator
);
default:
return String(rawValue);
}
@@ -216,10 +227,18 @@ const NumberInput = ({
}
// Clean input
const cleaned = cleanNumericInput(inputValue, allowDecimal, decimalSeparator);
const cleaned = cleanNumericInput(
inputValue,
allowDecimal,
decimalSeparator
);
// Parse to number
let numericValue = parseNumber(cleaned, thousandSeparator, decimalSeparator);
let numericValue = parseNumber(
cleaned,
thousandSeparator,
decimalSeparator
);
// Apply validation
if (!allowNegative && numericValue < 0) {
@@ -262,7 +281,11 @@ const NumberInput = ({
const handleIncrement = () => {
if (disabled || readOnly) return;
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
const currentValue = parseNumber(
displayValue,
thousandSeparator,
decimalSeparator
);
let newValue = currentValue + step;
// Apply max validation
@@ -296,7 +319,11 @@ const NumberInput = ({
const handleDecrement = () => {
if (disabled || readOnly) return;
const currentValue = parseNumber(displayValue, thousandSeparator, decimalSeparator);
const currentValue = parseNumber(
displayValue,
thousandSeparator,
decimalSeparator
);
let newValue = currentValue - step;
// Apply min validation (prevent negative if not allowed)
@@ -329,6 +356,9 @@ const NumberInput = ({
}
};
const showErrorMessage = Boolean(isError && errorMessage);
const feedbackMessage = showErrorMessage ? errorMessage : bottomLabel;
return (
<div
className={cn(
@@ -431,12 +461,11 @@ const NumberInput = ({
)}
</div>
{!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)}
{isError && errorMessage && (
<p className='w-full text-sm text-error'>{errorMessage}</p>
)}
<FieldMessage
message={feedbackMessage}
tone={showErrorMessage ? 'error' : 'info'}
isVisible={showErrorMessage || Boolean(bottomLabel)}
/>
</div>
);
};