/** * Removed: * - getCalendarContainer: use `getPopupContainer` instead * - onOk * * New Feature: * - picker * - allowEmpty * - selectable * * Tips: Should add faq about `datetime` mode with `defaultValue` */ import type { PickerPanelBaseProps, PickerPanelDateProps, PickerPanelTimeProps, } from './PickerPanel'; import PickerPanel from './PickerPanel'; import PickerTrigger from './PickerTrigger'; import { formatValue, isEqual, parseValue } from './utils/dateUtil'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import type { ContextOperationRefProps } from './PanelContext'; import { useProvidePanel } from './PanelContext'; import type { CustomFormat, PickerMode } from './interface'; import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; import usePickerInput from './hooks/usePickerInput'; import useTextValueMapping from './hooks/useTextValueMapping'; import useValueTexts from './hooks/useValueTexts'; import useHoverValue from './hooks/useHoverValue'; import type { CSSProperties, HTMLAttributes, Ref } from 'vue'; import { computed, defineComponent, ref, toRef, watch } from 'vue'; import type { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; import type { VueNode } from '../_util/type'; import type { AlignType } from '../vc-align/interface'; import useMergedState from '../_util/hooks/useMergedState'; import { warning } from '../vc-util/warning'; import classNames from '../_util/classNames'; import type { SharedTimeProps } from './panels/TimePanel'; import { useProviderTrigger } from '../vc-trigger/context'; import { legacyPropsWarning } from './utils/warnUtil'; export type PickerRefConfig = { focus: () => void; blur: () => void; }; export type PickerSharedProps = { dropdownClassName?: string; dropdownAlign?: AlignType; popupStyle?: CSSProperties; transitionName?: string; placeholder?: string; allowClear?: boolean; autofocus?: boolean; disabled?: boolean; tabindex?: number; open?: boolean; defaultOpen?: boolean; /** Make input readOnly to avoid popup keyboard in mobile */ inputReadOnly?: boolean; id?: string; // Value format?: string | CustomFormat | (string | CustomFormat)[]; // Render suffixIcon?: VueNode; clearIcon?: VueNode; prevIcon?: VueNode; nextIcon?: VueNode; superPrevIcon?: VueNode; superNextIcon?: VueNode; getPopupContainer?: (node: HTMLElement) => HTMLElement; panelRender?: (originPanel: VueNode) => VueNode; inputRender?: (props: HTMLAttributes) => VueNode; // Events onChange?: (value: DateType | null, dateString: string) => void; onOpenChange?: (open: boolean) => void; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; onMousedown?: MouseEventHandler; onMouseup?: MouseEventHandler; onMouseenter?: MouseEventHandler; onMouseleave?: MouseEventHandler; onClick?: MouseEventHandler; onContextmenu?: MouseEventHandler; onKeydown?: (event: KeyboardEvent, preventDefault: () => void) => void; // WAI-ARIA role?: string; name?: string; autocomplete?: string; direction?: 'ltr' | 'rtl'; showToday?: boolean; showTime?: boolean | SharedTimeProps; }; type OmitPanelProps = Omit< Props, 'onChange' | 'hideHeader' | 'pickerValue' | 'onPickerValueChange' >; export type PickerBaseProps = {} & PickerSharedProps & OmitPanelProps>; export type PickerDateProps = {} & PickerSharedProps & OmitPanelProps>; export type PickerTimeProps = { picker: 'time'; /** * @deprecated Please use `defaultValue` directly instead * since `defaultOpenValue` will confuse user of current value status */ defaultOpenValue?: DateType; } & PickerSharedProps & Omit>, 'format'>; export type PickerProps = | PickerBaseProps | PickerDateProps | PickerTimeProps; // TMP type to fit for ts 3.9.2 type OmitType = Omit, 'picker'> & Omit, 'picker'> & Omit, 'picker'>; type MergedPickerProps = { picker?: PickerMode; } & OmitType; function Picker() { return defineComponent>({ name: 'Picker', inheritAttrs: false, props: [ 'prefixCls', 'id', 'tabindex', 'dropdownClassName', 'dropdownAlign', 'popupStyle', 'transitionName', 'generateConfig', 'locale', 'inputReadOnly', 'allowClear', 'autofocus', 'showTime', 'showNow', 'showHour', 'showMinute', 'showSecond', 'picker', 'format', 'use12Hours', 'value', 'defaultValue', 'open', 'defaultOpen', 'defaultOpenValue', 'suffixIcon', 'clearIcon', 'disabled', 'disabledDate', 'placeholder', 'getPopupContainer', 'panelRender', 'inputRender', 'onChange', 'onOpenChange', 'onFocus', 'onBlur', 'onMousedown', 'onMouseup', 'onMouseenter', 'onMouseleave', 'onContextmenu', 'onClick', 'onKeydown', 'onSelect', 'direction', 'autocomplete', 'showToday', 'renderExtraFooter', 'dateRender', ] as any, // slots: [ // 'suffixIcon', // 'clearIcon', // 'prevIcon', // 'nextIcon', // 'superPrevIcon', // 'superNextIcon', // 'panelRender', // ], setup(props, { attrs, expose }) { const inputRef = ref(null); const picker = computed(() => props.picker ?? 'date'); const needConfirmButton = computed( () => (picker.value === 'date' && !!props.showTime) || picker.value === 'time', ); // ============================ Warning ============================ if (process.env.NODE_ENV !== 'production') { legacyPropsWarning(props); } // ============================= State ============================= const formatList = computed(() => toArray(getDefaultFormat(props.format, picker.value, props.showTime, props.use12Hours)), ); // Panel ref const panelDivRef = ref(null); const inputDivRef = ref(null); const containerRef = ref(null); // Real value const [mergedValue, setInnerValue] = useMergedState(null, { value: toRef(props, 'value'), defaultValue: props.defaultValue, }); const selectedValue = ref(mergedValue.value) as Ref; const setSelectedValue = (val: DateType) => { selectedValue.value = val; }; // Operation ref const operationRef = ref(null); // Open const [mergedOpen, triggerInnerOpen] = useMergedState(false, { value: toRef(props, 'open'), defaultValue: props.defaultOpen, postState: postOpen => (props.disabled ? false : postOpen), onChange: newOpen => { if (props.onOpenChange) { props.onOpenChange(newOpen); } if (!newOpen && operationRef.value && operationRef.value.onClose) { operationRef.value.onClose(); } }, }); // ============================= Text ============================== const [valueTexts, firstValueText] = useValueTexts(selectedValue, { formatList, generateConfig: toRef(props, 'generateConfig'), locale: toRef(props, 'locale'), }); const [text, triggerTextChange, resetText] = useTextValueMapping({ valueTexts, onTextChange: newText => { const inputDate = parseValue(newText, { locale: props.locale, formatList: formatList.value, generateConfig: props.generateConfig, }); if (inputDate && (!props.disabledDate || !props.disabledDate(inputDate))) { setSelectedValue(inputDate); } }, }); // ============================ Trigger ============================ const triggerChange = (newValue: DateType | null) => { const { onChange, generateConfig, locale } = props; setSelectedValue(newValue); setInnerValue(newValue); if (onChange && !isEqual(generateConfig, mergedValue.value, newValue)) { onChange( newValue, newValue ? formatValue(newValue, { generateConfig, locale, format: formatList.value[0] }) : '', ); } }; const triggerOpen = (newOpen: boolean) => { if (props.disabled && newOpen) { return; } triggerInnerOpen(newOpen); }; const forwardKeydown = (e: KeyboardEvent) => { if (mergedOpen.value && operationRef.value && operationRef.value.onKeydown) { // Let popup panel handle keyboard return operationRef.value.onKeydown(e); } /* istanbul ignore next */ /* eslint-disable no-lone-blocks */ { warning( false, 'Picker not correct forward Keydown operation. Please help to fire issue about this.', ); return false; } }; const onInternalMouseup: MouseEventHandler = (...args) => { if (props.onMouseup) { props.onMouseup(...args); } if (inputRef.value) { inputRef.value.focus(); triggerOpen(true); } }; // ============================= Input ============================= const [inputProps, { focused, typing }] = usePickerInput({ blurToCancel: needConfirmButton, open: mergedOpen, value: text, triggerOpen, forwardKeydown, isClickOutside: target => !elementsContains( [panelDivRef.value, inputDivRef.value, containerRef.value], target as HTMLElement, ), onSubmit: () => { if ( // When user typing disabledDate with keyboard and enter, this value will be empty !selectedValue.value || // Normal disabled check (props.disabledDate && props.disabledDate(selectedValue.value)) ) { return false; } triggerChange(selectedValue.value); triggerOpen(false); resetText(); return true; }, onCancel: () => { triggerOpen(false); setSelectedValue(mergedValue.value); resetText(); }, onKeydown: (e, preventDefault) => { props.onKeydown?.(e, preventDefault); }, onFocus: (e: FocusEvent) => { props.onFocus?.(e); }, onBlur: (e: FocusEvent) => { props.onBlur?.(e); }, }); // ============================= Sync ============================== // Close should sync back with text value watch([mergedOpen, valueTexts], () => { if (!mergedOpen.value) { setSelectedValue(mergedValue.value); if (!valueTexts.value.length || valueTexts.value[0] === '') { triggerTextChange(''); } else if (firstValueText.value !== text.value) { resetText(); } } }); // Change picker should sync back with text value watch(picker, () => { if (!mergedOpen.value) { resetText(); } }); // Sync innerValue with control mode watch(mergedValue, () => { // Sync select value setSelectedValue(mergedValue.value); }); const [hoverValue, onEnter, onLeave] = useHoverValue(text, { formatList, generateConfig: toRef(props, 'generateConfig'), locale: toRef(props, 'locale'), }); const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { if (type === 'submit' || (type !== 'key' && !needConfirmButton.value)) { // triggerChange will also update selected values triggerChange(date); triggerOpen(false); } }; useProvidePanel({ operationRef, hideHeader: computed(() => picker.value === 'time'), panelRef: panelDivRef, onSelect: onContextSelect, open: mergedOpen, defaultOpenValue: toRef(props, 'defaultOpenValue'), onDateMouseenter: onEnter, onDateMouseleave: onLeave, }); expose({ focus: () => { if (inputRef.value) { inputRef.value.focus(); } }, blur: () => { if (inputRef.value) { inputRef.value.blur(); } }, }); const getPortal = useProviderTrigger(); return () => { const { prefixCls = 'rc-picker', id, tabindex, dropdownClassName, dropdownAlign, popupStyle, transitionName, generateConfig, locale, inputReadOnly, allowClear, autofocus, picker = 'date', defaultOpenValue, suffixIcon, clearIcon, disabled, placeholder, getPopupContainer, panelRender, onMousedown, onMouseenter, onMouseleave, onContextmenu, onClick, onSelect, direction, autocomplete = 'off', } = props; // ============================= Panel ============================= const panelProps = { // Remove `picker` & `format` here since TimePicker is little different with other panel ...(props as Omit, 'picker' | 'format'>), ...attrs, class: classNames({ [`${prefixCls}-panel-focused`]: !typing.value, }), style: undefined, pickerValue: undefined, onPickerValueChange: undefined, onChange: null, }; let panelNode: VueNode = ( { onSelect?.(date); setSelectedValue(date); }} direction={direction} onPanelChange={(viewDate, mode) => { const { onPanelChange } = props; onLeave(true); onPanelChange?.(viewDate, mode); }} /> ); if (panelRender) { panelNode = panelRender(panelNode); } const panel = (
{ e.preventDefault(); }} > {panelNode}
); let suffixNode: VueNode; if (suffixIcon) { suffixNode = {suffixIcon}; } let clearNode: VueNode; if (allowClear && mergedValue.value && !disabled) { clearNode = ( { e.preventDefault(); e.stopPropagation(); }} onMouseup={e => { e.preventDefault(); e.stopPropagation(); triggerChange(null); triggerOpen(false); }} class={`${prefixCls}-clear`} role="button" > {clearIcon || } ); } const mergedInputProps: HTMLAttributes = { id, tabindex, disabled, readonly: inputReadOnly || typeof formatList.value[0] === 'function' || !typing.value, value: hoverValue.value || text.value, onInput: (e: ChangeEvent) => { triggerTextChange(e.target.value); }, autofocus, placeholder, ref: inputRef, title: text.value, ...inputProps.value, size: getInputSize(picker, formatList.value[0], generateConfig), ...getDataOrAriaProps(props), autocomplete, }; const inputNode = props.inputRender ? ( props.inputRender(mergedInputProps) ) : ( ); // ============================ Warning ============================ if (process.env.NODE_ENV !== 'production') { warning( !defaultOpenValue, '`defaultOpenValue` may confuse user for the current value status. Please use `defaultValue` instead.', ); } // ============================ Return ============================= const popupPlacement = direction === 'rtl' ? 'bottomRight' : 'bottomLeft'; return ( panel, }} >
{inputNode} {suffixNode} {clearNode}
{getPortal()}
); }; }, }); } export default Picker();