import classNames from 'classnames'; import { PickerPanel as RCPickerPanel } from 'rc-picker'; import type { GenerateConfig } from 'rc-picker/lib/generate'; import type { Locale } from 'rc-picker/lib/interface'; import type { PickerPanelBaseProps as RCPickerPanelBaseProps, PickerPanelDateProps as RCPickerPanelDateProps, PickerPanelTimeProps as RCPickerPanelTimeProps, } from 'rc-picker/lib/PickerPanel'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import * as React from 'react'; import { ConfigContext } from '../config-provider'; import { useLocale } from '../locale'; import CalendarHeader from './Header'; import enUS from './locale/en_US'; import useStyle from './style'; type InjectDefaultProps = Omit< Props, 'locale' | 'generateConfig' | 'prevIcon' | 'nextIcon' | 'superPrevIcon' | 'superNextIcon' > & { locale?: typeof enUS; size?: 'large' | 'default' | 'small'; }; // Picker Props export type PickerPanelBaseProps = InjectDefaultProps>; export type PickerPanelDateProps = InjectDefaultProps>; export type PickerPanelTimeProps = InjectDefaultProps>; export type PickerProps = | PickerPanelBaseProps | PickerPanelDateProps | PickerPanelTimeProps; 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 CalendarProps { prefixCls?: string; className?: string; rootClassName?: string; style?: React.CSSProperties; locale?: typeof enUS; validRange?: [DateType, DateType]; disabledDate?: (date: DateType) => boolean; dateFullCellRender?: (date: DateType) => React.ReactNode; dateCellRender?: (date: DateType) => React.ReactNode; monthFullCellRender?: (date: DateType) => React.ReactNode; monthCellRender?: (date: DateType) => 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) => void; } function generateCalendar(generateConfig: GenerateConfig) { function isSameYear(date1: DateType, date2: DateType) { return date1 && date2 && generateConfig.getYear(date1) === generateConfig.getYear(date2); } function isSameMonth(date1: DateType, date2: DateType) { return ( isSameYear(date1, date2) && generateConfig.getMonth(date1) === generateConfig.getMonth(date2) ); } function isSameDate(date1: DateType, date2: DateType) { return ( isSameMonth(date1, date2) && generateConfig.getDate(date1) === generateConfig.getDate(date2) ); } const Calendar = (props: CalendarProps) => { const { prefixCls: customizePrefixCls, className, rootClassName, style, dateFullCellRender, dateCellRender, monthFullCellRender, monthCellRender, headerRender, value, defaultValue, disabledDate, mode, validRange, fullscreen = true, onChange, onPanelChange, onSelect, } = props; const { getPrefixCls, direction } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('picker', customizePrefixCls); const calendarPrefixCls = `${prefixCls}-calendar`; const [wrapSSR, hashId] = useStyle(prefixCls); const today = generateConfig.getNow(); // ====================== 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)) { // Trigger when month panel switch month if ( (panelMode === 'date' && !isSameMonth(date, mergedValue)) || (panelMode === 'month' && !isSameYear(date, mergedValue)) ) { triggerPanelChange(date, mergedMode); } onChange?.(date); } }; const triggerModeChange = (newMode: CalendarMode) => { setMergedMode(newMode); triggerPanelChange(mergedValue, newMode); }; const onInternalSelect = (date: DateType) => { triggerChange(date); onSelect?.(date); }; // ====================== Locale ====================== const getDefaultLocale = () => { const { locale } = props; const result = { ...enUS, ...locale, }; result.lang = { ...result.lang, ...(locale || {}).lang, }; return result; }; // ====================== Render ====================== const dateRender = React.useCallback( (date: DateType): React.ReactNode => { if (dateFullCellRender) { return dateFullCellRender(date); } return (
{String(generateConfig.getDate(date)).padStart(2, '0')}
{dateCellRender && dateCellRender(date)}
); }, [dateFullCellRender, dateCellRender], ); const monthRender = React.useCallback( (date: DateType, locale: Locale): React.ReactNode => { if (monthFullCellRender) { return monthFullCellRender(date); } const months = locale.shortMonths || generateConfig.locale.getShortMonths!(locale.locale); return (
{months[generateConfig.getMonth(date)]}
{monthCellRender && monthCellRender(date)}
); }, [monthFullCellRender, monthCellRender], ); const [contextLocale] = useLocale('Calendar', getDefaultLocale); return wrapSSR(
{headerRender ? ( headerRender({ value: mergedValue, type: mergedMode, onChange: onInternalSelect, onTypeChange: triggerModeChange, }) ) : ( )} monthRender(date, contextLocale?.lang)} onSelect={onInternalSelect} mode={panelMode} picker={panelMode} disabledDate={mergedDisabledDate} hideHeader />
, ); }; if (process.env.NODE_ENV !== 'production') { Calendar.displayName = 'Calendar'; } return Calendar; } export default generateCalendar;