@yamada-ui/number-input
Version:
Yamada UI number input component
636 lines (634 loc) • 20.3 kB
JavaScript
"use client"
;
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