mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE-114): integrate inputmask for enhanced numeric input handling and validation
This commit is contained in:
@@ -6,101 +6,47 @@ import {
|
|||||||
FocusEventHandler,
|
FocusEventHandler,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import Inputmask from 'inputmask';
|
||||||
|
|
||||||
// Utility Functions
|
const createInputMask = (
|
||||||
const formatNumber = (
|
maskType: MaskType,
|
||||||
value: number | string,
|
decimals: number,
|
||||||
decimals: number = 0,
|
thousandSeparator: string,
|
||||||
thousandSeparator: string = '.',
|
decimalSeparator: string,
|
||||||
decimalSeparator: string = ','
|
allowNegative: boolean,
|
||||||
): string => {
|
oncomplete?: () => void,
|
||||||
if (value === '' || value === null || value === undefined) return '';
|
onincomplete?: () => void,
|
||||||
|
oncleared?: () => void
|
||||||
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
): Inputmask.Instance => {
|
||||||
if (isNaN(numValue)) return '';
|
const options: Inputmask.Options = {
|
||||||
|
alias: 'numeric',
|
||||||
const parts = numValue.toFixed(decimals).split('.');
|
groupSeparator: thousandSeparator,
|
||||||
const integerPart = parts[0].replace(
|
radixPoint: decimalSeparator,
|
||||||
/\B(?=(\d{3})+(?!\d))/g,
|
digits: decimals,
|
||||||
thousandSeparator
|
allowMinus: allowNegative,
|
||||||
);
|
rightAlign: false,
|
||||||
const decimalPart = parts[1];
|
insertMode: true,
|
||||||
|
autoUnmask: false,
|
||||||
return decimals > 0 && decimalPart
|
clearMaskOnLostFocus: false,
|
||||||
? `${integerPart}${decimalSeparator}${decimalPart}`
|
digitsOptional: decimals > 0,
|
||||||
: integerPart;
|
placeholder: '0',
|
||||||
|
numericInput: false,
|
||||||
|
positionCaretOnClick: 'radixFocus',
|
||||||
|
greedy: true,
|
||||||
|
oncomplete,
|
||||||
|
onincomplete,
|
||||||
|
oncleared
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseNumber = (
|
return new Inputmask(options);
|
||||||
value: string,
|
|
||||||
thousandSeparator: string = '.',
|
|
||||||
decimalSeparator: string = ','
|
|
||||||
): number => {
|
|
||||||
if (!value) return 0;
|
|
||||||
|
|
||||||
// Remove thousand separators and replace decimal separator with dot
|
|
||||||
const cleaned = value
|
|
||||||
.replace(new RegExp(`\\${thousandSeparator}`, 'g'), '')
|
|
||||||
.replace(decimalSeparator, '.');
|
|
||||||
|
|
||||||
const parsed = parseFloat(cleaned);
|
|
||||||
return isNaN(parsed) ? 0 : parsed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (
|
export type MaskType = 'currency' | 'weight' | 'decimal' | 'number' | 'text';
|
||||||
value: number | string,
|
|
||||||
prefix: string = 'Rp ',
|
|
||||||
decimals: number = 0
|
|
||||||
): string => {
|
|
||||||
if (value === '' || value === null || value === undefined) return '';
|
|
||||||
const formatted = formatNumber(value, decimals);
|
|
||||||
return formatted ? `${prefix}${formatted}` : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatWeight = (
|
|
||||||
value: number | string,
|
|
||||||
unit: string = 'kg',
|
|
||||||
decimals: number = 2
|
|
||||||
): string => {
|
|
||||||
if (value === '' || value === null || value === undefined) return '';
|
|
||||||
const formatted = formatNumber(value, decimals);
|
|
||||||
return formatted ? `${formatted} ${unit}` : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanNumericInput = (
|
|
||||||
value: string,
|
|
||||||
allowDecimal: boolean = false,
|
|
||||||
decimalSeparator: string = ','
|
|
||||||
): string => {
|
|
||||||
// Only allow numbers, decimal separator (if allowed), and minus sign at the start
|
|
||||||
let cleaned = value.replace(/[^\d,.-]/g, '');
|
|
||||||
|
|
||||||
// Handle decimal separator
|
|
||||||
if (allowDecimal) {
|
|
||||||
const parts = cleaned.split(decimalSeparator);
|
|
||||||
if (parts.length > 2) {
|
|
||||||
// Keep only first decimal separator
|
|
||||||
cleaned = parts[0] + decimalSeparator + parts.slice(1).join('');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleaned = cleaned.replace(new RegExp(decimalSeparator, 'g'), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle minus sign (only at start)
|
|
||||||
const hasMinusAtStart = cleaned.startsWith('-');
|
|
||||||
cleaned = cleaned.replace(/-/g, '');
|
|
||||||
if (hasMinusAtStart) cleaned = '-' + cleaned;
|
|
||||||
|
|
||||||
return cleaned;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export type MaskType = 'currency' | 'weight' | 'decimal' | 'number';
|
|
||||||
|
|
||||||
export interface NumberInputProps {
|
export interface NumberInputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -108,45 +54,43 @@ export interface NumberInputProps {
|
|||||||
name: string;
|
name: string;
|
||||||
value?: number | string;
|
value?: number | string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
||||||
className?: {
|
className?: {
|
||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
inputWrapper?: string;
|
inputWrapper?: string;
|
||||||
input?: string;
|
input?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
isValid?: boolean;
|
isValid?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
errorMessage?: string;
|
|
||||||
startAdornment?: ReactNode;
|
startAdornment?: ReactNode;
|
||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
|
||||||
// Masking Options
|
|
||||||
maskType?: MaskType;
|
maskType?: MaskType;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
thousandSeparator?: string;
|
thousandSeparator?: string;
|
||||||
decimalSeparator?: string;
|
decimalSeparator?: string;
|
||||||
|
|
||||||
// Currency specific
|
|
||||||
currencyPrefix?: string;
|
currencyPrefix?: string;
|
||||||
|
|
||||||
// Weight specific
|
|
||||||
weightUnit?: string;
|
weightUnit?: string;
|
||||||
|
|
||||||
// Validation
|
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
allowNegative?: boolean;
|
allowNegative?: boolean;
|
||||||
|
|
||||||
// Stepper (Increment/Decrement buttons)
|
oncomplete?: () => void;
|
||||||
showSteppers?: boolean;
|
onincomplete?: () => void;
|
||||||
step?: number;
|
oncleared?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumberInput = ({
|
const NumberInput = ({
|
||||||
@@ -174,186 +118,124 @@ const NumberInput = ({
|
|||||||
decimalSeparator = ',',
|
decimalSeparator = ',',
|
||||||
currencyPrefix = 'Rp ',
|
currencyPrefix = 'Rp ',
|
||||||
weightUnit = 'kg',
|
weightUnit = 'kg',
|
||||||
min,
|
|
||||||
max,
|
|
||||||
allowNegative = false,
|
allowNegative = false,
|
||||||
showSteppers = false,
|
oncomplete,
|
||||||
step = 1,
|
onincomplete,
|
||||||
|
oncleared,
|
||||||
}: NumberInputProps) => {
|
}: NumberInputProps) => {
|
||||||
const [displayValue, setDisplayValue] = useState<string>('');
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const inputmaskRef = useRef<Inputmask.Instance | null>(null);
|
||||||
// Determine if decimals are allowed based on maskType
|
const [maskComplete, setMaskComplete] = useState<boolean>(false);
|
||||||
const allowDecimal =
|
const [maskIncomplete, setMaskIncomplete] = useState<boolean>(false);
|
||||||
maskType === 'decimal' || maskType === 'weight' || decimals > 0;
|
const [maskCleared, setMaskCleared] = useState<boolean>(false);
|
||||||
|
|
||||||
// Format value for display based on maskType
|
|
||||||
const getFormattedValue = (rawValue: number | string): string => {
|
|
||||||
if (rawValue === '' || rawValue === null || rawValue === undefined)
|
|
||||||
return '';
|
|
||||||
|
|
||||||
|
const getInputPrefix = (): string => {
|
||||||
switch (maskType) {
|
switch (maskType) {
|
||||||
case 'currency':
|
case 'currency':
|
||||||
return formatCurrency(rawValue, currencyPrefix, decimals);
|
return currencyPrefix;
|
||||||
case 'weight':
|
|
||||||
return formatWeight(rawValue, weightUnit, decimals);
|
|
||||||
case 'decimal':
|
|
||||||
case 'number':
|
|
||||||
return formatNumber(
|
|
||||||
rawValue,
|
|
||||||
decimals,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return String(rawValue);
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInputSuffix = (): string => {
|
||||||
|
switch (maskType) {
|
||||||
|
case 'weight':
|
||||||
|
return weightUnit;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize display value when value prop changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayValue(getFormattedValue(value || ''));
|
if (inputRef.current && !readOnly && !disabled) {
|
||||||
|
if (inputmaskRef.current) {
|
||||||
|
try {
|
||||||
|
inputmaskRef.current.remove();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error removing Inputmask:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
setMaskComplete(true);
|
||||||
|
setMaskIncomplete(false);
|
||||||
|
setMaskCleared(false);
|
||||||
|
if (oncomplete) oncomplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIncomplete = () => {
|
||||||
|
setMaskIncomplete(true);
|
||||||
|
setMaskComplete(false);
|
||||||
|
setMaskCleared(false);
|
||||||
|
if (onincomplete) onincomplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCleared = () => {
|
||||||
|
setMaskCleared(true);
|
||||||
|
setMaskComplete(false);
|
||||||
|
setMaskIncomplete(false);
|
||||||
|
if (oncleared) oncleared();
|
||||||
|
};
|
||||||
|
|
||||||
|
const im = createInputMask(
|
||||||
|
maskType,
|
||||||
|
decimals,
|
||||||
|
thousandSeparator,
|
||||||
|
decimalSeparator,
|
||||||
|
allowNegative,
|
||||||
|
handleComplete,
|
||||||
|
handleIncomplete,
|
||||||
|
handleCleared
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
im.mask(inputRef.current);
|
||||||
|
inputmaskRef.current = im;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error applying Inputmask:', error);
|
||||||
|
inputmaskRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (inputmaskRef.current) {
|
||||||
|
try {
|
||||||
|
inputmaskRef.current.remove();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error removing Inputmask on cleanup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [maskType, decimals, thousandSeparator, decimalSeparator, allowNegative, readOnly, disabled, oncomplete, onincomplete, oncleared]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && value !== undefined) {
|
||||||
|
if (value === null || value === '') {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
} else {
|
||||||
|
inputRef.current.value = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
let inputValue = e.target.value;
|
const currentValue = (e.currentTarget as HTMLInputElement).value;
|
||||||
|
console.log('✅ After format:', currentValue);
|
||||||
|
|
||||||
// Remove prefix/suffix for editing
|
|
||||||
if (maskType === 'currency' && inputValue.startsWith(currencyPrefix)) {
|
|
||||||
inputValue = inputValue.slice(currencyPrefix.length);
|
|
||||||
}
|
|
||||||
if (maskType === 'weight' && inputValue.endsWith(` ${weightUnit}`)) {
|
|
||||||
inputValue = inputValue.slice(0, -weightUnit.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean input
|
|
||||||
const cleaned = cleanNumericInput(
|
|
||||||
inputValue,
|
|
||||||
allowDecimal,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
|
|
||||||
// Parse to number
|
|
||||||
let numericValue = parseNumber(
|
|
||||||
cleaned,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply validation
|
|
||||||
if (!allowNegative && numericValue < 0) {
|
|
||||||
numericValue = 0;
|
|
||||||
}
|
|
||||||
if (min !== undefined && numericValue < min) {
|
|
||||||
numericValue = min;
|
|
||||||
}
|
|
||||||
if (max !== undefined && numericValue > max) {
|
|
||||||
numericValue = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update display value
|
|
||||||
const formattedForDisplay = formatNumber(
|
|
||||||
numericValue,
|
|
||||||
decimals,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
|
|
||||||
setDisplayValue(formattedForDisplay);
|
|
||||||
|
|
||||||
// Call onChange with modified event
|
|
||||||
if (onChange) {
|
|
||||||
// Create a synthetic event with the numeric value
|
|
||||||
const syntheticEvent = {
|
|
||||||
...e,
|
|
||||||
target: {
|
|
||||||
...e.target,
|
|
||||||
name,
|
|
||||||
value: numericValue.toString(),
|
|
||||||
},
|
|
||||||
} as ChangeEvent<HTMLInputElement>;
|
|
||||||
|
|
||||||
onChange(syntheticEvent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Increment
|
|
||||||
const handleIncrement = () => {
|
|
||||||
if (disabled || readOnly) return;
|
|
||||||
|
|
||||||
const currentValue = parseNumber(
|
|
||||||
displayValue,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
let newValue = currentValue + step;
|
|
||||||
|
|
||||||
// Apply max validation
|
|
||||||
if (max !== undefined && newValue > max) {
|
|
||||||
newValue = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update display
|
|
||||||
const formattedForDisplay = formatNumber(
|
|
||||||
newValue,
|
|
||||||
decimals,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
setDisplayValue(formattedForDisplay);
|
|
||||||
|
|
||||||
// Call onChange with synthetic event
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
const syntheticEvent = {
|
const syntheticEvent = {
|
||||||
target: {
|
target: {
|
||||||
name,
|
name,
|
||||||
value: newValue.toString(),
|
value: currentValue,
|
||||||
},
|
},
|
||||||
} as ChangeEvent<HTMLInputElement>;
|
} as ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
onChange(syntheticEvent);
|
onChange(syntheticEvent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle Decrement
|
const inputPrefix = getInputPrefix();
|
||||||
const handleDecrement = () => {
|
const inputSuffix = getInputSuffix();
|
||||||
if (disabled || readOnly) return;
|
|
||||||
|
|
||||||
const currentValue = parseNumber(
|
|
||||||
displayValue,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
let newValue = currentValue - step;
|
|
||||||
|
|
||||||
// Apply min validation (prevent negative if not allowed)
|
|
||||||
if (!allowNegative && newValue < 0) {
|
|
||||||
newValue = 0;
|
|
||||||
}
|
|
||||||
if (min !== undefined && newValue < min) {
|
|
||||||
newValue = min;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update display
|
|
||||||
const formattedForDisplay = formatNumber(
|
|
||||||
newValue,
|
|
||||||
decimals,
|
|
||||||
thousandSeparator,
|
|
||||||
decimalSeparator
|
|
||||||
);
|
|
||||||
setDisplayValue(formattedForDisplay);
|
|
||||||
|
|
||||||
// Call onChange with synthetic event
|
|
||||||
if (onChange) {
|
|
||||||
const syntheticEvent = {
|
|
||||||
target: {
|
|
||||||
name,
|
|
||||||
value: newValue.toString(),
|
|
||||||
},
|
|
||||||
} as ChangeEvent<HTMLInputElement>;
|
|
||||||
|
|
||||||
onChange(syntheticEvent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -385,78 +267,140 @@ const NumberInput = ({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className='relative flex'>
|
||||||
|
{inputPrefix && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
|
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium select-none whitespace-nowrap',
|
||||||
|
{
|
||||||
|
'text-gray-600': !disabled,
|
||||||
|
'text-gray-400': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputPrefix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
|
'rounded-l-none!': inputPrefix,
|
||||||
|
'rounded-r-none!': inputSuffix,
|
||||||
|
'input-disabled': disabled,
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'bg-gray-50': disabled,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Decrement Button */}
|
|
||||||
{showSteppers && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={handleDecrement}
|
|
||||||
disabled={disabled || readOnly}
|
|
||||||
className={cn(
|
|
||||||
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
|
||||||
{
|
|
||||||
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<Icon icon='ic:round-minus' width={20} height={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{startAdornment && startAdornment}
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
placeholder={placeholder}
|
ref={inputRef}
|
||||||
value={displayValue}
|
placeholder={placeholder || '0'}
|
||||||
onChange={handleInputChange}
|
onKeyUp={handleKeyUp}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn('grow', className?.input)}
|
className={cn(
|
||||||
|
'grow bg-transparent outline-none',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'text-gray-500': disabled,
|
||||||
|
},
|
||||||
|
className?.input
|
||||||
|
)}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
inputMode='decimal'
|
inputMode='text'
|
||||||
|
autoComplete='off'
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isLoading || endAdornment) && (
|
{(isLoading || endAdornment) && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
{isLoading && <span className='loading loading-spinner' />}
|
{isLoading && <span className='loading loading-spinner' />}
|
||||||
|
|
||||||
{endAdornment && endAdornment}
|
{endAdornment && endAdornment}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Increment Button */}
|
{inputSuffix && (
|
||||||
{showSteppers && (
|
<div
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={handleIncrement}
|
|
||||||
disabled={disabled || readOnly}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'btn btn-ghost btn-sm h-8 w-8 min-h-0 p-0 rounded-md',
|
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
|
||||||
{
|
{
|
||||||
'opacity-50 cursor-not-allowed': disabled || readOnly,
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
tabIndex={-1}
|
|
||||||
>
|
>
|
||||||
<Icon icon='ic:round-plus' width={20} height={20} />
|
<span
|
||||||
</button>
|
className={cn(
|
||||||
|
'text-sm font-medium select-none whitespace-nowrap',
|
||||||
|
{
|
||||||
|
'text-gray-600': !disabled,
|
||||||
|
'text-gray-400': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputSuffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(maskType === 'text' || (oncomplete || onincomplete || oncleared)) && (
|
||||||
|
<div className='flex gap-2 text-xs'>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskComplete
|
||||||
|
? 'bg-green-100 text-green-700 border border-green-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Complete
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskIncomplete
|
||||||
|
? 'bg-yellow-100 text-yellow-700 border border-yellow-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Incomplete
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskCleared
|
||||||
|
? 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Cleared
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
@@ -468,3 +412,4 @@ const NumberInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default NumberInput;
|
export default NumberInput;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user