import React, { FC, useEffect, useMemo, useRef, useState } from "react"; import classNames from "classnames"; import { format, isAfter, isBefore, parse } from "date-fns"; import { Box } from "../Box"; import { Button, BUTTON_ICON_POSITION, BUTTON_VARIANT } from "../Button"; import { MenuRenderer } from "../MenuRenderer"; import { PopoverMenu } from "../PopoverMenu"; import { Icon, ICON_TYPE } from "../Icon"; import { Flex } from "../Flex"; import { Text } from "../Text"; import { CustomDateRangeInput } from "./CustomDateRangeInput"; import { DATE_FORMAT, parseDate } from "../../utilities"; import { DatePicker } from "../DatePicker"; import { isValidDate } from "../../utilities/isValidDate"; import { cn, RangePickerPreset, DATE_RANGE_PICKER_CUSTOM_PRESET, dateRangePickerFocusInput, } from "./utils"; import { DateRangePickerProps } from "./types"; export const DateRangePicker: FC = ({ className, onChange, align = "left", presets, value, minDate, maxDate, onCustomChange, onSave, onCancel, openToDate, disabled, customButton, ...rest }) => { const placement = align === "left" ? "bottom-start" : "bottom-end"; const [ customStartDateInputVal, setCustomStartDateInputVal, ] = useState(""); const [customStartDateError, setCustomStartDateError] = useState(""); const [customEndDateInputVal, setCustomEndDateInputVal] = useState( "", ); const [customEndDateError, setCustomEndDateError] = useState(""); const [customSelectingWhichDate, setCustomSelectingWhichDate] = useState< "start" | "end" >("start"); const startInput = useRef(null); const endInput = useRef(null); const handleClose = () => { setCustomStartDateError(""); }; const [selectedPresetName, setSelectedPresetName] = useState(value.name); const [currentPresetName, setCurrentPresetName] = useState(value.name); useEffect(() => { setSelectedPresetName(value.name); setCustomStartDateInputVal( value.startDate ? format(value.startDate, DATE_FORMAT) : "", ); setCustomEndDateInputVal( value.endDate ? format(value.endDate, DATE_FORMAT) : "", ); }, [value]); const customDateInputsValid = useMemo(() => { const parsedStart = parseDate(customStartDateInputVal); const parsedEnd = parseDate(customEndDateInputVal); return !!(parsedStart && parsedEnd); }, [customStartDateInputVal, customEndDateInputVal]); const displayText = useMemo(() => { if (!value) return "Select Date Range"; return currentPresetName; }, [currentPresetName, value]); useEffect(() => { if (customDateInputsValid) return; const inputToFocus = customSelectingWhichDate === "start" ? startInput : endInput; dateRangePickerFocusInput(inputToFocus); }, [customSelectingWhichDate, customDateInputsValid]); const validDates = useMemo(() => { let start = null; let end = null; // Parse start date if (customStartDateInputVal) { const parsed = parse(customStartDateInputVal, DATE_FORMAT, new Date()); start = isValidDate(parsed) ? parsed : null; } // Parse end date if (customEndDateInputVal) { const parsed = parse(customEndDateInputVal, DATE_FORMAT, new Date()); end = isValidDate(parsed) ? parsed : null; } // Start date before minDate or after maxDate if ( start && ((minDate && isBefore(start, minDate)) || (maxDate && isAfter(start, maxDate))) ) { setCustomStartDateError("Outside allowed date range"); start = null; } // End date before minDate or after maxDate if ( end && ((minDate && isBefore(end, minDate)) || (maxDate && isAfter(end, maxDate))) ) { setCustomEndDateError("Outside allowed date range"); end = null; } // Start date after endDate if (start && end && isAfter(start, end)) { setCustomEndDateError("End date cannot be before start date"); end = null; } // If start is a Date, remove any error if (start) { setCustomStartDateError(""); } // If end is a Date, remove any error if (end) { setCustomEndDateError(""); } return { start, end }; }, [customStartDateInputVal, customEndDateInputVal, minDate, maxDate]); const handleDatePickerChange = ([newStartDate, newEndDate]: [Date, Date]) => { if (!newEndDate) dateRangePickerFocusInput(endInput); const newStartString = newStartDate ? format(newStartDate, DATE_FORMAT) : ""; const newEndString = newEndDate ? format(newEndDate, DATE_FORMAT) : ""; setCustomStartDateInputVal(newStartString); setCustomEndDateInputVal(newEndString); }; const handleDatePickerSelect = (date: Date) => { // Handles situation where an end date is provided // without a start date if (!validDates.start && validDates.end) { setCustomStartDateInputVal(format(date, DATE_FORMAT)); } if (selectedPresetName !== DATE_RANGE_PICKER_CUSTOM_PRESET) { setSelectedPresetName(DATE_RANGE_PICKER_CUSTOM_PRESET); } }; const customApplyBtnDisabled = !validDates.start || !validDates.end || !!customStartDateError || !!customEndDateError; return ( customButton ? ( customButton( { children: displayText, disabled, ...triggerProps, }, triggerState, ) ) : ( ) } {...rest} > {({ listProps, onClose }) => ( {presets.map((p) => ( { onChange(p); setSelectedPresetName(p.name); setCustomStartDateInputVal( format(p.startDate, DATE_FORMAT), ); setCustomEndDateInputVal(format(p.endDate, DATE_FORMAT)); }} isSelected={p.name === selectedPresetName} > {p.name} ))} { onCustomChange(); setSelectedPresetName(DATE_RANGE_PICKER_CUSTOM_PRESET); setCustomStartDateInputVal(""); setCustomEndDateInputVal(""); if (!validDates.start) { setCustomSelectingWhichDate("start"); } }} isSelected={ selectedPresetName === DATE_RANGE_PICKER_CUSTOM_PRESET } > {DATE_RANGE_PICKER_CUSTOM_PRESET} setCustomSelectingWhichDate("start")} onChange={() => { if ( selectedPresetName !== DATE_RANGE_PICKER_CUSTOM_PRESET ) { setSelectedPresetName(DATE_RANGE_PICKER_CUSTOM_PRESET); } }} /> to setCustomSelectingWhichDate("end")} onChange={() => { if ( selectedPresetName !== DATE_RANGE_PICKER_CUSTOM_PRESET ) { setSelectedPresetName(DATE_RANGE_PICKER_CUSTOM_PRESET); } }} /> )} ); };