Files
InvokeAI/invokeai/frontend/web/src/common/components/InvNumberInput/InvNumberInput.tsx
2023-12-29 08:26:14 -05:00

134 lines
4.3 KiB
TypeScript

import { forwardRef, NumberInput as ChakraNumberInput } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $modifiers } from 'common/hooks/useGlobalModifiers';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es';
import type { FocusEventHandler } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { InvNumberInputField } from './InvNumberInputField';
import { InvNumberInputStepper } from './InvNumberInputStepper';
import type { InvNumberInputProps } from './types';
const isValidCharacter = (char: string) => /^[0-9\-.]$/i.test(char);
export const InvNumberInput = memo(
forwardRef<InvNumberInputProps, typeof ChakraNumberInput>(
(props: InvNumberInputProps, ref) => {
const {
value,
min = 0,
max,
step: _step = 1,
fineStep: _fineStep,
onChange: _onChange,
numberInputFieldProps,
...rest
} = props;
const [valueAsString, setValueAsString] = useState<string>(String(value));
const [valueAsNumber, setValueAsNumber] = useState<number>(value);
const modifiers = useStore($modifiers);
const step = useMemo(
() => (modifiers.shift ? _fineStep ?? _step : _step),
[modifiers.shift, _fineStep, _step]
);
const isInteger = useMemo(
() => Number.isInteger(_step) && Number.isInteger(_fineStep ?? 1),
[_step, _fineStep]
);
const inputMode = useMemo(
() => (isInteger ? 'numeric' : 'decimal'),
[isInteger]
);
const precision = useMemo(() => (isInteger ? 0 : 3), [isInteger]);
const onChange = useCallback(
(valueAsString: string, valueAsNumber: number) => {
setValueAsString(valueAsString);
if (isNaN(valueAsNumber)) {
return;
}
setValueAsNumber(valueAsNumber);
_onChange(valueAsNumber);
},
[_onChange]
);
// This appears to be unnecessary? Cannot figure out what it did but leaving it here in case
// it was important.
// const onClickStepper = useCallback(
// () => _onChange(Number(valueAsString)),
// [_onChange, valueAsString]
// );
const onBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
console.log('blur!');
if (!e.target.value) {
// If the input is empty, we set it to the minimum value
onChange(String(min), min);
} else {
// Otherwise, we round the value to the nearest multiple if integer, else 3 decimals
const roundedValue = isInteger
? roundToMultiple(valueAsNumber, _fineStep ?? _step)
: Number(valueAsNumber.toFixed(precision));
// Clamp to min/max
const clampedValue = clamp(roundedValue, min, max);
onChange(String(clampedValue), clampedValue);
}
},
[
_fineStep,
_step,
isInteger,
max,
min,
onChange,
precision,
valueAsNumber,
]
);
/**
* When `value` changes (e.g. from a diff source than this component), we need
* to update the internal `valueAsString`, but only if the actual value is different
* from the current value.
*/
useEffect(() => {
if (value !== valueAsNumber) {
setValueAsString(String(value));
setValueAsNumber(value);
}
}, [value, valueAsNumber]);
return (
<ChakraNumberInput
ref={ref}
min={min}
max={max}
step={step}
value={valueAsString}
onChange={onChange}
clampValueOnBlur={false}
isValidCharacter={isValidCharacter}
focusInputOnChange={false}
onPaste={stopPastePropagation}
inputMode={inputMode}
precision={precision}
variant="filled"
{...rest}
>
<InvNumberInputField onBlur={onBlur} {...numberInputFieldProps} />
<InvNumberInputStepper />
</ChakraNumberInput>
);
}
)
);
InvNumberInput.displayName = 'InvNumberInput';