import { memo, useCallback, useState } from "react"; import TextBox, { type TextBoxProps } from "./TextBox"; export interface NumberInputBoxProps extends Omit< TextBoxProps, "inputMode" | "onChange" | "value" | "min" | "max" > { value?: number | undefined; onChange?: (value: number | undefined) => void; min?: number; max?: number; integerOnly?: boolean; // TODO: // startAdornment?: string; // endAdornment?: string; } export default memo(function NumberInputBox(props: NumberInputBoxProps) { const [value, setValue] = useState(""); const onChangeParent = props.onChange; const { integerOnly, min, max } = props; const onChange = useCallback( (v: string) => { setValue(v); const value = v.replaceAll(/\./g, "").replaceAll(",", "."); const numberValue = Number(value); if ( (min !== undefined && numberValue < min) || (max !== undefined && numberValue > max) || Number.isNaN(numberValue) || !Number.isFinite(numberValue) ) { onChangeParent?.(undefined); return; } if (integerOnly && !Number.isSafeInteger(numberValue)) { onChangeParent?.(undefined); return; } onChangeParent?.(numberValue); }, [integerOnly, min, max, onChangeParent], ); return ( { if (!input) { return; } const handler = (e: InputEvent) => { const content = e.data; if (!content || !e.inputType.startsWith("insert")) { return; } if ( (integerOnly && !/^[-0-9]*$/.test(content)) || !/^[-0-9,]*$/.test(content) ) { e.preventDefault(); } }; input.addEventListener("beforeinput", handler); return () => input.removeEventListener("beforeinput", handler); }} {...props} min={undefined} max={undefined} onChange={onChange} value={value?.toString()} pattern={integerOnly ? "[-0-9]*" : "[-0-9,]*"} inputMode="numeric" /> ); });