..
This commit is contained in:
parent
9689bfcc18
commit
62a385e4c9
1 changed files with 2649 additions and 809 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue