import { CalendarDateTime, createCalendar } from "@internationalized/date"; import { useDateField, useDateSegment } from "@react-aria/datepicker"; import { useDateFieldState, type DateFieldState, type DateSegment, } from "@react-stately/datepicker"; import { type Granularity } from "@react-types/datepicker"; import { useEffect, useRef, useState } from "react"; import { cn } from "~/utils/cn"; import { Button } from "./Buttons"; const variants = { small: { fieldStyles: "h-5 text-xs rounded-sm px-0.5", nowButtonVariant: "tertiary/small" as const, clearButtonVariant: "tertiary/small" as const, }, medium: { fieldStyles: "h-7 text-sm rounded px-1", nowButtonVariant: "tertiary/medium" as const, clearButtonVariant: "minimal/medium" as const, }, }; type Variant = keyof typeof variants; type DateFieldProps = { label: string; defaultValue?: Date; minValue?: Date; maxValue?: Date; className?: string; fieldClassName?: string; granularity: Granularity; showGuide?: boolean; showNowButton?: boolean; showClearButton?: boolean; onValueChange?: (value: Date | undefined) => void; utc?: boolean; variant?: Variant; }; const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; export function DateField({ label, defaultValue, onValueChange, minValue, maxValue, granularity, className, fieldClassName, showGuide = false, showNowButton = false, showClearButton = false, utc = false, variant = "small", }: DateFieldProps) { const [value, setValue] = useState( utc ? utcDateToCalendarDate(defaultValue) : dateToCalendarDate(defaultValue) ); const state = useDateFieldState({ value: value, onChange: (value) => { if (value) { setValue(value); onValueChange?.(value.toDate(utc ? "utc" : deviceTimezone)); } }, minValue: utc ? utcDateToCalendarDate(minValue) : dateToCalendarDate(minValue), maxValue: utc ? utcDateToCalendarDate(maxValue) : dateToCalendarDate(maxValue), shouldForceLeadingZeros: true, granularity, locale: "en-US", createCalendar: (name: string) => { return createCalendar(name); }, }); //if the passed in value changes, we should update the date useEffect(() => { if (state.value === undefined && defaultValue === undefined) return; const calendarDate = utc ? utcDateToCalendarDate(defaultValue) : dateToCalendarDate(defaultValue); //unchanged if (state.value?.toDate("utc").getTime() === defaultValue?.getTime()) { return; } setValue(calendarDate); }, [defaultValue]); const ref = useRef(null); const { labelProps: _labelProps, fieldProps } = useDateField( { label, }, state, ref ); //render if reverse date order const yearSegment = state.segments.find((s) => s.type === "year")!; const monthSegment = state.segments.find((s) => s.type === "month")!; const daySegment = state.segments.find((s) => s.type === "day")!; const hourSegment = state.segments.find((s) => s.type === "hour")!; const minuteSegment = state.segments.find((s) => s.type === "minute")!; const secondSegment = state.segments.find((s) => s.type === "second")!; const dayPeriodSegment = state.segments.find((s) => s.type === "dayPeriod")!; return (
{showNowButton && ( )} {showClearButton && ( )}
{showGuide && (
{state.segments.map((segment, i) => ( ))}
)}
); } function utcDateToCalendarDate(date?: Date) { return date ? new CalendarDateTime( date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds() ) : undefined; } function dateToCalendarDate(date?: Date) { return date ? new CalendarDateTime( date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds() ) : undefined; } type DateSegmentProps = { segment: DateSegment; state: DateFieldState; variant: Variant; }; function DateSegment({ segment, state, variant }: DateSegmentProps) { const ref = useRef(null); const { segmentProps } = useDateSegment(segment, state, ref); const sizeVariant = variants[variant]; return (
{/* Always reserve space for the placeholder, to prevent layout shift when editing. */} {segment.isPlaceholder ? "" : segment.text}
); } function literalSegment(text: string): DateSegment { return { type: "literal", text, isPlaceholder: false, isEditable: false, placeholder: "", }; } function minWidthForSegment(segment: DateSegment) { if (segment.type === "literal") { return undefined; } return String(`${segment.maxValue}`).length + "ch"; } function DateSegmentGuide({ segment }: { segment: DateSegment }) { return (
{segment.type !== "literal" ? segment.placeholder : segment.text}
); }