UNPKG

@mantine/core

Version:

React components library focused on usability, accessibility and developer experience

357 lines (354 loc) 12.5 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useRef } from 'react'; import cx from 'clsx'; import { NumericFormat } from 'react-number-format'; import { useUncontrolled, clamp, assignRef, useMergedRef } from '@mantine/hooks'; import { noop } from '../../core/utils/noop/noop.mjs'; import { getSize } from '../../core/utils/get-size/get-size.mjs'; import { createVarsResolver } from '../../core/styles-api/create-vars-resolver/create-vars-resolver.mjs'; import { useResolvedStylesApi } from '../../core/styles-api/use-resolved-styles-api/use-resolved-styles-api.mjs'; import { useStyles } from '../../core/styles-api/use-styles/use-styles.mjs'; import '../../core/MantineProvider/Mantine.context.mjs'; import '../../core/MantineProvider/default-theme.mjs'; import '../../core/MantineProvider/MantineProvider.mjs'; import '../../core/MantineProvider/MantineThemeProvider/MantineThemeProvider.mjs'; import { useProps } from '../../core/MantineProvider/use-props/use-props.mjs'; import '../../core/MantineProvider/MantineCssVariables/MantineCssVariables.mjs'; import '../../core/Box/Box.mjs'; import { factory } from '../../core/factory/factory.mjs'; import '../../core/DirectionProvider/DirectionProvider.mjs'; import { InputBase } from '../InputBase/InputBase.mjs'; import { UnstyledButton } from '../UnstyledButton/UnstyledButton.mjs'; import { NumberInputChevron } from './NumberInputChevron.mjs'; import classes from './NumberInput.module.css.mjs'; const leadingDecimalZeroPattern = /^(0\.0*|-0(\.0*)?)$/; const leadingZerosPattern = /^-?0\d+(\.\d+)?\.?$/; function isNumberString(value) { return typeof value === "string" && value !== "" && !Number.isNaN(Number(value)); } function canIncrement(value) { if (typeof value === "number") { return value < Number.MAX_SAFE_INTEGER; } return value === "" || isNumberString(value) && Number(value) < Number.MAX_SAFE_INTEGER; } function getDecimalPlaces(inputValue) { return inputValue.toString().replace(".", "").length; } function isValidNumber(floatValue, value) { return (typeof floatValue === "number" ? floatValue < Number.MAX_SAFE_INTEGER : !Number.isNaN(Number(floatValue))) && !Number.isNaN(floatValue) && getDecimalPlaces(value) < 14 && value !== ""; } function isInRange(value, min, max) { if (value === void 0) { return true; } const minValid = min === void 0 || value >= min; const maxValid = max === void 0 || value <= max; return minValid && maxValid; } const defaultProps = { step: 1, clampBehavior: "blur", allowDecimal: true, allowNegative: true, withKeyboardEvents: true, allowLeadingZeros: true, trimLeadingZeroesOnBlur: true, startValue: 0 }; const varsResolver = createVarsResolver((_, { size }) => ({ controls: { "--ni-chevron-size": getSize(size, "ni-chevron-size") } })); function clampAndSanitizeInput(sanitizedValue, max, min) { const replaced = sanitizedValue.toString().replace(/^0+/, ""); const parsedValue = parseFloat(replaced); if (Number.isNaN(parsedValue)) { return replaced; } else if (parsedValue > Number.MAX_SAFE_INTEGER) { return max !== void 0 ? String(max) : replaced; } return clamp(parsedValue, min, max); } const NumberInput = factory((_props, ref) => { const props = useProps("NumberInput", defaultProps, _props); const { className, classNames, styles, unstyled, vars, onChange, onValueChange, value, defaultValue, max, min, step, hideControls, rightSection, isAllowed, clampBehavior, onBlur, allowDecimal, decimalScale, onKeyDown, onKeyDownCapture, handlersRef, startValue, disabled, rightSectionPointerEvents, allowNegative, readOnly, size, rightSectionWidth, stepHoldInterval, stepHoldDelay, allowLeadingZeros, withKeyboardEvents, trimLeadingZeroesOnBlur, ...others } = props; const getStyles = useStyles({ name: "NumberInput", classes, props, classNames, styles, unstyled, vars, varsResolver }); const { resolvedClassNames, resolvedStyles } = useResolvedStylesApi({ classNames, styles, props }); const [_value, setValue] = useUncontrolled({ value, defaultValue, finalValue: "", onChange }); const shouldUseStepInterval = stepHoldDelay !== void 0 && stepHoldInterval !== void 0; const inputRef = useRef(null); const onStepTimeoutRef = useRef(null); const stepCountRef = useRef(0); const handleValueChange = (payload, event) => { if (event.source === "event") { setValue( isValidNumber(payload.floatValue, payload.value) && !leadingDecimalZeroPattern.test(payload.value) && !(allowLeadingZeros ? leadingZerosPattern.test(payload.value) : false) ? payload.floatValue : payload.value ); } onValueChange?.(payload, event); }; const getDecimalPlaces2 = (inputValue) => { const match = String(inputValue).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); if (!match) { return 0; } return Math.max(0, (match[1] ? match[1].length : 0) - (match[2] ? +match[2] : 0)); }; const adjustCursor = (position) => { if (inputRef.current && typeof position !== "undefined") { inputRef.current.setSelectionRange(position, position); } }; const incrementRef = useRef(noop); incrementRef.current = () => { if (!canIncrement(_value)) { return; } let val; const currentValuePrecision = getDecimalPlaces2(_value); const stepPrecision = getDecimalPlaces2(step); const maxPrecision = Math.max(currentValuePrecision, stepPrecision); const factor = 10 ** maxPrecision; if (!isNumberString(_value) && (typeof _value !== "number" || Number.isNaN(_value))) { val = clamp(startValue, min, max); } else if (max !== void 0) { const incrementedValue = (Math.round(Number(_value) * factor) + Math.round(step * factor)) / factor; val = incrementedValue <= max ? incrementedValue : max; } else { val = (Math.round(Number(_value) * factor) + Math.round(step * factor)) / factor; } const formattedValue = val.toFixed(maxPrecision); setValue(parseFloat(formattedValue)); onValueChange?.( { floatValue: parseFloat(formattedValue), formattedValue, value: formattedValue }, { source: "increment" } ); setTimeout(() => adjustCursor(inputRef.current?.value.length), 0); }; const decrementRef = useRef(noop); decrementRef.current = () => { if (!canIncrement(_value)) { return; } let val; const minValue = min !== void 0 ? min : !allowNegative ? 0 : Number.MIN_SAFE_INTEGER; const currentValuePrecision = getDecimalPlaces2(_value); const stepPrecision = getDecimalPlaces2(step); const maxPrecision = Math.max(currentValuePrecision, stepPrecision); const factor = 10 ** maxPrecision; if (!isNumberString(_value) && typeof _value !== "number" || Number.isNaN(_value)) { val = clamp(startValue, minValue, max); } else { const decrementedValue = (Math.round(Number(_value) * factor) - Math.round(step * factor)) / factor; val = minValue !== void 0 && decrementedValue < minValue ? minValue : decrementedValue; } const formattedValue = val.toFixed(maxPrecision); setValue(parseFloat(formattedValue)); onValueChange?.( { floatValue: parseFloat(formattedValue), formattedValue, value: formattedValue }, { source: "decrement" } ); setTimeout(() => adjustCursor(inputRef.current?.value.length), 0); }; const handleKeyDown = (event) => { onKeyDown?.(event); if (readOnly || !withKeyboardEvents) { return; } if (event.key === "ArrowUp") { event.preventDefault(); incrementRef.current(); } if (event.key === "ArrowDown") { event.preventDefault(); decrementRef.current(); } }; const handleKeyDownCapture = (event) => { onKeyDownCapture?.(event); if (event.key === "Backspace") { const input = inputRef.current; if (input.selectionStart === 0 && input.selectionStart === input.selectionEnd) { event.preventDefault(); window.setTimeout(() => adjustCursor(0), 0); } } }; const handleBlur = (event) => { let sanitizedValue = _value; if (clampBehavior === "blur" && typeof sanitizedValue === "number") { sanitizedValue = clamp(sanitizedValue, min, max); } if (trimLeadingZeroesOnBlur && typeof sanitizedValue === "string" && getDecimalPlaces2(sanitizedValue) < 15) { sanitizedValue = clampAndSanitizeInput(sanitizedValue, max, min); } if (_value !== sanitizedValue) { setValue(sanitizedValue); } onBlur?.(event); }; assignRef(handlersRef, { increment: incrementRef.current, decrement: decrementRef.current }); const onStepHandleChange = (isIncrement) => { if (isIncrement) { incrementRef.current(); } else { decrementRef.current(); } stepCountRef.current += 1; }; const onStepLoop = (isIncrement) => { onStepHandleChange(isIncrement); if (shouldUseStepInterval) { const interval = typeof stepHoldInterval === "number" ? stepHoldInterval : stepHoldInterval(stepCountRef.current); onStepTimeoutRef.current = window.setTimeout(() => onStepLoop(isIncrement), interval); } }; const onStep = (event, isIncrement) => { event.preventDefault(); inputRef.current?.focus(); onStepHandleChange(isIncrement); if (shouldUseStepInterval) { onStepTimeoutRef.current = window.setTimeout(() => onStepLoop(isIncrement), stepHoldDelay); } }; const onStepDone = () => { if (onStepTimeoutRef.current) { window.clearTimeout(onStepTimeoutRef.current); } onStepTimeoutRef.current = null; stepCountRef.current = 0; }; const controls = /* @__PURE__ */ jsxs("div", { ...getStyles("controls"), children: [ /* @__PURE__ */ jsx( UnstyledButton, { ...getStyles("control"), tabIndex: -1, "aria-hidden": true, disabled: disabled || typeof _value === "number" && max !== void 0 && _value >= max, mod: { direction: "up" }, onMouseDown: (event) => event.preventDefault(), onPointerDown: (event) => { onStep(event, true); }, onPointerUp: onStepDone, onPointerLeave: onStepDone, children: /* @__PURE__ */ jsx(NumberInputChevron, { direction: "up" }) } ), /* @__PURE__ */ jsx( UnstyledButton, { ...getStyles("control"), tabIndex: -1, "aria-hidden": true, disabled: disabled || typeof _value === "number" && min !== void 0 && _value <= min, mod: { direction: "down" }, onMouseDown: (event) => event.preventDefault(), onPointerDown: (event) => { onStep(event, false); }, onPointerUp: onStepDone, onPointerLeave: onStepDone, children: /* @__PURE__ */ jsx(NumberInputChevron, { direction: "down" }) } ) ] }); return /* @__PURE__ */ jsx( InputBase, { component: NumericFormat, allowNegative, className: cx(classes.root, className), size, ...others, readOnly, disabled, value: _value, getInputRef: useMergedRef(ref, inputRef), onValueChange: handleValueChange, rightSection: hideControls || readOnly || !canIncrement(_value) ? rightSection : rightSection || controls, classNames: resolvedClassNames, styles: resolvedStyles, unstyled, __staticSelector: "NumberInput", decimalScale: allowDecimal ? decimalScale : 0, onKeyDown: handleKeyDown, onKeyDownCapture: handleKeyDownCapture, rightSectionPointerEvents: rightSectionPointerEvents ?? (disabled ? "none" : void 0), rightSectionWidth: rightSectionWidth ?? `var(--ni-right-section-width-${size || "sm"})`, allowLeadingZeros, onBlur: handleBlur, isAllowed: (val) => { if (clampBehavior === "strict") { if (isAllowed) { return isAllowed(val) && isInRange(val.floatValue, min, max); } return isInRange(val.floatValue, min, max); } return isAllowed ? isAllowed(val) : true; } } ); }); NumberInput.classes = { ...InputBase.classes, ...classes }; NumberInput.displayName = "@mantine/core/NumberInput"; export { NumberInput }; //# sourceMappingURL=NumberInput.mjs.map