|
| 1 | +import { useDatePicker, useDateRangePicker } from "@react-aria/datepicker"; |
| 2 | +import { useDatePickerState, useDateRangePickerState } from "@react-stately/datepicker"; |
| 3 | +import { useRef } from "react"; |
| 4 | +import { match } from "ts-pattern"; |
| 5 | +import { FieldProps } from "../Field/FieldProps"; |
| 6 | +import { CalendarDate, DateValue, getLocalTimeZone } from "@internationalized/date"; |
| 7 | +import { BaseDateInput } from "./BaseDateInput"; |
| 8 | +import { RangeValue } from "@react-types/shared"; |
| 9 | +import { DateProps, ShortcutProps } from "./types"; |
| 10 | + |
| 11 | +type StandaloneProps<T> = Pick< |
| 12 | + FieldProps<T>, |
| 13 | + "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value" |
| 14 | +>; |
| 15 | + |
| 16 | +type SingleDateProps = { |
| 17 | + type?: "single"; |
| 18 | + shortcuts?: ShortcutProps<Date | null>[]; |
| 19 | +}; |
| 20 | + |
| 21 | +type RangeDateProps = { |
| 22 | + type: "range"; |
| 23 | + shortcuts?: ShortcutProps<[Date, Date] | null>[]; |
| 24 | +}; |
| 25 | + |
| 26 | +type SingleDateInputProps = SingleDateProps & |
| 27 | + StandaloneProps<Date | null> & |
| 28 | + DateProps & { validationState: "valid" | "invalid" }; |
| 29 | + |
| 30 | +type RangeDateInputProps = RangeDateProps & |
| 31 | + StandaloneProps<[Date, Date] | null> & |
| 32 | + DateProps & { validationState: "valid" | "invalid" }; |
| 33 | + |
| 34 | +function dateToCalendarDate(date: Date): CalendarDate { |
| 35 | + return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); |
| 36 | +} |
| 37 | + |
| 38 | +function SingleDateInput(props: SingleDateInputProps) { |
| 39 | + const localTimeZone = getLocalTimeZone(); |
| 40 | + const ref = useRef(null); |
| 41 | + |
| 42 | + const internalProps = { |
| 43 | + ...props, |
| 44 | + value: props.value ? dateToCalendarDate(props.value) : props.value, |
| 45 | + onChange: (date: CalendarDate | null) => { |
| 46 | + props.onChange(date?.toDate(localTimeZone) ?? null); |
| 47 | + }, |
| 48 | + isDisabled: props.disabled, |
| 49 | + isReadOnly: props.isReadOnly ?? props.readOnly, |
| 50 | + validationState: props.validationState, |
| 51 | + minValue: props.minDate ? dateToCalendarDate(props.minDate) : undefined, |
| 52 | + maxValue: props.maxDate ? dateToCalendarDate(props.maxDate) : undefined, |
| 53 | + isDateUnavailable: props.shouldDisableDate |
| 54 | + ? (date: DateValue) => props.shouldDisableDate!(date.toDate(localTimeZone)) |
| 55 | + : undefined, |
| 56 | + shouldForceLeadingZeros: true, |
| 57 | + } as const; |
| 58 | + |
| 59 | + const datePickerState = useDatePickerState(internalProps); |
| 60 | + const datePickerAria = useDatePicker(internalProps, datePickerState, ref); |
| 61 | + |
| 62 | + return ( |
| 63 | + <BaseDateInput |
| 64 | + type="single" |
| 65 | + value={props.value} |
| 66 | + onChange={props.onChange} |
| 67 | + shortcuts={props.shortcuts} |
| 68 | + datePickerAria={datePickerAria} |
| 69 | + datePickerState={datePickerState} |
| 70 | + inputRef={ref} |
| 71 | + /> |
| 72 | + ); |
| 73 | +} |
| 74 | + |
| 75 | +function RangeDateInput(props: RangeDateInputProps) { |
| 76 | + const localTimeZone = getLocalTimeZone(); |
| 77 | + const ref = useRef(null); |
| 78 | + |
| 79 | + const internalProps = { |
| 80 | + ...props, |
| 81 | + value: props.value |
| 82 | + ? { |
| 83 | + start: dateToCalendarDate(props.value[0]), |
| 84 | + end: dateToCalendarDate(props.value[1]), |
| 85 | + } |
| 86 | + : props.value, |
| 87 | + onChange: (range: RangeValue<CalendarDate> | null) => { |
| 88 | + if (!range) { |
| 89 | + props.onChange(null); |
| 90 | + } else { |
| 91 | + props.onChange([range.start.toDate(localTimeZone), range.end.toDate(localTimeZone)]); |
| 92 | + } |
| 93 | + }, |
| 94 | + isDisabled: props.disabled, |
| 95 | + isReadOnly: props.isReadOnly ?? props.readOnly, |
| 96 | + validationState: props.validationState, |
| 97 | + minValue: props.minDate ? dateToCalendarDate(props.minDate) : undefined, |
| 98 | + maxValue: props.maxDate ? dateToCalendarDate(props.maxDate) : undefined, |
| 99 | + isDateUnavailable: props.shouldDisableDate |
| 100 | + ? (date: DateValue) => props.shouldDisableDate!(date.toDate(localTimeZone)) |
| 101 | + : undefined, |
| 102 | + shouldForceLeadingZeros: true, |
| 103 | + } as const; |
| 104 | + |
| 105 | + const rangeDatePickerState = useDateRangePickerState(internalProps); |
| 106 | + const rangeDatePickerAria = useDateRangePicker(internalProps, rangeDatePickerState, ref); |
| 107 | + |
| 108 | + return ( |
| 109 | + <BaseDateInput |
| 110 | + type="range" |
| 111 | + value={props.value} |
| 112 | + onChange={props.onChange} |
| 113 | + shortcuts={props.shortcuts} |
| 114 | + dateRangePickerAria={rangeDatePickerAria} |
| 115 | + dateRangePickerState={rangeDatePickerState} |
| 116 | + inputRef={ref} |
| 117 | + /> |
| 118 | + ); |
| 119 | +} |
| 120 | + |
| 121 | +export function DateInput(props: SingleDateInputProps | RangeDateInputProps) { |
| 122 | + return match(props) |
| 123 | + .with({ type: "single" }, { type: undefined }, (props) => <SingleDateInput {...props} />) |
| 124 | + .with({ type: "range" }, (props) => <RangeDateInput {...props} />) |
| 125 | + .exhaustive(); |
| 126 | +} |
0 commit comments