import { createContext, ReactNode, useContext } from 'react'; import { addDays, differenceInCalendarDays, isSameDay, subDays } from 'date-fns'; import { DayPickerBase } from 'types/DayPickerBase'; import { DayPickerRangeProps, isDayPickerRange } from 'types/DayPickerRange'; import { DayClickEventHandler } from 'types/EventHandlers'; import { DateRange } from 'types/Matchers'; import { InternalModifier, Modifiers } from 'types/Modifiers'; import { addToRange } from './utils/addToRange'; /** Represent the modifiers that are changed by the range selection. */ export type SelectRangeModifiers = Pick< Modifiers, | InternalModifier.Disabled | InternalModifier.RangeEnd | InternalModifier.RangeMiddle | InternalModifier.RangeStart >; /** Represents the value of a {@link SelectRangeContext}. */ export interface SelectRangeContextValue { /** The range of days that has been selected. */ selected: DateRange | undefined; /** The modifiers for the corresponding selection. */ modifiers: SelectRangeModifiers; /** Event handler to attach to the day button to enable the range select. */ onDayClick?: DayClickEventHandler; } /** * The SelectRange context shares details about the selected days when in * range selection mode. * * Access this context from the {@link useSelectRange} hook. */ export const SelectRangeContext = createContext< SelectRangeContextValue | undefined >(undefined); export interface SelectRangeProviderProps { initialProps: DayPickerBase; children?: ReactNode; } /** Provides the values for the {@link SelectRangeProvider}. */ export function SelectRangeProvider( props: SelectRangeProviderProps ): JSX.Element { if (!isDayPickerRange(props.initialProps)) { const emptyContextValue: SelectRangeContextValue = { selected: undefined, modifiers: { range_start: [], range_end: [], range_middle: [], disabled: [] } }; return ( {props.children} ); } return ( ); } /** @private */ export interface SelectRangeProviderInternalProps { initialProps: DayPickerRangeProps; children?: ReactNode; } export function SelectRangeProviderInternal({ initialProps, children }: SelectRangeProviderInternalProps): JSX.Element { const { selected } = initialProps; const { from: selectedFrom, to: selectedTo } = selected || {}; const min = initialProps.min; const max = initialProps.max; const onDayClick: DayClickEventHandler = (day, activeModifiers, e) => { initialProps.onDayClick?.(day, activeModifiers, e); const newRange = addToRange(day, selected); initialProps.onSelect?.(newRange, day, activeModifiers, e); }; const modifiers: SelectRangeModifiers = { range_start: [], range_end: [], range_middle: [], disabled: [] }; if (selectedFrom) { modifiers.range_start = [selectedFrom]; if (!selectedTo) { modifiers.range_end = [selectedFrom]; } else { modifiers.range_end = [selectedTo]; if (!isSameDay(selectedFrom, selectedTo)) { modifiers.range_middle = [ { after: selectedFrom, before: selectedTo } ]; } } } else if (selectedTo) { modifiers.range_start = [selectedTo]; modifiers.range_end = [selectedTo]; } if (min) { if (selectedFrom && !selectedTo) { modifiers.disabled.push({ after: subDays(selectedFrom, min - 1), before: addDays(selectedFrom, min - 1) }); } if (selectedFrom && selectedTo) { modifiers.disabled.push({ after: selectedFrom, before: addDays(selectedFrom, min - 1) }); } if (!selectedFrom && selectedTo) { modifiers.disabled.push({ after: subDays(selectedTo, min - 1), before: addDays(selectedTo, min - 1) }); } } if (max) { if (selectedFrom && !selectedTo) { modifiers.disabled.push({ before: addDays(selectedFrom, -max + 1) }); modifiers.disabled.push({ after: addDays(selectedFrom, max - 1) }); } if (selectedFrom && selectedTo) { const selectedCount = differenceInCalendarDays(selectedTo, selectedFrom) + 1; const offset = max - selectedCount; modifiers.disabled.push({ before: subDays(selectedFrom, offset) }); modifiers.disabled.push({ after: addDays(selectedTo, offset) }); } if (!selectedFrom && selectedTo) { modifiers.disabled.push({ before: addDays(selectedTo, -max + 1) }); modifiers.disabled.push({ after: addDays(selectedTo, max - 1) }); } } return ( {children} ); } /** * Hook to access the {@link SelectRangeContextValue}. * * This hook is meant to be used inside internal or custom components. */ export function useSelectRange(): SelectRangeContextValue { const context = useContext(SelectRangeContext); if (!context) { throw new Error('useSelectRange must be used within a SelectRangeProvider'); } return context; }