@gravity-ui/uikit
Version:
Gravity UI base styling and components
142 lines (141 loc) • 6.53 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import * as React from 'react';
import { KeyCode } from "../../constants.js";
import { useControlledState, useForkRef } from "../../hooks/index.js";
import { useFormResetHandler } from "../../hooks/private/index.js";
import { TextInput } from "../controls/TextInput/index.js";
import { getInputControlState } from "../controls/utils.js";
import { block } from "../utils/cn.js";
import { NumericArrows } from "./NumericArrows/NumericArrows.js";
import { areStringRepresentationOfNumbersEqual, clampToNearestStepValue, getInputPattern, getInternalState, getParsedValue, getPossibleNumberSubstring, updateCursorPosition, } from "./utils.js";
import "./NumberInput.css";
const b = block('number-input');
function getStringValue(value) {
return value === null ? '' : String(value);
}
export const NumberInput = React.forwardRef(function NumberInput({ endContent, defaultValue: externalDefaultValue, ...props }, ref) {
const { value: externalValue, onChange: handleChange, onUpdate: externalOnUpdate, min: externalMin, max: externalMax, shiftMultiplier: externalShiftMultiplier = 10, step: externalStep, size = 'm', view = 'normal', disabled, hiddenControls, validationState, onBlur, onKeyDown, allowDecimal = false, className, } = props;
const { min, max, step: baseStep, value: internalValue, defaultValue, shiftMultiplier, } = getInternalState({
min: externalMin,
max: externalMax,
step: externalStep,
shiftMultiplier: externalShiftMultiplier,
allowDecimal,
value: externalValue,
defaultValue: externalDefaultValue,
});
const [value, setValue] = useControlledState(internalValue, defaultValue ?? null, externalOnUpdate);
const [inputValue, setInputValue] = React.useState(getStringValue(value));
React.useEffect(() => {
const stringPropsValue = getStringValue(value);
setInputValue((currentInputValue) => {
if (!areStringRepresentationOfNumbersEqual(currentInputValue, stringPropsValue)) {
return stringPropsValue;
}
return currentInputValue;
});
}, [value]);
const clamp = !(allowDecimal && !externalStep);
const safeValue = value ?? 0;
const state = getInputControlState(validationState);
const canIncrementNumber = safeValue < (max ?? Number.MAX_SAFE_INTEGER);
const canDecrementNumber = safeValue > (min ?? Number.MIN_SAFE_INTEGER);
const innerControlRef = React.useRef(null);
const fieldRef = useFormResetHandler({
initialValue: value,
onReset: setValue,
});
const handleRef = useForkRef(props.controlRef, innerControlRef, fieldRef);
const handleValueDelta = (e, direction) => {
const step = e.shiftKey ? shiftMultiplier * baseStep : baseStep;
const deltaWithSign = direction === 'up' ? step : -step;
if (direction === 'up' ? canIncrementNumber : canDecrementNumber) {
const newValue = clampToNearestStepValue({
value: safeValue + deltaWithSign,
step: baseStep,
min,
max,
direction,
});
setValue?.(newValue);
setInputValue(newValue.toString());
}
};
const handleKeyDown = (e) => {
if (e.key === KeyCode.ARROW_DOWN) {
e.preventDefault();
handleValueDelta(e, 'down');
}
else if (e.key === KeyCode.ARROW_UP) {
e.preventDefault();
handleValueDelta(e, 'up');
}
else if (e.key === KeyCode.HOME) {
e.preventDefault();
if (min !== undefined) {
setValue?.(min);
setInputValue(min.toString());
}
}
else if (e.key === KeyCode.END) {
e.preventDefault();
if (max !== undefined) {
const newValue = clampToNearestStepValue({
value: max,
step: baseStep,
min,
max,
});
setValue?.(newValue);
setInputValue(newValue.toString());
}
}
onKeyDown?.(e);
};
const handleBlur = (e) => {
if (clamp && value !== null) {
const clampedValue = clampToNearestStepValue({
value,
step: baseStep,
min,
max,
});
if (value !== clampedValue) {
setValue?.(clampedValue);
}
setInputValue(clampedValue.toString());
}
onBlur?.(e);
};
const handleUpdate = (v) => {
setInputValue(v);
const preparedStringValue = getPossibleNumberSubstring(v, allowDecimal);
updateCursorPosition(innerControlRef, v, preparedStringValue);
const { valid, value: parsedNumberValue } = getParsedValue(preparedStringValue);
if (valid && parsedNumberValue !== value) {
setValue?.(parsedNumberValue);
}
};
const handleInput = (e) => {
const preparedStringValue = getPossibleNumberSubstring(e.currentTarget.value, allowDecimal);
updateCursorPosition(innerControlRef, e.currentTarget.value, preparedStringValue);
};
return (_jsx(TextInput, { ...props, className: b({ size, view, state }, className), controlProps: {
onInput: handleInput,
...props.controlProps,
role: 'spinbutton',
inputMode: allowDecimal ? 'decimal' : 'numeric',
pattern: props.controlProps?.pattern ?? getInputPattern(allowDecimal, false),
'aria-valuemin': props.min,
'aria-valuemax': props.max,
'aria-valuenow': value === null ? undefined : value,
}, controlRef: handleRef, value: inputValue, onChange: handleChange, onUpdate: handleUpdate, onKeyDown: handleKeyDown, onBlur: handleBlur, ref: ref, endContent: _jsxs(React.Fragment, { children: [endContent, hiddenControls ? null : (_jsx(NumericArrows, { className: b('arrows'), size: size, disabled: disabled, onUpClick: (e) => {
innerControlRef.current?.focus();
handleValueDelta(e, 'up');
}, onDownClick: (e) => {
innerControlRef.current?.focus();
handleValueDelta(e, 'down');
} }))] }) }));
});
//# sourceMappingURL=NumberInput.js.map