104 lines
2.6 KiB
TypeScript
104 lines
2.6 KiB
TypeScript
import { useCallback, useRef, useState } from 'react';
|
|
import {
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupInput,
|
|
InputGroupText,
|
|
} from '@/components/ui/input-group';
|
|
|
|
type NumberInputProps = {
|
|
id?: string;
|
|
name?: string;
|
|
defaultValue?: number;
|
|
value?: number;
|
|
onValueChange?: (value: number) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
min?: number;
|
|
max?: number;
|
|
suffix?: string;
|
|
className?: string;
|
|
};
|
|
|
|
function formatDisplay(value: number): string {
|
|
return value.toLocaleString('id-ID');
|
|
}
|
|
|
|
function parseText(value: string): number {
|
|
const cleaned = value.replace(/[^0-9]/g, '');
|
|
|
|
return cleaned === '' ? 0 : parseInt(cleaned, 10);
|
|
}
|
|
|
|
export function NumberInput({
|
|
id,
|
|
name,
|
|
defaultValue = 0,
|
|
value,
|
|
onValueChange,
|
|
placeholder = '0',
|
|
disabled = false,
|
|
min,
|
|
max,
|
|
suffix,
|
|
className,
|
|
}: NumberInputProps) {
|
|
const isControlled = value !== undefined;
|
|
const [displayValue, setDisplayValue] = useState(
|
|
formatDisplay(isControlled ? value : defaultValue),
|
|
);
|
|
const lastValidRef = useRef(isControlled ? value : defaultValue);
|
|
const [prevValue, setPrevValue] = useState(value);
|
|
|
|
if (isControlled && value !== prevValue) {
|
|
setPrevValue(value);
|
|
setDisplayValue(formatDisplay(value));
|
|
}
|
|
|
|
const handleChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const raw = parseText(e.target.value);
|
|
let clamped = raw;
|
|
|
|
if (min !== undefined && raw < min) {
|
|
clamped = min;
|
|
}
|
|
|
|
if (max !== undefined && raw > max) {
|
|
clamped = max;
|
|
}
|
|
|
|
lastValidRef.current = clamped;
|
|
setDisplayValue(formatDisplay(clamped));
|
|
onValueChange?.(clamped);
|
|
},
|
|
[min, max, onValueChange],
|
|
);
|
|
|
|
const handleBlur = useCallback(() => {
|
|
setDisplayValue(formatDisplay(lastValidRef.current));
|
|
}, []);
|
|
|
|
return (
|
|
<InputGroup className={className}>
|
|
<InputGroupInput
|
|
id={id}
|
|
name={name}
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={displayValue}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
autoComplete="off"
|
|
/>
|
|
{suffix && (
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>{suffix}</InputGroupText>
|
|
</InputGroupAddon>
|
|
)}
|
|
</InputGroup>
|
|
);
|
|
}
|