Fire native change events from Input number steppers
Route stepper value changes through the native input setter and dispatch input/change events so React onChange and DOM listeners see the update. Read the live DOM value while holding a stepper so repeats don't lag.
This commit is contained in:
@@ -9,6 +9,12 @@ type Props = Pick<InputProps<any>, "min" | "max" | "step" | "decimal"> & {
|
|||||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
buttonDownRef: React.MutableRefObject<boolean>;
|
buttonDownRef: React.MutableRefObject<boolean>;
|
||||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
/**
|
||||||
|
* Applies the stepped value through the native input setter so that the
|
||||||
|
* `input`/`change` events (React's `onChange`) actually fire. Falls back to
|
||||||
|
* `setValue` when omitted.
|
||||||
|
*/
|
||||||
|
applyValue?: (nextValue: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +29,7 @@ export default function NumberInputButtons({
|
|||||||
buttonDownRef,
|
buttonDownRef,
|
||||||
inputRef,
|
inputRef,
|
||||||
decimal,
|
decimal,
|
||||||
|
applyValue,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const PRESS_TRIGGER_TIMEOUT = 200;
|
const PRESS_TRIGGER_TIMEOUT = 200;
|
||||||
const PRESS_REPEAT_INTERVAL = 50;
|
const PRESS_REPEAT_INTERVAL = 50;
|
||||||
@@ -65,19 +72,30 @@ export default function NumberInputButtons({
|
|||||||
|
|
||||||
const endsWithDecimalPoint = String(value).match(/\.$/);
|
const endsWithDecimalPoint = String(value).match(/\.$/);
|
||||||
|
|
||||||
|
/** The DOM value is the freshest source while a press repeats: the native
|
||||||
|
* setter writes it before React re-renders this component. */
|
||||||
|
function currentValue() {
|
||||||
|
const elementValue = inputRef.current?.value;
|
||||||
|
return twuiNumberfy(elementValue ?? value, decimal);
|
||||||
|
}
|
||||||
|
|
||||||
function applyDelta(delta: number) {
|
function applyDelta(delta: number) {
|
||||||
setValue((previousValue) => {
|
const nextValue = clamp(currentValue() + delta);
|
||||||
const currentValue = twuiNumberfy(previousValue, decimal);
|
|
||||||
const nextValue = clamp(currentValue + delta);
|
|
||||||
|
|
||||||
// Round off float drift (e.g. 0.1 + 0.2) before stringifying.
|
// Round off float drift (e.g. 0.1 + 0.2) before stringifying.
|
||||||
const roundedValue =
|
const roundedValue =
|
||||||
decimal === undefined
|
decimal === undefined
|
||||||
? Number(nextValue.toFixed(10))
|
? Number(nextValue.toFixed(10))
|
||||||
: Number(nextValue.toFixed(decimal));
|
: Number(nextValue.toFixed(decimal));
|
||||||
|
|
||||||
return String(roundedValue);
|
const finalValue = String(roundedValue);
|
||||||
});
|
|
||||||
|
if (applyValue) {
|
||||||
|
applyValue(finalValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(finalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepSize() {
|
function stepSize() {
|
||||||
|
|||||||
@@ -286,6 +286,22 @@ export default function Input<KeyType extends string>(
|
|||||||
props.onChange?.(e);
|
props.onChange?.(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stepper buttons must behave like typing: write through the native setter
|
||||||
|
* and dispatch `input` (React's `onChange`) plus `change` (native
|
||||||
|
* listeners). React's value tracker sees the value already reported by
|
||||||
|
* `input`, so the `change` dispatch cannot fire `onChange` twice. */
|
||||||
|
function applyStepperValue(nextValue: string) {
|
||||||
|
const element = inputRef?.current;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
setNativeInputValue(element, nextValue);
|
||||||
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(nextValue);
|
||||||
|
}
|
||||||
|
|
||||||
// function updateValue(
|
// function updateValue(
|
||||||
// v: string,
|
// v: string,
|
||||||
// el?: HTMLInputElement | HTMLTextAreaElement,
|
// el?: HTMLInputElement | HTMLTextAreaElement,
|
||||||
@@ -524,6 +540,7 @@ export default function Input<KeyType extends string>(
|
|||||||
step={props.step}
|
step={props.step}
|
||||||
buttonDownRef={buttonDownRef}
|
buttonDownRef={buttonDownRef}
|
||||||
decimal={decimal}
|
decimal={decimal}
|
||||||
|
applyValue={applyStepperValue}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user