import React from 'react'; import type { DatePickerProps } from '../../../types/date-picker'; import { Button } from '../../button/public'; import { View } from '../../layout/public'; import { Text } from '../../typography/public'; import { formatLocalDate, parseLocalDate } from '../utils/localDate'; const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const; const DAY_CELL_WIDTH = 42; /*** Renders the shared calendar content used by native BottomSheet and web Popover hosts. */ export function DatePickerContent({ description, interactionPolicy, label, maxDate, minDate, onDismiss, onSelect, testID, value, }: DatePickerContentProps) { const [displayMonth, setDisplayMonth] = React.useState(() => resolveInitialMonth(value, minDate)); const monthDays = React.useMemo(() => createMonthDays(displayMonth), [displayMonth]); const resolvedMaxDate = React.useMemo(() => parseLocalDate(maxDate), [maxDate]); const resolvedMinDate = React.useMemo(() => parseLocalDate(minDate), [minDate]); return ( ); } interface DatePickerContentProps extends Pick< DatePickerProps, 'description' | 'interactionPolicy' | 'label' | 'maxDate' | 'minDate' | 'testID' | 'value' > { onDismiss: () => void; onSelect: (value: Date) => void; } /*** Renders the shared title block used by picker overlays. */ function PickerHeader({ description, title, }: { description: React.ReactNode | undefined; title: React.ReactNode; }) { return ( {title} {description ? ( {description} ) : null} ); } /*** Renders previous and next month controls for the calendar. */ function DatePickerMonthNavigation({ displayMonth, interactionPolicy, maxDate, minDate, onMonthChange, }: { displayMonth: Date; interactionPolicy: DatePickerProps['interactionPolicy']; maxDate: Date | undefined; minDate: Date | undefined; onMonthChange: (month: Date) => void; }) { const previousMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() - 1, 1); const nextMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() + 1, 1); return ( {formatMonthLabel(displayMonth)} ); } /*** Renders weekday headings for the calendar grid. */ function DatePickerWeekdays() { return ( {WEEKDAY_LABELS.map((weekday) => ( {weekday} ))} ); } /*** Renders selectable day cells for the current calendar month. */ function DatePickerCalendar({ interactionPolicy, maxDate, minDate, monthDays, onSelect, testID, value, }: { interactionPolicy: DatePickerProps['interactionPolicy']; maxDate: Date | undefined; minDate: Date | undefined; monthDays: readonly (Date | null)[]; onSelect: (value: Date) => void; testID: string | undefined; value: DatePickerProps['value']; }) { return ( {monthDays.map((day, index) => { if (!day) return ; const selected = isSameLocalDay(value, day); return ( ); })} ); } /*** Compares a serialized date to one local calendar day. */ function isSameLocalDay(left: string | null, right: Date): boolean { return left === formatLocalDate(right); } /*** Normalizes a Date to the start of its local calendar day. */ function startOfLocalDay(value: Date): Date { return new Date(value.getFullYear(), value.getMonth(), value.getDate()); } /*** Reports whether one local calendar day precedes another. */ function isBeforeLocalDay(left: Date, right: Date): boolean { return startOfLocalDay(left).getTime() < startOfLocalDay(right).getTime(); } /*** Reports whether one local calendar day follows another. */ function isAfterLocalDay(left: Date, right: Date): boolean { return startOfLocalDay(left).getTime() > startOfLocalDay(right).getTime(); } /*** Reports whether a date falls outside the configured picker range. */ function isDateDisabled( value: Date, minDate: Date | undefined, maxDate: Date | undefined, ): boolean { if (minDate && isBeforeLocalDay(value, minDate)) return true; if (maxDate && isAfterLocalDay(value, maxDate)) return true; return false; } /*** Formats the current calendar month for the picker heading. */ function formatMonthLabel(value: Date): string { return value.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); } /*** Builds the padded calendar grid for one month. */ function createMonthDays(month: Date): (Date | null)[] { const firstDay = new Date(month.getFullYear(), month.getMonth(), 1); const dayCount = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate(); const populatedLength = firstDay.getDay() + dayCount; const paddedLength = Math.ceil(populatedLength / 7) * 7; return Array.from({ length: paddedLength }, (_, index) => index < firstDay.getDay() ? null : new Date(month.getFullYear(), month.getMonth(), index - firstDay.getDay() + 1), ); } /*** Resolves the month initially shown by the calendar. */ function resolveInitialMonth(value: string | null, minDate: string | undefined): Date { const base = parseLocalDate(value) ?? parseLocalDate(minDate) ?? new Date(); return new Date(base.getFullYear(), base.getMonth(), 1); } /*** Reports whether the configured range intersects a candidate month. */ function canNavigateToMonth( month: Date, minDate: Date | undefined, maxDate: Date | undefined, ): boolean { const firstDay = new Date(month.getFullYear(), month.getMonth(), 1); const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0); if (maxDate && isAfterLocalDay(firstDay, maxDate)) return false; if (minDate && isBeforeLocalDay(lastDay, minDate)) return false; return true; }