This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:58 +01:00
commit 62a385e4c9

View file

@ -1,6 +1,61 @@
import { useId, useState } from 'react';
import { useEffect, useId, useRef, useState } from 'react';
import type { InputHTMLAttributes } from 'react';
import type { Issue } from '../lib/types';
import { AlertCircle, Info, Check } from 'lucide-react';
const numberFromDraft = (text: string): number | null => {
if (!/^-?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) return null;
const number = Number(text);
return Number.isFinite(number) ? number : null;
};
/** Only unfinished typing is local; every complete numeric value is committed immediately. */
export function NumericInput({
value,
onChange,
onBlur,
onKeyDown,
...props
}: Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> & {
value: number;
onChange: (value: number) => void;
}) {
const [draft, setDraft] = useState(String(value));
const emitted = useRef(value);
useEffect(() => {
if (value !== emitted.current) setDraft(String(value));
emitted.current = value;
}, [value]);
return (
<input
{...props}
type="text"
inputMode={props.inputMode ?? (Number(props.min) < 0 ? 'text' : 'decimal')}
value={draft}
onChange={(event) => {
const text = event.target.value;
setDraft(text);
const number = numberFromDraft(text);
if (number !== null && number !== value) {
emitted.current = number;
onChange(number);
}
}}
onBlur={(event) => {
setDraft(String(value));
onBlur?.(event);
}}
onKeyDown={(event) => {
onKeyDown?.(event);
if (!event.defaultPrevented && (event.key === 'Enter' || event.key === 'Escape')) {
event.preventDefault();
event.currentTarget.blur();
}
}}
/>
);
}
export function NumberField({
label,
value,
@ -54,16 +109,14 @@ export function NumberField({
)}
</span>
<span className="number-input">
<input
<NumericInput
id={id}
type="number"
inputMode="decimal"
value={value}
min={min}
max={max}
step={step}
aria-describedby={hint ? `${id}-hint` : undefined}
onChange={(e) => onChange(Number(e.target.value))}
onChange={onChange}
/>
<span>{unit}</span>
</span>