import type { ChangeEvent, FocusEvent, InputHTMLAttributes } from 'react'; type NumberInputProps = Omit, 'type' | 'min' | 'max' | 'step'> & { min?: number; max?: number; // For a field that always holds a number (a level, a bonus): if it is left // empty, it goes back to its starting value instead of being saved blank. keepValue?: boolean; }; // A whole-number field that cannot hold a value outside its limits. A plain // only limits the arrow buttons; typing 500 // into it works. This one corrects the text as it is typed: anything above // `max` becomes `max`, anything below `min` becomes `min`, decimals are // dropped. Works the same whether the field is controlled (`value`) or not // (`defaultValue`): the handlers passed in see the corrected value. export function NumberInput({ min, max, keepValue, onChange, onBlur, ...rest }: NumberInputProps) { function limit(input: HTMLInputElement, leaving: boolean) { const raw = input.value; if (raw === '' || raw === '-') { // Nothing typed yet, or the start of a negative number. if (leaving) input.value = keepValue ? String(rest.defaultValue ?? rest.value ?? min ?? 0) : ''; return; } let value = Math.trunc(Number(raw)); if (Number.isNaN(value)) { input.value = ''; return; } if (max !== undefined && value > max) value = max; // While typing, a value below a minimum above 1 may just be unfinished // ("1" on the way to "15"), so that case waits until the field is left. if (min !== undefined && value < min && (leaving || min <= 1)) value = min; if (String(value) !== raw) input.value = String(value); } function handleChange(e: ChangeEvent) { limit(e.target, false); onChange?.(e); } function handleBlur(e: FocusEvent) { limit(e.target, true); onBlur?.(e); } return ; }