import React, { forwardRef, useState, useEffect, useCallback } from 'react' import { DateRangePickerProps, DateRangePreset, DateRange, DateRangePickerSize, PresetSelectionData, } from './types' import { formatDate, formatDateDisplay, handleDateInputChange, handleTimeChange, handleCalendarDateSelect, handlePresetSelection, formatTriggerDisplay, validateDateTimeRange, DateValidationResult, detectPresetFromRange, processCustomPresets, getFilteredPresets, validateCustomRangeConfig, getPresetLabelWithCustom, getPresetLabel, getTodayInTimezone, validateDateInput, isControlledDateRange, isValidDate, } from './utils' import CalendarGrid from './CalendarGrid' import QuickRangeSelector from './QuickRangeSelector' import TimeSelector from './TimeSelector' import MobileDrawerPresets from './MobileDrawerPresets' import { CalendarTokenType } from './dateRangePicker.tokens' import Block from '../Primitives/Block/Block' import { PopoverV2, PopoverV2Align, PopoverV2Side } from '../PopoverV2' import { InputSizeV2, TextInputV2 } from '../InputsV2/TextInputV2' import PrimitiveText from '../Primitives/PrimitiveText/PrimitiveText' import { ButtonV2, ButtonV2Size, ButtonV2Type } from '../ButtonV2' import { TooltipV2 } from '../TooltipV2' import { useBreakpoints } from '../../hooks/useBreakPoints' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useTheme } from '../../context' import { renderPickerTrigger } from '../shared/datetime/PickerTrigger' import MonthYearGrid from '../shared/datetime/MonthYearGrid' import { nextMonthRange } from '../shared/datetime/granularity' type DateInputsSectionProps = { startDate?: string endDate?: string startTime?: string endTime?: string showDateTimePicker: boolean isSingleDatePicker: boolean allowSingleDateSelection: boolean selectedRange?: DateRange startDateValidation: DateValidationResult endDateValidation: DateValidationResult onStartDateChange: (e: React.ChangeEvent) => void onEndDateChange: (e: React.ChangeEvent) => void onStartTimeChange: (time: string) => void onEndTimeChange: (time: string) => void calendarToken: CalendarTokenType disablePastDates: boolean disableFutureDates: boolean today: Date timezone?: string } const DateInputsSection: React.FC = ({ startDate, endDate, startTime, endTime, showDateTimePicker, isSingleDatePicker, allowSingleDateSelection, selectedRange, startDateValidation, endDateValidation, onStartDateChange, onEndDateChange, onStartTimeChange, onEndTimeChange, calendarToken, disablePastDates, disableFutureDates, today, timezone, }) => { const startDateId = React.useId() const endDateId = React.useId() const startTimeId = React.useId() const endTimeId = React.useId() return ( Start {showDateTimePicker && ( )} {!isSingleDatePicker && selectedRange && (!allowSingleDateSelection || (allowSingleDateSelection && selectedRange.startDate.getTime() !== selectedRange.endDate?.getTime())) && ( End {showDateTimePicker && ( )} )} ) } type CalendarSectionProps = { selectedRange: DateRange | undefined today: Date allowSingleDateSelection: boolean disableFutureDates: boolean disablePastDates: boolean hideFutureDates: boolean hidePastDates: boolean customDisableDates?: (date: Date, currentRange?: DateRange) => boolean customRangeConfig?: import('./types').CustomRangeConfig onDateSelect: (range: DateRange) => void showDateTimePicker: boolean timezone?: string isSingleDatePicker?: boolean maxYearOffset?: number minDate?: Date maxDate?: Date maxRangeDays?: number granularity?: 'day' | 'month' } const CalendarSection: React.FC< CalendarSectionProps & { resetScrollPosition?: number } > = ({ selectedRange, today, allowSingleDateSelection, disableFutureDates, disablePastDates, hideFutureDates, hidePastDates, customDisableDates, customRangeConfig, onDateSelect, showDateTimePicker, resetScrollPosition, timezone, isSingleDatePicker, maxYearOffset, minDate, maxDate, maxRangeDays, granularity = 'day', }) => granularity === 'month' ? ( onDateSelect( isSingleDatePicker ? { startDate: periodStart } : nextMonthRange(selectedRange, periodStart) ) } today={today} isRange={!isSingleDatePicker} timezone={timezone} disableFutureDates={disableFutureDates} disablePastDates={disablePastDates} hideFutureDates={hideFutureDates} hidePastDates={hidePastDates} customDisableDates={customDisableDates} resetScrollPosition={resetScrollPosition} maxYearOffset={maxYearOffset} minDate={minDate} maxDate={maxDate} /> ) : ( ) type FooterControlsProps = { onCancel: () => void onApply: () => void isApplyDisabled: boolean applyDisabledMessage?: string calendarToken: CalendarTokenType } const FooterControls: React.FC = ({ onCancel, onApply, isApplyDisabled, applyDisabledMessage, calendarToken, }) => ( {isApplyDisabled ? ( ) : ( )} ) const DateRangePicker = forwardRef( ( { value, onChange, onPresetSelection, showDateInput = true, showDateTimePicker = true, showPresets: shouldShowPresets = true, customPresets, isDisabled = false, dateFormat = 'dd/MM/yyyy', granularity = 'day', allowSingleDateSelection = false, isSingleDatePicker = false, disableFutureDates = false, disablePastDates = false, hideFutureDates = false, hidePastDates = false, customDisableDates, customRangeConfig, minDate, maxDate, maxRangeDays, triggerElement = null, useDrawerOnMobile = true, skipQuickFiltersOnMobile = false, size = DateRangePickerSize.MEDIUM, formatConfig, triggerConfig, popoverConfig, maxMenuHeight = 250, showPreset = false, timezone, maxYearOffset, }, ref ) => { const [isOpen, setIsOpen] = useState(false) const [popoverKey, setPopoverKey] = useState(0) const [isQuickRangeOpen, setIsQuickRangeOpen] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false) const calendarToken = useResponsiveTokens('CALENDAR') const { foundationTokens } = useTheme() const { innerWidth } = useBreakpoints() const isMobile = innerWidth < 1024 // Month mode turns off the three surfaces that only make sense for // days: the presets are all day ranges, the DD/MM/YYYY text inputs // would let a keystroke produce a range that starts mid-month, and the // mobile drawer's scroll wheel has no month-range mode at all — so // mobile falls through to the same popover desktop uses. See the // `granularity` docs on `DateRangePickerProps`. const isMonthGranularity = granularity === 'month' const showPresets = shouldShowPresets && !isSingleDatePicker && !isMonthGranularity const showDateInputs = showDateInput && !isMonthGranularity const useDrawer = useDrawerOnMobile && !isMonthGranularity const [selectedRange, setSelectedRange] = useState< DateRange | undefined >(() => (isControlledDateRange(value) ? value : undefined)) const lastExternalValueRef = React.useRef<{ start: number | null end: number | null dateFormat: string } | null>(null) const [activePreset, setActivePreset] = useState( DateRangePreset.CUSTOM ) const [startTime, setStartTime] = useState( selectedRange && formatDate(selectedRange.startDate, 'HH:mm', timezone) ) const [endTime, setEndTime] = useState( isSingleDatePicker ? undefined : selectedRange && selectedRange.endDate && formatDate(selectedRange.endDate, 'HH:mm', timezone) ) const [startDate, setStartDate] = useState( selectedRange && formatDate(selectedRange.startDate, dateFormat, timezone) ) const [endDate, setEndDate] = useState( isSingleDatePicker ? undefined : selectedRange && selectedRange.endDate && formatDate(selectedRange.endDate, dateFormat, timezone) ) const [startDateValidation, setStartDateValidation] = useState({ isValid: true, error: 'none' }) const [endDateValidation, setEndDateValidation] = useState({ isValid: isSingleDatePicker || !!selectedRange?.endDate, error: isSingleDatePicker || selectedRange?.endDate ? 'none' : 'invalid-date', }) const today = getTodayInTimezone(timezone) const presetConfigs = React.useMemo(() => { return processCustomPresets(customPresets) }, [customPresets]) const availablePresets = React.useMemo(() => { return getFilteredPresets( presetConfigs, disableFutureDates, disablePastDates ) }, [presetConfigs, disableFutureDates, disablePastDates]) // Calculate if apply button should be disabled and get validation message const applyButtonValidation = React.useMemo(() => { // Check if date header are invalid if (!startDateValidation.isValid) { return { isDisabled: true, message: startDateValidation.message || 'Invalid start date', } } if (!isSingleDatePicker && !endDateValidation.isValid) { return { isDisabled: true, message: endDateValidation.message || 'Invalid end date', } } // Check if we have valid dates if (isSingleDatePicker && !selectedRange) { return { isDisabled: true, message: 'Please select start date', } } else if ( !isSingleDatePicker && (!selectedRange || !selectedRange.endDate) ) { return { isDisabled: true, message: 'Please select both start and end dates', } } // Validate the date/time range const validation = validateDateTimeRange( selectedRange as DateRange, isSingleDatePicker ) return { isDisabled: !validation.isValid, message: validation.message || 'Invalid date/time range', } }, [ selectedRange, startDateValidation, endDateValidation, allowSingleDateSelection, isSingleDatePicker, ]) const isApplyDisabled = applyButtonValidation.isDisabled const resetValues = useCallback( (dateRangeObj?: DateRange) => { const normalizedRange = isControlledDateRange(dateRangeObj) ? dateRangeObj : undefined setSelectedRange(normalizedRange) setActivePreset( normalizedRange ? detectPresetFromRange( normalizedRange, timezone, presetConfigs ) : DateRangePreset.CUSTOM ) setStartDate( normalizedRange && formatDate( normalizedRange.startDate, dateFormat, timezone ) ) if (normalizedRange?.endDate) { setEndDate( formatDate( normalizedRange.endDate, dateFormat, timezone ) ) } else if (!normalizedRange) { setEndDate(undefined) } setStartTime( normalizedRange && formatDate(normalizedRange.startDate, 'HH:mm', timezone) ) if (normalizedRange?.endDate) { setEndTime( formatDate(normalizedRange.endDate, 'HH:mm', timezone) ) } else if (!normalizedRange) { setEndTime(undefined) } setStartDateValidation({ isValid: true, error: 'none' }) setEndDateValidation({ isValid: true, error: 'none', }) }, [timezone, dateFormat, presetConfigs] ) useEffect(() => { if (!isControlledDateRange(value)) { if (lastExternalValueRef.current !== null) { lastExternalValueRef.current = null resetValues(undefined) } return } const nextSignature = { start: value.startDate.getTime(), end: value.endDate && isValidDate(value.endDate) ? value.endDate.getTime() : null, dateFormat, } const prevSignature = lastExternalValueRef.current const hasRangeChanged = !prevSignature || prevSignature.start !== nextSignature.start || prevSignature.end !== nextSignature.end const hasFormatChanged = !prevSignature || prevSignature.dateFormat !== dateFormat if (!hasRangeChanged && !hasFormatChanged) { return } lastExternalValueRef.current = nextSignature resetValues(value) }, [value, isOpen, dateFormat, timezone, resetValues]) const handleDateSelect = useCallback( (range: DateRange) => { setSelectedRange(range) setStartTime(formatDate(range.startDate, 'HH:mm', timezone)) if (!isSingleDatePicker) setEndTime( range.endDate && formatDate(range.endDate, 'HH:mm', timezone) ) setStartDate(formatDate(range.startDate, dateFormat, timezone)) const startDateValidation = validateDateInput( formatDate(range.startDate, dateFormat, timezone), dateFormat, disableFutureDates, disablePastDates, timezone ) setEndDateValidation(startDateValidation) if (!isSingleDatePicker) setEndDate( range.endDate && formatDate(range.endDate, dateFormat, timezone) ) const endDateValidation = range.endDate && validateDateInput( formatDate(range.endDate, dateFormat, timezone), dateFormat, disableFutureDates, disablePastDates, timezone ) if (endDateValidation) setEndDateValidation(endDateValidation) }, [dateFormat, timezone] ) useEffect(() => { if (customRangeConfig) { const validation = validateCustomRangeConfig(customRangeConfig) if (!validation.isValid) { console.warn( 'DateRangePicker: Invalid customRangeConfig:', validation.error ) } } }, [customRangeConfig]) const handlePresetSelect = useCallback( (preset: DateRangePreset) => { if (isSingleDatePicker) return const result = handlePresetSelection( preset, dateFormat, timezone ) if (!result) return setSelectedRange(result.updatedRange) setActivePreset(preset) setStartDate(result.formattedStartDate) setEndDate(result.formattedEndDate) setStartTime(result.formattedStartTime) setEndTime(result.formattedEndTime) // Trigger preset selection callback if provided if (onPresetSelection && preset !== DateRangePreset.CUSTOM) { const presetSelectionData: PresetSelectionData = { preset, label: getPresetLabelWithCustom(preset, presetConfigs), dateRange: result.updatedRange, formattedStartDate: result.formattedStartDate, formattedEndDate: result.formattedEndDate, formattedStartTime: result.formattedStartTime, formattedEndTime: result.formattedEndTime, } onPresetSelection(presetSelectionData) } // For presets, immediately update the committed value (different from calendar selection) if (preset !== DateRangePreset.CUSTOM) { onChange?.(result.updatedRange) if (showPreset) { setIsQuickRangeOpen(false) setIsOpen(false) setDrawerOpen(false) setPopoverKey((prev) => prev + 1) } } }, [ dateFormat, onChange, showPreset, onPresetSelection, presetConfigs, timezone, ] ) const handleStartDateChange = useCallback( (value: string) => { const result = handleDateInputChange( value, dateFormat, selectedRange, startTime, true, disableFutureDates || hideFutureDates, disablePastDates || hidePastDates, timezone ) setStartDate(result.formattedValue) setStartDateValidation(result.validation) if (result.updatedRange) { setSelectedRange(result.updatedRange) setStartTime( formatDate(result.updatedRange.startDate, 'HH:mm') ) } }, [ selectedRange, startTime, dateFormat, disableFutureDates, disablePastDates, hideFutureDates, hidePastDates, timezone, ] ) const handleEndDateChange = useCallback( (value: string) => { if (isSingleDatePicker) return const result = handleDateInputChange( value, dateFormat, selectedRange, endTime, false, disableFutureDates || hideFutureDates, disablePastDates || hidePastDates, timezone ) setEndDate(result.formattedValue) setEndDateValidation(result.validation) if (result.updatedRange && result.updatedRange.endDate) { setSelectedRange(result.updatedRange) setEndTime(formatDate(result.updatedRange.endDate, 'HH:mm')) } }, [ selectedRange, endTime, dateFormat, disableFutureDates, disablePastDates, hideFutureDates, hidePastDates, timezone, ] ) const handleStartTimeChange = useCallback( (time: string) => { if (!selectedRange) return setStartTime(time) const updatedRange = handleTimeChange( time, selectedRange, timezone, true ) setSelectedRange(updatedRange) }, [selectedRange, timezone] ) const handleEndTimeChange = useCallback( (time: string) => { if (isSingleDatePicker || !selectedRange) return setEndTime(time) const updatedRange = handleTimeChange( time, selectedRange, timezone, false ) setSelectedRange(updatedRange) }, [selectedRange, timezone] ) const handleApply = useCallback(() => { if ( !startTime || !selectedRange || (!isSingleDatePicker && (!endTime || !selectedRange.endDate)) ) return // Apply time from time selectors to the selected range const result = handleCalendarDateSelect( selectedRange, startTime, dateFormat, timezone, endTime ) // Update the input fields to match the applied range setStartDate(result.formattedStartDate) if (!isSingleDatePicker) setEndDate(result.formattedEndDate) onChange?.(result.updatedRange) setIsOpen(false) setDrawerOpen(false) setPopoverKey((prev) => prev + 1) }, [selectedRange, startTime, endTime, dateFormat, onChange, timezone]) const handleCancel = useCallback(() => { resetValues(value) setIsOpen(false) setDrawerOpen(false) setPopoverKey((prev) => prev + 1) }, [value, timezone, resetValues]) useEffect(() => { if (isDisabled) { setIsOpen(false) setIsQuickRangeOpen(false) } }, [isDisabled, isOpen]) const handleDateSelectCallback = useCallback(handleDateSelect, [ handleDateSelect, ]) const handleStartTimeChangeCallback = useCallback( handleStartTimeChange, [handleStartTimeChange] ) const handleEndTimeChangeCallback = useCallback(handleEndTimeChange, [ handleEndTimeChange, ]) const handleStartDateChangeCallback = useCallback( (e: React.ChangeEvent) => { handleStartDateChange(e.target.value) }, [handleStartDateChange] ) const handleEndDateChangeCallback = useCallback( (e: React.ChangeEvent) => { if (isSingleDatePicker) return handleEndDateChange(e.target.value) }, [handleEndDateChange] ) const renderTrigger = () => { // Use the committed value (value prop) for trigger display, not the selectedRange const displayRange = value // The caller-supplied-element branch returns before any formatting // ran in the original inline implementation. Keep it that way: a // consumer's `formatConfig.customFormat` must not be invoked (and // must not be able to throw) when it owns the trigger markup. const usesCustomElement = !triggerConfig?.renderTrigger && Boolean(triggerConfig?.element || triggerElement) // A month range is two month names, not two timestamps: // `formatDateDisplay` would render "Sep 1, 2025, 12:00 AM - Sep 30, // 2025, 12:00 AM" for what the user picked as "September". const formatMonthRangeDisplay = ( range: DateRange | undefined ): string => { if (!range?.startDate) return 'Select month range' const options: Intl.DateTimeFormatOptions = { month: 'short', year: 'numeric', ...(timezone && { timeZone: timezone }), } const startStr = range.startDate.toLocaleDateString( 'en-US', options ) if (!range.endDate) return startStr const endStr = range.endDate.toLocaleDateString( 'en-US', options ) return startStr === endStr ? startStr : `${startStr} - ${endStr}` } const displayText = usesCustomElement ? '' : formatConfig ? formatTriggerDisplay( displayRange, formatConfig, isSingleDatePicker, triggerConfig?.placeholder, timezone ) : isMonthGranularity ? formatMonthRangeDisplay(displayRange) : formatDateDisplay( displayRange, allowSingleDateSelection, timezone, isSingleDatePicker ) const formatMobileDateRange = ( range: DateRange | undefined ): string => { if (formatConfig) { return formatTriggerDisplay( range, formatConfig, isSingleDatePicker, isSingleDatePicker ? 'Select date' : 'Select dates', timezone ) } const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: '2-digit', ...(timezone && { timeZone: timezone }), } const startStr = range ? range.startDate.toLocaleDateString('en-US', formatOptions) : '' const endStr = range?.endDate ? range.endDate.toLocaleDateString('en-US', formatOptions) : '' if ( range && (!range.endDate || range.startDate.getTime() === range.endDate.getTime()) ) { return startStr } else if (!range) { return isSingleDatePicker ? 'Select date' : 'Select dates' } return `${startStr} - ${endStr}` } return renderPickerTrigger({ displayText, mobileText: isMobile && useDrawer ? formatMobileDateRange(displayRange) : undefined, displayRange, isOpen, isDisabled, size, calendarToken, foundationTokens, hasQuickSelector: showPresets, triggerConfig, triggerElement, isMobileDrawer: isMobile && useDrawer, onToggle: () => setIsOpen(!isOpen), onMobileOpen: () => setDrawerOpen(true), ariaLabel: `Date range picker, ${displayText || 'Select date range'}`, dataDatePicker: 'dateRangePicker-Filter', }) } if (isMobile && useDrawer) { const getMobilePresets = () => { const presetsWithCustom = [...availablePresets] if (!presetsWithCustom.includes(DateRangePreset.CUSTOM)) { presetsWithCustom.push(DateRangePreset.CUSTOM) } return presetsWithCustom } return ( ) } // Check if custom trigger is being used const hasCustomTrigger = !!( triggerConfig?.renderTrigger || triggerConfig?.element || triggerElement ) if (showPreset && showPresets) { return ( !isDisabled && setIsQuickRangeOpen(!isQuickRangeOpen) } activePreset={activePreset} onPresetSelect={handlePresetSelect} excludeCustom={true} customPresets={presetConfigs} disableFutureDates={disableFutureDates} disablePastDates={disablePastDates} isDisabled={isDisabled} size={size} maxMenuHeight={maxMenuHeight} isStandalone={true} calendarToken={calendarToken} /> ) } return ( {showPresets && !hasCustomTrigger && ( !isDisabled && setIsQuickRangeOpen(!isQuickRangeOpen) } activePreset={activePreset} onPresetSelect={handlePresetSelect} excludeCustom={true} customPresets={presetConfigs} disableFutureDates={disableFutureDates} disablePastDates={disablePastDates} isDisabled={isDisabled} size={size} maxMenuHeight={maxMenuHeight} calendarToken={calendarToken} /> )} { setIsOpen(open) }} trigger={renderTrigger()} side={ (popoverConfig?.side as PopoverV2Side) || PopoverV2Side.BOTTOM } align={ (popoverConfig?.align as PopoverV2Align) || PopoverV2Align.START } sideOffset={popoverConfig?.sideOffset ?? 4} shadow="sm" useDrawerOnMobile={useDrawer} > {showDateInputs && ( )} ) } ) DateRangePicker.displayName = 'DateRangePicker' export default DateRangePicker