import React, { useId, useRef, useState, type RefObject } from 'react' import cx from 'classnames' import { enUS } from 'date-fns/locale' import { isMatch, type DateRange } from 'react-day-picker' import { FocusOn } from 'react-focus-on' import { calculateDisabledDays, isDisabledDate, type DisabledDayMatchers, } from '~components/Calendar' import { CalendarPopover } from '~components/Calendar/CalendarPopover' import { LegacyCalendarRange, type LegacyCalendarRangeProps, } from '~components/Calendar/LegacyCalendarRange' import { Icon } from '~components/Icon' import { Label } from '~components/Label' import { VisuallyHidden } from '~components/VisuallyHidden' import styles from './DateRangePicker.module.scss' export type DateRangePickerProps = { id?: string classNameOverride?: string labelText: string isDisabled?: boolean buttonRef?: RefObject description?: string /** * Selected date range which is being updated in handleDayClick and checked * if within range/not disabled and then passed back to the client to update * the state. */ selectedDateRange?: DateRange /** * String that is formatted by the client with our helper formatDateRangeValue * and then passed into the button to display the readable range. */ value?: string /** * Accepts a DayOfWeek value to start the week on that day. * By default it adapts to the provided locale. */ weekStartsOn?: LegacyCalendarRangeProps['weekStartsOn'] /** * Accepts a date to display that month on first render. */ defaultMonth?: LegacyCalendarRangeProps['defaultMonth'] /** * Event passed from consumer to handle the date on change. */ onChange: (dateRange: DateRange) => void } & DisabledDayMatchers /** * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082094237/Date+Range+Picker Guidance} | * {@link https://cultureamp.design/storybook/?path=/docs/components-date-controls-daterangepicker--docs Storybook} */ export const DateRangePicker = ({ id: propsId, buttonRef, description: _description, // not used labelText, isDisabled = false, classNameOverride, disabledDates, disabledDaysOfWeek, disabledRange, disabledBeforeAfter, disabledBefore, disabledAfter, weekStartsOn, defaultMonth, selectedDateRange, value, onChange, ...inputProps }: DateRangePickerProps): JSX.Element => { const fallbackRef = useRef(null) const ref = buttonRef ?? fallbackRef const reactId = useId() const id = propsId ?? reactId const containerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const disabledDays = calculateDisabledDays({ disabledDates, disabledDaysOfWeek, disabledRange, disabledBeforeAfter, disabledBefore, disabledAfter, }) const handleOpenClose = (): void => { setIsOpen(!isOpen) } const handleReturnFocus = (): void => { if (ref.current) { ref.current.focus() } } const handleDayClick = (day: Date): void => { /** react-day-picker will fire events for disabled days by default. * We're checking here if it includes the CSS Modules class for disabled * on the modifier to then return early. * */ if (isDisabledDate(day, disabledDays)) { return } if (!selectedDateRange) return /** If user has already selected range and then selects again, treat first * click as the start of the new range. * */ if (isSelectingFirstDay(selectedDateRange, day)) { onChange({ from: day, to: undefined, }) } else { // Otherwise, treat click as the final selection. onChange({ from: selectedDateRange.from, to: day, }) handleOpenClose() } } const isSelectingFirstDay = (range: DateRange, day: Date): boolean => { const isBeforeFirstDay = !!range.from && isMatch(day, [ { before: range.from, }, ]) const isRangeSelected = !!range.from && !!range.to return !range.from || isBeforeFirstDay || isRangeSelected } return (
{isOpen && ( Select dates from calendar for: )}
) } DateRangePicker.displayName = 'DateRangePicker'