/** * Logic: * When `mode` === `picker`, * click will trigger `onSelect` (if value changed trigger `onChange` also). * Panel change will not trigger `onSelect` but trigger `onPanelChange` */ import * as React from 'react'; import classNames from 'classnames'; import KeyCode from 'rc-util/lib/KeyCode'; import warning from 'rc-util/lib/warning'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import type { SharedTimeProps } from './panels/TimePanel'; import TimePanel from './panels/TimePanel'; import DatetimePanel from './panels/DatetimePanel'; import DatePanel from './panels/DatePanel'; import WeekPanel from './panels/WeekPanel'; import MonthPanel from './panels/MonthPanel'; import QuarterPanel from './panels/QuarterPanel'; import YearPanel from './panels/YearPanel'; import DecadePanel from './panels/DecadePanel'; import type { GenerateConfig } from './generate'; import type { Locale, PanelMode, PanelRefProps, PickerMode, DisabledTime, OnPanelChange, Components, } from './interface'; import { isEqual } from './utils/dateUtil'; import PanelContext from './PanelContext'; import type { DateRender } from './panels/DatePanel/DateBody'; import { PickerModeMap } from './utils/uiUtil'; import type { MonthCellRender } from './panels/MonthPanel/MonthBody'; import RangeContext from './RangeContext'; import getExtraFooter from './utils/getExtraFooter'; import getRanges from './utils/getRanges'; import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil'; export type PickerPanelSharedProps = { prefixCls?: string; className?: string; style?: React.CSSProperties; /** @deprecated Will be removed in next big version. Please use `mode` instead */ mode?: PanelMode; tabIndex?: number; // Locale locale: Locale; generateConfig: GenerateConfig; // Value value?: DateType | null; defaultValue?: DateType; /** [Legacy] Set default display picker view date */ pickerValue?: DateType; /** [Legacy] Set default display picker view date */ defaultPickerValue?: DateType; // Date disabledDate?: (date: DateType) => boolean; // Render dateRender?: DateRender; monthCellRender?: MonthCellRender; renderExtraFooter?: (mode: PanelMode) => React.ReactNode; // Event onSelect?: (value: DateType) => void; onChange?: (value: DateType) => void; onPanelChange?: OnPanelChange; onMouseDown?: React.MouseEventHandler; onOk?: (date: DateType) => void; direction?: 'ltr' | 'rtl'; /** @private This is internal usage. Do not use in your production env */ hideHeader?: boolean; /** @private This is internal usage. Do not use in your production env */ onPickerValueChange?: (date: DateType) => void; /** @private Internal usage. Do not use in your production env */ components?: Components; }; export type PickerPanelBaseProps = { picker: Exclude; } & PickerPanelSharedProps; export type PickerPanelDateProps = { picker?: 'date'; showToday?: boolean; showNow?: boolean; // Time showTime?: boolean | SharedTimeProps; disabledTime?: DisabledTime; } & PickerPanelSharedProps; export type PickerPanelTimeProps = { picker: 'time'; } & PickerPanelSharedProps & SharedTimeProps; export type PickerPanelProps = | PickerPanelBaseProps | PickerPanelDateProps | PickerPanelTimeProps; // TMP type to fit for ts 3.9.2 type OmitType = Omit, 'picker'> & Omit, 'picker'> & Omit, 'picker'>; type MergedPickerPanelProps = { picker?: PickerMode; } & OmitType; function PickerPanel(props: PickerPanelProps) { const { prefixCls = 'rc-picker', className, style, locale, generateConfig, value, defaultValue, pickerValue, defaultPickerValue, disabledDate, mode, picker = 'date', tabIndex = 0, showNow, showTime, showToday, renderExtraFooter, hideHeader, onSelect, onChange, onPanelChange, onMouseDown, onPickerValueChange, onOk, components, direction, hourStep = 1, minuteStep = 1, secondStep = 1, } = props as MergedPickerPanelProps; const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; const isHourStepValid = 24 % hourStep === 0; const isMinuteStepValid = 60 % minuteStep === 0; const isSecondStepValid = 60 % secondStep === 0; if (process.env.NODE_ENV !== 'production') { warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `value`.'); warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `defaultValue`.'); warning(isHourStepValid, `\`hourStep\` ${hourStep} is invalid. It should be a factor of 24.`); warning( isMinuteStepValid, `\`minuteStep\` ${minuteStep} is invalid. It should be a factor of 60.`, ); warning( isSecondStepValid, `\`secondStep\` ${secondStep} is invalid. It should be a factor of 60.`, ); } // ============================ State ============================= const panelContext = React.useContext(PanelContext); const { operationRef, panelRef: panelDivRef, onSelect: onContextSelect, hideRanges, defaultOpenValue, } = panelContext; const { inRange, panelPosition, rangedValue, hoverRangedValue } = React.useContext(RangeContext); const panelRef = React.useRef({}); // Handle init logic const initRef = React.useRef(true); // Value const [mergedValue, setInnerValue] = useMergedState(null, { value, defaultValue, postState: (val) => { if (!val && defaultOpenValue && picker === 'time') { return defaultOpenValue; } return val; }, }); // View date control const [viewDate, setInnerViewDate] = useMergedState(null, { value: pickerValue, defaultValue: defaultPickerValue || mergedValue, postState: (date) => { const now = generateConfig.getNow(); if (!date) return now; // When value is null and set showTime if (!mergedValue && showTime) { if (typeof showTime === 'object') { return setDateTime(generateConfig, date, showTime.defaultValue || now); } if (defaultValue) { return setDateTime(generateConfig, date, defaultValue); } return setDateTime(generateConfig, date, now); } return date; }, }); const setViewDate = (date: DateType) => { setInnerViewDate(date); if (onPickerValueChange) { onPickerValueChange(date); } }; // Panel control const getInternalNextMode = (nextMode: PanelMode): PanelMode => { const getNextMode = PickerModeMap[picker!]; if (getNextMode) { return getNextMode(nextMode); } return nextMode; }; // Save panel is changed from which panel const [mergedMode, setInnerMode] = useMergedState( () => { if (picker === 'time') { return 'time'; } return getInternalNextMode('date'); }, { value: mode, }, ); React.useEffect(() => { setInnerMode(picker); }, [picker]); const [sourceMode, setSourceMode] = React.useState(() => mergedMode); const onInternalPanelChange = (newMode: PanelMode | null, viewValue: DateType) => { const nextMode = getInternalNextMode(newMode || mergedMode); setSourceMode(mergedMode); setInnerMode(nextMode); if (onPanelChange && (mergedMode !== nextMode || isEqual(generateConfig, viewDate, viewDate))) { onPanelChange(viewValue, nextMode); } }; const triggerSelect = ( date: DateType, type: 'key' | 'mouse' | 'submit', forceTriggerSelect: boolean = false, ) => { if (mergedMode === picker || forceTriggerSelect) { setInnerValue(date); if (onSelect) { onSelect(date); } if (onContextSelect) { onContextSelect(date, type); } if (onChange && !isEqual(generateConfig, date, mergedValue) && !disabledDate?.(date)) { onChange(date); } } }; // ========================= Interactive ========================== const onInternalKeyDown = (e: React.KeyboardEvent) => { if (panelRef.current && panelRef.current.onKeyDown) { if ( [ KeyCode.LEFT, KeyCode.RIGHT, KeyCode.UP, KeyCode.DOWN, KeyCode.PAGE_UP, KeyCode.PAGE_DOWN, KeyCode.ENTER, ].includes(e.which) ) { e.preventDefault(); } return panelRef.current.onKeyDown(e); } /* istanbul ignore next */ /* eslint-disable no-lone-blocks */ { warning( false, 'Panel not correct handle keyDown event. Please help to fire issue about this.', ); return false; } /* eslint-enable no-lone-blocks */ }; const onInternalBlur: React.FocusEventHandler = (e) => { if (panelRef.current && panelRef.current.onBlur) { panelRef.current.onBlur(e); } }; if (operationRef && panelPosition !== 'right') { operationRef.current = { onKeyDown: onInternalKeyDown, onClose: () => { if (panelRef.current && panelRef.current.onClose) { panelRef.current.onClose(); } }, }; } // ============================ Effect ============================ React.useEffect(() => { if (value && !initRef.current) { setInnerViewDate(value); } }, [value]); React.useEffect(() => { initRef.current = false; }, []); // ============================ Panels ============================ let panelNode: React.ReactNode; const pickerProps = { ...(props as MergedPickerPanelProps), operationRef: panelRef, prefixCls, viewDate, value: mergedValue, onViewDateChange: setViewDate, sourceMode, onPanelChange: onInternalPanelChange, disabledDate, }; delete pickerProps.onChange; delete pickerProps.onSelect; switch (mergedMode) { case 'decade': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'year': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'month': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'quarter': panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'week': panelNode = ( { setViewDate(date); triggerSelect(date, type); }} /> ); break; case 'time': delete pickerProps.showTime; panelNode = ( {...pickerProps} {...(typeof showTime === 'object' ? showTime : null)} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); break; default: if (showTime) { panelNode = ( { setViewDate(date); triggerSelect(date, type); }} /> ); } else { panelNode = ( {...pickerProps} onSelect={(date, type) => { setViewDate(date); triggerSelect(date, type); }} /> ); } } // ============================ Footer ============================ let extraFooter: React.ReactNode; let rangesNode: React.ReactNode; const onNow = () => { const now = generateConfig.getNow(); const lowerBoundTime = getLowerBoundTime( generateConfig.getHour(now), generateConfig.getMinute(now), generateConfig.getSecond(now), isHourStepValid ? hourStep : 1, isMinuteStepValid ? minuteStep : 1, isSecondStepValid ? secondStep : 1, ); const adjustedNow = setTime( generateConfig, now, lowerBoundTime[0], // hour lowerBoundTime[1], // minute lowerBoundTime[2], // second ); triggerSelect(adjustedNow, 'submit'); }; if (!hideRanges) { extraFooter = getExtraFooter(prefixCls, mergedMode, renderExtraFooter); rangesNode = getRanges({ prefixCls, components, needConfirmButton, okDisabled: !mergedValue || (disabledDate && disabledDate(mergedValue)), locale, showNow, onNow: needConfirmButton && onNow, onOk: () => { if (mergedValue) { triggerSelect(mergedValue, 'submit', true); if (onOk) { onOk(mergedValue); } } }, }); } let todayNode: React.ReactNode; if (showToday && mergedMode === 'date' && picker === 'date' && !showTime) { const now = generateConfig.getNow(); const todayCls = `${prefixCls}-today-btn`; const disabled = disabledDate && disabledDate(now); todayNode = ( { if (!disabled) { triggerSelect(now, 'mouse', true); } }} > {locale.today} ); } return (
{panelNode} {extraFooter || rangesNode || todayNode ? (
{extraFooter} {rangesNode} {todayNode}
) : null}
); } export default PickerPanel; /* eslint-enable */