ant-design-vue/components/vc-picker/PickerPanel.tsx

570 lines
15 KiB
Vue

/**
* 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<DateType> = {
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<DateType>;
// 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<DateType>;
monthCellRender?: MonthCellRender<DateType>;
renderExtraFooter?: (mode: PanelMode) => React.ReactNode;
// Event
onSelect?: (value: DateType) => void;
onChange?: (value: DateType) => void;
onPanelChange?: OnPanelChange<DateType>;
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
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<DateType> = {
picker: Exclude<PickerMode, 'date' | 'time'>;
} & PickerPanelSharedProps<DateType>;
export type PickerPanelDateProps<DateType> = {
picker?: 'date';
showToday?: boolean;
showNow?: boolean;
// Time
showTime?: boolean | SharedTimeProps<DateType>;
disabledTime?: DisabledTime<DateType>;
} & PickerPanelSharedProps<DateType>;
export type PickerPanelTimeProps<DateType> = {
picker: 'time';
} & PickerPanelSharedProps<DateType> & SharedTimeProps<DateType>;
export type PickerPanelProps<DateType> =
| PickerPanelBaseProps<DateType>
| PickerPanelDateProps<DateType>
| PickerPanelTimeProps<DateType>;
// TMP type to fit for ts 3.9.2
type OmitType<DateType> = Omit<PickerPanelBaseProps<DateType>, 'picker'> &
Omit<PickerPanelDateProps<DateType>, 'picker'> &
Omit<PickerPanelTimeProps<DateType>, 'picker'>;
type MergedPickerPanelProps<DateType> = {
picker?: PickerMode;
} & OmitType<DateType>;
function PickerPanel<DateType>(props: PickerPanelProps<DateType>) {
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<DateType>;
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<PanelRefProps>({});
// 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<DateType | null, DateType>(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<PanelMode>(() => 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<HTMLElement>) => {
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<HTMLElement> = (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<DateType>),
operationRef: panelRef,
prefixCls,
viewDate,
value: mergedValue,
onViewDateChange: setViewDate,
sourceMode,
onPanelChange: onInternalPanelChange,
disabledDate,
};
delete pickerProps.onChange;
delete pickerProps.onSelect;
switch (mergedMode) {
case 'decade':
panelNode = (
<DecadePanel<DateType>
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
case 'year':
panelNode = (
<YearPanel<DateType>
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
case 'month':
panelNode = (
<MonthPanel<DateType>
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
case 'quarter':
panelNode = (
<QuarterPanel<DateType>
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
case 'week':
panelNode = (
<WeekPanel
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
case 'time':
delete pickerProps.showTime;
panelNode = (
<TimePanel<DateType>
{...pickerProps}
{...(typeof showTime === 'object' ? showTime : null)}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
break;
default:
if (showTime) {
panelNode = (
<DatetimePanel
{...pickerProps}
onSelect={(date, type) => {
setViewDate(date);
triggerSelect(date, type);
}}
/>
);
} else {
panelNode = (
<DatePanel<DateType>
{...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 = (
<a
class={classNames(todayCls, disabled && `${todayCls}-disabled`)}
aria-disabled={disabled}
onClick={() => {
if (!disabled) {
triggerSelect(now, 'mouse', true);
}
}}
>
{locale.today}
</a>
);
}
return (
<PanelContext.Provider
value={{
...panelContext,
mode: mergedMode,
hideHeader: 'hideHeader' in props ? hideHeader : panelContext.hideHeader,
hidePrevBtn: inRange && panelPosition === 'right',
hideNextBtn: inRange && panelPosition === 'left',
}}
>
<div
tabindex={tabIndex}
class={classNames(`${prefixCls}-panel`, className, {
[`${prefixCls}-panel-has-range`]: rangedValue && rangedValue[0] && rangedValue[1],
[`${prefixCls}-panel-has-range-hover`]:
hoverRangedValue && hoverRangedValue[0] && hoverRangedValue[1],
[`${prefixCls}-panel-rtl`]: direction === 'rtl',
})}
style={style}
onKeydown={onInternalKeyDown}
onBlur={onInternalBlur}
onMousedown={onMouseDown}
ref={panelDivRef}
>
{panelNode}
{extraFooter || rangesNode || todayNode ? (
<div class={`${prefixCls}-footer`}>
{extraFooter}
{rangesNode}
{todayNode}
</div>
) : null}
</div>
</PanelContext.Provider>
);
}
export default PickerPanel;
/* eslint-enable */