import { clsx } from "clsx"; import React, { Component, cloneElement } from "react"; import Calendar, { OUTSIDE_CLICK_IGNORE_CLASS } from "./calendar"; import CalendarIcon from "./calendar_icon"; import { newDate, isDate, isBefore, isAfter, isEqual, setTime, isValid, getSeconds, getMinutes, getHours, addDays, addMinutes, addMonths, addWeeks, subDays, subMonths, subWeeks, addYears, subYears, isDayDisabled, isDayInRange, getEffectiveMinDate, getEffectiveMaxDate, parseDate, parseDateForNavigation, formatDate, safeDateFormat, safeDateRangeFormat, getHighLightDaysMap, getYear, getMonth, getStartOfWeek, getEndOfWeek, registerLocale, setDefaultLocale, getDefaultLocale, DEFAULT_YEAR_ITEM_NUMBER, isSameDay, isMonthDisabled, isYearDisabled, safeMultipleDatesFormat, getHolidaysMap, isDateBefore, getStartOfDay, getEndOfDay, isSameMinute, toZonedTime, fromZonedTime, safeToDate, type HighlightDate, type HolidayItem, type TimeZone, KeyType, DATE_RANGE_SEPARATOR, } from "./date_utils"; import PopperComponent from "./popper_component"; import Portal from "./portal"; import TabLoop from "./tab_loop"; import type { ClickOutsideHandler } from "./click_outside_wrapper"; export { default as CalendarContainer } from "./calendar_container"; export { registerLocale, setDefaultLocale, getDefaultLocale }; export { ReactDatePickerCustomHeaderProps, ReactDatePickerCustomDayNameProps, } from "./calendar"; // Compares dates year+month combinations function hasPreSelectionChanged( date1?: Date | null, date2?: Date | null, ): boolean { if (date1 && date2) { return ( getMonth(date1) !== getMonth(date2) || getYear(date1) !== getYear(date2) ); } return date1 !== date2; } /** * General datepicker component. */ const INPUT_ERR_1 = "Date input not valid."; interface Holiday { date: string; holidayName: string; } type CalendarProps = React.ComponentPropsWithoutRef; interface CalendarIconProps extends React.ComponentPropsWithoutRef< typeof CalendarIcon > {} interface PortalProps extends React.ComponentPropsWithoutRef {} interface PopperComponentProps extends React.ComponentPropsWithoutRef< typeof PopperComponent > {} // see https://github.com/microsoft/TypeScript/issues/31501 // eslint-disable-next-line @typescript-eslint/no-explicit-any type OmitUnion = T extends any ? Omit : never; export type DatePickerProps = OmitUnion< CalendarProps, | "setOpen" | "dateFormat" | "preSelection" | "onSelect" | "onClickOutside" | "highlightDates" | "holidays" | "shouldFocusDayInline" | "monthSelectedIn" | "onDropdownFocus" | "onTimeChange" | "className" | "container" | "handleOnKeyDown" | "handleOnDayKeyDown" | "isInputFocused" | "setPreSelection" | "selectsRange" | "selectsMultiple" | "dropdownMode" > & Partial> & OmitUnion & OmitUnion< PopperComponentProps, | "className" | "hidePopper" | "targetComponent" | "popperComponent" | "popperOnKeyDown" | "showArrow" > & { dateFormatCalendar?: CalendarProps["dateFormat"]; calendarClassName?: CalendarProps["className"]; calendarContainer?: CalendarProps["container"]; dropdownMode?: CalendarProps["dropdownMode"]; onKeyDown?: (event: React.KeyboardEvent) => void; popperClassName?: PopperComponentProps["className"]; showPopperArrow?: PopperComponentProps["showArrow"]; popperTargetRef?: React.RefObject; open?: boolean; disabled?: boolean; readOnly?: boolean; startOpen?: boolean; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onClickOutside?: ClickOutsideHandler; onInputClick?: VoidFunction; preventOpenOnFocus?: boolean; closeOnScroll?: boolean | ((event: Event) => boolean); isClearable?: boolean; clearButtonTitle?: string; clearButtonClassName?: string; ariaLabelClose?: string; className?: string; customInput?: Parameters[0]; dateFormat?: string | string[]; showDateSelect?: boolean; highlightDates?: (Date | HighlightDate)[]; onCalendarOpen?: VoidFunction; onCalendarClose?: VoidFunction; strictParsing?: boolean; swapRange?: boolean; onInputError?: (error: { code: 1; msg: string }) => void; allowSameDay?: boolean; withPortal?: boolean; focusSelectedMonth?: boolean; showIcon?: boolean; calendarIconClassname?: never; calendarIconClassName?: string; toggleCalendarOnIconClick?: boolean; holidays?: Holiday[]; startDate?: Date | null; endDate?: Date | null; selected?: Date | null; /** * The IANA timezone identifier (e.g., "America/New_York", "UTC", "Europe/London"). * When set, the datepicker will display dates/times in this timezone and * the onChange callback will return dates adjusted to this timezone. * * Requires the optional peer dependency `date-fns-tz` to be installed: * ``` * npm install date-fns-tz * ``` * * @example * ```tsx * setSelectedDate(date)} * /> * ``` */ timeZone?: TimeZone; value?: string; customInputRef?: string; id?: string; name?: string; form?: string; autoFocus?: boolean; placeholderText?: string; autoComplete?: string; title?: string; required?: boolean; tabIndex?: number; ariaDescribedBy?: string; ariaInvalid?: string; ariaLabel?: string; ariaLabelledBy?: string; ariaRequired?: string; "aria-describedby"?: string; "aria-invalid"?: string; "aria-label"?: string; "aria-labelledby"?: string; "aria-required"?: string; rangeSeparator?: string; onChangeRaw?: ( event?: React.MouseEvent | React.KeyboardEvent, selectionMeta?: { date: Date; formattedDate: string; }, ) => void; onSelect?: ( date: Date | null, event?: | React.MouseEvent | React.KeyboardEvent, ) => void; } & ( | { selectsRange?: false | undefined; selectsMultiple?: false | undefined; formatMultipleDates?: never; onChange?: ( date: Date | null, event?: | React.MouseEvent | React.KeyboardEvent, ) => void; } | { selectsRange?: true; selectsMultiple?: false | undefined; formatMultipleDates?: never; onChange?: ( date: [Date | null, Date | null], event?: | React.MouseEvent | React.KeyboardEvent, ) => void; } | { selectsRange?: false | undefined; selectsMultiple?: true; formatMultipleDates?: ( dates: Date[], formatDate: (date: Date) => string, ) => string; onChange?: ( dates: Date[] | null, event?: | React.MouseEvent | React.KeyboardEvent, ) => void; } ); // Internal types for onChange handlers - used for type assertions within the component type OnChangeSingle = ( date: Date | null, event?: React.MouseEvent | React.KeyboardEvent, ) => void; type OnChangeRange = ( date: [Date | null, Date | null], event?: React.MouseEvent | React.KeyboardEvent, ) => void; type OnChangeMultiple = ( dates: Date[] | null, event?: React.MouseEvent | React.KeyboardEvent, ) => void; interface DatePickerState { open: boolean; wasHidden: boolean; lastPreSelectChange?: | typeof PRESELECT_CHANGE_VIA_INPUT | typeof PRESELECT_CHANGE_VIA_NAVIGATE; inputValue: string | null; preventFocus: boolean; preSelection?: CalendarProps["preSelection"]; shouldFocusDayInline?: CalendarProps["shouldFocusDayInline"]; monthSelectedIn?: CalendarProps["monthSelectedIn"]; focused?: CalendarProps["isInputFocused"]; highlightDates: Required["highlightDates"]; isRenderAriaLiveMessage?: boolean; } export class DatePicker extends Component { static get defaultProps() { return { allowSameDay: false, dateFormat: "MM/dd/yyyy", dateFormatCalendar: "LLLL yyyy", disabled: false, disabledKeyboardNavigation: false, dropdownMode: "scroll" as const, preventOpenOnFocus: false, monthsShown: 1, outsideClickIgnoreClass: OUTSIDE_CLICK_IGNORE_CLASS, readOnly: false, rangeSeparator: DATE_RANGE_SEPARATOR, withPortal: false, selectsDisabledDaysInRange: false, shouldCloseOnSelect: true, showTimeSelect: false, showTimeInput: false, showPreviousMonths: false, showMonthYearPicker: false, showFullMonthYearPicker: false, showTwoColumnMonthYearPicker: false, showFourColumnMonthYearPicker: false, showYearPicker: false, showQuarterYearPicker: false, showWeekPicker: false, strictParsing: false, swapRange: false, timeIntervals: 30, timeCaption: "Time", previousMonthAriaLabel: "Previous Month", previousMonthButtonLabel: "Previous Month", nextMonthAriaLabel: "Next Month", nextMonthButtonLabel: "Next Month", previousYearAriaLabel: "Previous Year", previousYearButtonLabel: "Previous Year", nextYearAriaLabel: "Next Year", nextYearButtonLabel: "Next Year", timeInputLabel: "Time", enableTabLoop: true, yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER, focusSelectedMonth: false, showPopperArrow: true, excludeScrollbar: true, customTimeInput: null, calendarStartDay: undefined, toggleCalendarOnIconClick: false, usePointerEvent: false, }; } constructor(props: DatePickerProps) { super(props); this.state = this.calcInitialState(); this.preventFocusTimeout = undefined; } componentDidMount(): void { window.addEventListener("scroll", this.onScroll, true); document.addEventListener( "visibilitychange", this.setHiddenStateOnVisibilityHidden, ); } componentDidUpdate( prevProps: DatePickerProps, prevState: DatePickerState, ): void { // Update preSelection when selected/startDate prop changes to a different month/year. // This ensures the calendar view updates when dates are programmatically set // (e.g., via "Today" or "This Week" buttons). (Fix for #3367) if ( this.props.selectsRange && hasPreSelectionChanged(prevProps.startDate, this.props.startDate) ) { this.setPreSelection(this.props.startDate); } else if ( hasPreSelectionChanged(prevProps.selected, this.props.selected) ) { this.setPreSelection(this.props.selected); } if ( this.state.monthSelectedIn !== undefined && prevProps.monthsShown !== this.props.monthsShown ) { this.setState({ monthSelectedIn: 0 }); } // Reset monthSelectedIn when calendar opens for range selection // This ensures startDate is displayed as the first month when reopening // (Fix for #5939), but we don't reset during active selection to avoid // the view jumping when clicking dates in the second calendar (Fix for #5275) if ( this.props.selectsRange && prevState.open === false && this.state.open === true && this.state.monthSelectedIn !== 0 ) { this.setState({ monthSelectedIn: 0 }); } if (prevProps.highlightDates !== this.props.highlightDates) { this.setState({ highlightDates: getHighLightDaysMap(this.props.highlightDates), }); } if ( !prevState.focused && !isEqual(prevProps.selected, this.props.selected) ) { this.setState({ inputValue: null }); } if (prevState.open !== this.state.open) { if (prevState.open === false && this.state.open === true) { this.props.onCalendarOpen?.(); } if (prevState.open === true && this.state.open === false) { this.props.onCalendarClose?.(); } } } componentWillUnmount(): void { this.clearPreventFocusTimeout(); window.removeEventListener("scroll", this.onScroll, true); document.removeEventListener( "visibilitychange", this.setHiddenStateOnVisibilityHidden, ); } preventFocusTimeout: ReturnType | undefined; inputFocusTimeout: ReturnType | undefined; calendar: Calendar | null = null; input: HTMLElement | null = null; getPreSelection = (): Date => { const { timeZone } = this.props; const baseDate = this.props.openToDate ? this.props.openToDate : this.props.selectsEnd && this.props.startDate ? this.props.startDate : this.props.selectsStart && this.props.endDate ? this.props.endDate : newDate(); // Convert to the specified timezone for display return timeZone ? toZonedTime(baseDate, timeZone) : baseDate; }; // Convert the date from string format to standard Date format // Uses parseDate with ISO format to parse as local time, preventing // dates from shifting in timezones west of UTC. See issue #6105. modifyHolidays = () => this.props.holidays?.reduce((accumulator, holiday) => { const date = parseDate(holiday.date, "yyyy-MM-dd", undefined, false); if (!date) { return accumulator; } return [...accumulator, { ...holiday, date }]; }, []); calcInitialState = (): DatePickerState => { const { timeZone } = this.props; const defaultPreSelection = this.getPreSelection(); const minDate = getEffectiveMinDate(this.props); const maxDate = getEffectiveMaxDate(this.props); const boundedPreSelection = minDate && isBefore(defaultPreSelection, getStartOfDay(minDate)) ? minDate : maxDate && isAfter(defaultPreSelection, getEndOfDay(maxDate)) ? maxDate : defaultPreSelection; // Convert selected/startDate to zoned time for display if timezone is specified let initialPreSelection = this.props.selectsRange ? this.props.startDate : this.props.selected; if (initialPreSelection && timeZone) { initialPreSelection = toZonedTime(initialPreSelection, timeZone); } return { open: this.props.startOpen || false, preventFocus: false, inputValue: null, preSelection: initialPreSelection ?? boundedPreSelection, // transforming highlighted days (perhaps nested array) // to flat Map for faster access in day.jsx highlightDates: getHighLightDaysMap(this.props.highlightDates), focused: false, // used to focus day in inline version after month has changed, but not on // initial render shouldFocusDayInline: false, isRenderAriaLiveMessage: false, wasHidden: false, }; }; getInputValue = (): string => { const { locale, startDate, endDate, rangeSeparator, selected, selectedDates, selectsMultiple, selectsRange, formatMultipleDates, value, } = this.props; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const { inputValue } = this.state; if (typeof value === "string") { return value; } else if (typeof inputValue === "string") { return inputValue; } else if (selectsRange) { return safeDateRangeFormat(startDate, endDate, { dateFormat, locale, rangeSeparator, }); } else if (selectsMultiple) { if (formatMultipleDates) { const formatDateFn = (date: Date) => safeDateFormat(date, { dateFormat, locale }); return formatMultipleDates(selectedDates ?? [], formatDateFn); } return safeMultipleDatesFormat(selectedDates ?? [], { dateFormat, locale, }); } return safeDateFormat(selected, { dateFormat, locale, }); }; resetHiddenStatus = (): void => { this.setState({ ...this.state, wasHidden: false, }); }; setHiddenStatus = (): void => { this.setState({ ...this.state, wasHidden: true, }); }; setHiddenStateOnVisibilityHidden = (): void => { if (document.visibilityState !== "hidden") { return; } this.setHiddenStatus(); }; clearPreventFocusTimeout = () => { if (this.preventFocusTimeout) { clearTimeout(this.preventFocusTimeout); } }; setFocus = () => { this.input?.focus?.({ preventScroll: true }); }; setBlur = () => { this.input?.blur?.(); this.cancelFocusInput(); }; deferBlur = () => { requestAnimationFrame(() => { this.setBlur(); }); }; setOpen = (open: boolean, skipSetBlur: boolean = false): void => { this.setState( { open: open, preSelection: open && this.state.open ? this.state.preSelection : this.calcInitialState().preSelection, lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE, }, () => { if (!open) { this.setState( (prev: DatePickerState) => ({ focused: skipSetBlur ? prev.focused : false, }), () => { !skipSetBlur && this.deferBlur(); this.setState({ inputValue: null }); }, ); } }, ); }; inputOk = (): boolean => isDate(this.state.preSelection); isCalendarOpen = () => this.props.open === undefined ? this.state.open && !this.props.disabled && !this.props.readOnly : this.props.open; handleFocus = (event: React.FocusEvent): void => { const isAutoReFocus = this.state.wasHidden; const isOpenAllowed = isAutoReFocus ? this.state.open : true; if (isAutoReFocus) { this.resetHiddenStatus(); } if (!this.state.preventFocus) { this.props.onFocus?.(event); if ( isOpenAllowed && !this.props.preventOpenOnFocus && !this.props.readOnly ) { this.setOpen(true); } } this.setState({ focused: true }); }; sendFocusBackToInput = (): void => { // Clear previous timeout if it exists if (this.preventFocusTimeout) { this.clearPreventFocusTimeout(); } // close the popper and refocus the input // stop the input from auto opening onFocus // setFocus to the input this.setState({ preventFocus: true }, (): void => { this.preventFocusTimeout = setTimeout((): void => { this.setFocus(); this.setState({ preventFocus: false }); }); }); }; cancelFocusInput = () => { clearTimeout(this.inputFocusTimeout); this.inputFocusTimeout = undefined; }; deferFocusInput = () => { this.cancelFocusInput(); this.inputFocusTimeout = setTimeout(() => this.setFocus(), 1); }; handleDropdownFocus = () => { this.cancelFocusInput(); }; resetInputValue = () => { this.setState({ ...this.state, inputValue: null, }); }; handleBlur = (event: React.FocusEvent) => { if (!this.state.open || this.props.withPortal || this.props.showTimeInput) { this.props.onBlur?.(event); } // If user cleared the input via a mask library (inputValue has no date-like // characters), clear the selection on blur (fixes issue #5814 with mask inputs) const { inputValue } = this.state; if (typeof inputValue === "string" && inputValue.length > 0) { // Check if input looks like a cleared mask (no alphanumeric characters) // This distinguishes between: // - "__/__/____" (cleared mask) → should clear selection // - "2025-02-45" (invalid date) → should keep previous selection const hasDateCharacters = /[a-zA-Z0-9]/.test(inputValue); if (!hasDateCharacters && this.props.selected) { this.setSelected(null, undefined, true); } } this.resetInputValue(); if (this.state.open && this.props.open === false) { this.setOpen(false); } this.setState({ focused: false }); }; handleCalendarClickOutside = (event: MouseEvent) => { // Call user's onClickOutside first, allowing them to call preventDefault() this.props.onClickOutside?.(event); // Only close if not prevented and not inline if (!this.props.inline && !event.defaultPrevented) { this.setOpen(false); } if (this.props.withPortal) { event.preventDefault(); } }; // handleChange is called when user types in the textbox handleChange = ( ...allArgs: Parameters["onChangeRaw"]> ) => { const event = allArgs[0]; if (this.props.onChangeRaw) { this.props.onChangeRaw.apply(this, allArgs); if ( !event || typeof event.isDefaultPrevented !== "function" || event.isDefaultPrevented() ) { return; } } this.setState({ inputValue: event?.target instanceof HTMLInputElement ? event.target.value : null, lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT, }); const { selectsRange, startDate, endDate } = this.props; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const strictParsing = this.props.strictParsing ?? DatePicker.defaultProps.strictParsing; const value = event?.target instanceof HTMLInputElement ? event.target.value : ""; if (selectsRange) { const rangeSeparator = this.props.rangeSeparator as string; const trimmedRangeSeparator = rangeSeparator.trim(); const [valueStart, valueEnd] = value .split( dateFormat.includes(trimmedRangeSeparator) ? rangeSeparator : trimmedRangeSeparator, 2, ) .map((val) => val.trim()); const startDateNew = parseDate( valueStart ?? "", dateFormat, this.props.locale, strictParsing, ); const endDateNew = startDateNew ? parseDate( valueEnd ?? "", dateFormat, this.props.locale, strictParsing, ) : null; const startChanged = safeToDate(startDate)?.getTime() !== startDateNew?.getTime(); const endChanged = safeToDate(endDate)?.getTime() !== endDateNew?.getTime(); if (!startChanged && !endChanged) { return; } if (startDateNew && isDayDisabled(startDateNew, this.props)) { return; } if (endDateNew && isDayDisabled(endDateNew, this.props)) { return; } // Update preSelection to keep calendar viewport consistent when reopening // Use startDate for preSelection to match calcInitialState behavior if (startDateNew) { this.setState({ preSelection: startDateNew }); } this.props.onChange?.([startDateNew, endDateNew], event); } else { // not selectsRange const date = parseDate( value, dateFormat, this.props.locale, strictParsing, this.props.selected ?? undefined, ); // Update selection if either (1) date was successfully parsed, or (2) input field is empty if (date || !value) { this.setSelected(date, event, true); } else if (!this.props.inline) { // If full date parsing failed but we have partial input, // try to extract date info for calendar navigation const navDate = parseDateForNavigation( value, this.state.preSelection ?? undefined, ); // Only update preSelection if navDate is valid and within min/max bounds if ( navDate && (!this.props.minDate || !isBefore(navDate, this.props.minDate)) && (!this.props.maxDate || !isAfter(navDate, this.props.maxDate)) ) { this.setState({ preSelection: navDate }); } } } }; handleSelect = ( date: Date, event?: React.MouseEvent | React.KeyboardEvent, monthSelectedIn?: number, ) => { if (this.props.readOnly) return; const { selectsRange, startDate, endDate, locale, swapRange } = this.props; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const isDateSelectionComplete = !selectsRange || (startDate && !endDate && (swapRange || !isDateBefore(date, startDate))); if ( this.props.shouldCloseOnSelect && !this.props.showTimeSelect && isDateSelectionComplete ) { // Preventing onFocus event to fix issue // https://github.com/Hacker0x01/react-datepicker/issues/628 this.sendFocusBackToInput(); } if (this.props.onChangeRaw) { const formattedDate = safeDateFormat(date, { dateFormat, locale, }); this.props.onChangeRaw(event, { date, formattedDate }); } this.setSelected(date, event, false, monthSelectedIn); if (this.props.showDateSelect) { this.setState({ isRenderAriaLiveMessage: true }); } if (!this.props.shouldCloseOnSelect || this.props.showTimeSelect) { this.setPreSelection(date); } else if (isDateSelectionComplete) { this.setOpen(false); } }; // setSelected is called either from handleChange (user typed date into textbox and it was parsed) or handleSelect (user selected date from calendar using mouse or keyboard) setSelected = ( date: Date | null, event?: React.MouseEvent | React.KeyboardEvent, keepInput?: boolean, monthSelectedIn?: number, ) => { const { timeZone } = this.props; // If timezone is specified, convert the selected date from zoned time to UTC // This ensures the onChange callback receives a proper UTC Date object let changedDate = date; if (changedDate && timeZone) { changedDate = fromZonedTime(changedDate, timeZone); } // Early return if selected year/month/day is disabled if (this.props.showYearPicker) { if ( changedDate !== null && isYearDisabled(getYear(changedDate), this.props) ) { return; } } else if (this.props.showMonthYearPicker) { if (changedDate !== null && isMonthDisabled(changedDate, this.props)) { return; } } else { if (changedDate !== null && isDayDisabled(changedDate, this.props)) { return; } } const { onChange, selectsRange, startDate, endDate, selectsMultiple, selectedDates, minTime, swapRange, } = this.props; if ( !isEqual(this.props.selected, changedDate) || this.props.allowSameDay || selectsRange || selectsMultiple ) { if (changedDate !== null) { // Preserve previously selected time if only date is currently being changed if ( this.props.selected && (!keepInput || (!this.props.showTimeSelect && !this.props.showTimeSelectOnly && !this.props.showTimeInput)) ) { changedDate = setTime(changedDate, { hour: getHours(this.props.selected), minute: getMinutes(this.props.selected), second: getSeconds(this.props.selected), }); } // If minTime is present then set the time to minTime if ( !keepInput && (this.props.showTimeSelect || this.props.showTimeSelectOnly) ) { if (minTime) { changedDate = setTime(changedDate, { hour: minTime.getHours(), minute: minTime.getMinutes(), second: minTime.getSeconds(), }); } } if (!this.props.inline) { this.setState({ preSelection: changedDate, }); } if (!this.props.focusSelectedMonth) { this.setState({ monthSelectedIn: monthSelectedIn }); } } if (selectsRange) { const onChangeRange = onChange as OnChangeRange | undefined; const noRanges = !startDate && !endDate; const hasStartRange = startDate && !endDate; const hasOnlyEndRange = !startDate && !!endDate; const isRangeFilled = startDate && endDate; if (noRanges) { onChangeRange?.([changedDate, null], event); } else if (hasStartRange) { if (changedDate === null) { onChangeRange?.([null, null], event); } else if (isDateBefore(changedDate, startDate)) { if (swapRange) { onChangeRange?.([changedDate, startDate], event); } else { onChangeRange?.([changedDate, null], event); } } else { onChangeRange?.([startDate, changedDate], event); } } else if (hasOnlyEndRange) { if (changedDate && isDateBefore(changedDate, endDate)) { onChangeRange?.([changedDate, endDate], event); } else { onChangeRange?.([changedDate, null], event); } } if (isRangeFilled) { onChangeRange?.([changedDate, null], event); } } else if (selectsMultiple) { const onChangeMultiple = onChange as OnChangeMultiple | undefined; if (changedDate !== null) { if (!selectedDates?.length) { onChangeMultiple?.([changedDate], event); } else { const isChangedDateAlreadySelected = selectedDates.some( (selectedDate) => isSameDay(selectedDate, changedDate), ); if (isChangedDateAlreadySelected) { const nextDates = selectedDates.filter( (selectedDate) => !isSameDay(selectedDate, changedDate), ); onChangeMultiple?.(nextDates, event); } else { onChangeMultiple?.([...selectedDates, changedDate], event); } } } } else { (onChange as OnChangeSingle | undefined)?.(changedDate, event); } } if (!keepInput) { this.props.onSelect?.(changedDate, event); this.setState({ inputValue: null }); } }; // When checking preSelection via min/maxDate, times need to be manipulated via getStartOfDay/getEndOfDay setPreSelection = (date?: Date | null): void => { if (this.props.readOnly) return; const hasMinDate = isDate(this.props.minDate); const hasMaxDate = isDate(this.props.maxDate); let isValidDateSelection = true; if (date) { const dateStartOfDay = getStartOfDay(date); if (hasMinDate && hasMaxDate) { // isDayInRange uses getStartOfDay internally, so not necessary to manipulate times here isValidDateSelection = isDayInRange( date, this.props.minDate, this.props.maxDate, ); } else if (hasMinDate) { const minDateStartOfDay = getStartOfDay(this.props.minDate); isValidDateSelection = isAfter(date, minDateStartOfDay) || isEqual(dateStartOfDay, minDateStartOfDay); } else if (hasMaxDate) { const maxDateEndOfDay = getEndOfDay(this.props.maxDate); isValidDateSelection = isBefore(date, maxDateEndOfDay) || isEqual(dateStartOfDay, maxDateEndOfDay); } } if (isValidDateSelection) { this.setState({ preSelection: date, }); } }; toggleCalendar = (): void => { this.setOpen(!this.state.open); }; handleTimeChange = (time: Date, modifyDateType?: "start" | "end"): void => { if (this.props.selectsMultiple) { return; } const { selectsRange, startDate, endDate, onChange, timeZone } = this.props; if (selectsRange) { const onChangeRange = onChange as OnChangeRange | undefined; // In range mode, apply time to the appropriate date // If modifyDateType is specified, use that to determine which date to modify // Otherwise, use the legacy behavior: // - If we have a startDate but no endDate, apply time to startDate // - If we have both, apply time to endDate if (modifyDateType === "start") { // Explicitly modify start date if (startDate) { let changedStartDate = setTime(startDate, { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedStartDate, }); // Convert from zoned time to UTC if timezone is specified if (timeZone) { changedStartDate = fromZonedTime(changedStartDate, timeZone); } onChangeRange?.( [ changedStartDate, endDate ? timeZone ? fromZonedTime(endDate, timeZone) : endDate : null, ], undefined, ); } } else if (modifyDateType === "end") { // Explicitly modify end date if (endDate) { let changedEndDate = setTime(endDate, { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedEndDate, }); // Convert from zoned time to UTC if timezone is specified if (timeZone) { changedEndDate = fromZonedTime(changedEndDate, timeZone); } onChangeRange?.( [ startDate ? timeZone ? fromZonedTime(startDate, timeZone) : startDate : null, changedEndDate, ], undefined, ); } } else { // Legacy behavior for showTimeSelect (single time picker) const hasStartRange = startDate && !endDate; if (hasStartRange) { // Apply time to startDate let changedStartDate = setTime(startDate, { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedStartDate, }); // Convert from zoned time to UTC if timezone is specified if (timeZone) { changedStartDate = fromZonedTime(changedStartDate, timeZone); } onChangeRange?.([changedStartDate, null], undefined); } else if (startDate && endDate) { // Apply time to endDate let changedEndDate = setTime(endDate, { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedEndDate, }); // Convert from zoned time to UTC if timezone is specified if (timeZone) { changedEndDate = fromZonedTime(changedEndDate, timeZone); } onChangeRange?.( [ timeZone ? fromZonedTime(startDate, timeZone) : startDate, changedEndDate, ], undefined, ); } else { // No dates selected yet, just update preSelection const changedDate = setTime(this.getPreSelection(), { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedDate, }); } } } else { // Single date mode (original behavior) const selected = this.props.selected ? this.props.selected : this.getPreSelection(); let changedDate = this.props.selected ? time : setTime(selected, { hour: getHours(time), minute: getMinutes(time), }); this.setState({ preSelection: changedDate, }); // Convert from zoned time to UTC if timezone is specified if (changedDate && timeZone) { changedDate = fromZonedTime(changedDate, timeZone); } (this.props.onChange as OnChangeSingle | undefined)?.(changedDate); } if (this.props.shouldCloseOnSelect && !this.props.showTimeInput) { this.sendFocusBackToInput(); this.setOpen(false); } if (this.props.showTimeInput) { this.setOpen(true); } if (this.props.showTimeSelectOnly || this.props.showTimeSelect) { this.setState({ isRenderAriaLiveMessage: true }); } this.setState({ inputValue: null }); }; onInputClick = (): void => { if (!this.props.disabled && !this.props.readOnly) { this.setOpen(true); } this.props.onInputClick?.(); }; handleTimeOnlyArrowKey = (eventKey: string): void => { const currentTime = safeToDate(this.props.selected) || this.state.preSelection || newDate(); const timeIntervals = this.props.timeIntervals ?? 30; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const formatStr = Array.isArray(dateFormat) ? dateFormat[0] : dateFormat; const baseDate = getStartOfDay(currentTime); const currentMinutes = getHours(currentTime) * 60 + getMinutes(currentTime); const maxMinutes = 23 * 60 + 60 - timeIntervals; // Cap at last valid interval of the day let newTime: Date; if (eventKey === KeyType.ArrowUp) { const newMinutes = Math.max(0, currentMinutes - timeIntervals); newTime = addMinutes(baseDate, newMinutes); } else { const newMinutes = Math.min(maxMinutes, currentMinutes + timeIntervals); newTime = addMinutes(baseDate, newMinutes); } const formattedTime = formatDate( newTime, formatStr || DatePicker.defaultProps.dateFormat, this.props.locale, ); this.setState({ preSelection: newTime, inputValue: formattedTime, }); if (this.props.selectsRange || this.props.selectsMultiple) { return; } const selected = this.props.selected ? this.props.selected : this.getPreSelection(); const changedDate = this.props.selected ? newTime : setTime(selected, { hour: getHours(newTime), minute: getMinutes(newTime), }); (this.props.onChange as OnChangeSingle | undefined)?.(changedDate); if (this.props.showTimeSelectOnly || this.props.showTimeSelect) { this.setState({ isRenderAriaLiveMessage: true }); } requestAnimationFrame(() => { this.scrollToTimeOption(newTime); }); }; handleTimeOnlyEnterKey = (event: React.KeyboardEvent): void => { const inputElement = event.target as HTMLInputElement; const inputValue = inputElement.value; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const timeFormat = this.props.timeFormat || "p"; const defaultTime = this.state.preSelection || safeToDate(this.props.selected) || newDate(); const parsedDate = parseDate( inputValue, dateFormat, this.props.locale, this.props.strictParsing ?? false, defaultTime, ); let timeToCommit: Date = defaultTime; if (parsedDate && isValid(parsedDate)) { timeToCommit = parsedDate; } else { const highlightedItem = this.calendar?.containerRef.current instanceof Element && this.calendar.containerRef.current.querySelector( ".react-datepicker__time-list-item[tabindex='0']", ); if (highlightedItem instanceof HTMLElement) { const itemText = highlightedItem.textContent?.trim(); if (itemText) { const itemTime = parseDate( itemText, timeFormat, this.props.locale, false, defaultTime, ); if (itemTime && isValid(itemTime)) { timeToCommit = itemTime; } } } } this.handleTimeChange(timeToCommit); this.setOpen(false); this.sendFocusBackToInput(); }; scrollToTimeOption = (time: Date): void => { if (!this.calendar?.containerRef.current) { return; } const container = this.calendar.containerRef.current; const timeListItems = Array.from( container.querySelectorAll( ".react-datepicker__time-list-item", ), ); let targetItem: HTMLLIElement | null = null; let closestTimeDiff = Infinity; const timeFormat = this.props.timeFormat || "p"; for (const item of timeListItems) { const itemText = item.textContent?.trim(); if (itemText) { const itemTime = parseDate( itemText, timeFormat, this.props.locale, false, time, ); if (itemTime && isValid(itemTime)) { if (isSameMinute(itemTime, time)) { targetItem = item; break; } const timeDiff = Math.abs(itemTime.getTime() - time.getTime()); if (timeDiff < closestTimeDiff) { closestTimeDiff = timeDiff; targetItem = item; } } } } if (targetItem) { timeListItems.forEach((item) => { item.setAttribute("tabindex", "-1"); }); targetItem.setAttribute("tabindex", "0"); targetItem.scrollIntoView({ behavior: "smooth", block: "center", }); } }; onInputKeyDown = (event: React.KeyboardEvent): void => { this.props.onKeyDown?.(event); const eventKey = event.key; if ( !this.state.open && !this.props.inline && !this.props.preventOpenOnFocus ) { if ( eventKey === KeyType.ArrowDown || eventKey === KeyType.ArrowUp || eventKey === KeyType.Enter ) { this.onInputClick?.(); } return; } if (this.state.open && this.props.showTimeSelectOnly) { if (eventKey === KeyType.ArrowDown || eventKey === KeyType.ArrowUp) { event.preventDefault(); this.handleTimeOnlyArrowKey(eventKey); return; } if (eventKey === KeyType.Enter) { event.preventDefault(); this.handleTimeOnlyEnterKey(event); return; } } // if calendar is open, these keys will focus the selected item if (this.state.open) { if (eventKey === KeyType.ArrowDown || eventKey === KeyType.ArrowUp) { event.preventDefault(); const selectorString = this.props.showTimeSelectOnly ? ".react-datepicker__time-list-item[tabindex='0']" : this.props.showWeekPicker && this.props.showWeekNumbers ? '.react-datepicker__week-number[tabindex="0"]' : this.props.showFullMonthYearPicker || this.props.showMonthYearPicker ? '.react-datepicker__month-text[tabindex="0"]' : '.react-datepicker__day[tabindex="0"]'; const selectedItem = this.calendar?.containerRef.current instanceof Element && this.calendar.containerRef.current.querySelector(selectorString); selectedItem instanceof HTMLElement && selectedItem.focus({ preventScroll: true }); return; } const copy = newDate(this.state.preSelection); if (eventKey === KeyType.Enter) { event.preventDefault(); (event.target as HTMLInputElement).blur(); if ( this.inputOk() && this.state.lastPreSelectChange === PRESELECT_CHANGE_VIA_NAVIGATE ) { this.handleSelect(copy, event); !this.props.shouldCloseOnSelect && this.setPreSelection(copy); } else { this.setOpen(false); } } else if (eventKey === KeyType.Escape) { event.preventDefault(); (event.target as HTMLInputElement).blur(); this.sendFocusBackToInput(); this.setOpen(false); } else if (eventKey === KeyType.Tab) { this.setOpen(false); } if (!this.inputOk()) { this.props.onInputError?.({ code: 1, msg: INPUT_ERR_1 }); } } }; onPortalKeyDown = (event: React.KeyboardEvent): void => { const eventKey = event.key; if (eventKey === KeyType.Escape) { event.preventDefault(); this.setState( { preventFocus: true, }, () => { this.setOpen(false); setTimeout(() => { this.setFocus(); this.setState({ preventFocus: false }); }); }, ); } }; // keyDown events passed down to day.jsx onDayKeyDown = (event: React.KeyboardEvent) => { const { minDate, maxDate, disabledKeyboardNavigation, showWeekPicker, shouldCloseOnSelect, locale, calendarStartDay, adjustDateOnChange, inline, } = this.props; this.props.onKeyDown?.(event); if (disabledKeyboardNavigation) return; const eventKey = event.key as KeyType; const isShiftKeyActive = event.shiftKey; const copy = newDate(this.state.preSelection); const calculateNewDate = (eventKey: KeyType, date: Date): Date => { let newCalculatedDate = date; switch (eventKey) { case KeyType.ArrowRight: newCalculatedDate = showWeekPicker ? addWeeks(date, 1) : addDays(date, 1); break; case KeyType.ArrowLeft: newCalculatedDate = showWeekPicker ? subWeeks(date, 1) : subDays(date, 1); break; case KeyType.ArrowUp: newCalculatedDate = subWeeks(date, 1); break; case KeyType.ArrowDown: newCalculatedDate = addWeeks(date, 1); break; case KeyType.PageUp: newCalculatedDate = isShiftKeyActive ? subYears(date, 1) : subMonths(date, 1); break; case KeyType.PageDown: newCalculatedDate = isShiftKeyActive ? addYears(date, 1) : addMonths(date, 1); break; case KeyType.Home: newCalculatedDate = getStartOfWeek(date, locale, calendarStartDay); break; case KeyType.End: newCalculatedDate = getEndOfWeek(date); break; } return newCalculatedDate; }; const getNewDate = (eventKey: KeyType, date: Date): Date => { const MAX_ITERATIONS = 40; let eventKeyCopy = eventKey; let validDateFound = false; let iterations = 0; let newSelection = calculateNewDate(eventKey, date); while (!validDateFound) { if (iterations >= MAX_ITERATIONS) { newSelection = date; break; } // if minDate exists and the new selection is before the min date, get the nearest date that isn't disabled if (minDate && newSelection < minDate) { eventKeyCopy = KeyType.ArrowRight; newSelection = isDayDisabled(minDate, this.props) ? calculateNewDate(eventKeyCopy, newSelection) : minDate; } // if maxDate exists and the new selection is after the max date, get the nearest date that isn't disabled if (maxDate && newSelection > maxDate) { eventKeyCopy = KeyType.ArrowLeft; newSelection = isDayDisabled(maxDate, this.props) ? calculateNewDate(eventKeyCopy, newSelection) : maxDate; } if (isDayDisabled(newSelection, this.props)) { // if PageUp and Home is pressed to a disabled date, it will try to find the next available date after if ( eventKeyCopy === KeyType.PageUp || eventKeyCopy === KeyType.Home ) { eventKeyCopy = KeyType.ArrowRight; } // if PageDown and End is pressed to a disabled date, it will try to find the next available date before if ( eventKeyCopy === KeyType.PageDown || eventKeyCopy === KeyType.End ) { eventKeyCopy = KeyType.ArrowLeft; } newSelection = calculateNewDate(eventKeyCopy, newSelection); } else { validDateFound = true; } iterations++; } return newSelection; }; if (eventKey === KeyType.Enter) { event.preventDefault(); this.handleSelect(copy, event); !shouldCloseOnSelect && this.setPreSelection(copy); return; } else if (eventKey === KeyType.Escape) { event.preventDefault(); this.setOpen(false); if (!this.inputOk()) { this.props.onInputError?.({ code: 1, msg: INPUT_ERR_1 }); } return; } let newSelection = null; switch (eventKey) { case KeyType.ArrowLeft: case KeyType.ArrowRight: case KeyType.ArrowUp: case KeyType.ArrowDown: case KeyType.PageUp: case KeyType.PageDown: case KeyType.Home: case KeyType.End: newSelection = getNewDate(eventKey, copy); break; } if (!newSelection) { this.props.onInputError?.({ code: 1, msg: INPUT_ERR_1 }); return; } event.preventDefault(); this.setState({ lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE }); if (adjustDateOnChange) { this.setSelected(newSelection); } this.setPreSelection(newSelection); // In inline mode, always set shouldFocusDayInline to true when navigating via keyboard. // This ensures focus is properly transferred to the new day element regardless of // whether the month changed. The user initiated this navigation from a focused day, // so we should always focus the destination day. if (inline) { this.setState({ shouldFocusDayInline: true }); } }; // handle generic key down events in the popper that do not adjust or select dates // ex: while focusing prev and next month buttons onPopperKeyDown = (event: React.KeyboardEvent): void => { const eventKey = event.key; if (eventKey === KeyType.Escape) { event.preventDefault(); this.sendFocusBackToInput(); this.setOpen(false); } }; onClearClick = (event?: React.MouseEvent): void => { if (event) { if (event.preventDefault) { event.preventDefault(); } } this.sendFocusBackToInput(); const { selectsRange, onChange } = this.props; if (selectsRange) { (onChange as OnChangeRange | undefined)?.([null, null], event); } else { (onChange as OnChangeSingle | undefined)?.(null, event); } this.setState({ inputValue: null }); }; clear = () => { this.onClearClick(); }; onScroll = (event: Event): void => { if ( typeof this.props.closeOnScroll === "boolean" && this.props.closeOnScroll ) { if ( event.target === document || event.target === document.documentElement || event.target === document.body ) { this.setOpen(false); } } else if (typeof this.props.closeOnScroll === "function") { if (this.props.closeOnScroll(event)) { this.setOpen(false); } } }; handleMonthSelectedInChange = (monthSelectedIn: number): void => { this.setState({ monthSelectedIn }); }; renderCalendar = () => { if (!this.props.inline && !this.isCalendarOpen()) { return null; } return ( { this.calendar = elem; }} {...this.props} {...this.state} setOpen={this.setOpen} dateFormat={ this.props.dateFormatCalendar ?? DatePicker.defaultProps.dateFormatCalendar } onSelect={this.handleSelect} onClickOutside={this.handleCalendarClickOutside} holidays={getHolidaysMap(this.modifyHolidays())} outsideClickIgnoreClass={this.props.outsideClickIgnoreClass} onDropdownFocus={this.handleDropdownFocus} onTimeChange={this.handleTimeChange} className={this.props.calendarClassName} container={this.props.calendarContainer} handleOnKeyDown={this.props.onKeyDown} handleOnDayKeyDown={this.onDayKeyDown} setPreSelection={this.setPreSelection} dropdownMode={ this.props.dropdownMode ?? DatePicker.defaultProps.dropdownMode } onMonthSelectedInChange={this.handleMonthSelectedInChange} > {this.props.children} ); }; renderAriaLiveRegion = () => { const { locale } = this.props; const dateFormat = this.props.dateFormat ?? DatePicker.defaultProps.dateFormat; const isContainsTime = this.props.showTimeInput || this.props.showTimeSelect; const longDateFormat = isContainsTime ? "PPPPp" : "PPPP"; let ariaLiveMessage; if (this.props.selectsRange) { ariaLiveMessage = `Selected start date: ${safeDateFormat( this.props.startDate, { dateFormat: longDateFormat, locale, }, )}. ${ this.props.endDate ? "End date: " + safeDateFormat(this.props.endDate, { dateFormat: longDateFormat, locale, }) : "" }`; } else { if (this.props.showTimeSelectOnly) { ariaLiveMessage = `Selected time: ${safeDateFormat( this.props.selected, { dateFormat, locale }, )}`; } else if (this.props.showYearPicker) { ariaLiveMessage = `Selected year: ${safeDateFormat( this.props.selected, { dateFormat: "yyyy", locale }, )}`; } else if (this.props.showMonthYearPicker) { ariaLiveMessage = `Selected month: ${safeDateFormat( this.props.selected, { dateFormat: "MMMM yyyy", locale }, )}`; } else if (this.props.showQuarterYearPicker) { ariaLiveMessage = `Selected quarter: ${safeDateFormat( this.props.selected, { dateFormat: "yyyy, QQQ", locale, }, )}`; } else { ariaLiveMessage = `Selected date: ${safeDateFormat( this.props.selected, { dateFormat: longDateFormat, locale, }, )}`; } } return ( {ariaLiveMessage} ); }; renderDateInput = () => { const className = clsx(this.props.className, { [this.props.outsideClickIgnoreClass || DatePicker.defaultProps.outsideClickIgnoreClass]: this.state.open, }); const customInput = this.props.customInput || ; const customInputRef = this.props.customInputRef || "ref"; // Build aria props object, only including defined values to avoid // overwriting aria attributes that may be set on the custom input const ariaProps: Record = {}; const ariaDescribedBy = this.props["aria-describedby"] ?? this.props.ariaDescribedBy; const ariaInvalid = this.props["aria-invalid"] ?? this.props.ariaInvalid; const ariaLabel = this.props["aria-label"] ?? this.props.ariaLabel; const ariaLabelledBy = this.props["aria-labelledby"] ?? this.props.ariaLabelledBy; const ariaRequired = this.props["aria-required"] ?? this.props.ariaRequired; if (ariaDescribedBy != null) ariaProps["aria-describedby"] = ariaDescribedBy; if (ariaInvalid != null) ariaProps["aria-invalid"] = ariaInvalid; if (ariaLabel != null) ariaProps["aria-label"] = ariaLabel; if (ariaLabelledBy != null) ariaProps["aria-labelledby"] = ariaLabelledBy; if (ariaRequired != null) ariaProps["aria-required"] = ariaRequired; return cloneElement(customInput, { [customInputRef]: (input: HTMLElement | null) => { this.input = input; }, value: this.getInputValue(), onBlur: this.handleBlur, onChange: this.handleChange, onClick: this.onInputClick, onFocus: this.handleFocus, onKeyDown: this.onInputKeyDown, id: this.props.id, name: this.props.name, form: this.props.form, autoFocus: this.props.autoFocus, placeholder: this.props.placeholderText, disabled: this.props.disabled, autoComplete: this.props.autoComplete, className: clsx(customInput.props.className, className), title: this.props.title, readOnly: this.props.readOnly, required: this.props.required, tabIndex: this.props.tabIndex, ...ariaProps, }); }; renderClearButton = (): React.ReactElement | null => { const { isClearable, disabled, selected, startDate, endDate, clearButtonTitle, clearButtonClassName = "", ariaLabelClose = "Close", selectedDates, readOnly, } = this.props; if ( isClearable && !readOnly && (selected != null || startDate != null || endDate != null || selectedDates?.length) ) { return (