/ proof-of-concept / plate-mechanics-sim
Sign in
proof-of-concept / plate-mechanics-sim
plate-mechanics-sim / packages / components / src / params / NumberField.tsx
57 lines · 1.8 KBBlameHistoryRaw
1import { useEffect, useState } from "react";
3export interface NumberFieldProps {
4 label: string;
5 value: number;
6 onChange: (v: number) => void;
7 /** Display = value * scale (e.g. 1000 to show metres as millimetres) */
8 scale?: number;
9 unit?: string;
10 step?: number;
11 min?: number;
12 max?: number;
13 integer?: boolean;
14 invalid?: boolean;
15 disabled?: boolean;
16 title?: string;
19/** A numeric input that tolerates partial typing and commits parsed values. */
20export function NumberField(props: NumberFieldProps) {
21 const { label, value, onChange, scale = 1, unit, step, min, max, integer, invalid, disabled, title } = props;
22 const shown = formatShown(value * scale);
23 const [text, setText] = useState(shown);
24 useEffect(() => {
25 // Sync from outside unless the text already means the same number
26 if (Number(text) * 1 !== value * scale) setText(shown);
27 // eslint-disable-next-line react-hooks/exhaustive-deps
28 }, [value, scale]);
29 return (
30 <div className={`pl-field${disabled ? " pl-disabled" : ""}`} title={title}>
31 <label>{label}</label>
32 <input
33 type="number"
34 className={invalid ? "pl-invalid" : undefined}
35 value={text}
36 step={step ?? (integer ? 1 : "any")}
37 min={min}
38 max={max}
39 disabled={disabled}
40 onChange={(e) => {
41 setText(e.target.value);
42 const n = Number(e.target.value);
43 if (e.target.value.trim() === "" || !Number.isFinite(n)) return;
44 onChange(integer ? Math.round(n) : n / scale);
45 }}
46 onBlur={() => setText(shown)}
47 />
48 <span className="pl-unit">{unit ?? ""}</span>
49 </div>
50 );
53function formatShown(x: number): string {
54 if (!Number.isFinite(x)) return "";
55 const r = Number(x.toPrecision(10));
56 return String(r);
↑↓ move↵openescclose