From 2415f105bff80f3496cd8995e087459deb356f8e Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Thu, 11 Apr 2024 00:04:48 -0300 Subject: [PATCH 1/6] chore(date-range-picker): in progress --- .../calendar/src/calendar-picker.tsx | 5 +- .../calendar/src/use-calendar-picker.ts | 1 + .../date-picker/src/date-picker.tsx | 2 +- .../date-picker/src/date-range-picker.tsx | 89 +++++ .../date-picker/src/use-date-picker-base.ts | 346 ++++++++++++++++++ .../date-picker/src/use-date-picker.ts | 324 ++-------------- .../date-picker/src/use-date-range-picker.ts | 149 ++++++++ .../stories/date-picker.stories.tsx | 3 +- 8 files changed, 626 insertions(+), 293 deletions(-) create mode 100644 packages/components/date-picker/src/date-range-picker.tsx create mode 100644 packages/components/date-picker/src/use-date-picker-base.ts create mode 100644 packages/components/date-picker/src/use-date-range-picker.ts diff --git a/packages/components/calendar/src/calendar-picker.tsx b/packages/components/calendar/src/calendar-picker.tsx index 3652535c40..3762af7ee5 100644 --- a/packages/components/calendar/src/calendar-picker.tsx +++ b/packages/components/calendar/src/calendar-picker.tsx @@ -24,6 +24,7 @@ export function CalendarPicker(props: CalendarPickerProps) { yearsListRef, classNames, getItemRef, + isHeaderExpanded, onPickerItemPressed, onPickerItemKeyDown, } = useCalendarPicker(props); @@ -82,7 +83,7 @@ export function CalendarPicker(props: CalendarPickerProps) { ref={(node) => getItemRef(node, month.value, "months")} className={slots?.pickerItem({class: classNames?.pickerItem})} data-value={month.value} - tabIndex={state.focusedDate?.month === month.value ? 0 : -1} + tabIndex={!isHeaderExpanded || state.focusedDate?.month !== month.value ? -1 : 0} onKeyDown={(e) => onPickerItemKeyDown(e, month.value, "months")} onPress={(e) => onPickerItemPressed(e, "months")} > @@ -103,7 +104,7 @@ export function CalendarPicker(props: CalendarPickerProps) { ref={(node) => getItemRef(node, year.value, "years")} className={slots?.pickerItem({class: classNames?.pickerItem})} data-value={year.value} - tabIndex={state.focusedDate?.year === year.value ? 0 : -1} + tabIndex={!isHeaderExpanded || state.focusedDate?.year !== year.value ? -1 : 0} onKeyDown={(e) => onPickerItemKeyDown(e, year.value, "years")} onPress={(e) => onPickerItemPressed(e, "years")} > diff --git a/packages/components/calendar/src/use-calendar-picker.ts b/packages/components/calendar/src/use-calendar-picker.ts index fd93535779..ff9dce3109 100644 --- a/packages/components/calendar/src/use-calendar-picker.ts +++ b/packages/components/calendar/src/use-calendar-picker.ts @@ -234,6 +234,7 @@ export function useCalendarPicker(props: CalendarPickerProps) { monthsListRef, yearsListRef, getItemRef, + isHeaderExpanded, onPickerItemPressed, onPickerItemKeyDown, }; diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index 9ff90198ee..54d3e0ff36 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -50,7 +50,7 @@ function DatePicker(props: Props, ref: ForwardedRef { if (isCalendarHeaderExpanded) return null; diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx new file mode 100644 index 0000000000..9ff90198ee --- /dev/null +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -0,0 +1,89 @@ +import type {DateValue} from "@internationalized/date"; + +import {ForwardedRef, ReactElement, Ref, useMemo} from "react"; +import {cloneElement, isValidElement} from "react"; +import {forwardRef} from "@nextui-org/system"; +import {Button} from "@nextui-org/button"; +import {DateInput, TimeInput} from "@nextui-org/date-input"; +import {FreeSoloPopover} from "@nextui-org/popover"; +import {Calendar} from "@nextui-org/calendar"; +import {AnimatePresence} from "framer-motion"; +import {CalendarBoldIcon} from "@nextui-org/shared-icons"; + +import {UseDatePickerProps, useDatePicker} from "./use-date-picker"; + +export interface Props + extends Omit, "hasMultipleMonths"> {} + +function DatePicker(props: Props, ref: ForwardedRef) { + const { + state, + endContent, + selectorIcon, + showTimeField, + disableAnimation, + isCalendarHeaderExpanded, + getDateInputProps, + getPopoverProps, + getTimeInputProps, + getSelectorButtonProps, + getSelectorIconProps, + getCalendarProps, + CalendarTopContent, + CalendarBottomContent, + } = useDatePicker({...props, ref}); + + const selectorContent = isValidElement(selectorIcon) ? ( + cloneElement(selectorIcon, getSelectorIconProps()) + ) : ( + + ); + + const calendarBottomContent = useMemo(() => { + if (isCalendarHeaderExpanded) return null; + + return showTimeField ? ( + <> + + {CalendarBottomContent} + + ) : ( + CalendarBottomContent + ); + }, [showTimeField, CalendarBottomContent, isCalendarHeaderExpanded]); + + const calendarTopContent = useMemo(() => { + if (isCalendarHeaderExpanded) return null; + + return CalendarTopContent; + }, [showTimeField, CalendarTopContent, isCalendarHeaderExpanded]); + + const popoverContent = state.isOpen ? ( + + + + ) : null; + + return ( + <> + {endContent || selectorContent}} + /> + {disableAnimation ? popoverContent : {popoverContent}} + + ); +} + +DatePicker.displayName = "NextUI.DatePicker"; + +export type DatePickerProps = Props & {ref?: Ref}; + +// forwardRef doesn't support generic parameters, so cast the result to the correct type +export default forwardRef(DatePicker) as ( + props: DatePickerProps, +) => ReactElement; diff --git a/packages/components/date-picker/src/use-date-picker-base.ts b/packages/components/date-picker/src/use-date-picker-base.ts new file mode 100644 index 0000000000..e0c9938816 --- /dev/null +++ b/packages/components/date-picker/src/use-date-picker-base.ts @@ -0,0 +1,346 @@ +import type {DateValue} from "@internationalized/date"; +import type {AriaDatePickerBaseProps} from "@react-types/datepicker"; +import type {DateInputProps, TimeInputProps} from "@nextui-org/date-input"; +import type {ButtonProps} from "@nextui-org/button"; +import type {CalendarProps} from "@nextui-org/calendar"; +import type {PopoverProps} from "@nextui-org/popover"; +import type {ReactNode} from "react"; + +import { + DatePickerVariantProps, + DatePickerSlots, + SlotsToClasses, + dateInput, +} from "@nextui-org/theme"; +import {useMemo, useState} from "react"; +import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; +import {datePicker} from "@nextui-org/theme"; +import {mergeProps} from "@react-aria/utils"; +import {useDOMRef} from "@nextui-org/react-utils"; +import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; +import {useLocalizedStringFormatter} from "@react-aria/i18n"; + +import intlMessages from "../intl/messages"; + +type NextUIBaseProps = Omit< + HTMLNextUIProps<"div">, + keyof AriaDatePickerBaseProps | "onChange" +>; + +interface Props extends NextUIBaseProps { + /** + * The icon to toggle the date picker popover. Usually a calendar icon. + */ + selectorIcon?: ReactNode; + /** + * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. + * @default visible + */ + pageBehavior?: CalendarProps["pageBehavior"]; + /** + * The number of months to display at once. Up to 3 months are supported. + * Passing a number greater than 1 will disable the `showMonthAndYearPickers` prop. + * + * @default 1 + */ + visibleMonths?: CalendarProps["visibleMonths"]; + /** + * The width to be applied to the calendar component. + * + * @default 256 + */ + calendarWidth?: number; + /** + * Top content to be rendered in the calendar component. + */ + CalendarTopContent?: CalendarProps["topContent"]; + /** + * Bottom content to be rendered in the calendar component. + */ + CalendarBottomContent?: CalendarProps["bottomContent"]; + /** + * Whether the calendar should show month and year pickers. + * + * @default false + */ + showMonthAndYearPickers?: CalendarProps["showMonthAndYearPickers"]; + /** + * Props to be passed to the popover component. + * + * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 13 } + */ + popoverProps?: Partial; + /** + * Props to be passed to the selector button component. + * @default { size: "sm", variant: "light", radius: "full", isIconOnly: true } + */ + selectorButtonProps?: Partial; + /** + * Props to be passed to the calendar component. + * @default {} + */ + calendarProps?: Partial>; + + /** + * Props to be passed to the time input component. + * + * @default {} + */ + timeInputProps?: TimeInputProps; + /** + * Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. + */ + isDateUnavailable?: CalendarProps["isDateUnavailable"]; + /** + * Whether to disable all animations in the date picker. Including the DateInput, Button, Calendar, and Popover. + * + * @default false + */ + disableAnimation?: boolean; + /** + * 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 & DateInputProps["classNames"]; +} + +type Variants = + | "color" + | "size" + | "isDisabled" + | "disableAnimation" + | "variant" + | "radius" + | "labelPlacement" + | "fullWidth"; + +export type UseDatePickerBaseProps = Props & + DatePickerVariantProps & + Pick< + DateInputProps, + Variants | "ref" | "createCalendar" | "startContent" | "endContent" | "inputRef" + > & + Omit, "minValue" | "maxValue">; + +export function useDatePickerBase(originalProps: UseDatePickerBaseProps) { + const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys); + + const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useState(false); + + const { + as, + ref, + label, + selectorIcon, + inputRef, + isInvalid, + errorMessage, + description, + startContent, + endContent, + validationState, + validationBehavior, + visibleMonths = 1, + pageBehavior = "visible", + calendarWidth = 256, + isDateUnavailable, + shouldForceLeadingZeros, + showMonthAndYearPickers = false, + selectorButtonProps: userSelectorButtonProps = {}, + popoverProps: userPopoverProps = {}, + timeInputProps: userTimeInputProps = {}, + calendarProps: userCalendarProps = {}, + CalendarTopContent, + CalendarBottomContent, + createCalendar, + className, + classNames, + } = props; + + const domRef = useDOMRef(ref); + const disableAnimation = originalProps.disableAnimation ?? false; + + const baseStyles = clsx(classNames?.base, className); + + let stringFormatter = useLocalizedStringFormatter(intlMessages); + + const isDefaultColor = originalProps.color === "default" || !originalProps.color; + const hasMultipleMonths = visibleMonths > 1; + + // Time field values + const placeholder = originalProps?.placeholderValue; + const timePlaceholder = placeholder && "hour" in placeholder ? placeholder : null; + + const slotsProps: { + popoverProps: UseDatePickerBaseProps["popoverProps"]; + selectorButtonProps: ButtonProps; + calendarProps: CalendarProps; + } = { + popoverProps: mergeProps( + { + offset: 13, + placement: "bottom", + triggerScaleOnOpen: false, + disableAnimation, + }, + userPopoverProps, + ), + selectorButtonProps: mergeProps( + { + isIconOnly: true, + radius: "full", + size: "sm", + variant: "light", + disableAnimation, + }, + userSelectorButtonProps, + ), + calendarProps: mergeProps( + { + showHelper: false, + visibleMonths, + pageBehavior, + isDateUnavailable, + showMonthAndYearPickers, + onHeaderExpandedChange: setIsCalendarHeaderExpanded, + color: + (originalProps.variant === "bordered" || originalProps.variant === "underlined") && + isDefaultColor + ? "foreground" + : isDefaultColor + ? "primary" + : originalProps.color, + disableAnimation, + }, + userCalendarProps, + ), + }; + + const slots = useMemo( + () => + datePicker({ + ...variantProps, + hasMultipleMonths, + className, + }), + [objectToDeps(variantProps), hasMultipleMonths, className], + ); + + const dateInputProps = { + as, + label, + ref: domRef, + inputRef, + description, + startContent, + validationState, + validationBehavior, + shouldForceLeadingZeros, + isInvalid, + errorMessage, + "data-invalid": dataAttr(originalProps?.isInvalid), + className: slots.base({class: baseStyles}), + classNames, + } as DateInputProps; + + const timeInputProps = { + ...userTimeInputProps, + size: "sm", + labelPlacement: "outside-left", + classNames: { + base: slots.timeInput({ + class: clsx(classNames?.timeInput, userTimeInputProps?.classNames?.base), + }), + label: slots.timeInputLabel({ + class: clsx(classNames?.timeInputLabel, userTimeInputProps?.classNames?.label), + }), + }, + label: userTimeInputProps?.label || stringFormatter.format("time"), + placeholderValue: timePlaceholder, + hourCycle: props.hourCycle, + hideTimeZone: props.hideTimeZone, + } as TimeInputProps; + + const popoverProps = { + ...mergeProps(slotsProps.popoverProps, props), + triggerRef: domRef, + classNames: { + content: slots.popoverContent({ + class: clsx( + classNames?.popoverContent, + slotsProps.popoverProps?.classNames?.["content"], + props.className, + ), + }), + }, + } as PopoverProps; + + const calendarProps = { + ...slotsProps.calendarProps, + "data-slot": "calendar", + classNames: { + base: slots.calendar({class: classNames?.calendar}), + content: slots.calendarContent({class: classNames?.calendarContent}), + }, + style: mergeProps( + hasMultipleMonths + ? { + // @ts-ignore + "--visible-months": visibleMonths, + } + : {}, + {"--calendar-width": `${calendarWidth}px`}, + slotsProps.calendarProps.style, + ), + } as CalendarProps; + + const selectorButtonProps = { + ...slotsProps.selectorButtonProps, + "data-slot": "selector-button", + className: slots.selectorButton({class: classNames?.selectorButton}), + } as ButtonProps; + + const selectorIconProps = { + "data-slot": "selector-icon", + className: slots.selectorIcon({class: classNames?.selectorIcon}), + }; + + return { + domRef, + endContent, + selectorIcon, + createCalendar, + isCalendarHeaderExpanded, + disableAnimation, + CalendarTopContent, + CalendarBottomContent, + variantProps, + dateInputProps, + timeInputProps, + popoverProps, + calendarProps, + selectorButtonProps, + selectorIconProps, + }; +} + +export type UseDatePickerBaseReturn = ReturnType; diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index 38adeffcd8..f0d01b6f05 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -1,180 +1,46 @@ import type {DateValue} from "@internationalized/date"; -import type {AriaDatePickerProps} from "@react-types/datepicker"; import type {DateInputProps, TimeInputProps} from "@nextui-org/date-input"; import type {DatePickerState} from "@react-stately/datepicker"; import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; -import type {ReactNode} from "react"; +import type {UseDatePickerBaseProps} from "./use-date-picker-base"; -import { - DatePickerVariantProps, - DatePickerSlots, - SlotsToClasses, - dateInput, -} from "@nextui-org/theme"; -import {useMemo, useState} from "react"; import {DOMAttributes} from "@nextui-org/system"; import {useDatePickerState} from "@react-stately/datepicker"; -import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; -import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; -import {datePicker} from "@nextui-org/theme"; +import {AriaDatePickerProps, useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; +import {dataAttr} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; -import {useDOMRef} from "@nextui-org/react-utils"; -import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; -import {useLocalizedStringFormatter} from "@react-aria/i18n"; -import intlMessages from "../intl/messages"; +import {useDatePickerBase} from "./use-date-picker-base"; -type NextUIBaseProps = Omit< - HTMLNextUIProps<"div">, - keyof AriaDatePickerProps | "onChange" ->; - -interface Props extends NextUIBaseProps { - /** - * The icon to toggle the date picker popover. Usually a calendar icon. - */ - selectorIcon?: ReactNode; - /** - * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. - * @default visible - */ - pageBehavior?: CalendarProps["pageBehavior"]; - /** - * The number of months to display at once. Up to 3 months are supported. - * Passing a number greater than 1 will disable the `showMonthAndYearPickers` prop. - * - * @default 1 - */ - visibleMonths?: CalendarProps["visibleMonths"]; - /** - * The width to be applied to the calendar component. - * - * @default 256 - */ - calendarWidth?: number; - /** - * Top content to be rendered in the calendar component. - */ - CalendarTopContent?: CalendarProps["topContent"]; - /** - * Bottom content to be rendered in the calendar component. - */ - CalendarBottomContent?: CalendarProps["bottomContent"]; - /** - * Whether the calendar should show month and year pickers. - * - * @default false - */ - showMonthAndYearPickers?: CalendarProps["showMonthAndYearPickers"]; - /** - * Props to be passed to the popover component. - * - * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 13 } - */ - popoverProps?: Partial; - /** - * Props to be passed to the selector button component. - * @default { size: "sm", variant: "light", radius: "full", isIconOnly: true } - */ - selectorButtonProps?: Partial; - /** - * Props to be passed to the calendar component. - * @default {} - */ - calendarProps?: Partial>; - - /** - * Props to be passed to the time input component. - * - * @default {} - */ - timeInputProps?: TimeInputProps; - /** - * Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. - */ - isDateUnavailable?: CalendarProps["isDateUnavailable"]; - /** - * Whether to disable all animations in the date picker. Including the DateInput, Button, Calendar, and Popover. - * - * @default false - */ - disableAnimation?: boolean; - /** - * 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 & DateInputProps["classNames"]; -} +interface Props extends UseDatePickerBaseProps {} export type UseDatePickerProps = Props & - DatePickerVariantProps & - Omit, "groupProps" | "fieldProps" | "labelProps" | "errorMessageProps">; + UseDatePickerBaseProps & + AriaDatePickerProps; export function useDatePicker(originalProps: UseDatePickerProps) { - const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys); - - const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useState(false); - const { - as, - ref, - label, - selectorIcon, - inputRef, - isInvalid, - errorMessage, - description, - startContent, + domRef, endContent, - validationState, - validationBehavior, - visibleMonths = 1, - pageBehavior = "visible", - calendarWidth = 256, - isDateUnavailable, - shouldForceLeadingZeros, - showMonthAndYearPickers = false, - popoverProps = {}, - timeInputProps = {}, - selectorButtonProps = {}, - calendarProps: userCalendarProps = {}, + selectorIcon, + createCalendar, + isCalendarHeaderExpanded, + disableAnimation, CalendarTopContent, CalendarBottomContent, - minValue, - maxValue, - createCalendar, - className, - classNames, - } = props; - - const domRef = useDOMRef(ref); - const disableAnimation = originalProps.disableAnimation ?? false; + dateInputProps, + timeInputProps, + popoverProps, + calendarProps, + variantProps, + selectorButtonProps, + selectorIconProps, + } = useDatePickerBase(originalProps); let state: DatePickerState = useDatePickerState({ ...originalProps, - minValue, - maxValue, shouldCloseOnSelect: () => !state.hasTime, }); @@ -184,23 +50,16 @@ export function useDatePicker(originalProps: UseDatePickerP fieldProps, buttonProps, dialogProps, - calendarProps, + calendarProps: ariaCalendarProps, descriptionProps, errorMessageProps, } = useAriaDatePicker(originalProps, state, domRef); - const baseStyles = clsx(classNames?.base, className); - - let stringFormatter = useLocalizedStringFormatter(intlMessages); - - const isDefaultColor = originalProps.color === "default" || !originalProps.color; - const hasMultipleMonths = visibleMonths > 1; - // Time field values - const placeholder = originalProps?.placeholderValue; - const timePlaceholder = placeholder && "hour" in placeholder ? placeholder : null; - const timeMinValue = props.minValue && "hour" in props.minValue ? props.minValue : null; - const timeMaxValue = props.maxValue && "hour" in props.maxValue ? props.maxValue : null; + const timeMinValue = + originalProps.minValue && "hour" in originalProps.minValue ? originalProps.minValue : null; + const timeMaxValue = + originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; const timeGranularity = state.granularity === "hour" || state.granularity === "minute" || state.granularity === "second" ? state.granularity @@ -208,89 +67,21 @@ export function useDatePicker(originalProps: UseDatePickerP const showTimeField = !!timeGranularity; - const slotsProps: { - popoverProps: UseDatePickerProps["popoverProps"]; - selectorButtonProps: ButtonProps; - calendarProps: CalendarProps; - } = { - popoverProps: mergeProps( - { - offset: 13, - placement: "bottom", - triggerScaleOnOpen: false, - disableAnimation, - }, - popoverProps, - ), - selectorButtonProps: mergeProps( - { - isIconOnly: true, - radius: "full", - size: "sm", - variant: "light", - disableAnimation, - }, - selectorButtonProps, - ), - calendarProps: mergeProps( - { - showHelper: false, - visibleMonths, - pageBehavior, - isDateUnavailable, - showMonthAndYearPickers, - onHeaderExpandedChange: setIsCalendarHeaderExpanded, - color: - (originalProps.variant === "bordered" || originalProps.variant === "underlined") && - isDefaultColor - ? "foreground" - : isDefaultColor - ? "primary" - : originalProps.color, - disableAnimation, - }, - userCalendarProps, - ), - }; - - const slots = useMemo( - () => - datePicker({ - ...variantProps, - hasMultipleMonths, - className, - }), - [objectToDeps(variantProps), hasMultipleMonths, className], - ); - const getDateInputProps = () => { return { - as, - label, - ref: domRef, - inputRef, - description, - startContent, - validationState, - validationBehavior, - shouldForceLeadingZeros, - isInvalid, - errorMessage, + ...dateInputProps, groupProps, labelProps, createCalendar, errorMessageProps, descriptionProps, ...mergeProps(variantProps, fieldProps, { - minValue, - maxValue, + minValue: originalProps.minValue, + maxValue: originalProps.maxValue, fullWidth: true, disableAnimation, }), - "data-invalid": dataAttr(originalProps?.isInvalid), "data-open": dataAttr(state.isOpen), - className: slots.base({class: baseStyles}), - classNames, } as DateInputProps; }; @@ -299,25 +90,11 @@ export function useDatePicker(originalProps: UseDatePickerP return { ...timeInputProps, - size: "sm", - labelPlacement: "outside-left", - classNames: { - base: slots.timeInput({ - class: clsx(classNames?.timeInput, timeInputProps?.classNames?.base), - }), - label: slots.timeInputLabel({ - class: clsx(classNames?.timeInputLabel, timeInputProps?.classNames?.label), - }), - }, - label: timeInputProps?.label || stringFormatter.format("time"), value: state.timeValue, onChange: state.setTimeValue, - placeholderValue: timePlaceholder, granularity: timeGranularity, minValue: timeMinValue, maxValue: timeMaxValue, - hourCycle: props.hourCycle, - hideTimeZone: props.hideTimeZone, } as TimeInputProps; }; @@ -325,56 +102,27 @@ export function useDatePicker(originalProps: UseDatePickerP return { state, dialogProps, - ...mergeProps(slotsProps.popoverProps, props), - triggerRef: domRef, - classNames: { - content: slots.popoverContent({ - class: clsx( - classNames?.popoverContent, - slotsProps.popoverProps?.classNames?.["content"], - props.className, - ), - }), - }, - } as unknown as PopoverProps; + ...popoverProps, + ...props, + } as PopoverProps; }; const getCalendarProps = () => { return { + ...ariaCalendarProps, ...calendarProps, - ...slotsProps.calendarProps, - "data-slot": "calendar", - classNames: { - base: slots.calendar({class: classNames?.calendar}), - content: slots.calendarContent({class: classNames?.calendarContent}), - }, - style: mergeProps( - hasMultipleMonths - ? { - // @ts-ignore - "--visible-months": visibleMonths, - } - : {}, - {"--calendar-width": `${calendarWidth}px`}, - slotsProps.calendarProps.style, - ), - } as unknown as CalendarProps; + } as CalendarProps; }; const getSelectorButtonProps = () => { return { ...buttonProps, - ...slotsProps.selectorButtonProps, - "data-slot": "selector-button", - className: slots.selectorButton({class: classNames?.selectorButton}), - } as unknown as ButtonProps; + ...selectorButtonProps, + } as ButtonProps; }; const getSelectorIconProps = () => { - return { - "data-slot": "selector-icon", - className: slots.selectorIcon({class: classNames?.selectorIcon}), - }; + return selectorIconProps; }; return { diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts new file mode 100644 index 0000000000..3a15a54c98 --- /dev/null +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -0,0 +1,149 @@ +import type {DateValue} from "@internationalized/date"; +import type {DateInputProps, TimeInputProps} from "@nextui-org/date-input"; +import type {ButtonProps} from "@nextui-org/button"; +import type {CalendarProps} from "@nextui-org/calendar"; +import type {PopoverProps} from "@nextui-org/popover"; +import type {AriaDateRangePickerProps} from "@react-types/datepicker"; +import type {DateRangePickerState} from "@react-stately/datepicker"; +import type {UseDatePickerBaseProps} from "./use-date-picker-base"; + +import {useDateRangePickerState} from "@react-stately/datepicker"; +import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepicker"; +import {DOMAttributes} from "@nextui-org/system"; +import {dataAttr} from "@nextui-org/shared-utils"; +import {mergeProps} from "@react-aria/utils"; + +import {useDatePickerBase} from "./use-date-picker-base"; +interface Props extends UseDatePickerBaseProps {} + +export type UseDateRangePickerProps = Props & + UseDatePickerBaseProps & + AriaDateRangePickerProps; + +export function useDateRangePicker(originalProps: UseDateRangePickerProps) { + const { + domRef, + endContent, + selectorIcon, + createCalendar, + isCalendarHeaderExpanded, + disableAnimation, + CalendarTopContent, + CalendarBottomContent, + dateInputProps, + timeInputProps, + popoverProps, + calendarProps, + variantProps, + selectorButtonProps, + selectorIconProps, + } = useDatePickerBase(originalProps); + + let state: DateRangePickerState = useDateRangePickerState({ + ...originalProps, + shouldCloseOnSelect: () => !state.hasTime, + }); + + originalProps.minValue; + + let { + groupProps, + labelProps, + startFieldProps, + endFieldProps, + buttonProps, + dialogProps, + calendarProps: ariaCalendarProps, + descriptionProps, + errorMessageProps, + } = useAriaDateRangePicker(originalProps, state, domRef); + + // Time field values + const timeMinValue = + originalProps.minValue && "hour" in originalProps.minValue ? originalProps.minValue : null; + const timeMaxValue = + originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; + const timeGranularity = + state.granularity === "hour" || state.granularity === "minute" || state.granularity === "second" + ? state.granularity + : null; + + const showTimeField = !!timeGranularity; + + const getDateInputProps = () => { + return { + ...dateInputProps, + groupProps, + labelProps, + createCalendar, + errorMessageProps, + descriptionProps, + ...mergeProps(variantProps, fieldProps, { + minValue: originalProps.minValue, + maxValue: originalProps.maxValue, + fullWidth: true, + disableAnimation, + }), + "data-open": dataAttr(state.isOpen), + } as DateInputProps; + }; + + const getTimeInputProps = () => { + if (!showTimeField) return {}; + + return { + ...timeInputProps, + value: state.timeValue, + onChange: state.setTimeValue, + granularity: timeGranularity, + minValue: timeMinValue, + maxValue: timeMaxValue, + } as TimeInputProps; + }; + + const getPopoverProps = (props: DOMAttributes = {}) => { + return { + state, + dialogProps, + ...popoverProps, + ...props, + } as PopoverProps; + }; + + const getCalendarProps = () => { + return { + ...ariaCalendarProps, + ...calendarProps, + } as CalendarProps; + }; + + const getSelectorButtonProps = () => { + return { + ...buttonProps, + ...selectorButtonProps, + } as ButtonProps; + }; + + const getSelectorIconProps = () => { + return selectorIconProps; + }; + + return { + state, + endContent, + selectorIcon, + showTimeField, + isCalendarHeaderExpanded, + disableAnimation, + CalendarTopContent, + CalendarBottomContent, + getDateInputProps, + getPopoverProps, + getSelectorButtonProps, + getCalendarProps, + getTimeInputProps, + getSelectorIconProps, + }; +} + +export type UseDateRangePickerReturn = ReturnType; diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index 0118eabc8e..0db7446cd8 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import {Meta} from "@storybook/react"; -import {datePicker, dateInput} from "@nextui-org/theme"; +import {dateInput} from "@nextui-org/theme"; import { DateValue, getLocalTimeZone, @@ -73,7 +73,6 @@ const defaultProps = { label: "Birth Date", className: "max-w-[256px]", ...dateInput.defaultVariants, - ...datePicker.defaultVariants, }; const Template = (args: DatePickerProps) => ; From 7a43df04f371bb56db7ae31d51f0c2481d11f594 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Thu, 11 Apr 2024 22:05:57 -0300 Subject: [PATCH 2/6] chore(date-range-picker): in progress --- .../date-picker/src/date-range-picker.tsx | 54 ++++++--- packages/components/date-picker/src/index.ts | 6 +- .../date-picker/src/use-date-picker-base.ts | 58 +++------ .../date-picker/src/use-date-picker.ts | 68 +++++++++-- .../date-picker/src/use-date-range-picker.ts | 110 +++++++++++++----- .../stories/date-range-picker.stories.tsx | 69 +++++++++++ .../core/theme/src/components/date-picker.ts | 15 ++- 7 files changed, 280 insertions(+), 100 deletions(-) create mode 100644 packages/components/date-picker/stories/date-range-picker.stories.tsx diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx index 9ff90198ee..8329d80fc3 100644 --- a/packages/components/date-picker/src/date-range-picker.tsx +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -6,32 +6,35 @@ import {forwardRef} from "@nextui-org/system"; import {Button} from "@nextui-org/button"; import {DateInput, TimeInput} from "@nextui-org/date-input"; import {FreeSoloPopover} from "@nextui-org/popover"; -import {Calendar} from "@nextui-org/calendar"; +import {RangeCalendar} from "@nextui-org/calendar"; import {AnimatePresence} from "framer-motion"; import {CalendarBoldIcon} from "@nextui-org/shared-icons"; -import {UseDatePickerProps, useDatePicker} from "./use-date-picker"; +import {UseDateRangePickerProps, useDateRangePicker} from "./use-date-range-picker"; export interface Props - extends Omit, "hasMultipleMonths"> {} + extends Omit, "hasMultipleMonths"> {} -function DatePicker(props: Props, ref: ForwardedRef) { +function DateRangePicker(props: Props, ref: ForwardedRef) { const { state, endContent, selectorIcon, showTimeField, + groupProps, disableAnimation, isCalendarHeaderExpanded, - getDateInputProps, + getStartDateInputProps, + getEndDateInputProps, getPopoverProps, - getTimeInputProps, + getStartTimeInputProps, + getEndTimeInputProps, getSelectorButtonProps, getSelectorIconProps, getCalendarProps, CalendarTopContent, CalendarBottomContent, - } = useDatePicker({...props, ref}); + } = useDateRangePicker({...props, ref}); const selectorContent = isValidElement(selectorIcon) ? ( cloneElement(selectorIcon, getSelectorIconProps()) @@ -43,10 +46,16 @@ function DatePicker(props: Props, ref: ForwardedRef - +
+ + {CalendarBottomContent} - +
) : ( CalendarBottomContent ); @@ -60,7 +69,7 @@ function DatePicker(props: Props, ref: ForwardedRef - (props: Props, ref: ForwardedRef - + + + + + + {/* {endContent || selectorContent}} - /> + /> */} {disableAnimation ? popoverContent : {popoverContent}} - + ); } -DatePicker.displayName = "NextUI.DatePicker"; +DateRangePicker.displayName = "NextUI.DateRangePicker"; -export type DatePickerProps = Props & {ref?: Ref}; +export type DateRangePickerProps = Props & { + ref?: Ref; +}; // forwardRef doesn't support generic parameters, so cast the result to the correct type -export default forwardRef(DatePicker) as ( - props: DatePickerProps, +export default forwardRef(DateRangePicker) as ( + props: DateRangePickerProps, ) => ReactElement; diff --git a/packages/components/date-picker/src/index.ts b/packages/components/date-picker/src/index.ts index 7f848bfa74..9a5b018bcc 100644 --- a/packages/components/date-picker/src/index.ts +++ b/packages/components/date-picker/src/index.ts @@ -1,10 +1,12 @@ import DatePicker from "./date-picker"; +import DateRangePicker from "./date-range-picker"; // export types export type {DatePickerProps} from "./date-picker"; +export type {DateRangePickerProps} from "./date-range-picker"; // export hooks export {useDatePicker} from "./use-date-picker"; -// export component -export {DatePicker}; +// export components +export {DatePicker, DateRangePicker}; diff --git a/packages/components/date-picker/src/use-date-picker-base.ts b/packages/components/date-picker/src/use-date-picker-base.ts index e0c9938816..2633a0c2fa 100644 --- a/packages/components/date-picker/src/use-date-picker-base.ts +++ b/packages/components/date-picker/src/use-date-picker-base.ts @@ -5,6 +5,7 @@ import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; import type {ReactNode} from "react"; +import type {ValueBase} from "@react-types/shared"; import { DatePickerVariantProps, @@ -12,12 +13,11 @@ import { SlotsToClasses, dateInput, } from "@nextui-org/theme"; -import {useMemo, useState} from "react"; +import {useState} from "react"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; -import {datePicker} from "@nextui-org/theme"; import {mergeProps} from "@react-aria/utils"; import {useDOMRef} from "@nextui-org/react-utils"; -import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; +import {clsx, dataAttr} from "@nextui-org/shared-utils"; import {useLocalizedStringFormatter} from "@react-aria/i18n"; import intlMessages from "../intl/messages"; @@ -80,7 +80,6 @@ interface Props extends NextUIBaseProps { * @default {} */ calendarProps?: Partial>; - /** * Props to be passed to the time input component. * @@ -139,7 +138,7 @@ export type UseDatePickerBaseProps = Props & DateInputProps, Variants | "ref" | "createCalendar" | "startContent" | "endContent" | "inputRef" > & - Omit, "minValue" | "maxValue">; + Omit, keyof ValueBase | "validate">; export function useDatePickerBase(originalProps: UseDatePickerBaseProps) { const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys); @@ -181,7 +180,7 @@ export function useDatePickerBase(originalProps: UseDatePic const baseStyles = clsx(classNames?.base, className); - let stringFormatter = useLocalizedStringFormatter(intlMessages); + let stringFormatter = useLocalizedStringFormatter(intlMessages) as any; const isDefaultColor = originalProps.color === "default" || !originalProps.color; const hasMultipleMonths = visibleMonths > 1; @@ -189,6 +188,10 @@ export function useDatePickerBase(originalProps: UseDatePic // Time field values const placeholder = originalProps?.placeholderValue; const timePlaceholder = placeholder && "hour" in placeholder ? placeholder : null; + const timeMinValue = + originalProps.minValue && "hour" in originalProps.minValue ? originalProps.minValue : null; + const timeMaxValue = + originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; const slotsProps: { popoverProps: UseDatePickerBaseProps["popoverProps"]; @@ -235,16 +238,6 @@ export function useDatePickerBase(originalProps: UseDatePic ), }; - const slots = useMemo( - () => - datePicker({ - ...variantProps, - hasMultipleMonths, - className, - }), - [objectToDeps(variantProps), hasMultipleMonths, className], - ); - const dateInputProps = { as, label, @@ -258,7 +251,6 @@ export function useDatePickerBase(originalProps: UseDatePic isInvalid, errorMessage, "data-invalid": dataAttr(originalProps?.isInvalid), - className: slots.base({class: baseStyles}), classNames, } as DateInputProps; @@ -266,14 +258,7 @@ export function useDatePickerBase(originalProps: UseDatePic ...userTimeInputProps, size: "sm", labelPlacement: "outside-left", - classNames: { - base: slots.timeInput({ - class: clsx(classNames?.timeInput, userTimeInputProps?.classNames?.base), - }), - label: slots.timeInputLabel({ - class: clsx(classNames?.timeInputLabel, userTimeInputProps?.classNames?.label), - }), - }, + label: userTimeInputProps?.label || stringFormatter.format("time"), placeholderValue: timePlaceholder, hourCycle: props.hourCycle, @@ -283,24 +268,11 @@ export function useDatePickerBase(originalProps: UseDatePic const popoverProps = { ...mergeProps(slotsProps.popoverProps, props), triggerRef: domRef, - classNames: { - content: slots.popoverContent({ - class: clsx( - classNames?.popoverContent, - slotsProps.popoverProps?.classNames?.["content"], - props.className, - ), - }), - }, } as PopoverProps; const calendarProps = { ...slotsProps.calendarProps, "data-slot": "calendar", - classNames: { - base: slots.calendar({class: classNames?.calendar}), - content: slots.calendarContent({class: classNames?.calendarContent}), - }, style: mergeProps( hasMultipleMonths ? { @@ -316,12 +288,10 @@ export function useDatePickerBase(originalProps: UseDatePic const selectorButtonProps = { ...slotsProps.selectorButtonProps, "data-slot": "selector-button", - className: slots.selectorButton({class: classNames?.selectorButton}), } as ButtonProps; const selectorIconProps = { "data-slot": "selector-icon", - className: slots.selectorIcon({class: classNames?.selectorIcon}), }; return { @@ -329,6 +299,12 @@ export function useDatePickerBase(originalProps: UseDatePic endContent, selectorIcon, createCalendar, + stringFormatter, + hasMultipleMonths, + slotsProps, + timeMinValue, + timeMaxValue, + baseStyles, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, @@ -338,8 +314,10 @@ export function useDatePickerBase(originalProps: UseDatePic timeInputProps, popoverProps, calendarProps, + userTimeInputProps, selectorButtonProps, selectorIconProps, + classNames, }; } diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index f0d01b6f05..b56a3d0c1f 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -5,38 +5,51 @@ import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; import type {UseDatePickerBaseProps} from "./use-date-picker-base"; +import type {DOMAttributes} from "@nextui-org/system"; -import {DOMAttributes} from "@nextui-org/system"; +import {useMemo} from "react"; +import {datePicker} from "@nextui-org/theme"; import {useDatePickerState} from "@react-stately/datepicker"; import {AriaDatePickerProps, useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; -import {dataAttr} from "@nextui-org/shared-utils"; +import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; import {useDatePickerBase} from "./use-date-picker-base"; interface Props extends UseDatePickerBaseProps {} -export type UseDatePickerProps = Props & - UseDatePickerBaseProps & - AriaDatePickerProps; +interface Props + extends Omit, keyof AriaDatePickerProps> {} -export function useDatePicker(originalProps: UseDatePickerProps) { +export type UseDatePickerProps = Props & AriaDatePickerProps; + +export function useDatePicker({ + className, + ...originalProps +}: UseDatePickerProps) { const { domRef, endContent, selectorIcon, + baseStyles, createCalendar, + hasMultipleMonths, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, + slotsProps, + timeMinValue, + timeMaxValue, CalendarBottomContent, dateInputProps, timeInputProps, popoverProps, calendarProps, variantProps, + userTimeInputProps, selectorButtonProps, selectorIconProps, + classNames, } = useDatePickerBase(originalProps); let state: DatePickerState = useDatePickerState({ @@ -44,6 +57,16 @@ export function useDatePicker(originalProps: UseDatePickerP shouldCloseOnSelect: () => !state.hasTime, }); + const slots = useMemo( + () => + datePicker({ + ...variantProps, + hasMultipleMonths, + className, + }), + [objectToDeps(variantProps), hasMultipleMonths, className], + ); + let { groupProps, labelProps, @@ -56,10 +79,7 @@ export function useDatePicker(originalProps: UseDatePickerP } = useAriaDatePicker(originalProps, state, domRef); // Time field values - const timeMinValue = - originalProps.minValue && "hour" in originalProps.minValue ? originalProps.minValue : null; - const timeMaxValue = - originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; + originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; const timeGranularity = state.granularity === "hour" || state.granularity === "minute" || state.granularity === "second" ? state.granularity @@ -81,6 +101,7 @@ export function useDatePicker(originalProps: UseDatePickerP fullWidth: true, disableAnimation, }), + className: slots.base({class: baseStyles}), "data-open": dataAttr(state.isOpen), } as DateInputProps; }; @@ -95,6 +116,14 @@ export function useDatePicker(originalProps: UseDatePickerP granularity: timeGranularity, minValue: timeMinValue, maxValue: timeMaxValue, + classNames: { + base: slots.timeInput({ + class: clsx(classNames?.timeInput, userTimeInputProps?.classNames?.base), + }), + label: slots.timeInputLabel({ + class: clsx(classNames?.timeInputLabel, userTimeInputProps?.classNames?.label), + }), + }, } as TimeInputProps; }; @@ -104,6 +133,15 @@ export function useDatePicker(originalProps: UseDatePickerP dialogProps, ...popoverProps, ...props, + classNames: { + content: slots.popoverContent({ + class: clsx( + classNames?.popoverContent, + slotsProps.popoverProps?.classNames?.["content"], + props.className, + ), + }), + }, } as PopoverProps; }; @@ -111,6 +149,10 @@ export function useDatePicker(originalProps: UseDatePickerP return { ...ariaCalendarProps, ...calendarProps, + classNames: { + base: slots.calendar({class: classNames?.calendar}), + content: slots.calendarContent({class: classNames?.calendarContent}), + }, } as CalendarProps; }; @@ -118,11 +160,15 @@ export function useDatePicker(originalProps: UseDatePickerP return { ...buttonProps, ...selectorButtonProps, + className: slots.selectorButton({class: classNames?.selectorButton}), } as ButtonProps; }; const getSelectorIconProps = () => { - return selectorIconProps; + return { + ...selectorIconProps, + className: slots.selectorIcon({class: classNames?.selectorIcon}), + }; }; return { diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 3a15a54c98..5b296afd56 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -1,31 +1,40 @@ import type {DateValue} from "@internationalized/date"; import type {DateInputProps, TimeInputProps} from "@nextui-org/date-input"; import type {ButtonProps} from "@nextui-org/button"; -import type {CalendarProps} from "@nextui-org/calendar"; +import type {RangeCalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; import type {AriaDateRangePickerProps} from "@react-types/datepicker"; import type {DateRangePickerState} from "@react-stately/datepicker"; import type {UseDatePickerBaseProps} from "./use-date-picker-base"; +import {useMemo} from "react"; import {useDateRangePickerState} from "@react-stately/datepicker"; import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepicker"; import {DOMAttributes} from "@nextui-org/system"; -import {dataAttr} from "@nextui-org/shared-utils"; +import {dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; +import {dateRangePicker} from "@nextui-org/theme"; import {useDatePickerBase} from "./use-date-picker-base"; -interface Props extends UseDatePickerBaseProps {} -export type UseDateRangePickerProps = Props & - UseDatePickerBaseProps & - AriaDateRangePickerProps; +interface Props + extends Omit, keyof AriaDateRangePickerProps> {} -export function useDateRangePicker(originalProps: UseDateRangePickerProps) { +export type UseDateRangePickerProps = Props & AriaDateRangePickerProps; + +export function useDateRangePicker({ + className, + ...originalProps +}: UseDateRangePickerProps) { const { domRef, endContent, selectorIcon, + slotsProps, createCalendar, + stringFormatter, + timeMinValue, + timeMaxValue, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, @@ -35,8 +44,10 @@ export function useDateRangePicker(originalProps: UseDateRa popoverProps, calendarProps, variantProps, + hasMultipleMonths, selectorButtonProps, selectorIconProps, + classNames, } = useDatePickerBase(originalProps); let state: DateRangePickerState = useDateRangePickerState({ @@ -58,11 +69,18 @@ export function useDateRangePicker(originalProps: UseDateRa errorMessageProps, } = useAriaDateRangePicker(originalProps, state, domRef); + const slots = useMemo( + () => + dateRangePicker({ + ...variantProps, + hasMultipleMonths, + className, + }), + [objectToDeps(variantProps), hasMultipleMonths, className], + ); + // Time field values - const timeMinValue = - originalProps.minValue && "hour" in originalProps.minValue ? originalProps.minValue : null; - const timeMaxValue = - originalProps.maxValue && "hour" in originalProps.maxValue ? originalProps.maxValue : null; + const timeGranularity = state.granularity === "hour" || state.granularity === "minute" || state.granularity === "second" ? state.granularity @@ -70,17 +88,37 @@ export function useDateRangePicker(originalProps: UseDateRa const showTimeField = !!timeGranularity; - const getDateInputProps = () => { + const getStartDateInputProps = () => { + return { + ...startFieldProps, + label: "From", + // groupProps, + // labelProps, + // createCalendar, + // errorMessageProps, + // descriptionProps, + ...mergeProps(variantProps, startFieldProps, { + // minValue: originalProps.minValue, + // maxValue: originalProps.maxValue, + fullWidth: true, + disableAnimation, + }), + "data-open": dataAttr(state.isOpen), + } as DateInputProps; + }; + + const getEndDateInputProps = () => { return { - ...dateInputProps, - groupProps, - labelProps, - createCalendar, - errorMessageProps, - descriptionProps, - ...mergeProps(variantProps, fieldProps, { - minValue: originalProps.minValue, - maxValue: originalProps.maxValue, + ...startFieldProps, + label: "To", + // groupProps, + // labelProps, + // createCalendar, + // errorMessageProps, + // descriptionProps, + ...mergeProps(variantProps, endFieldProps, { + // minValue: originalProps.minValue, + // maxValue: originalProps.maxValue, fullWidth: true, disableAnimation, }), @@ -88,13 +126,28 @@ export function useDateRangePicker(originalProps: UseDateRa } as DateInputProps; }; - const getTimeInputProps = () => { + const getStartTimeInputProps = () => { if (!showTimeField) return {}; return { ...timeInputProps, - value: state.timeValue, - onChange: state.setTimeValue, + label: stringFormatter.format("startTime"), + value: state.timeRange?.start || null, + onChange: (v) => state.setTime("start", v), + granularity: timeGranularity, + minValue: timeMinValue, + maxValue: timeMaxValue, + } as TimeInputProps; + }; + + const getEndTimeInputProps = () => { + if (!showTimeField) return {}; + + return { + ...timeInputProps, + label: stringFormatter.format("startTime"), + value: state.timeRange?.end || null, + onChange: (v) => state.setTime("end", v), granularity: timeGranularity, minValue: timeMinValue, maxValue: timeMaxValue, @@ -114,7 +167,7 @@ export function useDateRangePicker(originalProps: UseDateRa return { ...ariaCalendarProps, ...calendarProps, - } as CalendarProps; + } as RangeCalendarProps; }; const getSelectorButtonProps = () => { @@ -130,6 +183,7 @@ export function useDateRangePicker(originalProps: UseDateRa return { state, + groupProps, endContent, selectorIcon, showTimeField, @@ -137,11 +191,13 @@ export function useDateRangePicker(originalProps: UseDateRa disableAnimation, CalendarTopContent, CalendarBottomContent, - getDateInputProps, + getStartDateInputProps, + getEndDateInputProps, + getStartTimeInputProps, + getEndTimeInputProps, getPopoverProps, getSelectorButtonProps, getCalendarProps, - getTimeInputProps, getSelectorIconProps, }; } diff --git a/packages/components/date-picker/stories/date-range-picker.stories.tsx b/packages/components/date-picker/stories/date-range-picker.stories.tsx new file mode 100644 index 0000000000..23e14726e5 --- /dev/null +++ b/packages/components/date-picker/stories/date-range-picker.stories.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import {Meta} from "@storybook/react"; +import {dateInput} from "@nextui-org/theme"; + +import {DateRangePicker, DateRangePickerProps} from "../src"; + +export default { + title: "Components/DateRangePicker", + component: DateRangePicker, + argTypes: { + variant: { + control: { + type: "select", + }, + options: ["flat", "faded", "bordered", "underlined"], + }, + color: { + control: { + type: "select", + }, + options: ["default", "primary", "secondary", "success", "warning", "danger"], + }, + radius: { + control: { + type: "select", + }, + options: ["none", "sm", "md", "lg", "full"], + }, + size: { + control: { + type: "select", + }, + options: ["sm", "md", "lg"], + }, + labelPlacement: { + control: { + type: "select", + }, + options: ["inside", "outside", "outside-left"], + }, + isDisabled: { + control: { + type: "boolean", + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} as Meta; + +const defaultProps = { + label: "Birth Date", + className: "max-w-[256px]", + ...dateInput.defaultVariants, +}; + +const Template = (args: DateRangePickerProps) => ; + +export const Default = { + render: Template, + args: { + ...defaultProps, + }, +}; diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 32566786a3..a786dfac95 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -32,8 +32,21 @@ const datePicker = tv({ }, }); +const dateRangePicker = tv({ + extend: datePicker, + slots: { + base: "flex", + }, +}); + +/** Base */ export type DatePickerReturnType = ReturnType; export type DatePickerVariantProps = VariantProps; export type DatePickerSlots = keyof ReturnType; -export {datePicker}; +/** Range */ +export type DateRangePickerReturnType = ReturnType; +export type DateRangePickerVariantProps = VariantProps; +export type DateRangePickerSlots = keyof ReturnType; + +export {datePicker, dateRangePicker}; From f1f77e77749ca5815c4ac2369cdd54454771b243 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Fri, 12 Apr 2024 17:14:07 -0300 Subject: [PATCH 3/6] feat(date-input): components separated into multiple pieces to be able to implement the date range picker --- .../date-input/src/date-input-field.tsx | 45 +++++++ .../date-input/src/date-input-group.tsx | 111 ++++++++++++++++++ .../components/date-input/src/date-input.tsx | 98 +++------------- packages/components/date-input/src/index.ts | 4 + .../components/date-input/src/time-input.tsx | 97 +++------------ .../date-input/src/use-date-input.ts | 86 ++++++-------- .../date-input/src/use-time-input.ts | 84 ++++++------- .../date-input/stories/date-input.stories.tsx | 2 +- .../date-picker/src/date-range-picker.tsx | 30 +++-- .../date-picker/src/use-date-picker-base.ts | 5 +- .../date-picker/src/use-date-range-picker.ts | 109 ++++++++++++++++- .../core/theme/src/components/date-picker.ts | 14 +-- 12 files changed, 397 insertions(+), 288 deletions(-) create mode 100644 packages/components/date-input/src/date-input-field.tsx create mode 100644 packages/components/date-input/src/date-input-group.tsx diff --git a/packages/components/date-input/src/date-input-field.tsx b/packages/components/date-input/src/date-input-field.tsx new file mode 100644 index 0000000000..67982209f8 --- /dev/null +++ b/packages/components/date-input/src/date-input-field.tsx @@ -0,0 +1,45 @@ +import type {InputHTMLAttributes} from "react"; +import type {GroupDOMAttributes} from "@react-types/shared"; +import type {DateInputReturnType, DateInputSlots, SlotsToClasses} from "@nextui-org/theme"; +import type {DateFieldState} from "@react-stately/datepicker"; +import type {HTMLNextUIProps} from "@nextui-org/system"; + +import {forwardRef} from "react"; + +import {DateInputSegment} from "./date-input-segment"; + +type NextUIBaseProps = Omit, keyof GroupDOMAttributes | "onChange">; + +export interface DateInputFieldProps extends NextUIBaseProps, GroupDOMAttributes { + /** State for the date field. */ + state: DateFieldState; + /** Props for the hidden input element for HTML form submission. */ + inputProps: InputHTMLAttributes; + /** DateInput classes slots. */ + slots: DateInputReturnType; + /** DateInput classes. */ + classNames?: SlotsToClasses; +} + +export const DateInputField = forwardRef<"div", DateInputFieldProps>((props, ref) => { + const {as, state, slots, inputProps, classNames, ...otherProps} = props; + + const Component = as || "div"; + + return ( + + {state.segments.map((segment, i) => ( + + ))} + + + ); +}); + +DateInputField.displayName = "NextUI.DateInputField"; diff --git a/packages/components/date-input/src/date-input-group.tsx b/packages/components/date-input/src/date-input-group.tsx new file mode 100644 index 0000000000..5cd3dbbd3f --- /dev/null +++ b/packages/components/date-input/src/date-input-group.tsx @@ -0,0 +1,111 @@ +import type {HTMLAttributes, ReactElement, ReactNode} from "react"; +import type {GroupDOMAttributes} from "@react-types/shared"; + +import {useMemo} from "react"; +import {forwardRef} from "@nextui-org/system"; +import {dataAttr} from "@nextui-org/shared-utils"; + +// TODO: Use HelpTextProps from "@react-types/shared"; once we upgrade react-aria packages to the latest version. +export interface ValidationResult { + /** Whether the input value is invalid. */ + isInvalid: boolean; + /** The current error messages for the input if it is invalid, otherwise an empty array. */ + validationErrors: string[]; + /** The native validation details for the input. */ + validationDetails: ValidityState; +} + +export interface DateInputGroupProps extends ValidationResult { + children?: ReactElement; + shouldLabelBeOutside?: boolean; + label?: ReactNode; + startContent?: React.ReactNode; + endContent?: React.ReactNode; + groupProps?: GroupDOMAttributes; + wrapperProps?: HTMLAttributes; // <- inner wrapper props + helperWrapperProps?: HTMLAttributes; + labelProps?: HTMLAttributes; + descriptionProps?: HTMLAttributes; + errorMessageProps?: HTMLAttributes; + /** A description for the field. Provides a hint such as specific requirements for what to choose. */ + description?: ReactNode; + /** An error message for the field. */ + errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode); +} + +export const DateInputGroup = forwardRef<"div", DateInputGroupProps>((props, ref) => { + const { + as, + label, + children, + description, + startContent, + endContent, + errorMessage: errorMessageProp, + shouldLabelBeOutside, + isInvalid, + groupProps, + labelProps, + wrapperProps, + helperWrapperProps, + errorMessageProps, + descriptionProps, + validationErrors, + validationDetails, + ...otherProps + } = props; + + const Component = as || "div"; + + const labelContent = label ? {label} : null; + + const errorMessage = + typeof errorMessageProp === "function" + ? errorMessageProp({ + isInvalid, + validationErrors, + validationDetails, + }) + : errorMessageProp || validationErrors?.join(" "); + + const hasHelper = !!description || !!errorMessage; + + const helperWrapper = useMemo(() => { + if (!hasHelper) return null; + + return ( +
+ {errorMessage ? ( +
{errorMessage}
+ ) : description ? ( +
{description}
+ ) : null} +
+ ); + }, [ + hasHelper, + errorMessage, + description, + helperWrapperProps, + errorMessageProps, + descriptionProps, + ]); + + return ( + + {shouldLabelBeOutside ? labelContent : null} +
+ {!shouldLabelBeOutside ? labelContent : null} +
+ {startContent} + {children} + {endContent} +
+ {shouldLabelBeOutside ? helperWrapper : null} +
+ {!shouldLabelBeOutside ? helperWrapper : null} +
+ ); +}); + +DateInputGroup.displayName = "NextUI.DateInputGroup"; diff --git a/packages/components/date-input/src/date-input.tsx b/packages/components/date-input/src/date-input.tsx index 75ad7e497d..daa549d115 100644 --- a/packages/components/date-input/src/date-input.tsx +++ b/packages/components/date-input/src/date-input.tsx @@ -1,97 +1,31 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import type {DateValue} from "@internationalized/date"; import type {ForwardedRef, ReactElement, Ref} from "react"; -import {useMemo} from "react"; import {forwardRef} from "@nextui-org/system"; import {UseDateInputProps, useDateInput} from "./use-date-input"; -import {DateInputSegment} from "./date-input-segment"; +import {DateInputGroup} from "./date-input-group"; +import {DateInputField} from "./date-input-field"; export interface Props extends UseDateInputProps {} function DateInput(props: Props, ref: ForwardedRef) { - const { - Component, - state, - label, - slots, - hasHelper, - errorMessage, - description, - startContent, - endContent, - shouldLabelBeOutside, - classNames, - getBaseProps, - getInputProps, - getFieldProps, - getLabelProps, - getInputWrapperProps, - getInnerWrapperProps, - getDescriptionProps, - getHelperWrapperProps, - getErrorMessageProps, - } = useDateInput({ - ...props, - ref, - }); - - const labelContent = label ? {label} : null; - - const helperWrapper = useMemo(() => { - if (!hasHelper) return null; - - return ( -
- {errorMessage ? ( -
{errorMessage}
- ) : description ? ( -
{description}
- ) : null} -
- ); - }, [ - hasHelper, - errorMessage, - description, - getHelperWrapperProps, - getErrorMessageProps, - getDescriptionProps, - ]); - - const inputContent = useMemo( - () => ( -
- {state.segments.map((segment, i) => ( - - ))} - -
- ), - [state, slots, classNames?.segment, getFieldProps], - ); + const {state, slots, classNames, getBaseGroupProps, getInputProps, getFieldProps} = + useDateInput({ + ...props, + ref, + }); return ( - - {shouldLabelBeOutside ? labelContent : null} -
- {!shouldLabelBeOutside ? labelContent : null} -
- {startContent} - {inputContent} - {endContent} -
- {shouldLabelBeOutside ? helperWrapper : null} -
- {!shouldLabelBeOutside ? helperWrapper : null} -
+ + + ); } diff --git a/packages/components/date-input/src/index.ts b/packages/components/date-input/src/index.ts index b064e8c251..2bb1ffa3a6 100644 --- a/packages/components/date-input/src/index.ts +++ b/packages/components/date-input/src/index.ts @@ -6,10 +6,14 @@ export type {DateInputProps} from "./date-input"; export type {TimeInputProps} from "./time-input"; export type {DateValue as DateInputValue} from "@react-types/datepicker"; export type {TimeValue as TimeInputValue} from "@react-types/datepicker"; +export type {DateInputGroupProps} from "./date-input-group"; +export type {DateInputFieldProps} from "./date-input-field"; // export hooks export {useDateInput} from "./use-date-input"; export {useTimeInput} from "./use-time-input"; // export components +export {DateInputGroup} from "./date-input-group"; +export {DateInputField} from "./date-input-field"; export {DateInput, TimeInput}; diff --git a/packages/components/date-input/src/time-input.tsx b/packages/components/date-input/src/time-input.tsx index ccec206139..a393fc7a17 100644 --- a/packages/components/date-input/src/time-input.tsx +++ b/packages/components/date-input/src/time-input.tsx @@ -1,96 +1,31 @@ import type {TimeValue} from "@react-types/datepicker"; import type {ForwardedRef, ReactElement, Ref} from "react"; -import {useMemo} from "react"; import {forwardRef} from "@nextui-org/system"; import {UseTimeInputProps, useTimeInput} from "./use-time-input"; -import {DateInputSegment} from "./date-input-segment"; +import {DateInputField} from "./date-input-field"; +import {DateInputGroup} from "./date-input-group"; export interface Props extends UseTimeInputProps {} function TimeInput(props: Props, ref: ForwardedRef) { - const { - Component, - state, - label, - slots, - hasHelper, - errorMessage, - description, - startContent, - endContent, - shouldLabelBeOutside, - classNames, - getBaseProps, - getInputProps, - getFieldProps, - getLabelProps, - getInputWrapperProps, - getInnerWrapperProps, - getDescriptionProps, - getHelperWrapperProps, - getErrorMessageProps, - } = useTimeInput({ - ...props, - ref, - }); - - const labelContent = label ? {label} : null; - - const helperWrapper = useMemo(() => { - if (!hasHelper) return null; - - return ( -
- {errorMessage ? ( -
{errorMessage}
- ) : description ? ( -
{description}
- ) : null} -
- ); - }, [ - hasHelper, - errorMessage, - description, - getHelperWrapperProps, - getErrorMessageProps, - getDescriptionProps, - ]); - - const inputContent = useMemo( - () => ( -
- {state.segments.map((segment, i) => ( - - ))} - -
- ), - [state, slots, classNames?.segment, getFieldProps], - ); + const {state, slots, classNames, getBaseGroupProps, getInputProps, getFieldProps} = + useTimeInput({ + ...props, + ref, + }); return ( - - {shouldLabelBeOutside ? labelContent : null} -
- {!shouldLabelBeOutside ? labelContent : null} -
- {startContent} - {inputContent} - {endContent} -
- {shouldLabelBeOutside ? helperWrapper : null} -
- {!shouldLabelBeOutside ? helperWrapper : null} -
+ + + ); } diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index f3157e9a1d..ee58c9527b 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -4,6 +4,7 @@ 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"; @@ -137,15 +138,14 @@ export function useDateInput(originalProps: UseDateInputPro maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), createCalendar: createCalendarProp = providerContext?.createCalendar ?? null, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, - errorMessage: errorMessageProp, + errorMessage, } = props; const domRef = useDOMRef(ref); const inputRef = useDOMRef(inputRefProp); - const Component = as || "div"; - const {locale} = useLocale(); + const state = useDateFieldState({ ...originalProps, label, @@ -175,17 +175,6 @@ export function useDateInput(originalProps: UseDateInputPro const isInvalid = isInvalidProp || ariaIsInvalid; - const errorMessage = - typeof errorMessageProp === "function" - ? errorMessageProp({ - isInvalid, - validationErrors, - validationDetails, - }) - : errorMessageProp || validationErrors.join(" "); - - const hasHelper = !!description || !!errorMessage; - const labelPlacement = useMemo(() => { if ( (!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && @@ -209,20 +198,6 @@ export function useDateInput(originalProps: UseDateInputPro [objectToDeps(variantProps), labelPlacement, className], ); - const getBaseProps: PropGetter = () => { - return { - "data-slot": "base", - "data-has-helper": dataAttr(hasHelper), - "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), - className: slots.base({class: baseStyles}), - }; - }; - const getLabelProps: PropGetter = (props) => { return { ...mergeProps(labelProps, labelPropsProp, props), @@ -241,18 +216,18 @@ export function useDateInput(originalProps: UseDateInputPro }; }; - const getFieldProps: PropGetter = (props) => { + const getFieldProps = (props: DOMAttributes = {}) => { return { ref: domRef, - "data-slot": "input", + "data-slot": "input-field", ...mergeProps(fieldProps, fieldPropsProp, props), className: slots.input({ class: clsx(classNames?.input, props?.className), }), - }; + } as GroupDOMAttributes; }; - const getInputWrapperProps: PropGetter = (props) => { + const getInputWrapperProps = (props = {}) => { return { ...props, ...groupProps, @@ -261,7 +236,7 @@ export function useDateInput(originalProps: UseDateInputPro class: classNames?.inputWrapper, }), onClick: fieldProps.onClick, - }; + } as GroupDOMAttributes; }; const getInnerWrapperProps: PropGetter = (props) => { @@ -300,29 +275,44 @@ export function useDateInput(originalProps: UseDateInputPro }; }; + 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 { - Component, state, domRef, slots, - label, - hasHelper, - shouldLabelBeOutside, classNames, - description, - errorMessage, labelPlacement, - startContent, - endContent, - getBaseProps, - getLabelProps, + getBaseGroupProps, getFieldProps, getInputProps, - getInputWrapperProps, - getInnerWrapperProps, - getHelperWrapperProps, - getErrorMessageProps, - getDescriptionProps, }; } diff --git a/packages/components/date-input/src/use-time-input.ts b/packages/components/date-input/src/use-time-input.ts index 1ff2f0130a..21e009857d 100644 --- a/packages/components/date-input/src/use-time-input.ts +++ b/packages/components/date-input/src/use-time-input.ts @@ -2,6 +2,7 @@ import type {DateInputVariantProps, DateInputSlots, SlotsToClasses} from "@nextu import type {AriaTimeFieldProps, TimeValue} from "@react-types/datepicker"; 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 {mergeProps} from "@react-aria/utils"; @@ -95,15 +96,14 @@ export function useTimeInput(originalProps: UseTimeInputPro minValue, maxValue, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, - errorMessage: errorMessageProp, + errorMessage, } = props; const domRef = useDOMRef(ref); const inputRef = useDOMRef(inputRefProp); - const Component = as || "div"; - const {locale} = useLocale(); + const state = useTimeFieldState({ ...originalProps, label, @@ -129,17 +129,6 @@ export function useTimeInput(originalProps: UseTimeInputPro const isInvalid = isInvalidProp || ariaIsInvalid; - const errorMessage = - typeof errorMessageProp === "function" - ? errorMessageProp({ - isInvalid, - validationErrors, - validationDetails, - }) - : errorMessageProp || validationErrors.join(" "); - - const hasHelper = !!description || !!errorMessage; - const labelPlacement = useMemo(() => { if ( (!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && @@ -163,20 +152,6 @@ export function useTimeInput(originalProps: UseTimeInputPro [objectToDeps(variantProps), labelPlacement, className], ); - const getBaseProps: PropGetter = () => { - return { - "data-slot": "base", - "data-has-helper": dataAttr(hasHelper), - "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), - className: slots.base({class: baseStyles}), - }; - }; - const getLabelProps: PropGetter = (props) => { return { ...mergeProps(labelProps, labelPropsProp, props), @@ -195,7 +170,7 @@ export function useTimeInput(originalProps: UseTimeInputPro }; }; - const getFieldProps: PropGetter = (props) => { + const getFieldProps = (props: DOMAttributes = {}) => { return { ref: domRef, "data-slot": "input", @@ -203,10 +178,10 @@ export function useTimeInput(originalProps: UseTimeInputPro className: slots.input({ class: clsx(classNames?.input, props?.className), }), - }; + } as GroupDOMAttributes; }; - const getInputWrapperProps: PropGetter = (props) => { + const getInputWrapperProps = (props = {}) => { return { ...props, ...groupProps, @@ -215,7 +190,7 @@ export function useTimeInput(originalProps: UseTimeInputPro class: classNames?.inputWrapper, }), onClick: fieldProps.onClick, - }; + } as GroupDOMAttributes; }; const getInnerWrapperProps: PropGetter = (props) => { @@ -254,29 +229,44 @@ export function useTimeInput(originalProps: UseTimeInputPro }; }; + 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 { - Component, state, domRef, slots, - label, - hasHelper, - shouldLabelBeOutside, classNames, - description, - errorMessage, labelPlacement, - startContent, - endContent, - getBaseProps, - getLabelProps, + getBaseGroupProps, getFieldProps, getInputProps, - getInputWrapperProps, - getInnerWrapperProps, - getHelperWrapperProps, - getErrorMessageProps, - getDescriptionProps, }; } diff --git a/packages/components/date-input/stories/date-input.stories.tsx b/packages/components/date-input/stories/date-input.stories.tsx index fc6081f75e..a23e4e211b 100644 --- a/packages/components/date-input/stories/date-input.stories.tsx +++ b/packages/components/date-input/stories/date-input.stories.tsx @@ -300,7 +300,7 @@ export const MaxDateValue = { args: { ...defaultProps, maxValue: today(getLocalTimeZone()), - defaultValue: parseDate("2024-04-05"), + defaultValue: today(getLocalTimeZone()).add({days: 1}), }, }; diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx index 8329d80fc3..66ffa591e7 100644 --- a/packages/components/date-picker/src/date-range-picker.tsx +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -4,7 +4,7 @@ import {ForwardedRef, ReactElement, Ref, useMemo} from "react"; import {cloneElement, isValidElement} from "react"; import {forwardRef} from "@nextui-org/system"; import {Button} from "@nextui-org/button"; -import {DateInput, TimeInput} from "@nextui-org/date-input"; +import {DateInput, TimeInput, DateInputGroup} from "@nextui-org/date-input"; import {FreeSoloPopover} from "@nextui-org/popover"; import {RangeCalendar} from "@nextui-org/calendar"; import {AnimatePresence} from "framer-motion"; @@ -78,18 +78,34 @@ function DateRangePicker(props: Props, ref: ForwardedRef ) : null; return ( -
- - - + <> + {endContent || selectorContent}} + errorMessage={errorMessage} + errorMessageProps={getErrorMessageProps()} + groupProps={getInputWrapperProps()} + helperWrapperProps={getHelperWrapperProps()} + label={label} + labelProps={getLabelProps()} + shouldLabelBeOutside={shouldLabelBeOutside} + startContent={startContent} + wrapperProps={getInnerWrapperProps()} + > + + + + - {/* {endContent || selectorContent}} /> */} {disableAnimation ? popoverContent : {popoverContent}} -
+ ); } diff --git a/packages/components/date-picker/src/use-date-picker-base.ts b/packages/components/date-picker/src/use-date-picker-base.ts index 2633a0c2fa..ec955c0c7b 100644 --- a/packages/components/date-picker/src/use-date-picker-base.ts +++ b/packages/components/date-picker/src/use-date-picker-base.ts @@ -8,10 +8,10 @@ import type {ReactNode} from "react"; import type {ValueBase} from "@react-types/shared"; import { + dateInput, DatePickerVariantProps, DatePickerSlots, SlotsToClasses, - dateInput, } from "@nextui-org/theme"; import {useState} from "react"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; @@ -149,13 +149,13 @@ export function useDatePickerBase(originalProps: UseDatePic as, ref, label, + endContent, selectorIcon, inputRef, isInvalid, errorMessage, description, startContent, - endContent, validationState, validationBehavior, visibleMonths = 1, @@ -258,7 +258,6 @@ export function useDatePickerBase(originalProps: UseDatePic ...userTimeInputProps, size: "sm", labelPlacement: "outside-left", - label: userTimeInputProps?.label || stringFormatter.format("time"), placeholderValue: timePlaceholder, hourCycle: props.hourCycle, diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 5b296afd56..41d257e311 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -6,14 +6,15 @@ import type {PopoverProps} from "@nextui-org/popover"; import type {AriaDateRangePickerProps} from "@react-types/datepicker"; import type {DateRangePickerState} from "@react-stately/datepicker"; import type {UseDatePickerBaseProps} from "./use-date-picker-base"; +import type {PropGetter} from "@nextui-org/system"; import {useMemo} from "react"; import {useDateRangePickerState} from "@react-stately/datepicker"; import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepicker"; import {DOMAttributes} from "@nextui-org/system"; -import {dataAttr, objectToDeps} from "@nextui-org/shared-utils"; +import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; -import {dateRangePicker} from "@nextui-org/theme"; +import {datePicker, dateInput} from "@nextui-org/theme"; import {useDatePickerBase} from "./use-date-picker-base"; @@ -23,13 +24,18 @@ interface Props export type UseDateRangePickerProps = Props & AriaDateRangePickerProps; export function useDateRangePicker({ + label, + isInvalid, + description, + startContent, + endContent, + selectorIcon, + errorMessage, className, ...originalProps }: UseDateRangePickerProps) { const { domRef, - endContent, - selectorIcon, slotsProps, createCalendar, stringFormatter, @@ -39,7 +45,6 @@ export function useDateRangePicker({ disableAnimation, CalendarTopContent, CalendarBottomContent, - dateInputProps, timeInputProps, popoverProps, calendarProps, @@ -71,7 +76,7 @@ export function useDateRangePicker({ const slots = useMemo( () => - dateRangePicker({ + datePicker({ ...variantProps, hasMultipleMonths, className, @@ -181,12 +186,104 @@ export function useDateRangePicker({ return selectorIconProps; }; + /** + * DateInput Props + */ + + const dateInputSlots = useMemo( + () => + dateInput({ + ...variantProps, + hasMultipleMonths, + className, + }), + [objectToDeps(variantProps), hasMultipleMonths, className], + ); + + const hasHelper = !!description || !!errorMessage; + + const getBaseProps: PropGetter = () => { + return { + "data-slot": "base", + "data-has-helper": dataAttr(hasHelper), + "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), + className: slots.base({class: baseStyles}), + }; + }; + + const getLabelProps: PropGetter = (props) => { + return { + ...mergeProps(labelProps, labelPropsProp, props), + "data-slot": "label", + className: slots.label({ + class: clsx(classNames?.label, props?.className), + }), + }; + }; + + 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)}), + }; + }; + return { state, + label, groupProps, + labelProps, endContent, selectorIcon, showTimeField, + descriptionProps, + errorMessageProps, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index a786dfac95..f63349cedc 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -32,21 +32,9 @@ const datePicker = tv({ }, }); -const dateRangePicker = tv({ - extend: datePicker, - slots: { - base: "flex", - }, -}); - /** Base */ export type DatePickerReturnType = ReturnType; export type DatePickerVariantProps = VariantProps; export type DatePickerSlots = keyof ReturnType; -/** Range */ -export type DateRangePickerReturnType = ReturnType; -export type DateRangePickerVariantProps = VariantProps; -export type DateRangePickerSlots = keyof ReturnType; - -export {datePicker, dateRangePicker}; +export {datePicker}; From 8ae2bbf6dab435c6c0185cb2c21af96baf3d3204 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Fri, 12 Apr 2024 19:08:28 -0300 Subject: [PATCH 4/6] feat(date-range-picker): first version of it working --- .../date-input/src/date-input-group.tsx | 2 +- packages/components/date-input/src/index.ts | 1 + .../src/date-range-picker-field.tsx | 82 ++++++ .../date-picker/src/date-range-picker.tsx | 32 +-- .../date-picker/src/use-date-picker-base.ts | 40 +-- .../date-picker/src/use-date-picker.ts | 33 ++- .../date-picker/src/use-date-range-picker.ts | 263 +++++++++++++----- .../stories/date-range-picker.stories.tsx | 3 +- .../core/theme/src/components/date-input.ts | 2 +- .../core/theme/src/components/date-picker.ts | 14 +- 10 files changed, 331 insertions(+), 141 deletions(-) create mode 100644 packages/components/date-picker/src/date-range-picker-field.tsx diff --git a/packages/components/date-input/src/date-input-group.tsx b/packages/components/date-input/src/date-input-group.tsx index 5cd3dbbd3f..f87c2b731d 100644 --- a/packages/components/date-input/src/date-input-group.tsx +++ b/packages/components/date-input/src/date-input-group.tsx @@ -16,7 +16,7 @@ export interface ValidationResult { } export interface DateInputGroupProps extends ValidationResult { - children?: ReactElement; + children?: ReactElement | ReactElement[]; shouldLabelBeOutside?: boolean; label?: ReactNode; startContent?: React.ReactNode; diff --git a/packages/components/date-input/src/index.ts b/packages/components/date-input/src/index.ts index 2bb1ffa3a6..63409e3734 100644 --- a/packages/components/date-input/src/index.ts +++ b/packages/components/date-input/src/index.ts @@ -16,4 +16,5 @@ export {useTimeInput} from "./use-time-input"; // export components export {DateInputGroup} from "./date-input-group"; export {DateInputField} from "./date-input-field"; +export {DateInputSegment} from "./date-input-segment"; export {DateInput, TimeInput}; diff --git a/packages/components/date-picker/src/date-range-picker-field.tsx b/packages/components/date-picker/src/date-range-picker-field.tsx new file mode 100644 index 0000000000..4cce73bae2 --- /dev/null +++ b/packages/components/date-picker/src/date-range-picker-field.tsx @@ -0,0 +1,82 @@ +import type {DateInputReturnType, DateInputSlots, SlotsToClasses} from "@nextui-org/theme"; +import type {AriaDatePickerProps} from "@react-types/datepicker"; +import type {HTMLNextUIProps} from "@nextui-org/system"; +import type {DateInputProps} from "@nextui-org/date-input"; + +import {createCalendar} from "@internationalized/date"; +import {forwardRef, useRef} from "react"; +import {DateValue} from "@react-types/datepicker"; +import {useDateField as useAriaDateField} from "@react-aria/datepicker"; +import {ForwardedRef, ReactElement, Ref} from "react"; +import {useDateFieldState} from "@react-stately/datepicker"; +import {DateInputSegment} from "@nextui-org/date-input"; +import {filterDOMProps, useDOMRef} from "@nextui-org/react-utils"; +import {useLocale} from "@react-aria/i18n"; +import {mergeProps} from "@react-aria/utils"; + +type NextUIBaseProps = Omit< + HTMLNextUIProps<"div">, + keyof AriaDatePickerProps | "onChange" +>; + +export interface Props + extends NextUIBaseProps, + AriaDatePickerProps, + Pick { + /** DateInput classes slots. */ + slots: DateInputReturnType; + /** DateInput classes. */ + classNames?: SlotsToClasses; +} + +function DateRangePickerField( + props: Props, + ref: ForwardedRef, +) { + const {as, slots, createCalendar: createCalendarProp, classNames, ...otherProps} = props; + + const Component = as || "div"; + + const domRef = useDOMRef(ref); + + const {locale} = useLocale(); + + const state = useDateFieldState({ + ...otherProps, + locale, + createCalendar: + !createCalendarProp || typeof createCalendarProp !== "function" + ? createCalendar + : (createCalendarProp as typeof createCalendar), + }); + + const inputRef = useRef(null); + + const {fieldProps, inputProps} = useAriaDateField({...otherProps, inputRef}, state, domRef); + + return ( + + {state.segments.map((segment, i) => ( + + ))} + + + ); +} + +DateRangePickerField.displayName = "NextUI.DateRangePickerField"; + +export type DateRangePickerFieldProps = Props & { + ref?: Ref; +}; + +// forwardRef doesn't support generic parameters, so cast the result to the correct type +export default forwardRef(DateRangePickerField) as ( + props: DateRangePickerFieldProps, +) => ReactElement; diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx index 66ffa591e7..3d4d6e7158 100644 --- a/packages/components/date-picker/src/date-range-picker.tsx +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -4,12 +4,13 @@ import {ForwardedRef, ReactElement, Ref, useMemo} from "react"; import {cloneElement, isValidElement} from "react"; import {forwardRef} from "@nextui-org/system"; import {Button} from "@nextui-org/button"; -import {DateInput, TimeInput, DateInputGroup} from "@nextui-org/date-input"; +import {TimeInput, DateInputGroup} from "@nextui-org/date-input"; import {FreeSoloPopover} from "@nextui-org/popover"; import {RangeCalendar} from "@nextui-org/calendar"; import {AnimatePresence} from "framer-motion"; import {CalendarBoldIcon} from "@nextui-org/shared-icons"; +import DateRangePickerField from "./date-range-picker-field"; import {UseDateRangePickerProps, useDateRangePicker} from "./use-date-range-picker"; export interface Props @@ -21,12 +22,13 @@ function DateRangePicker(props: Props, ref: ForwardedRef endContent, selectorIcon, showTimeField, - groupProps, disableAnimation, isCalendarHeaderExpanded, + getDateInputGroupProps, getStartDateInputProps, getEndDateInputProps, getPopoverProps, + getSeparatorProps, getStartTimeInputProps, getEndTimeInputProps, getSelectorButtonProps, @@ -80,30 +82,16 @@ function DateRangePicker(props: Props, ref: ForwardedRef return ( <> {endContent || selectorContent}} - errorMessage={errorMessage} - errorMessageProps={getErrorMessageProps()} - groupProps={getInputWrapperProps()} - helperWrapperProps={getHelperWrapperProps()} - label={label} - labelProps={getLabelProps()} - shouldLabelBeOutside={shouldLabelBeOutside} - startContent={startContent} - wrapperProps={getInnerWrapperProps()} > - - - + + + - {/* {endContent || selectorContent}} - /> */} {disableAnimation ? popoverContent : {popoverContent}} ); diff --git a/packages/components/date-picker/src/use-date-picker-base.ts b/packages/components/date-picker/src/use-date-picker-base.ts index ec955c0c7b..b5713358c9 100644 --- a/packages/components/date-picker/src/use-date-picker-base.ts +++ b/packages/components/date-picker/src/use-date-picker-base.ts @@ -7,17 +7,12 @@ import type {PopoverProps} from "@nextui-org/popover"; import type {ReactNode} from "react"; import type {ValueBase} from "@react-types/shared"; -import { - dateInput, - DatePickerVariantProps, - DatePickerSlots, - SlotsToClasses, -} from "@nextui-org/theme"; +import {dateInput, DatePickerVariantProps} from "@nextui-org/theme"; import {useState} from "react"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {mergeProps} from "@react-aria/utils"; import {useDOMRef} from "@nextui-org/react-utils"; -import {clsx, dataAttr} from "@nextui-org/shared-utils"; +import {dataAttr} from "@nextui-org/shared-utils"; import {useLocalizedStringFormatter} from "@react-aria/i18n"; import intlMessages from "../intl/messages"; @@ -96,30 +91,6 @@ interface Props extends NextUIBaseProps { * @default false */ disableAnimation?: boolean; - /** - * 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 & DateInputProps["classNames"]; } type Variants = @@ -171,15 +142,11 @@ export function useDatePickerBase(originalProps: UseDatePic CalendarTopContent, CalendarBottomContent, createCalendar, - className, - classNames, } = props; const domRef = useDOMRef(ref); const disableAnimation = originalProps.disableAnimation ?? false; - const baseStyles = clsx(classNames?.base, className); - let stringFormatter = useLocalizedStringFormatter(intlMessages) as any; const isDefaultColor = originalProps.color === "default" || !originalProps.color; @@ -251,7 +218,6 @@ export function useDatePickerBase(originalProps: UseDatePic isInvalid, errorMessage, "data-invalid": dataAttr(originalProps?.isInvalid), - classNames, } as DateInputProps; const timeInputProps = { @@ -303,7 +269,6 @@ export function useDatePickerBase(originalProps: UseDatePic slotsProps, timeMinValue, timeMaxValue, - baseStyles, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, @@ -316,7 +281,6 @@ export function useDatePickerBase(originalProps: UseDatePic userTimeInputProps, selectorButtonProps, selectorIconProps, - classNames, }; } diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index b56a3d0c1f..19f96eb412 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -6,6 +6,7 @@ import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; import type {UseDatePickerBaseProps} from "./use-date-picker-base"; import type {DOMAttributes} from "@nextui-org/system"; +import type {DatePickerSlots, SlotsToClasses} from "@nextui-org/theme"; import {useMemo} from "react"; import {datePicker} from "@nextui-org/theme"; @@ -19,19 +20,44 @@ import {useDatePickerBase} from "./use-date-picker-base"; interface Props extends UseDatePickerBaseProps {} interface Props - extends Omit, keyof AriaDatePickerProps> {} + extends Omit, keyof AriaDatePickerProps> { + /** + * 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 & DateInputProps["classNames"]; +} export type UseDatePickerProps = Props & AriaDatePickerProps; export function useDatePicker({ className, + classNames, ...originalProps }: UseDatePickerProps) { const { domRef, endContent, selectorIcon, - baseStyles, createCalendar, hasMultipleMonths, isCalendarHeaderExpanded, @@ -49,7 +75,6 @@ export function useDatePicker({ userTimeInputProps, selectorButtonProps, selectorIconProps, - classNames, } = useDatePickerBase(originalProps); let state: DatePickerState = useDatePickerState({ @@ -57,6 +82,8 @@ export function useDatePicker({ shouldCloseOnSelect: () => !state.hasTime, }); + const baseStyles = clsx(classNames?.base, className); + const slots = useMemo( () => datePicker({ diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 41d257e311..4344d1ce37 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -1,29 +1,60 @@ import type {DateValue} from "@internationalized/date"; -import type {DateInputProps, TimeInputProps} from "@nextui-org/date-input"; +import type {DateInputVariantProps} from "@nextui-org/theme"; +import type {TimeInputProps} from "@nextui-org/date-input"; import type {ButtonProps} from "@nextui-org/button"; import type {RangeCalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; +import type {DOMAttributes, GroupDOMAttributes} from "@react-types/shared"; import type {AriaDateRangePickerProps} from "@react-types/datepicker"; import type {DateRangePickerState} from "@react-stately/datepicker"; import type {UseDatePickerBaseProps} from "./use-date-picker-base"; import type {PropGetter} from "@nextui-org/system"; +import type {DateRangePickerFieldProps} from "./date-range-picker-field"; +import type {DateInputGroupProps} from "@nextui-org/date-input"; +import type {DateRangePickerSlots, SlotsToClasses} from "@nextui-org/theme"; +import type {DateInputProps} from "@nextui-org/date-input"; -import {useMemo} from "react"; +import {useMemo, useRef} from "react"; import {useDateRangePickerState} from "@react-stately/datepicker"; import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepicker"; -import {DOMAttributes} from "@nextui-org/system"; import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {mergeProps} from "@react-aria/utils"; -import {datePicker, dateInput} from "@nextui-org/theme"; +import {dateRangePicker, dateInput} from "@nextui-org/theme"; import {useDatePickerBase} from "./use-date-picker-base"; - interface Props - extends Omit, keyof AriaDateRangePickerProps> {} + extends Omit, keyof AriaDateRangePickerProps> { + /** + * 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 & DateInputProps["classNames"]; +} export type UseDateRangePickerProps = Props & AriaDateRangePickerProps; export function useDateRangePicker({ + as, label, isInvalid, description, @@ -32,6 +63,7 @@ export function useDateRangePicker({ selectorIcon, errorMessage, className, + classNames, ...originalProps }: UseDateRangePickerProps) { const { @@ -49,10 +81,10 @@ export function useDateRangePicker({ popoverProps, calendarProps, variantProps, + userTimeInputProps, hasMultipleMonths, selectorButtonProps, selectorIconProps, - classNames, } = useDatePickerBase(originalProps); let state: DateRangePickerState = useDateRangePickerState({ @@ -60,6 +92,8 @@ export function useDateRangePicker({ shouldCloseOnSelect: () => !state.hasTime, }); + const popoverTriggerRef = useRef(null); + originalProps.minValue; let { @@ -70,13 +104,15 @@ export function useDateRangePicker({ buttonProps, dialogProps, calendarProps: ariaCalendarProps, + validationDetails, + validationErrors, descriptionProps, errorMessageProps, } = useAriaDateRangePicker(originalProps, state, domRef); const slots = useMemo( () => - datePicker({ + dateRangePicker({ ...variantProps, hasMultipleMonths, className, @@ -93,44 +129,21 @@ export function useDateRangePicker({ const showTimeField = !!timeGranularity; - const getStartDateInputProps = () => { - return { - ...startFieldProps, - label: "From", - // groupProps, - // labelProps, - // createCalendar, - // errorMessageProps, - // descriptionProps, - ...mergeProps(variantProps, startFieldProps, { - // minValue: originalProps.minValue, - // maxValue: originalProps.maxValue, - fullWidth: true, - disableAnimation, - }), - "data-open": dataAttr(state.isOpen), - } as DateInputProps; - }; + const labelPlacement = useMemo(() => { + if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) { + return "outside"; + } - const getEndDateInputProps = () => { - return { - ...startFieldProps, - label: "To", - // groupProps, - // labelProps, - // createCalendar, - // errorMessageProps, - // descriptionProps, - ...mergeProps(variantProps, endFieldProps, { - // minValue: originalProps.minValue, - // maxValue: originalProps.maxValue, - fullWidth: true, - disableAnimation, - }), - "data-open": dataAttr(state.isOpen), - } as DateInputProps; - }; + return originalProps.labelPlacement ?? "inside"; + }, [originalProps.labelPlacement, label]); + + const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left"; + /** + * ------------------------------ + * DateRangePicker Props + * ------------------------------ + */ const getStartTimeInputProps = () => { if (!showTimeField) return {}; @@ -142,6 +155,14 @@ export function useDateRangePicker({ granularity: timeGranularity, minValue: timeMinValue, maxValue: timeMaxValue, + classNames: { + base: slots.timeInput({ + class: clsx(classNames?.timeInput, userTimeInputProps?.classNames?.base), + }), + label: slots.timeInputLabel({ + class: clsx(classNames?.timeInputLabel, userTimeInputProps?.classNames?.label), + }), + }, } as TimeInputProps; }; @@ -156,6 +177,14 @@ export function useDateRangePicker({ granularity: timeGranularity, minValue: timeMinValue, maxValue: timeMaxValue, + classNames: { + base: slots.timeInput({ + class: clsx(classNames?.timeInput, userTimeInputProps?.classNames?.base), + }), + label: slots.timeInputLabel({ + class: clsx(classNames?.timeInputLabel, userTimeInputProps?.classNames?.label), + }), + }, } as TimeInputProps; }; @@ -163,8 +192,18 @@ export function useDateRangePicker({ return { state, dialogProps, - ...popoverProps, ...props, + ...popoverProps, + triggerRef: popoverTriggerRef, + classNames: { + content: slots.popoverContent({ + class: clsx( + classNames?.popoverContent, + slotsProps.popoverProps?.classNames?.["content"], + props.className, + ), + }), + }, } as PopoverProps; }; @@ -172,6 +211,10 @@ export function useDateRangePicker({ return { ...ariaCalendarProps, ...calendarProps, + classNames: { + base: slots.calendar({class: classNames?.calendar}), + content: slots.calendarContent({class: classNames?.calendarContent}), + }, } as RangeCalendarProps; }; @@ -179,48 +222,89 @@ export function useDateRangePicker({ return { ...buttonProps, ...selectorButtonProps, + className: slots.selectorButton({class: classNames?.selectorButton}), } as ButtonProps; }; + const getSeparatorProps = () => { + return { + "data-slot": "separator", + className: slots.separator({class: classNames?.separator}), + }; + }; + const getSelectorIconProps = () => { - return selectorIconProps; + return { + ...selectorIconProps, + className: slots.selectorIcon({class: classNames?.selectorIcon}), + }; }; /** + * ------------------------------ * DateInput Props + * ------------------------------ */ + const baseStyles = clsx(classNames?.base, className); + const dateInputSlots = useMemo( () => dateInput({ ...variantProps, - hasMultipleMonths, + labelPlacement, className, }), [objectToDeps(variantProps), hasMultipleMonths, className], ); - const hasHelper = !!description || !!errorMessage; + const getStartDateInputProps = (props: DOMAttributes = {}) => { + return { + ...startFieldProps, + ref: popoverTriggerRef, + "data-slot": "start-input", + slots: dateInputSlots, + createCalendar, + ...mergeProps(variantProps, startFieldProps, { + fullWidth: true, + disableAnimation, + }), + "data-open": dataAttr(state.isOpen), + classNames, + style: { + ...props.style, + maxWidth: "fit-content", + }, + className: dateInputSlots.input({ + class: clsx(classNames?.input, props?.className), + }), + } as DateRangePickerFieldProps; + }; - const getBaseProps: PropGetter = () => { + const getEndDateInputProps = (props: DOMAttributes = {}) => { return { - "data-slot": "base", - "data-has-helper": dataAttr(hasHelper), - "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), - className: slots.base({class: baseStyles}), - }; + ...endFieldProps, + "data-slot": "end-input", + slots: dateInputSlots, + createCalendar, + ...mergeProps(variantProps, endFieldProps, { + fullWidth: true, + disableAnimation, + }), + "data-open": dataAttr(state.isOpen), + classNames, + className: dateInputSlots.input({ + class: clsx(classNames?.input, props?.className), + }), + } as DateRangePickerFieldProps; }; const getLabelProps: PropGetter = (props) => { return { - ...mergeProps(labelProps, labelPropsProp, props), + ...props, + ...labelProps, "data-slot": "label", - className: slots.label({ + className: dateInputSlots.label({ class: clsx(classNames?.label, props?.className), }), }; @@ -231,10 +315,10 @@ export function useDateRangePicker({ ...props, ...groupProps, "data-slot": "input-wrapper", - className: slots.inputWrapper({ + className: dateInputSlots.inputWrapper({ class: classNames?.inputWrapper, }), - onClick: fieldProps.onClick, + // onClick: startFieldProps.onClick, } as GroupDOMAttributes; }; @@ -242,7 +326,7 @@ export function useDateRangePicker({ return { ...props, "data-slot": "inner-wrapper", - className: slots.innerWrapper({ + className: dateInputSlots.innerWrapper({ class: classNames?.innerWrapper, }), }; @@ -252,7 +336,7 @@ export function useDateRangePicker({ return { ...props, "data-slot": "helper-wrapper", - className: slots.helperWrapper({ + className: dateInputSlots.helperWrapper({ class: clsx(classNames?.helperWrapper, props?.className), }), }; @@ -260,30 +344,61 @@ export function useDateRangePicker({ const getErrorMessageProps: PropGetter = (props = {}) => { return { - ...mergeProps(errorMessageProps, errorMessagePropsProp, props), + ...props, + ...errorMessageProps, "data-slot": "error-message", - className: slots.errorMessage({class: clsx(classNames?.errorMessage, props?.className)}), + className: dateInputSlots.errorMessage({ + class: clsx(classNames?.errorMessage, props?.className), + }), }; }; const getDescriptionProps: PropGetter = (props = {}) => { return { - ...mergeProps(descriptionProps, descriptionPropsProp, props), + ...props, + ...descriptionProps, "data-slot": "description", - className: slots.description({class: clsx(classNames?.description, props?.className)}), + className: dateInputSlots.description({ + class: clsx(classNames?.description, props?.className), + }), }; }; + const getDateInputGroupProps = () => { + 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: dateInputSlots.base({class: baseStyles}), + } as DateInputGroupProps; + }; + return { state, label, - groupProps, - labelProps, endContent, selectorIcon, showTimeField, - descriptionProps, - errorMessageProps, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, @@ -295,7 +410,9 @@ export function useDateRangePicker({ getPopoverProps, getSelectorButtonProps, getCalendarProps, + getSeparatorProps, getSelectorIconProps, + getDateInputGroupProps, }; } diff --git a/packages/components/date-picker/stories/date-range-picker.stories.tsx b/packages/components/date-picker/stories/date-range-picker.stories.tsx index 23e14726e5..433d6002ad 100644 --- a/packages/components/date-picker/stories/date-range-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-range-picker.stories.tsx @@ -54,8 +54,7 @@ export default { } as Meta; const defaultProps = { - label: "Birth Date", - className: "max-w-[256px]", + label: "Stay duration", ...dateInput.defaultVariants, }; diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index ed70100f01..bbff942348 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -28,7 +28,7 @@ const dateInput = tv({ "group-data-[invalid=true]:text-danger", ], // this wraps the input and the start/end content segment: [ - "group -ml-0.5 px-0.5 my-auto box-content tabular-nums text-start", + "group first:-ml-0.5 [&:not(:first-child)]:-ml-1 px-0.5 my-auto box-content tabular-nums text-start", "inline-block outline-none focus:shadow-sm rounded-md", "text-foreground-500 data-[editable=true]:text-foreground", "data-[editable=true]:data-[placeholder=true]:text-foreground-500", diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index f63349cedc..e629ca722b 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -32,9 +32,21 @@ const datePicker = tv({ }, }); +const dateRangePicker = tv({ + extend: datePicker, + slots: { + separator: "-mx-0.5 text-inherit", + }, +}); + /** Base */ export type DatePickerReturnType = ReturnType; export type DatePickerVariantProps = VariantProps; export type DatePickerSlots = keyof ReturnType; -export {datePicker}; +/** Range */ +export type DateRangePickerReturnType = ReturnType; +export type DateRangePickerVariantProps = VariantProps; +export type DateRangePickerSlots = keyof ReturnType; + +export {datePicker, dateRangePicker}; From 5aa7ca7e0bfa080e729c754fd8d0dd063ba1fc8a Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 13 Apr 2024 11:10:11 -0300 Subject: [PATCH 5/6] chore(date-picker): hyphen symbol changed --- packages/components/date-picker/src/date-range-picker.tsx | 4 ++-- packages/core/theme/src/components/date-picker.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx index 3d4d6e7158..4127152a05 100644 --- a/packages/components/date-picker/src/date-range-picker.tsx +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -70,7 +70,7 @@ function DateRangePicker(props: Props, ref: ForwardedRef }, [showTimeField, CalendarTopContent, isCalendarHeaderExpanded]); const popoverContent = state.isOpen ? ( - + (props: Props, ref: ForwardedRef > diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index e629ca722b..af62d11e91 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -35,7 +35,7 @@ const datePicker = tv({ const dateRangePicker = tv({ extend: datePicker, slots: { - separator: "-mx-0.5 text-inherit", + separator: "-mx-1 text-inherit", }, }); From fa4f42cfd159d24ce26b5db1d2273bc223019ced Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sun, 14 Apr 2024 11:12:05 -0300 Subject: [PATCH 6/6] feat(date-range-picker): stories done --- .../date-picker/src/date-range-picker.tsx | 17 +- .../date-picker/src/use-date-picker-base.ts | 2 + .../date-picker/src/use-date-range-picker.ts | 9 +- .../stories/date-range-picker.stories.tsx | 515 +++++++++++++++++- .../core/theme/src/components/date-picker.ts | 3 + 5 files changed, 534 insertions(+), 12 deletions(-) diff --git a/packages/components/date-picker/src/date-range-picker.tsx b/packages/components/date-picker/src/date-range-picker.tsx index 4127152a05..5db390b9bc 100644 --- a/packages/components/date-picker/src/date-range-picker.tsx +++ b/packages/components/date-picker/src/date-range-picker.tsx @@ -19,9 +19,11 @@ export interface Props function DateRangePicker(props: Props, ref: ForwardedRef) { const { state, + slots, endContent, selectorIcon, showTimeField, + classNames, disableAnimation, isCalendarHeaderExpanded, getDateInputGroupProps, @@ -48,20 +50,17 @@ function DateRangePicker(props: Props, ref: ForwardedRef if (isCalendarHeaderExpanded) return null; return showTimeField ? ( -
- - +
+
+ + +
{CalendarBottomContent}
) : ( CalendarBottomContent ); - }, [showTimeField, CalendarBottomContent, isCalendarHeaderExpanded]); + }, [state, showTimeField, CalendarBottomContent, isCalendarHeaderExpanded]); const calendarTopContent = useMemo(() => { if (isCalendarHeaderExpanded) return null; diff --git a/packages/components/date-picker/src/use-date-picker-base.ts b/packages/components/date-picker/src/use-date-picker-base.ts index b5713358c9..e03498d7d6 100644 --- a/packages/components/date-picker/src/use-date-picker-base.ts +++ b/packages/components/date-picker/src/use-date-picker-base.ts @@ -238,6 +238,7 @@ export function useDatePickerBase(originalProps: UseDatePic const calendarProps = { ...slotsProps.calendarProps, "data-slot": "calendar", + "data-has-multiple-months": dataAttr(hasMultipleMonths), style: mergeProps( hasMultipleMonths ? { @@ -269,6 +270,7 @@ export function useDatePickerBase(originalProps: UseDatePic slotsProps, timeMinValue, timeMaxValue, + visibleMonths, isCalendarHeaderExpanded, disableAnimation, CalendarTopContent, diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 4344d1ce37..f21c3755f4 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -42,6 +42,8 @@ interface Props * input: "input-classes", * segment: "segment-classes", * separator: "separator-classes", + * bottomContent: "bottom-content-classes", + * timeInputWrapper: "time-input-wrapper-classes", * helperWrapper: "helper-wrapper-classes", * description: "description-classes", * errorMessage: "error-message-classes", @@ -171,7 +173,7 @@ export function useDateRangePicker({ return { ...timeInputProps, - label: stringFormatter.format("startTime"), + label: stringFormatter.format("endTime"), value: state.timeRange?.end || null, onChange: (v) => state.setTime("end", v), granularity: timeGranularity, @@ -318,7 +320,7 @@ export function useDateRangePicker({ className: dateInputSlots.inputWrapper({ class: classNames?.inputWrapper, }), - // onClick: startFieldProps.onClick, + onClick: labelProps.onClick, } as GroupDOMAttributes; }; @@ -382,6 +384,7 @@ export function useDateRangePicker({ "data-readonly": dataAttr(originalProps.isReadOnly), "data-invalid": dataAttr(isInvalid), "data-has-start-content": dataAttr(!!startContent), + "data-has-multiple-months": dataAttr(hasMultipleMonths), "data-has-end-content": dataAttr(!!endContent), descriptionProps: getDescriptionProps(), errorMessageProps: getErrorMessageProps(), @@ -396,6 +399,8 @@ export function useDateRangePicker({ return { state, label, + slots, + classNames, endContent, selectorIcon, showTimeField, diff --git a/packages/components/date-picker/stories/date-range-picker.stories.tsx b/packages/components/date-picker/stories/date-range-picker.stories.tsx index 433d6002ad..2dbaa2217a 100644 --- a/packages/components/date-picker/stories/date-range-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-range-picker.stories.tsx @@ -1,6 +1,24 @@ import React from "react"; import {Meta} from "@storybook/react"; import {dateInput} from "@nextui-org/theme"; +import { + endOfMonth, + endOfWeek, + getLocalTimeZone, + isWeekend, + parseAbsoluteToLocal, + parseDate, + parseZonedDateTime, + startOfMonth, + startOfWeek, + today, +} from "@internationalized/date"; +import {RangeValue} from "@react-types/shared"; +import {DateValue} from "@react-types/datepicker"; +import {I18nProvider, useDateFormatter, useLocale} from "@react-aria/i18n"; +import {Button, ButtonGroup} from "@nextui-org/button"; +import {Radio, RadioGroup} from "@nextui-org/radio"; +import {cn} from "@nextui-org/system"; import {DateRangePicker, DateRangePickerProps} from "../src"; @@ -46,7 +64,7 @@ export default { }, decorators: [ (Story) => ( -
+
), @@ -60,9 +78,504 @@ const defaultProps = { const Template = (args: DateRangePickerProps) => ; +const LabelPlacementTemplate = (args: DateRangePickerProps) => ( +
+ + + +
+); + +const ControlledTemplate = (args: DateRangePickerProps) => { + const [value, setValue] = React.useState>({ + start: parseDate("2024-04-01"), + end: parseDate("2024-04-08"), + }); + + let formatter = useDateFormatter({dateStyle: "long"}); + + return ( +
+
+ +

+ Selected date:{" "} + {value + ? formatter.formatRange( + value.start.toDate(getLocalTimeZone()), + value.end.toDate(getLocalTimeZone()), + ) + : "--"} +

+
+ +
+ ); +}; + +const TimeZonesTemplate = (args: DateRangePickerProps) => ( +
+ + +
+); + +const GranularityTemplate = (args: DateRangePickerProps) => { + let [date, setDate] = React.useState>({ + start: parseAbsoluteToLocal("2024-04-01T18:45:22Z"), + end: parseAbsoluteToLocal("2024-04-08T19:15:22Z"), + }); + + return ( +
+ + +
+ ); +}; + +const InternationalCalendarsTemplate = (args: DateRangePickerProps) => { + let [date, setDate] = React.useState>({ + start: parseAbsoluteToLocal("2021-04-01T18:45:22Z"), + end: parseAbsoluteToLocal("2021-04-14T19:15:22Z"), + }); + + return ( +
+ + + +
+ ); +}; + +const UnavailableDatesTemplate = (args: DateRangePickerProps) => { + let now = today(getLocalTimeZone()); + + let disabledRanges = [ + [now, now.add({days: 5})], + [now.add({days: 14}), now.add({days: 16})], + [now.add({days: 23}), now.add({days: 24})], + ]; + + return ( + + disabledRanges.some( + (interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0, + ) + } + minValue={today(getLocalTimeZone())} + validate={(value) => + disabledRanges.some( + (interval) => + value && value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0, + ) + ? "Selected date range may not include unavailable dates." + : null + } + validationBehavior="native" + {...args} + /> + ); +}; + +const NonContiguousRangesTemplate = (args: DateRangePickerProps) => { + let {locale} = useLocale(); + + return ( + isWeekend(date, locale)} + label="Time off request" + minValue={today(getLocalTimeZone())} + visibleMonths={2} + /> + ); +}; + +const PresetsTemplate = (args: DateRangePickerProps) => { + let defaultDate = { + start: today(getLocalTimeZone()), + end: today(getLocalTimeZone()).add({days: 7}), + }; + + const [value, setValue] = React.useState>(defaultDate); + + let {locale} = useLocale(); + let formatter = useDateFormatter({dateStyle: "full"}); + + let now = today(getLocalTimeZone()); + let nextWeek = { + start: startOfWeek(now.add({weeks: 1}), locale), + end: endOfWeek(now.add({weeks: 1}), locale), + }; + let nextMonth = { + start: startOfMonth(now.add({months: 1})), + end: endOfMonth(now.add({months: 1})), + }; + + const CustomRadio = (props) => { + const {children, ...otherProps} = props; + + return ( + + {children} + + ); + }; + + return ( +
+ + Exact dates + 1 day + 2 days + 3 days + 7 days + 14 days + + } + CalendarTopContent={ + + + + + + } + calendarProps={{ + focusedValue: value.start, + onFocusChange: (val) => setValue({...value, start: val}), + nextButtonProps: { + variant: "bordered", + }, + prevButtonProps: { + variant: "bordered", + }, + }} + value={value} + onChange={setValue} + {...args} + label="Event date" + /> +

+ Selected date:{" "} + {value + ? formatter.formatRange( + value.start.toDate(getLocalTimeZone()), + value.end.toDate(getLocalTimeZone()), + ) + : "--"} +

+
+ ); +}; + export const Default = { render: Template, args: { ...defaultProps, }, }; + +export const VisibleMonths = { + render: Template, + args: { + ...defaultProps, + visibleMonths: 2, + }, +}; + +export const LabelPlacement = { + render: LabelPlacementTemplate, + + args: { + ...defaultProps, + }, +}; + +export const WithTimeField = { + render: Template, + args: { + ...defaultProps, + label: "Event duration", + hideTimeZone: true, + visibleMonths: 2, + defaultValue: { + start: parseZonedDateTime("2024-04-01T00:45[America/Los_Angeles]"), + end: parseZonedDateTime("2024-04-08T11:15[America/Los_Angeles]"), + }, + }, +}; + +export const Controlled = { + render: ControlledTemplate, + args: { + ...defaultProps, + }, +}; + +export const Required = { + render: Template, + args: { + ...defaultProps, + isRequired: true, + }, +}; + +export const Disabled = { + render: Template, + args: { + ...defaultProps, + isDisabled: true, + defaultValue: { + start: parseDate("2024-04-01"), + end: parseDate("2024-04-08"), + }, + }, +}; + +export const ReadOnly = { + render: Template, + args: { + ...defaultProps, + isReadOnly: true, + defaultValue: { + start: parseDate("2024-04-01"), + end: parseDate("2024-04-08"), + }, + }, +}; + +export const WithoutLabel = { + render: Template, + + args: { + ...defaultProps, + label: null, + "aria-label": "Stay duration", + }, +}; + +export const WithDescription = { + render: Template, + + args: { + ...defaultProps, + description: "Please enter your stay duration", + }, +}; + +export const SelectorIcon = { + render: Template, + + args: { + ...defaultProps, + selectorIcon: ( + + + + + + + + ), + }, +}; + +export const WithErrorMessage = { + render: Template, + + args: { + ...defaultProps, + errorMessage: "Please enter your stay duration", + }, +}; + +export const IsInvalid = { + render: Template, + + args: { + ...defaultProps, + variant: "bordered", + isInvalid: true, + defaultValue: { + start: parseDate("2024-04-01"), + end: parseDate("2024-04-08"), + }, + errorMessage: "Please enter a valid date", + }, +}; + +export const TimeZones = { + render: TimeZonesTemplate, + + args: { + ...defaultProps, + label: "Event date", + defaultValue: parseZonedDateTime("2022-11-07T00:45[America/Los_Angeles]"), + }, +}; + +export const Granularity = { + render: GranularityTemplate, + + args: { + ...defaultProps, + visibleMonths: 2, + }, +}; + +export const InternationalCalendars = { + render: InternationalCalendarsTemplate, + + args: { + ...defaultProps, + hideTimeZone: true, + }, +}; + +export const MinDateValue = { + render: Template, + + args: { + ...defaultProps, + minValue: today(getLocalTimeZone()), + defaultValue: { + start: today(getLocalTimeZone()).subtract({days: 1}), + end: parseDate("2024-04-08"), + }, + }, +}; + +export const MaxDateValue = { + render: Template, + + args: { + ...defaultProps, + maxValue: today(getLocalTimeZone()), + defaultValue: { + start: parseDate("2024-04-01"), + end: today(getLocalTimeZone()).add({days: 1}), + }, + }, +}; + +export const UnavailableDates = { + render: UnavailableDatesTemplate, + args: { + ...defaultProps, + }, +}; + +export const PageBehavior = { + render: Template, + args: { + ...defaultProps, + visibleMonths: 2, + pageBehavior: "single", + }, +}; + +export const NonContiguous = { + render: NonContiguousRangesTemplate, + args: { + ...defaultProps, + }, +}; + +export const Presets = { + render: PresetsTemplate, + args: { + ...defaultProps, + visibleMonths: 2, + }, +}; diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 2519c0969a..5e0ae8dc82 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -36,6 +36,9 @@ const datePicker = tv({ const dateRangePicker = tv({ extend: datePicker, slots: { + calendar: "group", + bottomContent: "flex flex-col gap-y-2", + timeInputWrapper: "flex flex-col group-data-[has-multiple-months=true]:flex-row", separator: "-mx-1 text-inherit", }, });