import * as React from 'react'; import classNames from 'classnames'; import type { BasePickerPanelProps as RcBasePickerPanelProps } from 'rc-picker'; import { PickerPanel as RCPickerPanel } from 'rc-picker'; import type { GenerateConfig } from 'rc-picker/lib/generate'; import type { CellRenderInfo } from 'rc-picker/lib/interface'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import type { AnyObject } from '../_util/type'; import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import { useLocale } from '../locale'; import CalendarHeader from './Header'; import enUS from './locale/en_US'; import useStyle from './style'; export type CalendarMode = 'year' | 'month'; export type HeaderRender = (config: { value: DateType; type: CalendarMode; onChange: (date: DateType) => void; onTypeChange: (type: CalendarMode) => void; }) => React.ReactNode; export interface SelectInfo { source: 'year' | 'month' | 'date' | 'customize'; } export interface CalendarProps { prefixCls?: string; className?: string; rootClassName?: string; style?: React.CSSProperties; locale?: typeof enUS; validRange?: [DateType, DateType]; disabledDate?: (date: DateType) => boolean; /** @deprecated Please use fullCellRender instead. */ dateFullCellRender?: (date: DateType) => React.ReactNode; /** @deprecated Please use cellRender instead. */ dateCellRender?: (date: DateType) => React.ReactNode; /** @deprecated Please use fullCellRender instead. */ monthFullCellRender?: (date: DateType) => React.ReactNode; /** @deprecated Please use cellRender instead. */ monthCellRender?: (date: DateType) => React.ReactNode; cellRender?: (date: DateType, info: CellRenderInfo) => React.ReactNode; fullCellRender?: (date: DateType, info: CellRenderInfo) => React.ReactNode; headerRender?: HeaderRender; value?: DateType; defaultValue?: DateType; mode?: CalendarMode; fullscreen?: boolean; onChange?: (date: DateType) => void; onPanelChange?: (date: DateType, mode: CalendarMode) => void; onSelect?: (date: DateType, selectInfo: SelectInfo) => void; } const isSameYear = (date1: T, date2: T, config: GenerateConfig) => { const { getYear } = config; return date1 && date2 && getYear(date1) === getYear(date2); }; const isSameMonth = (date1: T, date2: T, config: GenerateConfig) => { const { getMonth } = config; return isSameYear(date1, date2, config) && getMonth(date1) === getMonth(date2); }; const isSameDate = (date1: T, date2: T, config: GenerateConfig) => { const { getDate } = config; return isSameMonth(date1, date2, config) && getDate(date1) === getDate(date2); }; const generateCalendar = (generateConfig: GenerateConfig) => { const Calendar: React.FC>> = (props) => { const { prefixCls: customizePrefixCls, className, rootClassName, style, dateFullCellRender, dateCellRender, monthFullCellRender, monthCellRender, cellRender, fullCellRender, headerRender, value, defaultValue, disabledDate, mode, validRange, fullscreen = true, onChange, onPanelChange, onSelect, } = props; const { getPrefixCls, direction, calendar } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('picker', customizePrefixCls); const calendarPrefixCls = `${prefixCls}-calendar`; const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, calendarPrefixCls); const today = generateConfig.getNow(); // ====================== Warning ======================= if (process.env.NODE_ENV !== 'production') { const warning = devUseWarning('Calendar'); warning.deprecated(!dateFullCellRender, 'dateFullCellRender', 'fullCellRender'); warning.deprecated(!dateCellRender, 'dateCellRender', 'cellRender'); warning.deprecated(!monthFullCellRender, 'monthFullCellRender', 'fullCellRender'); warning.deprecated(!monthCellRender, 'monthCellRender', 'cellRender'); } // ====================== State ======================= // Value const [mergedValue, setMergedValue] = useMergedState(() => value || generateConfig.getNow(), { defaultValue, value, }); // Mode const [mergedMode, setMergedMode] = useMergedState('month', { value: mode, }); const panelMode = React.useMemo<'month' | 'date'>( () => (mergedMode === 'year' ? 'month' : 'date'), [mergedMode], ); // Disabled Date const mergedDisabledDate = React.useCallback( (date: DateType) => { const notInRange = validRange ? generateConfig.isAfter(validRange[0], date) || generateConfig.isAfter(date, validRange[1]) : false; return notInRange || !!disabledDate?.(date); }, [disabledDate, validRange], ); // ====================== Events ====================== const triggerPanelChange = (date: DateType, newMode: CalendarMode) => { onPanelChange?.(date, newMode); }; const triggerChange = (date: DateType) => { setMergedValue(date); if (!isSameDate(date, mergedValue, generateConfig)) { // Trigger when month panel switch month if ( (panelMode === 'date' && !isSameMonth(date, mergedValue, generateConfig)) || (panelMode === 'month' && !isSameYear(date, mergedValue, generateConfig)) ) { triggerPanelChange(date, mergedMode); } onChange?.(date); } }; const triggerModeChange = (newMode: CalendarMode) => { setMergedMode(newMode); triggerPanelChange(mergedValue, newMode); }; const onInternalSelect = (date: DateType, source: SelectInfo['source']) => { triggerChange(date); onSelect?.(date, { source }); }; // ====================== Render ====================== const dateRender = React.useCallback( (date: DateType, info: CellRenderInfo): React.ReactNode => { if (fullCellRender) { return fullCellRender(date, info); } if (dateFullCellRender) { return dateFullCellRender(date); } return (
{String(generateConfig.getDate(date)).padStart(2, '0')}
{cellRender ? cellRender(date, info) : dateCellRender?.(date)}
); }, [dateFullCellRender, dateCellRender, cellRender, fullCellRender], ); const monthRender = React.useCallback( (date: DateType, info: CellRenderInfo): React.ReactNode => { if (fullCellRender) { return fullCellRender(date, info); } if (monthFullCellRender) { return monthFullCellRender(date); } const months = info.locale!.shortMonths || generateConfig.locale.getShortMonths!(info.locale!.locale); return (
{months[generateConfig.getMonth(date)]}
{cellRender ? cellRender(date, info) : monthCellRender?.(date)}
); }, [monthFullCellRender, monthCellRender, cellRender, fullCellRender], ); const [contextLocale] = useLocale('Calendar', enUS); const locale = { ...contextLocale, ...props.locale! }; const mergedCellRender: RcBasePickerPanelProps['cellRender'] = (current, info) => { if (info.type === 'date') { return dateRender(current, info); } if (info.type === 'month') { return monthRender(current, { ...info, locale: locale?.lang, }); } }; return wrapCSSVar(
{headerRender ? ( headerRender({ value: mergedValue, type: mergedMode, onChange: (nextDate) => { onInternalSelect(nextDate, 'customize'); }, onTypeChange: triggerModeChange, }) ) : ( )} { onInternalSelect(nextDate, panelMode); }} mode={panelMode} picker={panelMode} disabledDate={mergedDisabledDate} hideHeader />
, ); }; if (process.env.NODE_ENV !== 'production') { Calendar.displayName = 'Calendar'; } return Calendar; }; export default generateCalendar;