import moment from 'moment' import * as React from 'react' import { type DateRange } from 'react-day-picker' import { Calendar } from './Calendar' import Button from '../Button/Button' import Icon from '../Icons/Icon' import Popover from '../Popover/Popover' import { SideDrawer } from '../SideDrawer/SideDrawer' import TextInput from '../Form/TextInput' import { useFocus } from '../../hooks/useFocus/useFocus' import { useMediaQuery } from '../../hooks/responsiveHooks' import styles from './_datePicker.module.scss' import { c } from '../../translations/LibraryTranslationService' import { type DatePickerProps } from './DatePicker.models' import { getDateForButtonType, getDateRangeForButtonType, isDateRangeValidWithRestrictions, isEmptyOrWhitespace, isSingleDateValidWithRestrictions, isValidDateString, parseDateRangeString, type RangeButtonType, renderActionButtons, type SingleButtonType, trimWhitespace, } from './DatePickerHelper' // Helper to render action buttons based on typ export default function DatePicker(props: DatePickerProps) { const { showDateValue = true, dateRestrictions = { beforeDate: undefined, afterDate: undefined, }, onDateChange, type = 'single', actionButtons = { last7Days: true, next7Days: true, thisWeekToDate: true, thisWeek: true, lastWeek: true, nextWeek: true, lastMonth: true, thisMonth: true, thisMonthToDate: true, nextMonth: true, thisQuarter: true, thisQuarterToDate: true, lastQuarter: true, nextQuarter: true, thisYear: true, thisYearToDate: true, lastYear: true, nextYear: true, today: true, yesterday: true, tomorrow: true, }, placeholder, labelText, selectedDate, selectedDateRange, isExposed = false, disabled = false, disableAutoClose = false, clear = false, reset = false, labelTooltip, error = { text: '', showError: false, }, layerPosition, position = 'bottom', qaTestId = 'date-picker', } = props const dateType = type === 'single' ? 'date-single' : 'date-range' const isEditingRef = React.useRef(false) const [selectedGroup, setSelectedGroup] = React.useState(null) const isMobileView = useMediaQuery({ type: 'max', breakpoint: 'sm' }) const [isDrawerOpen, setIsDrawerOpen] = React.useState(false) const ref = useFocus(isDrawerOpen) const handleSingleDateSelect = (selectedDate: Date | undefined) => { onDateChange(selectedDate) setIsDateValid(true) isEditingRef.current = false } const initialDateRef = React.useRef(selectedDate) const initialDateRangeRef = React.useRef( selectedDateRange, ) const handleReset = () => { if (type === 'single') { handleSingleDateSelect(initialDateRef.current) } else { handleRangeDateSelect(initialDateRangeRef.current, true) } } const handleNewRangeDateSelect = (selectedRange: DateRange) => { const isSameDate = ( fromDate: Date | undefined, toDate: Date | undefined, ) => { if (!fromDate || !toDate) return false return fromDate.getTime() === toDate.getTime() } if (!isSameDate(selectedDateRange?.from, selectedDateRange?.to)) { if (isSameDate(selectedRange.from, selectedDateRange?.from)) { selectedRange.from = selectedRange.to } else { selectedRange.to = selectedRange.from } } } const handleRangeDateSelect = ( selectedRange: DateRange | undefined, fromInput = false, ) => { if (selectedRange?.from && selectedRange?.to) { if (!fromInput) { handleNewRangeDateSelect(selectedRange) } isEditingRef.current = false if (isDateRangeValidWithRestrictions(selectedRange, dateRestrictions)) { onDateChange(selectedRange) // Adjust the month view if the selected dates are not visible in the current two-month view if (viewMonth && selectedRange.from && selectedRange.to) { const currentMonth = moment(viewMonth) const nextMonth = moment(viewMonth).add(1, 'month') const fromMonth = moment(selectedRange.from) const toMonth = moment(selectedRange.to) const isFromVisible = fromMonth.isSame(currentMonth, 'month') || fromMonth.isSame(nextMonth, 'month') const isToVisible = toMonth.isSame(currentMonth, 'month') || toMonth.isSame(nextMonth, 'month') // Adjust the view if either date is not currently visible if (!isFromVisible || !isToVisible) { setViewMonth(selectedRange.from) } } else if (fromInput) { // Always adjust view when input comes from text input setViewMonth(selectedRange.from) } } else { // If date range is not valid with restrictions, don't set it setIsDateValid(false) return } } else { onDateChange(selectedRange) setSelectedBtnType(null) } setIsDateValid(true) } const [isDateValid, setIsDateValid] = React.useState(true) const [selectedBtnType, setSelectedBtnType] = React.useState< RangeButtonType | SingleButtonType | null >(null) const [viewMonth, setViewMonth] = React.useState( type === 'single' ? selectedDate : selectedDateRange?.from, ) const [inputDate, setInputDate] = React.useState('') const isDateSelected = Boolean( selectedDate || (selectedDateRange && selectedDateRange.from && selectedDateRange.to) || inputDate, ) function getLabelWidthStyle() { if (!showDateValue) { return styles.withoutLabel } return type === 'single' ? styles.singleWithLabel : styles.rangeWithLabel } const selectSingleDate = (buttonType: SingleButtonType) => { const potentialDate = getDateForButtonType(buttonType) if (!isSingleDateValidWithRestrictions(potentialDate, dateRestrictions)) { return } handleSingleDateSelect(potentialDate) } const selectToday = () => selectSingleDate('today') const selectYesterday = () => selectSingleDate('yesterday') const selectTomorrow = () => selectSingleDate('tomorrow') const selectDateRange = (buttonType: RangeButtonType) => { const dateRange = getDateRangeForButtonType(buttonType) if (isDateRangeValidWithRestrictions(dateRange, dateRestrictions)) { onDateChange(dateRange) setViewMonth(dateRange.from) setSelectedBtnType(buttonType) } // If date range is not valid with restrictions, don't select it } const selectLast7Days = () => selectDateRange('last7Days') const selectNext7Days = () => selectDateRange('next7Days') const selectThisWeekToDate = () => selectDateRange('thisWeekToDate') const selectThisWeek = () => selectDateRange('thisWeek') const selectLastWeek = () => selectDateRange('lastWeek') const selectNextWeek = () => selectDateRange('nextWeek') const selectLastMonth = () => selectDateRange('lastMonth') const selectThisMonth = () => selectDateRange('thisMonth') const selectThisMonthToDate = () => selectDateRange('thisMonthToDate') const selectNextMonth = () => selectDateRange('nextMonth') const selectThisQuarter = () => selectDateRange('thisQuarter') const selectThisQuarterToDate = () => selectDateRange('thisQuarterToDate') const selectLastQuarter = () => selectDateRange('lastQuarter') const selectNextQuarter = () => selectDateRange('nextQuarter') const selectThisYear = () => selectDateRange('thisYear') const selectThisYearToDate = () => selectDateRange('thisYearToDate') const selectLastYear = () => selectDateRange('lastYear') const selectNextYear = () => selectDateRange('nextYear') const handleClear = () => { setInputDate('') if (type === 'single') { handleSingleDateSelect(undefined) } else { handleRangeDateSelect({ from: undefined, to: undefined }) } } const getErrorText = () => { const defaultErrorMessage = type === 'single' ? c('datePickerErrorMessageSingleDate') : c('datePickerErrorMessageDateRange') const errorMessages = [] if (!isDateValid) { errorMessages.push(defaultErrorMessage) } if (error.showError && error.text) { errorMessages.push(error.text) } if (errorMessages.length > 0) { return
{errorMessages.join(' ')}
} return null } const getFormattedDateValue = () => { if (!showDateValue) { return '' } if (type === 'single') { if (selectedDate) { return moment(selectedDate).format('MM/DD/YYYY') } return isEditingRef.current ? inputDate : '' } if (selectedDateRange?.from) { return `${moment(selectedDateRange.from).format('MM/DD/YYYY')} - ${moment(selectedDateRange.to).format('MM/DD/YYYY')}` } return isEditingRef.current ? inputDate : '' } const datePickerContent = (setVisible: (visible: boolean) => void) => { return (
{isMobileView && isDrawerOpen && showDateValue && !isExposed ? (
{PopoverOrSidedrawerContent({ setVisible: setIsDrawerOpen, visible: isDrawerOpen, })}
) : ( <> )} {type === 'single' ? ( { handleSingleDateSelect(date) if (!disableAutoClose) { setVisible(false) setIsDrawerOpen(false) } }} {...(!isMobileView ? { footer: renderActionButtons({ type, actionButtons, selectedBtnType, c, selectedGroup, setSelectedGroup, reset, handleReset, dateRestrictions, handlers: { selectToday, selectYesterday, selectTomorrow, }, }), } : {})} autoFocus className={`${styles.calendar} ${styles.singleDatePicker}`} dateRestrictions={dateRestrictions} required /> ) : ( { handleRangeDateSelect(selectedRange) }} autoFocus month={viewMonth} onMonthChange={(viewMonth) => { setViewMonth(viewMonth) }} {...(!isMobileView ? { footer: renderActionButtons({ type, actionButtons, selectedBtnType, c, selectedGroup, setSelectedGroup, reset, handleReset, dateRestrictions, handlers: { selectLast7Days, selectNext7Days, selectThisWeekToDate, selectThisWeek, selectLastWeek, selectNextWeek, selectThisMonth, selectLastMonth, selectThisMonthToDate, selectNextMonth, selectThisQuarter, selectThisQuarterToDate, selectLastQuarter, selectNextQuarter, selectThisYear, selectThisYearToDate, selectLastYear, selectNextYear, }, }), } : {})} defaultMonth={ selectedDateRange ? selectedDateRange.from : undefined } className={`${styles.calendar} ${styles.rangeDatePicker}`} numberOfMonths={2} dateRestrictions={dateRestrictions} required /> )}
) } const PopoverOrSidedrawerContent = ({ setVisible, visible, }: { setVisible: (visible: boolean) => void visible: boolean }) => { return (
{showDateValue ? (
{ setVisible(true) setIsDrawerOpen(true) setViewMonth( type === 'range' ? selectedDateRange?.from : selectedDate, ) }} callout={(_, value) => { const stringValue = String(value) setInputDate(stringValue) if (isEmptyOrWhitespace(stringValue)) { setIsDateValid(true) if (type === 'single') { onDateChange(undefined) } else { onDateChange({ from: undefined, to: undefined }) } return } if (type === 'single') { const isValidDate = isValidDateString(stringValue) if (isValidDate) { setIsDateValid(true) const newDate = moment( trimWhitespace(stringValue), 'MM/DD/YYYY', ).toDate() handleSingleDateSelect(newDate) if (!disableAutoClose) { setVisible(false) setIsDrawerOpen(false) } setViewMonth(newDate) } else { isEditingRef.current = true setIsDateValid(false) onDateChange(undefined) } } else if (type === 'range') { const dateRange = parseDateRangeString(stringValue) if (dateRange) { if ( isDateRangeValidWithRestrictions( dateRange, dateRestrictions, ) ) { setIsDateValid(true) handleRangeDateSelect(dateRange, true) setViewMonth(dateRange.from) } else { isEditingRef.current = true setIsDateValid(false) onDateChange({ from: undefined, to: undefined }) } } else { isEditingRef.current = true setIsDateValid(false) onDateChange({ from: undefined, to: undefined }) } } }} />
) : ( <> )}
{clear && isDateSelected ? ( <>
) : ( <> )}
) } return (
{!isExposed ? ( isMobileView ? ( <> {PopoverOrSidedrawerContent({ setVisible: setIsDrawerOpen, visible: isDrawerOpen, })} setIsDrawerOpen(false)} noContentPadding footerContent={renderActionButtons({ type, actionButtons, selectedBtnType, c, selectedGroup, setSelectedGroup, reset, handleReset, dateRestrictions, handlers: { selectToday, selectYesterday, selectTomorrow, selectLast7Days, selectNext7Days, selectThisWeekToDate, selectThisWeek, selectLastWeek, selectNextWeek, selectThisMonth, selectLastMonth, selectThisMonthToDate, selectNextMonth, selectThisQuarter, selectThisQuarterToDate, selectLastQuarter, selectNextQuarter, selectThisYear, selectThisYearToDate, selectLastYear, selectNextYear, }, })} > {datePickerContent(() => {})} ) : ( datePickerContent(setVisible)} position={position} maxWidth='600px' noPadding disableCloseOnScroll className={styles.DatePickerPopover} > {({ visible, setVisible }) => PopoverOrSidedrawerContent({ setVisible, visible }) } ) ) : ( datePickerContent(() => {}) )} {getErrorText()}
) }