74 lines
1.9 KiB
TypeScript
74 lines
1.9 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
interface NumberFieldProps {
|
|
value: number;
|
|
onChange: (value: number) => void;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
width?: number | string;
|
|
className?: string;
|
|
title?: string;
|
|
}
|
|
|
|
export function NumberField({ value, onChange, min, max, step, width, className, title }: NumberFieldProps) {
|
|
const [text, setText] = useState<string>(String(value));
|
|
|
|
useEffect(() => {
|
|
setText(String(value));
|
|
}, [value]);
|
|
|
|
const clamp = (n: number) => {
|
|
if (min !== undefined) n = Math.max(min, n);
|
|
if (max !== undefined) n = Math.min(max, n);
|
|
return n;
|
|
};
|
|
|
|
return (
|
|
<input
|
|
type="text"
|
|
inputMode="numeric"
|
|
className={className}
|
|
title={title}
|
|
style={width ? { width } : undefined}
|
|
value={text}
|
|
onClick={(e) => e.stopPropagation()}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
onChange={(e) => {
|
|
const raw = e.target.value;
|
|
if (!/^-?\d*$/.test(raw)) return;
|
|
setText(raw);
|
|
if (raw === '' || raw === '-') return;
|
|
const parsed = Number(raw);
|
|
if (!Number.isNaN(parsed)) {
|
|
const next = clamp(parsed);
|
|
if (next !== value) onChange(next);
|
|
}
|
|
}}
|
|
onBlur={() => {
|
|
if (text === '' || text === '-') {
|
|
const fallback = clamp(value);
|
|
onChange(fallback);
|
|
setText(String(fallback));
|
|
} else {
|
|
setText(String(value));
|
|
}
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'ArrowUp' && step) {
|
|
e.preventDefault();
|
|
const next = clamp(value + step);
|
|
onChange(next);
|
|
setText(String(next));
|
|
}
|
|
if (e.key === 'ArrowDown' && step) {
|
|
e.preventDefault();
|
|
const next = clamp(value - step);
|
|
onChange(next);
|
|
setText(String(next));
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
}
|