'use client'; import { createDateTimePicker, DateTimePickerProvider, useDateTimePicker, } from '@gluestack-ui/core/date-time-picker/creator'; import { UIIcon } from '@gluestack-ui/core/icon/creator'; import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; import { useStyleContext, withStyleContext, } from '@gluestack-ui/utils/nativewind-utils'; import DateTimePickerNative from '@react-native-community/datetimepicker'; import { styled } from 'nativewind'; import React, { useCallback, useMemo } from 'react'; import { Modal, Platform, Pressable, Text, TextInput, View, } from 'react-native'; import { dateTimePickerIconStyle, dateTimePickerInputStyle, dateTimePickerStyle, dateTimePickerTriggerStyle, } from './styles'; const SCOPE = 'DATE_TIME_PICKER'; export type DateTimePickerMode = 'date' | 'time' | 'datetime'; export interface DateTimePickerProps { value?: Date; onChange?: (date: Date | undefined) => void; mode?: DateTimePickerMode; minimumDate?: Date; maximumDate?: Date; locale?: string; timeZoneOffsetInMinutes?: number; is24Hour?: boolean; disabled?: boolean; placeholder?: string; format?: string; display?: 'modal' | 'inline'; // iOS only: 'modal' shows picker in modal with backdrop, 'inline' shows picker directly children?: React.ReactNode; } const DateTimePickerTriggerWrapper = React.forwardRef< React.ComponentRef, React.ComponentProps >(function DateTimePickerTriggerWrapper({ ...props }, ref) { return ; }); const StyledTextInput = styled(TextInput, { className: { target: 'style', nativeStyleToProp: { textAlign: true } }, }); const StyledUIIcon = styled(UIIcon, { className: 'style', }); const UIDateTimePicker = createDateTimePicker({ Root: withStyleContext(View, SCOPE), Trigger: withStyleContext(DateTimePickerTriggerWrapper, SCOPE), Input: StyledTextInput, Icon: StyledUIIcon, }); type IDateTimePickerProps = VariantProps & DateTimePickerProps & { className?: string }; const DateTimePicker = React.forwardRef< React.ComponentRef, IDateTimePickerProps >(function DateTimePicker( { className, value, onChange, mode = 'datetime', minimumDate, maximumDate, locale, timeZoneOffsetInMinutes, is24Hour, disabled, placeholder, format, display = 'modal', // Default to modal for iOS children, ...props }, ref ) { const handleNativeChange = useCallback( (event: any, selectedDate?: Date) => { // The native picker handles its own close if (selectedDate) { onChange?.(selectedDate); } }, [onChange] ); // On iOS, use custom trigger + spinner in modal or inline if (Platform.OS === 'ios') { return ( {children} {/* iOS spinner picker shown in modal or inline based on display prop */} ); } return ( {children} {/* Native picker is rendered directly on Android */} ); }); // Separate component to handle the native picker display (Android only) function DateTimePickerNativeWrapper({ value, mode, minimumDate, maximumDate, timeZoneOffsetInMinutes, is24Hour, onChange, }: { value?: Date; mode: DateTimePickerMode; minimumDate?: Date; maximumDate?: Date; timeZoneOffsetInMinutes?: number; is24Hour?: boolean; onChange: (event: any, date?: Date) => void; }) { const { isOpen, setIsOpen } = useDateTimePicker(); const handleChange = useCallback( (event: any, selectedDate?: Date) => { setIsOpen(false); onChange(event, selectedDate); }, [onChange, setIsOpen] ); // Android doesn't support 'datetime' mode - use two-step picker if (mode === 'datetime') { return ( ); } // Android: Use display="default" which opens system dialogs for date/time if (!isOpen) return null; return ( ); } // Android-specific datetime picker (uses two separate pickers) function AndroidDateTimePicker({ value, minimumDate, maximumDate, is24Hour, isOpen, onChange, setIsOpen, }: { value?: Date; minimumDate?: Date; maximumDate?: Date; is24Hour?: boolean; isOpen: boolean; onChange: (event: any, date?: Date) => void; setIsOpen: (open: boolean) => void; }) { const [step, setStep] = React.useState<'date' | 'time' | null>(null); const [tempDate, setTempDate] = React.useState(value); React.useEffect(() => { if (isOpen) { setStep('date'); setTempDate(value || new Date()); } else { setStep(null); } }, [isOpen, value]); const handleDateChange = React.useCallback( (event: any, selectedDate?: Date) => { if (selectedDate) { setTempDate(selectedDate); setStep('time'); } else { setIsOpen(false); } }, [setIsOpen] ); const handleTimeChange = React.useCallback( (event: any, selectedTime?: Date) => { setIsOpen(false); setStep(null); if (selectedTime && tempDate) { // Combine date and time const combinedDate = new Date(tempDate); combinedDate.setHours(selectedTime.getHours()); combinedDate.setMinutes(selectedTime.getMinutes()); onChange(event, combinedDate); } else if (selectedTime) { onChange(event, selectedTime); } }, [tempDate, onChange, setIsOpen] ); if (!isOpen || !step) return null; if (step === 'date') { return ( ); } return ( ); } // iOS-specific picker with spinner in modal or inline function IOSDateTimePicker({ value, mode, minimumDate, maximumDate, timeZoneOffsetInMinutes, is24Hour, display, onChange, }: { value?: Date; mode: DateTimePickerMode; minimumDate?: Date; maximumDate?: Date; timeZoneOffsetInMinutes?: number; is24Hour?: boolean; display: 'modal' | 'inline'; onChange: (event: any, date?: Date) => void; }) { const { isOpen, setIsOpen } = useDateTimePicker(); const [tempValue, setTempValue] = React.useState(value || new Date()); // Update temp value when picker opens React.useEffect(() => { if (isOpen) { setTempValue(value || new Date()); } }, [isOpen, value]); const handleChange = React.useCallback( (event: any, selectedDate?: Date) => { if (selectedDate) { setTempValue(selectedDate); // Update the parent immediately for live feedback onChange(event, selectedDate); } }, [onChange] ); const handleDone = React.useCallback(() => { setIsOpen(false); }, [setIsOpen]); const handleCancel = React.useCallback(() => { setIsOpen(false); // Revert to original value if (value) { onChange({ type: 'dismissed' }, value); } }, [setIsOpen, onChange, value]); if (!isOpen) return null; // Inline mode: show picker directly without modal if (display === 'inline') { return ( ); } // Modal mode: show picker in modal with backdrop return ( {/* Backdrop - separate touchable area */} {/* Picker container */} Cancel {mode === 'date' ? 'Select Date' : mode === 'time' ? 'Select Time' : 'Select Date & Time'} Done ); } type IDateTimePickerTriggerProps = VariantProps< typeof dateTimePickerTriggerStyle > & React.ComponentProps & { className?: string; }; const DateTimePickerTrigger = React.forwardRef< React.ComponentRef, IDateTimePickerTriggerProps >(function DateTimePickerTrigger( { className, size = 'md', variant = 'outline', ...props }, ref ) { const { disabled, setIsOpen } = useDateTimePicker(); return ( !disabled && setIsOpen(true)} {...props} /> ); }); type IDateTimePickerInputProps = VariantProps & React.ComponentProps & { className?: string }; const DateTimePickerInput = React.forwardRef< React.ComponentRef, IDateTimePickerInputProps >(function DateTimePickerInput({ className, ...props }, ref) { const { size: parentSize, variant: parentVariant } = useStyleContext(SCOPE); const { value, placeholder, format } = useDateTimePicker(); const displayValue = useMemo(() => { if (!value) return ''; if (format) { return formatDate(value, format); } return value.toLocaleString(); }, [value, format]); return ( ); }); type IDateTimePickerIconProps = VariantProps & React.ComponentProps & { className?: string }; const DateTimePickerIcon = React.forwardRef< React.ComponentRef, IDateTimePickerIconProps >(function DateTimePickerIcon({ className, size, ...props }, ref) { const { size: parentSize } = useStyleContext(SCOPE); if (typeof size === 'number') { return ( ); } return ( ); }); function formatDate(date: Date, format: string): string { const pad = (n: number) => n.toString().padStart(2, '0'); return format .replace('YYYY', date.getFullYear().toString()) .replace('MM', pad(date.getMonth() + 1)) .replace('DD', pad(date.getDate())) .replace('HH', pad(date.getHours())) .replace('mm', pad(date.getMinutes())) .replace('ss', pad(date.getSeconds())); } export { DateTimePicker, DateTimePickerIcon, DateTimePickerInput, DateTimePickerTrigger };