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 ;
}