Uecko_ERP/packages/rdx-ui/src/components/form/date-picker-field.tsx

150 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-04-03 16:15:25 +00:00
import {
Button,
Calendar,
Field,
FieldDescription,
FieldError,
FormControl,
FormField,
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { format, isValid, parseISO } from "date-fns";
import { CalendarIcon, LockIcon } from "lucide-react";
import React from "react";
import { type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { FormFieldLabel } from "./form-field-label.tsx";
type DatePickerFieldProps<TFormValues extends FieldValues> = {
name: FieldPath<TFormValues>;
label?: string;
description?: string;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
placeholder?: string;
orientation?: "vertical" | "horizontal" | "responsive";
className?: string;
inputClassName?: string;
formatDateFn?: (value: string) => string;
};
const parseFieldDate = (value?: string): Date | undefined => {
if (!value) return undefined;
const parsed = parseISO(value);
if (!isValid(parsed)) return undefined;
return parsed;
};
const toDateOnlyString = (date: Date): string => {
return format(date, "yyyy-MM-dd");
};
export function DatePickerField<TFormValues extends FieldValues>({
name,
label,
placeholder,
description,
disabled = false,
required = false,
readOnly = false,
orientation = "vertical",
className,
inputClassName,
formatDateFn = (value) => {
const parsed = parseFieldDate(value);
return parsed ? format(parsed, "dd/MM/yyyy") : value;
},
}: DatePickerFieldProps<TFormValues>) {
const triggerId = React.useId();
const { control, formState } = useFormContext<TFormValues>();
const isDisabled = Boolean(disabled || readOnly || formState.isSubmitting);
return (
<FormField
control={control}
name={name}
render={({ field, fieldState }) => {
const selectedDate = parseFieldDate(field.value);
const displayValue = field.value ? formatDateFn(field.value) : null;
return (
<Field
className={cn("gap-1", className)}
data-invalid={fieldState.invalid}
orientation={orientation}
>
{label ? (
<FormFieldLabel htmlFor={triggerId} required={required}>
{label}
</FormFieldLabel>
) : null}
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
aria-invalid={fieldState.invalid}
aria-required={required || undefined}
className={cn(
"h-9 w-full justify-start bg-muted/50 text-left font-medium",
"hover:border-ring/60",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
!displayValue && "text-muted-foreground",
inputClassName
)}
disabled={isDisabled}
id={triggerId}
type="button"
variant="outline"
>
{readOnly ? (
<LockIcon aria-hidden="true" className="mr-2 h-4 w-4 opacity-70" />
) : (
<CalendarIcon aria-hidden="true" className="mr-2 h-4 w-4" />
)}
<span className="truncate">{displayValue ?? placeholder ?? "Select date"}</span>
</Button>
</FormControl>
</PopoverTrigger>
{isDisabled ? null : (
<PopoverContent align="start" className="w-auto p-0">
<Calendar
initialFocus
mode="single"
onSelect={(date) => {
field.onChange(date ? toDateOnlyString(date) : "");
field.onBlur();
}}
selected={selectedDate}
/>
</PopoverContent>
)}
</Popover>
{description ? (
<FieldDescription>{description}</FieldDescription>
) : (
<div aria-hidden="true" className="min-h-5" />
)}
<FieldError errors={[fieldState.error]} />
</Field>
);
}}
/>
);
}