'use client'; import type { IntlTranslations } from '@zag-js/number-input'; import * as numberInput from '@zag-js/number-input'; import { normalizeProps, useMachine } from '@zag-js/react'; import { cva, VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { BasicField, BasicFieldOptions, extractBasicFieldProps, } from '../basic-field'; import { IconButton } from '../button'; import { cn, ComponentAnatomy, defineStyleAnatomy } from '../core/styling'; import { extractInputPartProps, InputAddon, InputAnatomy, InputContainer, InputIcon, InputStyling, } from '../input'; /* ------------------------------------------------------------------------------------------------- * Anatomy * -----------------------------------------------------------------------------------------------*/ export const NumberInputAnatomy = defineStyleAnatomy({ root: cva(['UI-NumberInput__root', 'z-[2]'], { variants: { hideControls: { true: false, false: 'border-r border-r-transparent hover:border-r-[--border]', }, size: { sm: null, md: null, lg: null, }, intent: { basic: null, filled: null, unstyled: 'border-r-0 hover:border-r-transparent', }, }, defaultVariants: { hideControls: false, }, }), control: cva(['UI-NumberInput__control', 'rounded-none h-[50%] ring-inset']), controlsContainer: cva( [ 'UI-NumberInput__controlsContainer', 'form-input w-auto p-0 flex flex-col items-stretch justify-center overflow-hidden max-h-full', 'border-l-0 relative z-[1]', 'shadow-xs', ], { variants: { size: { sm: 'h-8', md: 'h-10', lg: 'h-12', }, intent: { basic: null, filled: 'hover:bg-gray-100', unstyled: null, }, hasRightAddon: { true: 'border-r-0', false: null, }, }, } ), chevronIcon: cva(['UI-Combobox__chevronIcon', 'h-4 w-4 shrink-0']), }); /* ------------------------------------------------------------------------------------------------- * NumberInput * -----------------------------------------------------------------------------------------------*/ export type NumberInputProps = Omit< React.ComponentPropsWithoutRef<'input'>, 'value' | 'size' | 'defaultValue' > & ComponentAnatomy & Omit, 'size' | 'intent'> & BasicFieldOptions & InputStyling & { /** * The value of the input */ value?: number | string; /** * The callback to handle value changes */ onValueChange?: (value: number, valueAsString: string) => void; /** * Default value when uncontrolled */ defaultValue?: number | string; /** * The minimum value of the input */ min?: number; /** * The maximum value of the input */ max?: number; /** * The amount to increment or decrement the value by */ step?: number; /** * Whether to allow mouse wheel to change the value */ allowMouseWheel?: boolean; /** * Whether to allow the value overflow the min/max range */ allowOverflow?: boolean; /** * Whether to hide the controls */ hideControls?: boolean; /** * The format options for the value */ formatOptions?: Intl.NumberFormatOptions; /** * Whether to clamp the value when the input loses focus (blur) */ clampValueOnBlur?: boolean; /** * Accessibility * * Specifies the localized strings that identifies the accessibility elements and their states */ translations?: IntlTranslations; /** * The current locale. Based on the BCP 47 definition. */ locale?: string; /** * The document's text/writing direction. */ dir?: 'ltr' | 'rtl'; }; export const NumberInput = React.forwardRef( (props, ref) => { const [props1, basicFieldProps] = extractBasicFieldProps( props, React.useId() ); const [ { controlClass, controlsContainerClass, chevronIconClass, className, children, /**/ size, intent, leftAddon, leftIcon, rightAddon, rightIcon, placeholder, onValueChange, hideControls, value: controlledValue, min = 0, max, step, allowMouseWheel = true, formatOptions = { maximumFractionDigits: 2 }, clampValueOnBlur = true, translations, locale, dir, defaultValue, ...rest }, { inputContainerProps, leftAddonProps, leftIconProps, rightAddonProps, rightIconProps, }, ] = extractInputPartProps({ ...props1, size: props1.size ?? 'md', intent: props1.intent ?? 'basic', leftAddon: props1.leftAddon, leftIcon: props1.leftIcon, rightAddon: props1.rightAddon, rightIcon: props1.rightIcon, }); const service = useMachine(numberInput.machine, { id: basicFieldProps.id, name: basicFieldProps.name, disabled: basicFieldProps.disabled, readOnly: basicFieldProps.readonly, value: controlledValue ? String(controlledValue) : defaultValue ? String(defaultValue) : undefined, min, max, step, allowMouseWheel, formatOptions, clampValueOnBlur, translations, locale, dir, onValueChange: (details: { valueAsNumber: number; value: string }) => { onValueChange?.(details.valueAsNumber, details.value); }, }); const api = numberInput.connect(service, normalizeProps); const isFirst = React.useRef(true); React.useEffect(() => { if (!isFirst.current) { if ( typeof controlledValue === 'string' && !isNaN(Number(controlledValue)) ) { api.setValue(Number(controlledValue)); } else if (typeof controlledValue === 'number') { api.setValue(controlledValue); } else if (controlledValue === undefined) { api.setValue(min); } } isFirst.current = false; }, [controlledValue]); return ( {!hideControls && (
} /> } />
)}
); } ); NumberInput.displayName = 'NumberInput';