UNPKG

@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

445 lines (442 loc) 15.5 kB
"use client"; // src/use-number-input.ts import { useLabelPlacement, mapPropsVariants, useProviderContext } from "@v0xoss/system"; import { useSafeLayoutEffect } from "@v0xoss/use-safe-layout-effect"; import { useFocusRing } from "@react-aria/focus"; import { numberInput } from "@v0xoss/theme"; import { useDOMRef, filterDOMProps } from "@v0xoss/react-utils"; import { useFocusWithin, useHover, usePress } from "@react-aria/interactions"; import { useLocale } from "@react-aria/i18n"; import { clsx, dataAttr, isEmpty, objectToDeps, chain, mergeProps } from "@v0xoss/shared-utils"; import { useNumberFieldState } from "@react-stately/numberfield"; import { useNumberField as useAriaNumberInput } from "@react-aria/numberfield"; import { useMemo, useCallback, useState } from "react"; import { FormContext, useSlottedContext } from "@v0xoss/form"; function useNumberInput(originalProps) { var _a, _b, _c; const globalContext = useProviderContext(); const { validationBehavior: formValidationBehavior } = useSlottedContext(FormContext) || {}; const [props, variantProps] = mapPropsVariants(originalProps, 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] = 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 = useDOMRef(ref); const baseDomRef = useDOMRef(baseRef); const inputWrapperRef = useDOMRef(wrapperRef); const innerWrapperRef = useDOMRef(innerWrapperRefProp); const { locale } = useLocale(); const state = useNumberFieldState({ ...originalProps, validationBehavior, locale, onChange: chain(onValueChange, onChange) }); const { groupProps, labelProps, inputProps, incrementButtonProps, decrementButtonProps, descriptionProps, errorMessageProps, isInvalid, validationErrors, validationDetails } = useAriaNumberInput({ ...originalProps, validationBehavior }, state, domRef); const inputValue = isNaN(state.numberValue) ? "" : state.numberValue; const isFilled = !isEmpty(inputValue); const isFilledWithin = isFilled || isFocusWithin; const baseStyles = clsx(classNames == null ? void 0 : classNames.base, className, isFilled ? "is-filled" : ""); const handleClear = useCallback(() => { var _a2; state.setInputValue(""); onClear == null ? void 0 : onClear(); (_a2 = domRef.current) == null ? void 0 : _a2.focus(); }, [state.setInputValue, onClear]); useSafeLayoutEffect(() => { if (!domRef.current) return; state.setInputValue(domRef.current.value); }, [domRef.current]); const { isFocusVisible, isFocused, focusProps } = useFocusRing({ autoFocus, isTextInput: true }); const { isHovered, hoverProps } = useHover({ isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled) }); const { isHovered: isLabelHovered, hoverProps: labelHoverProps } = useHover({ isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled) }); const { focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible } = useFocusRing(); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setFocusWithin }); const { pressProps: clearPressProps } = usePress({ isDisabled: !!(originalProps == null ? void 0 : originalProps.isDisabled) || !!(originalProps == null ? void 0 : originalProps.isReadOnly), onPress: handleClear }); const labelPlacement = 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 = useMemo( () => numberInput({ ...variantProps, isInvalid, isClearable, disableAnimation }), [objectToDeps(variantProps), isInvalid, isClearable, hasStartContent, disableAnimation] ); const handleKeyDown = 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 = useCallback( (props2 = {}) => { return { ref: baseDomRef, className: slots.base({ class: baseStyles }), "data-slot": "base", "data-filled": dataAttr( isFilled || hasPlaceholder || hasStartContent || isPlaceholderShown ), "data-filled-within": dataAttr( isFilledWithin || hasPlaceholder || hasStartContent || isPlaceholderShown ), "data-focus-within": dataAttr(isFocusWithin), "data-focus-visible": dataAttr(isFocusVisible), "data-readonly": dataAttr(originalProps.isReadOnly), "data-focus": dataAttr(isFocused), "data-hover": dataAttr(isHovered || isLabelHovered), "data-required": dataAttr(originalProps.isRequired), "data-invalid": dataAttr(isInvalid), "data-disabled": dataAttr(originalProps.isDisabled), "data-has-elements": dataAttr(hasElements), "data-has-helper": dataAttr(hasHelper), "data-has-label": dataAttr(hasLabel), "data-has-value": 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 = useCallback( (props2 = {}) => { return { "data-slot": "label", className: slots.label({ class: classNames == null ? void 0 : classNames.label }), ...mergeProps(labelProps, labelHoverProps, props2) }; }, [slots, isLabelHovered, labelProps, classNames == null ? void 0 : classNames.label] ); const getNumberInputProps = useCallback( (props2 = {}) => { return { "data-slot": "input", "data-filled": dataAttr(isFilled), "data-has-start-content": dataAttr(hasStartContent), "data-has-end-content": dataAttr(!!endContent), className: slots.input({ class: clsx(classNames == null ? void 0 : classNames.input, isFilled ? "is-filled" : "") }), ...mergeProps( focusProps, inputProps, filterDOMProps(otherProps, { enabled: true, labelable: true, omitEventNames: new Set(Object.keys(inputProps)), omitPropNames: /* @__PURE__ */ new Set(["value"]) }), props2 ), "aria-readonly": dataAttr(originalProps.isReadOnly), onChange: chain(inputProps.onChange, onChange), onKeyDown: 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 = useCallback( (props2 = {}) => { return { name: originalProps.name, value: inputValue, "data-slot": "hidden-input", type: "hidden", ...props2 }; }, [inputValue, originalProps.name] ); const getInputWrapperProps = useCallback( (props2 = {}) => { return { ref: inputWrapperRef, "data-slot": "input-wrapper", "data-hover": dataAttr(isHovered || isLabelHovered), "data-focus-visible": dataAttr(isFocusVisible), "data-focus": dataAttr(isFocused), className: slots.inputWrapper({ class: clsx(classNames == null ? void 0 : classNames.inputWrapper, isFilled ? "is-filled" : "") }), ...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 = 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: clsx(classNames == null ? void 0 : classNames.innerWrapper, props2 == null ? void 0 : props2.className) }), ...mergeProps(groupProps, props2) }; }, [slots, classNames == null ? void 0 : classNames.innerWrapper] ); const getMainWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "main-wrapper", className: slots.mainWrapper({ class: clsx(classNames == null ? void 0 : classNames.mainWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.mainWrapper] ); const getHelperWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "helper-wrapper", className: slots.helperWrapper({ class: clsx(classNames == null ? void 0 : classNames.helperWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.helperWrapper] ); const getDescriptionProps = useCallback( (props2 = {}) => { return { ...props2, ...descriptionProps, "data-slot": "description", className: slots.description({ class: clsx(classNames == null ? void 0 : classNames.description, props2 == null ? void 0 : props2.className) }) }; }, [slots, classNames == null ? void 0 : classNames.description] ); const getErrorMessageProps = useCallback( (props2 = {}) => { return { ...props2, ...errorMessageProps, "data-slot": "error-message", className: slots.errorMessage({ class: clsx(classNames == null ? void 0 : classNames.errorMessage, props2 == null ? void 0 : props2.className) }) }; }, [slots, errorMessageProps, classNames == null ? void 0 : classNames.errorMessage] ); const getClearButtonProps = useCallback( (props2 = {}) => { return { ...props2, type: "button", tabIndex: -1, disabled: originalProps.isDisabled, "aria-label": "clear input", "data-slot": "clear-button", "data-focus-visible": dataAttr(isClearButtonFocusVisible), className: slots.clearButton({ class: clsx(classNames == null ? void 0 : classNames.clearButton, props2 == null ? void 0 : props2.className) }), ...mergeProps(clearPressProps, clearFocusProps) }; }, [slots, isClearButtonFocusVisible, clearPressProps, clearFocusProps, classNames == null ? void 0 : classNames.clearButton] ); const getStepperWrapperProps = useCallback( (props2 = {}) => { return { ...props2, "data-slot": "stepper-wrapper", className: slots.stepperWrapper({ class: clsx(classNames == null ? void 0 : classNames.stepperWrapper, props2 == null ? void 0 : props2.className) }) }; }, [slots] ); const getStepperIncreaseButtonProps = useCallback( (props2 = {}) => { return { ...props2, type: "button", disabled: originalProps.isDisabled, "data-slot": "increase-button", className: slots.stepperButton({ class: clsx(classNames == null ? void 0 : classNames.stepperButton, props2 == null ? void 0 : props2.className) }), ...mergeProps(incrementButtonProps, props2) }; }, [slots, incrementButtonProps, classNames == null ? void 0 : classNames.stepperButton] ); const getStepperDecreaseButtonProps = useCallback( (props2 = {}) => { return { type: "button", disabled: originalProps.isDisabled, "data-slot": "decrease-button", className: slots.stepperButton({ class: clsx(classNames == null ? void 0 : classNames.stepperButton, props2 == null ? void 0 : props2.className) }), ...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 }; } export { useNumberInput };