@v0xoss/number-input
Version:
The numeric input component is designed for users to enter a number, and increase or decrease the value using stepper buttons
585 lines (580 loc) • 23.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/number-input.tsx
var number_input_exports = {};
__export(number_input_exports, {
default: () => number_input_default
});
module.exports = __toCommonJS(number_input_exports);
var import_shared_icons2 = require("@v0xoss/shared-icons");
var import_react2 = require("react");
var import_system2 = require("@v0xoss/system");
// src/use-number-input.ts
var import_system = require("@v0xoss/system");
var import_use_safe_layout_effect = require("@v0xoss/use-safe-layout-effect");
var import_focus = require("@react-aria/focus");
var import_theme = require("@v0xoss/theme");
var import_react_utils = require("@v0xoss/react-utils");
var import_interactions = require("@react-aria/interactions");
var import_i18n = require("@react-aria/i18n");
var import_shared_utils = require("@v0xoss/shared-utils");
var import_numberfield = require("@react-stately/numberfield");
var import_numberfield2 = require("@react-aria/numberfield");
var import_react = require("react");
var import_form = require("@v0xoss/form");
function useNumberInput(originalProps) {
var _a, _b, _c;
const globalContext = (0, import_system.useProviderContext)();
const { validationBehavior: formValidationBehavior } = (0, import_form.useSlottedContext)(import_form.FormContext) || {};
const [props, variantProps] = (0, import_system.mapPropsVariants)(originalProps, import_theme.numberInput.variantKeys);
const {
ref,
as,
type,
label,
baseRef,
wrapperRef,
description,
className,
classNames,
autoFocus,
startContent,
endContent,
onClear,
onChange,
validationBehavior = (_a = formValidationBehavior != null ? formValidationBehavior : globalContext == null ? void 0 : globalContext.validationBehavior) != null ? _a : "native",
innerWrapperRef: innerWrapperRefProp,
onValueChange,
hideStepper,
...otherProps
} = props;
const [isFocusWithin, setFocusWithin] = (0, import_react.useState)(false);
const Component = as || "div";
const disableAnimation = (_c = (_b = originalProps.disableAnimation) != null ? _b : globalContext == null ? void 0 : globalContext.disableAnimation) != null ? _c : false;
const domRef = (0, import_react_utils.useDOMRef)(ref);
const baseDomRef = (0, import_react_utils.useDOMRef)(baseRef);
const inputWrapperRef = (0, import_react_utils.useDOMRef)(wrapperRef);
const innerWrapperRef = (0, import_react_utils.useDOMRef)(innerWrapperRefProp);
const { locale } = (0, import_i18n.useLocale)();
const state = (0, import_numberfield.useNumberFieldState)({
...originalProps,
validationBehavior,
locale,
onChange: (0, import_shared_utils.chain)(onValueChange, onChange)
});
const {
groupProps,
labelProps,
inputProps,
incrementButtonProps,
decrementButtonProps,
descriptionProps,
errorMessageProps,
isInvalid,
validationErrors,
validationDetails
} = (0, import_numberfield2.useNumberField)({ ...originalProps, validationBehavior }, state, domRef);
const inputValue = isNaN(state.numberValue) ? "" : state.numberValue;
const isFilled = !(0, import_shared_utils.isEmpty)(inputValue);
const isFilledWithin = isFilled || isFocusWithin;
const baseStyles = (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.base, className, isFilled ? "is-filled" : "");
const handleClear = (0, import_react.useCallback)(() => {
var _a2;
state.setInputValue("");
onClear == null ? void 0 : onClear();
(_a2 = domRef.current) == null ? void 0 : _a2.focus();
}, [state.setInputValue, onClear]);
(0, import_use_safe_layout_effect.useSafeLayoutEffect)(() => {
if (!domRef.current) return;
state.setInputValue(domRef.current.value);
}, [domRef.current]);
const { isFocusVisible, isFocused, focusProps } = (0, import_focus.useFocusRing)({
autoFocus,
isTextInput: true
});
const { isHovered, hoverProps } = (0, import_interactions.useHover)({ isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled) });
const { isHovered: isLabelHovered, hoverProps: labelHoverProps } = (0, import_interactions.useHover)({
isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled)
});
const { focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible } = (0, import_focus.useFocusRing)();
const { focusWithinProps } = (0, import_interactions.useFocusWithin)({
onFocusWithinChange: setFocusWithin
});
const { pressProps: clearPressProps } = (0, import_interactions.usePress)({
isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled) || !!(originalProps == null ? void 0 : originalProps.isReadOnly),
onPress: handleClear
});
const labelPlacement = (0, import_system.useLabelPlacement)({
labelPlacement: originalProps.labelPlacement,
label
});
const errorMessage = typeof props.errorMessage === "function" ? props.errorMessage({ isInvalid, validationErrors, validationDetails }) : props.errorMessage || (validationErrors == null ? void 0 : validationErrors.join(" "));
const isClearable = !!onClear || originalProps.isClearable;
const hasElements = !!label || !!description || !!errorMessage;
const hasPlaceholder = !!props.placeholder;
const hasLabel = !!label;
const hasHelper = !!description || !!errorMessage;
const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";
const shouldLabelBeInside = labelPlacement === "inside";
const isPlaceholderShown = domRef.current ? (!domRef.current.value || domRef.current.value === "" || !inputValue) && hasPlaceholder : false;
const isOutsideLeft = labelPlacement === "outside-left";
const hasStartContent = !!startContent;
const isLabelOutside = shouldLabelBeOutside ? labelPlacement === "outside-left" || hasPlaceholder || labelPlacement === "outside" && hasStartContent : false;
const isLabelOutsideAsPlaceholder = labelPlacement === "outside" && !hasPlaceholder && !hasStartContent;
const slots = (0, import_react.useMemo)(
() => (0, import_theme.numberInput)({
...variantProps,
isInvalid,
isClearable,
disableAnimation
}),
[(0, import_shared_utils.objectToDeps)(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation]
);
const handleKeyDown = (0, import_react.useCallback)(
(e) => {
if (e.key === "Escape" && inputValue && (isClearable || onClear) && !originalProps.isReadOnly) {
state.setInputValue("");
onClear == null ? void 0 : onClear();
}
},
[inputValue, state.setInputValue, onClear, isClearable, originalProps.isReadOnly]
);
const getBaseProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
ref: baseDomRef,
className: slots.base({ class: baseStyles }),
"data-slot": "base",
"data-filled": (0, import_shared_utils.dataAttr)(
isFilled || hasPlaceholder || hasStartContent || isPlaceholderShown
),
"data-filled-within": (0, import_shared_utils.dataAttr)(
isFilledWithin || hasPlaceholder || hasStartContent || isPlaceholderShown
),
"data-focus-within": (0, import_shared_utils.dataAttr)(isFocusWithin),
"data-focus-visible": (0, import_shared_utils.dataAttr)(isFocusVisible),
"data-readonly": (0, import_shared_utils.dataAttr)(originalProps.isReadOnly),
"data-focus": (0, import_shared_utils.dataAttr)(isFocused),
"data-hover": (0, import_shared_utils.dataAttr)(isHovered || isLabelHovered),
"data-required": (0, import_shared_utils.dataAttr)(originalProps.isRequired),
"data-invalid": (0, import_shared_utils.dataAttr)(isInvalid),
"data-disabled": (0, import_shared_utils.dataAttr)(originalProps.isDisabled),
"data-has-elements": (0, import_shared_utils.dataAttr)(hasElements),
"data-has-helper": (0, import_shared_utils.dataAttr)(hasHelper),
"data-has-label": (0, import_shared_utils.dataAttr)(hasLabel),
"data-has-value": (0, import_shared_utils.dataAttr)(!isPlaceholderShown),
...focusWithinProps,
...props2
};
},
[
slots,
baseStyles,
isFilled,
isFocused,
isHovered,
isLabelHovered,
isInvalid,
hasHelper,
hasLabel,
hasElements,
isPlaceholderShown,
hasStartContent,
isFocusWithin,
isFocusVisible,
hasPlaceholder,
focusWithinProps,
originalProps.isReadOnly,
originalProps.isRequired,
originalProps.isDisabled
]
);
const getLabelProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
"data-slot": "label",
className: slots.label({ class: classNames == null ? void 0 : classNames.label }),
...(0, import_shared_utils.mergeProps)(labelProps, labelHoverProps, props2)
};
},
[slots, isLabelHovered, labelProps, classNames == null ? void 0 : classNames.label]
);
const getNumberInputProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
"data-slot": "input",
"data-filled": (0, import_shared_utils.dataAttr)(isFilled),
"data-has-start-content": (0, import_shared_utils.dataAttr)(hasStartContent),
"data-has-end-content": (0, import_shared_utils.dataAttr)(!!endContent),
className: slots.input({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.input, isFilled ? "is-filled" : "")
}),
...(0, import_shared_utils.mergeProps)(
focusProps,
inputProps,
(0, import_react_utils.filterDOMProps)(otherProps, {
enabled: true,
labelable: true,
omitEventNames: new Set(Object.keys(inputProps)),
omitPropNames: /* @__PURE__ */ new Set(["value"])
}),
props2
),
"aria-readonly": (0, import_shared_utils.dataAttr)(originalProps.isReadOnly),
onChange: (0, import_shared_utils.chain)(inputProps.onChange, onChange),
onKeyDown: (0, import_shared_utils.chain)(inputProps.onKeyDown, props2.onKeyDown, handleKeyDown),
ref: domRef
};
},
[
slots,
focusProps,
inputProps,
otherProps,
isFilled,
hasStartContent,
endContent,
classNames == null ? void 0 : classNames.input,
originalProps.isReadOnly,
originalProps.isRequired,
onChange,
handleKeyDown
]
);
const getHiddenNumberInputProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
name: originalProps.name,
value: inputValue,
"data-slot": "hidden-input",
type: "hidden",
...props2
};
},
[inputValue, originalProps.name]
);
const getInputWrapperProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
ref: inputWrapperRef,
"data-slot": "input-wrapper",
"data-hover": (0, import_shared_utils.dataAttr)(isHovered || isLabelHovered),
"data-focus-visible": (0, import_shared_utils.dataAttr)(isFocusVisible),
"data-focus": (0, import_shared_utils.dataAttr)(isFocused),
className: slots.inputWrapper({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.inputWrapper, isFilled ? "is-filled" : "")
}),
...(0, import_shared_utils.mergeProps)(props2, hoverProps),
onClick: (e) => {
if (domRef.current && e.currentTarget === e.target) {
domRef.current.focus();
}
},
style: {
cursor: "text",
...props2.style
}
};
},
[
slots,
isHovered,
isLabelHovered,
isFocusVisible,
isFocused,
inputValue,
classNames == null ? void 0 : classNames.inputWrapper
]
);
const getInnerWrapperProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
ref: innerWrapperRef,
"data-slot": "inner-wrapper",
onClick: (e) => {
if (domRef.current && e.currentTarget === e.target) {
domRef.current.focus();
}
},
className: slots.innerWrapper({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.innerWrapper, props2 == null ? void 0 : props2.className)
}),
...(0, import_shared_utils.mergeProps)(groupProps, props2)
};
},
[slots, classNames == null ? void 0 : classNames.innerWrapper]
);
const getMainWrapperProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
"data-slot": "main-wrapper",
className: slots.mainWrapper({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.mainWrapper, props2 == null ? void 0 : props2.className)
})
};
},
[slots, classNames == null ? void 0 : classNames.mainWrapper]
);
const getHelperWrapperProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
"data-slot": "helper-wrapper",
className: slots.helperWrapper({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.helperWrapper, props2 == null ? void 0 : props2.className)
})
};
},
[slots, classNames == null ? void 0 : classNames.helperWrapper]
);
const getDescriptionProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
...descriptionProps,
"data-slot": "description",
className: slots.description({ class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.description, props2 == null ? void 0 : props2.className) })
};
},
[slots, classNames == null ? void 0 : classNames.description]
);
const getErrorMessageProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
...errorMessageProps,
"data-slot": "error-message",
className: slots.errorMessage({ class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.errorMessage, props2 == null ? void 0 : props2.className) })
};
},
[slots, errorMessageProps, classNames == null ? void 0 : classNames.errorMessage]
);
const getClearButtonProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
type: "button",
tabIndex: -1,
disabled: originalProps.isDisabled,
"aria-label": "clear input",
"data-slot": "clear-button",
"data-focus-visible": (0, import_shared_utils.dataAttr)(isClearButtonFocusVisible),
className: slots.clearButton({ class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.clearButton, props2 == null ? void 0 : props2.className) }),
...(0, import_shared_utils.mergeProps)(clearPressProps, clearFocusProps)
};
},
[slots, isClearButtonFocusVisible, clearPressProps, clearFocusProps, classNames == null ? void 0 : classNames.clearButton]
);
const getStepperWrapperProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
"data-slot": "stepper-wrapper",
className: slots.stepperWrapper({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.stepperWrapper, props2 == null ? void 0 : props2.className)
})
};
},
[slots]
);
const getStepperIncreaseButtonProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
...props2,
type: "button",
disabled: originalProps.isDisabled,
"data-slot": "increase-button",
className: slots.stepperButton({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.stepperButton, props2 == null ? void 0 : props2.className)
}),
...(0, import_shared_utils.mergeProps)(incrementButtonProps, props2)
};
},
[slots, incrementButtonProps, classNames == null ? void 0 : classNames.stepperButton]
);
const getStepperDecreaseButtonProps = (0, import_react.useCallback)(
(props2 = {}) => {
return {
type: "button",
disabled: originalProps.isDisabled,
"data-slot": "decrease-button",
className: slots.stepperButton({
class: (0, import_shared_utils.clsx)(classNames == null ? void 0 : classNames.stepperButton, props2 == null ? void 0 : props2.className)
}),
...(0, import_shared_utils.mergeProps)(decrementButtonProps, props2)
};
},
[slots, decrementButtonProps, classNames == null ? void 0 : classNames.stepperButton]
);
return {
Component,
classNames,
type,
domRef,
label,
description,
startContent,
endContent,
labelPlacement,
isClearable,
hasHelper,
hasStartContent,
isLabelOutside,
isOutsideLeft,
isLabelOutsideAsPlaceholder,
shouldLabelBeOutside,
shouldLabelBeInside,
hasPlaceholder,
isInvalid,
errorMessage,
hideStepper,
incrementButtonProps,
decrementButtonProps,
getBaseProps,
getLabelProps,
getNumberInputProps,
getHiddenNumberInputProps,
getMainWrapperProps,
getInputWrapperProps,
getInnerWrapperProps,
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
getStepperIncreaseButtonProps,
getStepperDecreaseButtonProps,
getStepperWrapperProps
};
}
// src/number-input-stepper.tsx
var import_button = require("@v0xoss/button");
var import_shared_icons = require("@v0xoss/shared-icons");
var import_jsx_runtime = require("react/jsx-runtime");
var NumberInputStepper = ({ direction, ...otherProps }) => {
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_button.Button, { disableRipple: true, isIconOnly: true, ...otherProps, children: direction == "up" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_shared_icons.ChevronUpIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_shared_icons.ChevronDownIcon, {}) });
};
NumberInputStepper.displayName = "HeroUI.NumberInputStepper";
var number_input_stepper_default = NumberInputStepper;
// src/number-input.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var NumberInput = (0, import_system2.forwardRef)((props, ref) => {
const {
Component,
label,
description,
isClearable,
startContent,
endContent,
labelPlacement,
hasHelper,
isOutsideLeft,
shouldLabelBeOutside,
errorMessage,
isInvalid,
hideStepper,
getBaseProps,
getLabelProps,
getNumberInputProps,
getHiddenNumberInputProps,
getInnerWrapperProps,
getInputWrapperProps,
getMainWrapperProps,
getHelperWrapperProps,
getDescriptionProps,
getErrorMessageProps,
getClearButtonProps,
getStepperIncreaseButtonProps,
getStepperDecreaseButtonProps,
getStepperWrapperProps
} = useNumberInput({ ...props, ref });
const labelContent = label ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { ...getLabelProps(), children: label }) : null;
const end = (0, import_react2.useMemo)(() => {
if (isClearable) {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { ...getClearButtonProps(), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_shared_icons2.CloseFilledIcon, {}) }),
endContent
] });
}
return endContent;
}, [isClearable, getClearButtonProps]);
const helperWrapper = (0, import_react2.useMemo)(() => {
const shouldShowError = isInvalid && errorMessage;
const hasContent = shouldShowError || description;
if (!hasHelper || !hasContent) return null;
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...getHelperWrapperProps(), children: shouldShowError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...getErrorMessageProps(), children: errorMessage }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...getDescriptionProps(), children: description }) });
}, [
hasHelper,
isInvalid,
errorMessage,
description,
getHelperWrapperProps,
getErrorMessageProps,
getDescriptionProps
]);
const innerWrapper = (0, import_react2.useMemo)(() => {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getInnerWrapperProps(), children: [
startContent,
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { ...getNumberInputProps() }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { ...getHiddenNumberInputProps() }),
end,
!hideStepper && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getStepperWrapperProps(), children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(number_input_stepper_default, { ...getStepperIncreaseButtonProps(), direction: "up" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(number_input_stepper_default, { ...getStepperDecreaseButtonProps(), direction: "down" })
] })
] });
}, [startContent, end, getNumberInputProps, getInnerWrapperProps]);
const mainWrapper = (0, import_react2.useMemo)(() => {
if (shouldLabelBeOutside) {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getMainWrapperProps(), children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getInputWrapperProps(), children: [
!isOutsideLeft ? labelContent : null,
innerWrapper
] }),
helperWrapper
] });
}
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...getInputWrapperProps(), children: [
labelContent,
innerWrapper
] }),
helperWrapper
] });
}, [
labelPlacement,
helperWrapper,
shouldLabelBeOutside,
labelContent,
innerWrapper,
errorMessage,
description,
getMainWrapperProps,
getInputWrapperProps,
getErrorMessageProps,
getDescriptionProps
]);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Component, { ...getBaseProps(), children: [
isOutsideLeft ? labelContent : null,
mainWrapper
] });
});
NumberInput.displayName = "HeroUI.NumberInput";
var number_input_default = NumberInput;