import {
forwardRef,
useLayoutEffect,
useRef,
useState,
useCallback,
useMemo,
useEffect,
} from 'react'
import styled, { CSSObject } from 'styled-components'
import { motion } from 'framer-motion'
import {
DateRange,
CustomRangeConfig,
CustomDateDisableFunction,
} from './types'
import { CalendarTokenType } from './dateRangePicker.tokens'
import Block from '../Primitives/Block/Block'
import Skeleton from '../Skeleton/Skeleton'
import {
handleCustomRangeCalendarDateClick,
getDayNames,
getMonthHeight,
findCurrentMonthIndex,
createCalendarMonthData,
calculateDayCellProps,
shouldHideDateFromCalendar,
getDatePartsInTimezone,
} from './utils'
import { FOUNDATION_THEME } from '../../tokens'
import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
import { DATE_RANGE_PICKER_CONSTANTS } from './constants'
type CalendarGridProps = {
selectedRange: DateRange | undefined
onDateSelect: (range: DateRange) => void
today: Date
allowSingleDateSelection?: boolean
disableFutureDates?: boolean
disablePastDates?: boolean
hideFutureDates?: boolean
hidePastDates?: boolean
customDisableDates?: CustomDateDisableFunction
customRangeConfig?: CustomRangeConfig
showDateTimePicker?: boolean
resetScrollPosition?: number // Used to trigger scroll reset when popover reopens
timezone?: string
isSingleDatePicker?: boolean
maxYearOffset?: number
minDate?: Date
maxDate?: Date
maxRangeDays?: number
}
const CONTAINER_HEIGHT = 340
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
duration: 0.2,
ease: [0.4, 0, 0.2, 1] as const,
},
},
}
const monthVariants = {
hidden: { opacity: 0, y: -10 },
visible: (custom: number) => ({
opacity: 1,
y: 0,
transition: {
duration: 0.3,
delay: custom * 0.03,
ease: [0.25, 0.46, 0.45, 0.94] as const,
},
}),
}
const dayCellVariants = {
hidden: { opacity: 0, scale: 0.92 },
visible: (custom: number) => ({
opacity: 1,
scale: 1,
transition: {
duration: 0.2,
delay: custom * 0.008,
ease: [0.4, 0, 0.2, 1] as const,
},
}),
}
// Styled Components with Motion
const MotionBlock = motion(Block)
const StyledDayCell = styled(MotionBlock)<{
$cellStyles: CSSObject
$textColor: string
$isDisabled: boolean
$isSelected: boolean
$calendarToken: CalendarTokenType
}>`
${(props) => props.$cellStyles}
color: ${(props) => props.$textColor};
cursor: ${(props) => (props.$isDisabled ? 'not-allowed' : 'pointer')};
position: relative;
${(props) =>
!props.$isDisabled &&
`
&:hover:not(:focus-visible) {
outline: ${props.$calendarToken.calendar.calendarGrid.day.cell.border.hover};
outline-offset: -1px;
border-radius: ${props.$calendarToken.calendar.calendarGrid.day.cell.borderRadius};
z-index: 1;
}
`}
${(props) =>
!props.$isDisabled &&
`
&:focus-visible {
outline: ${props.$calendarToken.calendar.calendarGrid.day.cell.border.active};
border-radius: ${props.$calendarToken.calendar.calendarGrid.day.cell.borderRadius};
}
`}
`
const CalendarSkeleton = ({
calendarToken,
}: {
calendarToken: CalendarTokenType
}) => {
return (
{Array.from({ length: 5 }).map((_, rowIndex) => (
))}
)
}
function generateMonthsList(
today: Date,
hideFutureDates: boolean = false,
hidePastDates: boolean = false,
maxYearOffset?: number
): { month: number; year: number }[] {
const months = []
const currentYear = today.getFullYear()
const currentMonth = today.getMonth()
const startYear = DATE_RANGE_PICKER_CONSTANTS.MIN_YEAR
const endYear =
currentYear +
(maxYearOffset && maxYearOffset >= 0
? maxYearOffset
: DATE_RANGE_PICKER_CONSTANTS.MAX_YEAR_OFFSET)
for (let year = startYear; year <= endYear; year++) {
for (let month = 0; month <= 11; month++) {
if (hideFutureDates) {
if (
year > currentYear ||
(year === currentYear && month > currentMonth)
) {
continue
}
}
if (hidePastDates) {
if (
year < currentYear ||
(year === currentYear && month < currentMonth)
) {
continue
}
}
months.push({ month, year })
}
}
return months
}
const CalendarGrid = forwardRef(
(
{
selectedRange,
onDateSelect,
today,
allowSingleDateSelection = false,
disableFutureDates = false,
disablePastDates = false,
hideFutureDates = false,
hidePastDates = false,
customDisableDates,
customRangeConfig,
showDateTimePicker = true,
resetScrollPosition,
timezone,
isSingleDatePicker,
maxYearOffset,
minDate,
maxDate,
maxRangeDays,
},
ref
) => {
const scrollContainerRef = useRef(null)
const cellsRef = useRef