UNPKG

@yamada-ui/number-input

Version:

Yamada UI number input component

636 lines (634 loc) • 20.3 kB
"use client" "use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { NumberInput: () => NumberInput, useNumberInput: () => useNumberInput }); module.exports = __toCommonJS(index_exports); // src/number-input.tsx var import_core = require("@yamada-ui/core"); var import_form_control = require("@yamada-ui/form-control"); var import_icon = require("@yamada-ui/icon"); var import_use_counter = require("@yamada-ui/use-counter"); var import_use_event_listener = require("@yamada-ui/use-event-listener"); var import_use_interval = require("@yamada-ui/use-interval"); var import_utils = require("@yamada-ui/utils"); var import_react = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var isDefaultValidCharacter = (character) => /^[Ee0-9+\-.]$/.test(character); var isValidNumericKeyboardEvent = ({ key, altKey, ctrlKey, metaKey }, isValid) => { const isModifierKey = ctrlKey || altKey || metaKey; const isSingleCharacterKey = key.length === 1; if (!isSingleCharacterKey || isModifierKey) return true; return isValid(key); }; var getStep = ({ ctrlKey, metaKey, shiftKey }) => { let ratio = 1; if (metaKey || ctrlKey) ratio = 0.1; if (shiftKey) ratio = 10; return ratio; }; var useNumberInput = (props = {}) => { const { id, name, allowMouseWheel, clampValueOnBlur = true, defaultValue, focusInputOnChange = true, format: formatProp, getAriaValueText: getAriaValueTextProp, inputMode = "decimal", isValidCharacter: isValidCharacterProp, keepWithinRange = true, max: maxValue = Number.MAX_SAFE_INTEGER, min: minValue = Number.MIN_SAFE_INTEGER, parse: parseProp, pattern = "[0-9]*(.[0-9]+)?", precision, step: stepProp, value: valueProp, onChange: onChangeProp, onInvalid: onInvalidProp, ...rest } = (0, import_form_control.useFormControlProps)(props); const { "aria-invalid": invalid, disabled, readOnly, required, onBlur: onBlurProp, onFocus: onFocusProp, ...formControlProps } = (0, import_utils.pickObject)(rest, import_form_control.formControlProperties); const [focused, setFocused] = (0, import_react.useState)(false); const interactive = !(readOnly || disabled); const inputRef = (0, import_react.useRef)(null); const inputSelectionRef = (0, import_react.useRef)(null); const incrementRef = (0, import_react.useRef)(null); const decrementRef = (0, import_react.useRef)(null); const onFocus = (0, import_utils.useCallbackRef)( (0, import_utils.handlerAll)(onFocusProp, (ev) => { var _a, _b; setFocused(true); if (!inputSelectionRef.current) return; ev.target.selectionStart = (_a = inputSelectionRef.current.start) != null ? _a : ev.currentTarget.value.length; ev.currentTarget.selectionEnd = (_b = inputSelectionRef.current.end) != null ? _b : ev.currentTarget.selectionStart; }) ); const onBlur = (0, import_utils.useCallbackRef)( (0, import_utils.handlerAll)(onBlurProp, () => { setFocused(false); if (clampValueOnBlur) validateAndClamp(); }) ); const onInvalid = (0, import_utils.useCallbackRef)(onInvalidProp); const getAriaValueText = (0, import_utils.useCallbackRef)(getAriaValueTextProp); const isValidCharacter = (0, import_utils.useCallbackRef)( isValidCharacterProp != null ? isValidCharacterProp : isDefaultValidCharacter ); const { cast, max, min, out, setValue, update, value, valueAsNumber, ...counter } = (0, import_use_counter.useCounter)({ defaultValue, keepWithinRange, max: maxValue, min: minValue, precision, step: stepProp, value: valueProp, onChange: onChangeProp }); const valueText = (0, import_react.useMemo)(() => { let text = getAriaValueText(value); if (text != null) return text; text = value.toString(); return !text ? void 0 : text; }, [value, getAriaValueText]); const sanitize = (0, import_react.useCallback)( (value2) => value2.split("").filter(isValidCharacter).join(""), [isValidCharacter] ); const parse = (0, import_react.useCallback)( (value2) => { var _a; return (_a = parseProp == null ? void 0 : parseProp(value2)) != null ? _a : value2; }, [parseProp] ); const format = (0, import_react.useCallback)( (value2) => { var _a; return ((_a = formatProp == null ? void 0 : formatProp(value2)) != null ? _a : value2).toString(); }, [formatProp] ); const increment = (0, import_react.useCallback)( (step = stepProp != null ? stepProp : 1) => { if (interactive) counter.increment(step); }, [interactive, counter, stepProp] ); const decrement = (0, import_react.useCallback)( (step = stepProp != null ? stepProp : 1) => { if (interactive) counter.decrement(step); }, [interactive, counter, stepProp] ); const validateAndClamp = (0, import_react.useCallback)(() => { let nextValue = value; if (value === "") return; const valueStartsWithE = /^[eE]/.test(value.toString()); if (valueStartsWithE) { setValue(""); } else { if (valueAsNumber < minValue) nextValue = minValue; if (valueAsNumber > maxValue) nextValue = maxValue; cast(nextValue); } }, [cast, maxValue, minValue, setValue, value, valueAsNumber]); const onChange = (0, import_react.useCallback)( (ev) => { if (ev.nativeEvent.isComposing) return; const parsedInput = parse(ev.currentTarget.value); update(sanitize(parsedInput)); inputSelectionRef.current = { end: ev.currentTarget.selectionEnd, start: ev.currentTarget.selectionStart }; }, [parse, update, sanitize] ); const onKeyDown = (0, import_react.useCallback)( (ev) => { if (ev.nativeEvent.isComposing) return; if (!isValidNumericKeyboardEvent(ev, isValidCharacter)) ev.preventDefault(); const step = getStep(ev) * (stepProp != null ? stepProp : 1); const keyMap = { ArrowDown: () => decrement(step), ArrowUp: () => increment(step), End: () => update(maxValue), Home: () => update(minValue) }; const action = keyMap[ev.key]; if (!action) return; ev.preventDefault(); action(ev); }, [ decrement, increment, isValidCharacter, maxValue, minValue, stepProp, update ] ); const { down, isSpinning, stop, up } = useSpinner(increment, decrement); useAttributeObserver(incrementRef, ["disabled"], isSpinning, stop); useAttributeObserver(decrementRef, ["disabled"], isSpinning, stop); const focusInput = (0, import_react.useCallback)(() => { if (focusInputOnChange) requestAnimationFrame(() => { var _a; (_a = inputRef.current) == null ? void 0 : _a.focus(); }); }, [focusInputOnChange]); const eventUp = (0, import_react.useCallback)( (ev) => { ev.preventDefault(); up(); focusInput(); }, [focusInput, up] ); const eventDown = (0, import_react.useCallback)( (ev) => { ev.preventDefault(); down(); focusInput(); }, [focusInput, down] ); (0, import_utils.useUpdateEffect)(() => { if (valueAsNumber > maxValue) { onInvalid("rangeOverflow", format(value), valueAsNumber); } else if (valueAsNumber < minValue) { onInvalid("rangeOverflow", format(value), valueAsNumber); } }, [valueAsNumber, value, format, onInvalid]); (0, import_utils.useSafeLayoutEffect)(() => { if (!inputRef.current) return; const notInSync = inputRef.current.value != value; if (!notInSync) return; const parsedInput = parse(inputRef.current.value); setValue(sanitize(parsedInput)); }, [parse, sanitize]); (0, import_use_event_listener.useEventListener)( () => inputRef.current, "wheel", (ev) => { var _a, _b; const ownerDocument = (_b = (_a = inputRef.current) == null ? void 0 : _a.ownerDocument) != null ? _b : document; const focused2 = ownerDocument.activeElement === inputRef.current; if (!allowMouseWheel || !focused2) return; ev.preventDefault(); const step = getStep(ev) * (stepProp != null ? stepProp : 1); const direction = Math.sign(ev.deltaY); if (direction === -1) { increment(step); } else if (direction === 1) { decrement(step); } }, { passive: false } ); const getInputProps = (0, import_react.useCallback)( (props2 = {}, ref = null) => ({ id, type: "text", name, disabled, inputMode, pattern, readOnly, required, role: "spinbutton", ...formControlProps, ...props2, ref: (0, import_utils.mergeRefs)(inputRef, ref), "aria-invalid": (0, import_utils.ariaAttr)(invalid != null ? invalid : out), "aria-valuemax": maxValue, "aria-valuemin": minValue, "aria-valuenow": Number.isNaN(valueAsNumber) ? void 0 : valueAsNumber, "aria-valuetext": valueText, autoComplete: "off", autoCorrect: "off", max: maxValue, min: minValue, step: stepProp, value: format(value), onBlur: (0, import_utils.handlerAll)(props2.onBlur, onBlur), onChange: (0, import_utils.handlerAll)(props2.onChange, onChange), onFocus: (0, import_utils.handlerAll)(props2.onFocus, onFocus), onKeyDown: (0, import_utils.handlerAll)(props2.onKeyDown, onKeyDown) }), [ id, name, inputMode, pattern, required, disabled, readOnly, formControlProps, maxValue, minValue, stepProp, format, value, valueAsNumber, valueText, invalid, out, onChange, onKeyDown, onFocus, onBlur ] ); const getIncrementProps = (0, import_react.useCallback)( (props2 = {}, ref = null) => { var _a; const trulyDisabled = disabled || keepWithinRange && max; return { "aria-label": "Increase", disabled: trulyDisabled, readOnly, required, ...formControlProps, ...props2, ref: (0, import_utils.mergeRefs)(ref, incrementRef), style: { ...props2.style, cursor: readOnly ? "not-allowed" : (_a = props2.style) == null ? void 0 : _a.cursor }, tabIndex: -1, onPointerDown: (0, import_utils.handlerAll)(props2.onPointerDown, (ev) => { if (ev.button === 0 && !trulyDisabled) eventUp(ev); }), onPointerLeave: (0, import_utils.handlerAll)(props2.onPointerLeave, stop), onPointerUp: (0, import_utils.handlerAll)(props2.onPointerUp, stop) }; }, [ disabled, keepWithinRange, max, required, readOnly, formControlProps, stop, eventUp ] ); const getDecrementProps = (0, import_react.useCallback)( (props2 = {}, ref = null) => { var _a; const trulyDisabled = disabled || keepWithinRange && min; return { "aria-label": "Decrease", disabled: trulyDisabled, readOnly, required, ...formControlProps, ...props2, ref: (0, import_utils.mergeRefs)(ref, decrementRef), style: { ...props2.style, cursor: readOnly ? "not-allowed" : (_a = props2.style) == null ? void 0 : _a.cursor }, tabIndex: -1, onPointerDown: (0, import_utils.handlerAll)(props2.onPointerDown, (ev) => { if (ev.button === 0 && !trulyDisabled) eventDown(ev); }), onPointerLeave: (0, import_utils.handlerAll)(props2.onPointerLeave, stop), onPointerUp: (0, import_utils.handlerAll)(props2.onPointerUp, stop) }; }, [ disabled, keepWithinRange, min, required, readOnly, formControlProps, stop, eventDown ] ); return { disabled, focused, /** * @deprecated Use `disabled` instead. */ isDisabled: disabled, /** * @deprecated Use `readOnly` instead. */ isReadOnly: readOnly, /** * @deprecated Use `required` instead. */ isRequired: required, props: rest, readOnly, required, value: format(value), valueAsNumber, getDecrementProps, getIncrementProps, getInputProps }; }; var INTERVAL = 50; var DELAY = 300; var useSpinner = (increment, decrement) => { const [isSpinning, setIsSpinning] = (0, import_react.useState)(false); const [action, setAction] = (0, import_react.useState)(null); const [isOnce, setIsOnce] = (0, import_react.useState)(true); const timeoutRef = (0, import_react.useRef)(null); const removeTimeout = () => clearTimeout(timeoutRef.current); (0, import_use_interval.useInterval)( () => { if (action === "increment") increment(); if (action === "decrement") decrement(); }, isSpinning ? INTERVAL : null ); const up = (0, import_react.useCallback)(() => { if (isOnce) increment(); timeoutRef.current = setTimeout(() => { setIsOnce(false); setIsSpinning(true); setAction("increment"); }, DELAY); }, [increment, isOnce]); const down = (0, import_react.useCallback)(() => { if (isOnce) decrement(); timeoutRef.current = setTimeout(() => { setIsOnce(false); setIsSpinning(true); setAction("decrement"); }, DELAY); }, [decrement, isOnce]); const stop = (0, import_react.useCallback)(() => { setIsOnce(true); setIsSpinning(false); removeTimeout(); }, []); (0, import_react.useEffect)(() => { return () => removeTimeout(); }, []); return { down, isSpinning, stop, up }; }; var useAttributeObserver = (ref, attributeFilter, enabled, func) => { (0, import_react.useEffect)(() => { var _a; if (!ref.current || !enabled) return; const ownerDocument = (_a = ref.current.ownerDocument.defaultView) != null ? _a : window; const observer = new ownerDocument.MutationObserver((changes) => { for (const { type, attributeName } of changes) { if (type === "attributes" && attributeName && attributeFilter.includes(attributeName)) func(); } }); observer.observe(ref.current, { attributeFilter, attributes: true }); return () => observer.disconnect(); }); }; var [NumberInputContextProvider, useNumberInputContext] = (0, import_utils.createContext)({ name: "NumberInputContext", errorMessage: `useNumberInputContext returned is 'undefined'. Seems you forgot to wrap the components in "<NumberInput />"` }); var NumberInput = (0, import_core.forwardRef)( (props, ref) => { const [styles, mergedProps] = (0, import_core.useComponentMultiStyle)("NumberInput", props); const { className, isStepper = true, stepper = isStepper, addonProps, containerProps, decrementProps, incrementProps, ...computedProps } = (0, import_core.omitThemeProps)(mergedProps); const { props: rest, getDecrementProps, getIncrementProps, getInputProps } = useNumberInput(computedProps); const css = { position: "relative", zIndex: 0, ...styles.container }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( NumberInputContextProvider, { value: { styles, getDecrementProps, getIncrementProps, getInputProps }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( import_core.ui.div, { className: (0, import_utils.cx)("ui-number-input", className), role: "group", __css: css, ...containerProps, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberInputField, { ...getInputProps(rest, ref) }), stepper ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(NumberInputAddon, { ...addonProps, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberIncrementStepper, { ...incrementProps }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NumberDecrementStepper, { ...decrementProps }) ] }) : null ] } ) } ); } ); NumberInput.displayName = "NumberInput"; NumberInput.__ui__ = "NumberInput"; var NumberInputField = (0, import_core.forwardRef)( ({ className, ...rest }, ref) => { const { styles } = useNumberInputContext(); const css = { width: "100%", ...styles.field }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_core.ui.input, { ref, className: (0, import_utils.cx)("ui-number-input__field", className), __css: css, ...rest } ); } ); NumberInputField.displayName = "NumberInputField"; NumberInputField.__ui__ = "NumberInputField"; var NumberInputAddon = (0, import_core.forwardRef)( ({ className, ...rest }, ref) => { const { styles } = useNumberInputContext(); const css = { display: "flex", flexDirection: "column", height: "calc(100% - 2px)", insetEnd: "0px", margin: "1px", position: "absolute", top: "0", zIndex: "fallback(yamcha, 1)", ...styles.addon }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_core.ui.div, { ref, className: (0, import_utils.cx)("ui-number-input__addon", className), "aria-hidden": true, __css: css, ...rest } ); } ); NumberInputAddon.displayName = "NumberInputAddon"; NumberInputAddon.__ui__ = "NumberInputAddon"; var Stepper = (0, import_core.ui)("button", { baseStyle: { alignItems: "center", cursor: "pointer", display: "flex", flex: 1, justifyContent: "center", lineHeight: "normal", transitionDuration: "normal", transitionProperty: "common", userSelect: "none" } }); var NumberIncrementStepper = (0, import_core.forwardRef)(({ className, children, ...rest }, ref) => { const { styles, getIncrementProps } = useNumberInputContext(); const css = { ...styles.stepper }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( Stepper, { className: (0, import_utils.cx)("ui-number-input__stepper--up", className), ...getIncrementProps(rest, ref), __css: css, children: children != null ? children : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon.ChevronIcon, { __css: { transform: "rotate(180deg)" } }) } ); }); NumberIncrementStepper.displayName = "NumberIncrementStepper"; NumberIncrementStepper.__ui__ = "NumberIncrementStepper"; var NumberDecrementStepper = (0, import_core.forwardRef)(({ className, children, ...rest }, ref) => { const { styles, getDecrementProps } = useNumberInputContext(); const css = { ...styles.stepper }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( Stepper, { className: (0, import_utils.cx)("ui-number-input__stepper--down", className), ...getDecrementProps(rest, ref), __css: css, children: children != null ? children : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon.ChevronIcon, {}) } ); }); NumberDecrementStepper.displayName = "NumberDecrementStepper"; NumberDecrementStepper.__ui__ = "NumberDecrementStepper"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { NumberInput, useNumberInput }); //# sourceMappingURL=index.js.map