import type {DateInputVariantProps, DateInputSlots, SlotsToClasses} from "@nextui-org/theme"; import type {AriaDateFieldProps} from "@react-types/datepicker"; import type {SupportedCalendars} from "@nextui-org/system"; import type {DateValue, Calendar} from "@internationalized/date"; import type {ReactRef} from "@nextui-org/react-utils"; import type {DOMAttributes, GroupDOMAttributes} from "@react-types/shared"; import type {DateInputGroupProps} from "./date-input-group"; import {useLocale} from "@react-aria/i18n"; import {CalendarDate} from "@internationalized/date"; import {mergeProps} from "@react-aria/utils"; import {PropGetter, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef} from "@nextui-org/react-utils"; import {useDateField as useAriaDateField} from "@react-aria/datepicker"; import {useDateFieldState} from "@react-stately/datepicker"; import {createCalendar} from "@internationalized/date"; import {objectToDeps, clsx, dataAttr} from "@nextui-org/shared-utils"; import {dateInput} from "@nextui-org/theme"; import {useMemo} from "react"; type NextUIBaseProps = Omit< HTMLNextUIProps<"div">, keyof AriaDateFieldProps | "onChange" >; interface Props extends NextUIBaseProps { /** * Ref to the DOM node. */ ref?: ReactRef; /** Props for the grouping element containing the date field and button. */ groupProps?: GroupDOMAttributes; /** Props for the date picker's visible label element, if any. */ labelProps?: DOMAttributes; /** Props for the date field. */ fieldProps?: DOMAttributes; /** Props for the description element, if any. */ descriptionProps?: DOMAttributes; /** Props for the error message element, if any. */ errorMessageProps?: DOMAttributes; /** * The value of the hidden input. */ inputRef?: ReactRef; /** * Element to be rendered in the left side of the input. */ startContent?: React.ReactNode; /** * Element to be rendered in the right side of the input. */ endContent?: React.ReactNode; /** * This function helps to reduce the bundle size by providing a custom calendar system. * * In the example above, the createCalendar function from the `@internationalized/date` package * is passed to the useCalendarState hook. This function receives a calendar identifier string, * and provides Calendar instances to React Stately, which are used to implement date manipulation. * * By default, this includes all calendar systems supported by @internationalized/date. However, * if your application supports a more limited set of regions, or you know you will only be picking dates * in a certain calendar system, you can reduce your bundle size by providing your own implementation * of `createCalendar` that includes a subset of these Calendar implementations. * * For example, if your application only supports Gregorian dates, you could implement a `createCalendar` * function like this: * * @example * * import {GregorianCalendar} from '@internationalized/date'; * * function createCalendar(identifier) { * switch (identifier) { * case 'gregory': * return new GregorianCalendar(); * default: * throw new Error(`Unsupported calendar ${identifier}`); * } * } * * This way, only GregorianCalendar is imported, and the other calendar implementations can be tree-shaken. * * You can also use the NextUIProvider to provide the createCalendar function to all nested components. * * @default all calendars */ createCalendar?: (calendar: SupportedCalendars) => Calendar | null; /** * Classname or List of classes to change the classNames of the element. * if `className` is passed, it will be added to the base slot. * * @example * ```ts * * ``` */ classNames?: SlotsToClasses; } export type UseDateInputProps = Props & DateInputVariantProps & AriaDateFieldProps; export function useDateInput(originalProps: UseDateInputProps) { const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys); const providerContext = useProviderContext(); const { ref, as, label, inputRef: inputRefProp, description, startContent, endContent, className, classNames, validationState, groupProps = {}, labelProps: labelPropsProp, fieldProps: fieldPropsProp, errorMessageProps: errorMessagePropsProp, descriptionProps: descriptionPropsProp, validationBehavior, shouldForceLeadingZeros = true, minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), createCalendar: createCalendarProp = providerContext?.createCalendar ?? null, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, errorMessage, } = props; const domRef = useDOMRef(ref); const inputRef = useDOMRef(inputRefProp); const {locale} = useLocale(); const state = useDateFieldState({ ...originalProps, label, locale, minValue, maxValue, validationBehavior, isInvalid: isInvalidProp, shouldForceLeadingZeros, createCalendar: !createCalendarProp || typeof createCalendarProp !== "function" ? createCalendar : (createCalendarProp as typeof createCalendar), }); const { labelProps, fieldProps, inputProps, validationErrors, validationDetails, descriptionProps, errorMessageProps, isInvalid: ariaIsInvalid, } = useAriaDateField({...originalProps, label, validationBehavior, inputRef}, state, domRef); const baseStyles = clsx(classNames?.base, className); const isInvalid = isInvalidProp || ariaIsInvalid; const labelPlacement = useMemo(() => { if ( (!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !props.label ) { return "outside"; } return originalProps.labelPlacement ?? "inside"; }, [originalProps.labelPlacement, props.label]); const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left"; const slots = useMemo( () => dateInput({ ...variantProps, labelPlacement, className, }), [objectToDeps(variantProps), labelPlacement, className], ); const getLabelProps: PropGetter = (props) => { return { ...mergeProps(labelProps, labelPropsProp, props), "data-slot": "label", className: slots.label({ class: clsx(classNames?.label, props?.className), }), }; }; const getInputProps: PropGetter = (props) => { return { ...props, ...inputProps, ref: inputRef, }; }; const getFieldProps = (props: DOMAttributes = {}) => { return { ref: domRef, "data-slot": "input-field", ...mergeProps(fieldProps, fieldPropsProp, props), className: slots.input({ class: clsx(classNames?.input, props?.className), }), } as GroupDOMAttributes; }; const getInputWrapperProps = (props = {}) => { return { ...props, ...groupProps, "data-slot": "input-wrapper", className: slots.inputWrapper({ class: classNames?.inputWrapper, }), onClick: fieldProps.onClick, } as GroupDOMAttributes; }; const getInnerWrapperProps: PropGetter = (props) => { return { ...props, "data-slot": "inner-wrapper", className: slots.innerWrapper({ class: classNames?.innerWrapper, }), }; }; const getHelperWrapperProps: PropGetter = (props) => { return { ...props, "data-slot": "helper-wrapper", className: slots.helperWrapper({ class: clsx(classNames?.helperWrapper, props?.className), }), }; }; const getErrorMessageProps: PropGetter = (props = {}) => { return { ...mergeProps(errorMessageProps, errorMessagePropsProp, props), "data-slot": "error-message", className: slots.errorMessage({class: clsx(classNames?.errorMessage, props?.className)}), }; }; const getDescriptionProps: PropGetter = (props = {}) => { return { ...mergeProps(descriptionProps, descriptionPropsProp, props), "data-slot": "description", className: slots.description({class: clsx(classNames?.description, props?.className)}), }; }; const getBaseGroupProps = () => { return { as, label, description, endContent, errorMessage, isInvalid, startContent, validationDetails, validationErrors, shouldLabelBeOutside, "data-slot": "base", "data-required": dataAttr(originalProps.isRequired), "data-disabled": dataAttr(originalProps.isDisabled), "data-readonly": dataAttr(originalProps.isReadOnly), "data-invalid": dataAttr(isInvalid), "data-has-start-content": dataAttr(!!startContent), "data-has-end-content": dataAttr(!!endContent), descriptionProps: getDescriptionProps(), errorMessageProps: getErrorMessageProps(), groupProps: getInputWrapperProps(), helperWrapperProps: getHelperWrapperProps(), labelProps: getLabelProps(), wrapperProps: getInnerWrapperProps(), className: slots.base({class: baseStyles}), } as DateInputGroupProps; }; return { state, domRef, slots, classNames, labelPlacement, getBaseGroupProps, getFieldProps, getInputProps, }; } export type UseDateInputReturn = ReturnType;