File size: 1,916 Bytes
df3ef41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import * as React from "react";
import { cn } from "@/lib/utils";
import { Input } from "./input";
type NumberInputProps = Omit<
React.ComponentProps<typeof Input>,
"type" | "value" | "onChange"
> & {
value: number | undefined | null;
onChange: (value: number | undefined) => void;
integer?: boolean;
};
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
({ value, onChange, integer = true, className, ...props }, ref) => {
const [display, setDisplay] = React.useState(
value == null ? "" : String(value)
);
// Track the last value we saw from props so we only resync the
// visible string when the prop changes externally. This lets the
// user clear the field even when the parent keeps the previous
// numeric value (because our onChange(undefined) was ignored).
const lastPropRef = React.useRef<number | undefined | null>(value);
React.useEffect(() => {
if (value !== lastPropRef.current) {
lastPropRef.current = value;
setDisplay(value == null ? "" : String(value));
}
}, [value]);
return (
<Input
ref={ref}
type="number"
inputMode={integer ? "numeric" : "decimal"}
value={display}
onChange={(e) => {
const next = e.target.value;
setDisplay(next);
if (next === "") {
onChange(undefined);
return;
}
const n = integer ? parseInt(next, 10) : parseFloat(next);
if (Number.isFinite(n)) onChange(n);
}}
className={cn(
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-outer-spin-button]:m-0",
className
)}
{...props}
/>
);
}
);
NumberInput.displayName = "NumberInput";
export { NumberInput };
|