import { useEffect, useState } from "react"; export interface NumberFieldProps { label: string; value: number; onChange: (v: number) => void; /** Display = value * scale (e.g. 1000 to show metres as millimetres) */ scale?: number; unit?: string; step?: number; min?: number; max?: number; integer?: boolean; invalid?: boolean; disabled?: boolean; title?: string; } /** A numeric input that tolerates partial typing and commits parsed values. */ export function NumberField(props: NumberFieldProps) { const { label, value, onChange, scale = 1, unit, step, min, max, integer, invalid, disabled, title, } = props; const shown = formatShown(value * scale); const [text, setText] = useState(shown); useEffect(() => { // Sync from outside unless the text already means the same number if (Number(text) * 1 !== value * scale) setText(shown); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, scale]); return (
{ setText(e.target.value); const n = Number(e.target.value); if (e.target.value.trim() === "" || !Number.isFinite(n)) return; onChange(integer ? Math.round(n) : n / scale); }} onBlur={() => setText(shown)} /> {unit ?? ""}
); } function formatShown(x: number): string { if (!Number.isFinite(x)) return ""; const r = Number(x.toPrecision(10)); return String(r); }