88 lines
2.2 KiB
TypeScript
88 lines
2.2 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
|
|
type BaseProps = Omit<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
"value" | "onChange"
|
|
> & {
|
|
className?: string;
|
|
};
|
|
|
|
type StringProps = BaseProps & {
|
|
value: string;
|
|
onValue: (v: string) => void;
|
|
valueCents?: never;
|
|
onChange?: never;
|
|
};
|
|
|
|
type CentsProps = BaseProps & {
|
|
valueCents: number;
|
|
onChange: (cents: number) => void;
|
|
value?: never;
|
|
onValue?: never;
|
|
};
|
|
|
|
type Props = StringProps | CentsProps;
|
|
|
|
export default function CurrencyInput({
|
|
className,
|
|
placeholder = "0.00",
|
|
...rest
|
|
}: Props) {
|
|
const mergedClass = ["input", className].filter(Boolean).join(" ");
|
|
const formatString = (raw: string) => {
|
|
const cleanedRaw = raw.replace(/,/g, ".").replace(/[^0-9.]/g, "");
|
|
const parts = cleanedRaw.split(".");
|
|
return parts.length === 1
|
|
? parts[0]
|
|
: `${parts[0]}.${parts.slice(1).join("").slice(0, 2)}`;
|
|
};
|
|
|
|
if ("valueCents" in rest) {
|
|
const { valueCents, onChange, ...inputProps } = rest as CentsProps;
|
|
const [display, setDisplay] = useState<string>("");
|
|
|
|
// keep display in sync when valueCents prop changes externally
|
|
useEffect(() => {
|
|
const asNumber = (valueCents ?? 0) / 100;
|
|
setDisplay(Number.isFinite(asNumber) ? asNumber.toString() : "");
|
|
}, [valueCents]);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const formatted = formatString(e.target.value);
|
|
setDisplay(formatted);
|
|
const parsed = Number.parseFloat(formatted);
|
|
if (Number.isFinite(parsed)) {
|
|
onChange(Math.round(parsed * 100));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<input
|
|
{...inputProps}
|
|
className={mergedClass}
|
|
inputMode="decimal"
|
|
placeholder={placeholder}
|
|
value={display}
|
|
onChange={handleChange}
|
|
pattern="[0-9]*[.,]?[0-9]*"
|
|
/>
|
|
);
|
|
}
|
|
|
|
const { value, onValue, ...inputProps } = rest as StringProps;
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
onValue(formatString(e.target.value));
|
|
};
|
|
return (
|
|
<input
|
|
{...inputProps}
|
|
className={mergedClass}
|
|
inputMode="decimal"
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={handleChange}
|
|
pattern="[0-9]*[.,]?[0-9]*"
|
|
/>
|
|
);
|
|
}
|