From 53f3c6e1a5aaf67dce7a1d6a84f223da8efe10c2 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 20 Jul 2021 17:32:49 +0800 Subject: [PATCH] refactor: date --- components/_util/EventInterface.ts | 5 + components/vc-picker/Picker.tsx | 270 ++- components/vc-picker/PickerPanel.tsx | 18 +- components/vc-picker/PickerTrigger.tsx | 1 - components/vc-picker/RangeContext.tsx | 55 +- components/vc-picker/RangePicker.tsx | 2024 +++++++++-------- components/vc-picker/hooks/useHoverValue.ts | 31 +- .../vc-picker/hooks/useRangeDisabled.ts | 157 +- .../vc-picker/hooks/useRangeViewDates.ts | 69 +- components/vc-picker/hooks/useValueTexts.ts | 8 +- 10 files changed, 1506 insertions(+), 1132 deletions(-) diff --git a/components/_util/EventInterface.ts b/components/_util/EventInterface.ts index f1bb28081..361eea747 100644 --- a/components/_util/EventInterface.ts +++ b/components/_util/EventInterface.ts @@ -1,3 +1,8 @@ export type FocusEventHandler = (e: FocusEvent) => void; export type MouseEventHandler = (e: MouseEvent) => void; export type KeyboardEventHandler = (e: KeyboardEvent) => void; +export type ChangeEvent = Event & { + target: { + value?: string | undefined; + }; +}; diff --git a/components/vc-picker/Picker.tsx b/components/vc-picker/Picker.tsx index bb78bb712..fcb00cbf2 100644 --- a/components/vc-picker/Picker.tsx +++ b/components/vc-picker/Picker.tsx @@ -20,8 +20,7 @@ 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 PanelContext from './PanelContext'; +import { ContextOperationRefProps, useProvidePanel } from './PanelContext'; import type { CustomFormat, PickerMode } from './interface'; import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; import usePickerInput from './hooks/usePickerInput'; @@ -30,20 +29,21 @@ import useValueTexts from './hooks/useValueTexts'; import useHoverValue from './hooks/useHoverValue'; import { computed, + createVNode, CSSProperties, defineComponent, HtmlHTMLAttributes, ref, Ref, toRef, - toRefs, + watch, } from 'vue'; -import { FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; +import { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; import { VueNode } from '../_util/type'; import { AlignType } from '../vc-align/interface'; import useMergedState from '../_util/hooks/useMergedState'; -import { locale } from 'dayjs'; import { warning } from '../vc-util/warning'; +import classNames from '../_util/classNames'; export type PickerRefConfig = { focus: () => void; @@ -92,10 +92,6 @@ export type PickerSharedProps = { onContextMenu?: MouseEventHandler; onKeyDown?: (event: KeyboardEvent, preventDefault: () => void) => void; - // Internal - /** @private Internal usage, do not use in production mode!!! */ - pickerRef?: Ref; - // WAI-ARIA role?: string; name?: string; @@ -169,7 +165,6 @@ function Picker() { 'disabledDate', 'placeholder', 'getPopupContainer', - 'pickerRef', 'panelRender', 'onChange', 'onOpenChange', @@ -196,7 +191,7 @@ function Picker() { 'superNextIcon', 'panelRender', ], - setup(props, { slots, attrs, expose }) { + setup(props, { attrs, expose }) { const inputRef = ref(null); const needConfirmButton = computed( () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', @@ -242,13 +237,11 @@ function Picker() { }); // ============================= Text ============================== - const texts = useValueTexts(selectedValue, { + const [valueTexts, firstValueText] = useValueTexts(selectedValue, { formatList, generateConfig: toRef(props, 'generateConfig'), locale: toRef(props, 'locale'), }); - const valueTexts = computed(() => texts.value[0]); - const firstValueText = computed(() => texts.value[1]); const [text, triggerTextChange, resetText] = useTextValueMapping({ valueTexts, @@ -351,11 +344,256 @@ function Picker() { }, }); + // ============================= 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( + () => props.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(() => props.picker === '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(); + } + }, + }); + return () => { - return null; + 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'>), + 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 || } + + ); + } + + // ============================ 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 ( + +
+
+ { + triggerTextChange(e.target.value); + }} + autofocus={autofocus} + placeholder={placeholder} + ref={inputRef} + title={text.value} + {...inputProps.value} + size={getInputSize(picker, formatList.value[0], generateConfig)} + {...getDataOrAriaProps(props)} + autocomplete={autocomplete} + /> + {suffixNode} + {clearNode} +
+
+
+ ); }; }, }); } -export default Picker(); +const InterPicker = Picker(); +export default (props: MergedPickerProps, { slots }): JSX.Element => + createVNode(InterPicker, props, slots); diff --git a/components/vc-picker/PickerPanel.tsx b/components/vc-picker/PickerPanel.tsx index 2ba28d7e7..cb415341b 100644 --- a/components/vc-picker/PickerPanel.tsx +++ b/components/vc-picker/PickerPanel.tsx @@ -33,7 +33,16 @@ import getExtraFooter from './utils/getExtraFooter'; import getRanges from './utils/getRanges'; import { getLowerBoundTime, setDateTime, setTime } from './utils/timeUtil'; import { VueNode } from '../_util/type'; -import { computed, defineComponent, ref, toRef, watch, watchEffect } from 'vue'; +import { + computed, + createVNode, + defineComponent, + HTMLAttributes, + ref, + toRef, + watch, + watchEffect, +} from 'vue'; import useMergedState from '../_util/hooks/useMergedState'; import { warning } from '../vc-util/warning'; import KeyCode from '../_util/KeyCode'; @@ -83,7 +92,7 @@ export type PickerPanelSharedProps = { /** @private Internal usage. Do not use in your production env */ components?: Components; -}; +} & HTMLAttributes; export type PickerPanelBaseProps = { picker: Exclude; @@ -598,5 +607,6 @@ function PickerPanel() { }, }); } - -export default PickerPanel(); +const InterPickerPanel = PickerPanel(); +export default (props: MergedPickerPanelProps): JSX.Element => + createVNode(InterPickerPanel, props); diff --git a/components/vc-picker/PickerTrigger.tsx b/components/vc-picker/PickerTrigger.tsx index a1f5f28a8..600f7346f 100644 --- a/components/vc-picker/PickerTrigger.tsx +++ b/components/vc-picker/PickerTrigger.tsx @@ -47,7 +47,6 @@ export type PickerTriggerProps = { visible: boolean; popupElement: VueNode; popupStyle?: CSSProperties; - children: VueNode; dropdownClassName?: string; transitionName?: string; getPopupContainer?: (node: HTMLElement) => HTMLElement; diff --git a/components/vc-picker/RangeContext.tsx b/components/vc-picker/RangeContext.tsx index b9a4ac8e7..2a10a9310 100644 --- a/components/vc-picker/RangeContext.tsx +++ b/components/vc-picker/RangeContext.tsx @@ -1,4 +1,14 @@ -import { inject, InjectionKey, provide, Ref } from 'vue'; +import { + defineComponent, + inject, + InjectionKey, + PropType, + provide, + ref, + Ref, + toRef, + watch, +} from 'vue'; import type { NullableDateType, RangeValue } from './interface'; export type RangeContextProps = { @@ -12,6 +22,17 @@ export type RangeContextProps = { panelPosition?: Ref<'left' | 'right' | false>; }; +type RangeContextProviderValue = { + /** + * Set displayed range value style. + * Panel only has one value, this is only style effect. + */ + rangedValue?: [NullableDateType, NullableDateType] | null; + hoverRangedValue?: RangeValue; + inRange?: boolean; + panelPosition?: 'left' | 'right' | false; +}; + const RangeContextKey: InjectionKey = Symbol('RangeContextProps'); export const useProvideRange = (props: RangeContextProps) => { @@ -22,4 +43,36 @@ export const useInjectRange = () => { return inject(RangeContextKey); }; +export const RangeContextProvider = defineComponent({ + name: 'PanelContextProvider', + inheritAttrs: false, + props: { + value: { + type: Object as PropType, + default: () => ({} as RangeContextProviderValue), + }, + }, + setup(props, { slots }) { + const value: RangeContextProps = { + rangedValue: ref(props.value.rangedValue), + hoverRangedValue: ref(props.value.hoverRangedValue), + inRange: ref(props.value.inRange), + panelPosition: ref(props.value.panelPosition), + }; + useProvideRange(value); + toRef; + watch( + () => props.value, + () => { + Object.keys(props.value).forEach(key => { + if (value[key]) { + value[key].value = props.value[key]; + } + }); + }, + ); + return () => slots.default?.(); + }, +}); + export default RangeContextKey; diff --git a/components/vc-picker/RangePicker.tsx b/components/vc-picker/RangePicker.tsx index c4ed77129..20e2501a3 100644 --- a/components/vc-picker/RangePicker.tsx +++ b/components/vc-picker/RangePicker.tsx @@ -1,18 +1,12 @@ -import * as React from 'react'; -import { useRef, useEffect, useState } from 'react'; -import classNames from 'classnames'; -import warning from 'rc-util/lib/warning'; -import useMergedState from 'rc-util/lib/hooks/useMergedState'; import type { DisabledTimes, PanelMode, PickerMode, RangeValue, EventValue } from './interface'; -import type { PickerBaseProps, PickerDateProps, PickerTimeProps, PickerRefConfig } from './Picker'; +import type { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; import type { SharedTimeProps } from './panels/TimePanel'; import PickerTrigger from './PickerTrigger'; import PickerPanel from './PickerPanel'; import usePickerInput from './hooks/usePickerInput'; import getDataOrAriaProps, { toArray, getValue, updateValues } from './utils/miscUtil'; import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; -import type { ContextOperationRefProps } from './PanelContext'; -import PanelContext from './PanelContext'; +import { ContextOperationRefProps, useProvidePanel } from './PanelContext'; import { isEqual, getClosingViewDate, @@ -26,13 +20,29 @@ import useValueTexts from './hooks/useValueTexts'; import useTextValueMapping from './hooks/useTextValueMapping'; import type { GenerateConfig } from './generate'; import type { PickerPanelProps } from '.'; -import RangeContext from './RangeContext'; +import { RangeContextProvider } from './RangeContext'; import useRangeDisabled from './hooks/useRangeDisabled'; import getExtraFooter from './utils/getExtraFooter'; import getRanges from './utils/getRanges'; import useRangeViewDates from './hooks/useRangeViewDates'; import type { DateRender } from './panels/DatePanel/DateBody'; import useHoverValue from './hooks/useHoverValue'; +import { VueNode } from '../_util/type'; +import { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; +import { + computed, + createVNode, + defineComponent, + HTMLAttributes, + ref, + toRef, + watch, + watchEffect, +} from 'vue'; +import useMergedState from '../_util/hooks/useMergedState'; +import { warning } from '../vc-util/warning'; +import useState from '../_util/hooks/useState'; +import classNames from '../_util/classNames'; function reorderValues( values: RangeValue, @@ -76,7 +86,7 @@ export type RangeDateRender = ( currentDate: DateType, today: DateType, info: RangeInfo, -) => React.ReactNode; +) => VueNode; export type RangePickerSharedProps = { id?: string; @@ -90,7 +100,7 @@ export type RangePickerSharedProps = { string, Exclude, null> | (() => Exclude, null>) >; - separator?: React.ReactNode; + separator?: VueNode; allowEmpty?: [boolean, boolean]; mode?: [PanelMode, PanelMode]; onChange?: (values: RangeValue, formatString: [string, string]) => void; @@ -100,18 +110,18 @@ export type RangePickerSharedProps = { info: RangeInfo, ) => void; onPanelChange?: (values: RangeValue, modes: [PanelMode, PanelMode]) => void; - onFocus?: React.FocusEventHandler; - onBlur?: React.FocusEventHandler; - onMouseEnter?: React.MouseEventHandler; - onMouseLeave?: React.MouseEventHandler; + onFocus?: FocusEventHandler; + onBlur?: FocusEventHandler; + onMouseEnter?: MouseEventHandler; + onMouseLeave?: MouseEventHandler; onOk?: (dates: RangeValue) => void; direction?: 'ltr' | 'rtl'; - autoComplete?: string; + autocomplete?: string; /** @private Internal control of active picker. Do not use since it's private usage */ activePickerIndex?: 0 | 1; dateRender?: RangeDateRender; - panelRender?: (originPanel: React.ReactNode) => React.ReactNode; -}; + panelRender?: (originPanel: VueNode) => VueNode; +} & HTMLAttributes; type OmitPickerProps = Omit< Props, @@ -137,15 +147,18 @@ type RangeShowTimeObject = Omit, 'defaultVal defaultValue?: DateType[]; }; -export type RangePickerBaseProps = {} & RangePickerSharedProps & OmitPickerProps>; +export type RangePickerBaseProps = {} & RangePickerSharedProps & + OmitPickerProps>; export type RangePickerDateProps = { showTime?: boolean | RangeShowTimeObject; -} & RangePickerSharedProps & OmitPickerProps>; +} & RangePickerSharedProps & + OmitPickerProps>; export type RangePickerTimeProps = { order?: boolean; -} & RangePickerSharedProps & OmitPickerProps>; +} & RangePickerSharedProps & + OmitPickerProps>; export type RangePickerProps = | RangePickerBaseProps @@ -160,1022 +173,1061 @@ type OmitType = Omit, 'picker'> & type MergedRangePickerProps = { picker?: PickerMode; } & OmitType; +function RangerPicker() { + return defineComponent>({ + props: [ + 'prefixCls', + 'id', + 'popupStyle', + 'dropdownClassName', + 'transitionName', + 'dropdownAlign', + 'getPopupContainer', + 'generateConfig', + 'locale', + 'placeholder', + 'autofocus', + 'disabled', + 'format', + 'picker', + 'showTime', + 'use12Hours', + 'separator', + 'value', + 'defaultValue', + 'defaultPickerValue', + 'open', + 'defaultOpen', + 'disabledDate', + 'disabledTime', + 'dateRender', + 'panelRender', + 'ranges', + 'allowEmpty', + 'allowClear', + 'suffixIcon', + 'clearIcon', + 'pickerRef', + 'inputReadOnly', + 'mode', + 'renderExtraFooter', + 'onChange', + 'onOpenChange', + 'onPanelChange', + 'onCalendarChange', + 'onFocus', + 'onBlur', + 'onMouseEnter', + 'onMouseLeave', + 'onOk', + 'onKeyDown', + 'components', + 'order', + 'direction', + 'activePickerIndex', + 'autocomplete', + ] as any, + setup(props, { attrs, expose }) { + const needConfirmButton = computed( + () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', + ); -function InnerRangePicker(props: RangePickerProps) { - const { - prefixCls = 'rc-picker', - id, - style, - className, - popupStyle, - dropdownClassName, - transitionName, - dropdownAlign, - getPopupContainer, - generateConfig, - locale, - placeholder, - autoFocus, - disabled, - format, - picker = 'date', - showTime, - use12Hours, - separator = '~', - value, - defaultValue, - defaultPickerValue, - open, - defaultOpen, - disabledDate, - disabledTime, - dateRender, - panelRender, - ranges, - allowEmpty, - allowClear, - suffixIcon, - clearIcon, - pickerRef, - inputReadOnly, - mode, - renderExtraFooter, - onChange, - onOpenChange, - onPanelChange, - onCalendarChange, - onFocus, - onBlur, - onMouseEnter, - onMouseLeave, - onOk, - onKeyDown, - components, - order, - direction, - activePickerIndex, - autoComplete = 'off', - } = props as MergedRangePickerProps; + // We record opened status here in case repeat open with picker + const openRecordsRef = ref>({}); - const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; + const containerRef = ref(null); + const panelDivRef = ref(null); + const startInputDivRef = ref(null); + const endInputDivRef = ref(null); + const separatorRef = ref(null); + const startInputRef = ref(null); + const endInputRef = ref(null); - // We record opened status here in case repeat open with picker - const openRecordsRef = useRef>({}); + // ============================= Misc ============================== + const formatList = computed(() => + toArray( + getDefaultFormat(props.format, props.picker, props.showTime, props.use12Hours), + ), + ); - const containerRef = useRef(null); - const panelDivRef = useRef(null); - const startInputDivRef = useRef(null); - const endInputDivRef = useRef(null); - const separatorRef = useRef(null); - const startInputRef = useRef(null); - const endInputRef = useRef(null); - - // ============================= Misc ============================== - const formatList = toArray(getDefaultFormat(format, picker, showTime, use12Hours)); - - // Active picker - const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState<0 | 1>(0, { - value: activePickerIndex, - }); - - // Operation ref - const operationRef: React.MutableRefObject = useRef< - ContextOperationRefProps - >(null); - - const mergedDisabled = React.useMemo<[boolean, boolean]>(() => { - if (Array.isArray(disabled)) { - return disabled; - } - - return [disabled || false, disabled || false]; - }, [disabled]); - - // ============================= Value ============================= - const [mergedValue, setInnerValue] = useMergedState>(null, { - value, - defaultValue, - postState: values => - picker === 'time' && !order ? values : reorderValues(values, generateConfig), - }); - - // =========================== View Date =========================== - // Config view panel - const [getViewDate, setViewDate] = useRangeViewDates({ - values: mergedValue, - picker, - defaultDates: defaultPickerValue, - generateConfig, - }); - - // ========================= Select Values ========================= - const [selectedValue, setSelectedValue] = useMergedState(mergedValue, { - postState: values => { - let postValues = values; - - if (mergedDisabled[0] && mergedDisabled[1]) { - return postValues; - } - - // Fill disabled unit - for (let i = 0; i < 2; i += 1) { - if (mergedDisabled[i] && !getValue(postValues, i) && !getValue(allowEmpty, i)) { - postValues = updateValues(postValues, generateConfig.getNow(), i); - } - } - return postValues; - }, - }); - - // ============================= Modes ============================= - const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>([picker, picker], { - value: mode, - }); - - useEffect(() => { - setInnerModes([picker, picker]); - }, [picker]); - - const triggerModesChange = (modes: [PanelMode, PanelMode], values: RangeValue) => { - setInnerModes(modes); - - if (onPanelChange) { - onPanelChange(values, modes); - } - }; - - // ========================= Disable Date ========================== - const [disabledStartDate, disabledEndDate] = useRangeDisabled( - { - picker, - selectedValue, - locale, - disabled: mergedDisabled, - disabledDate, - generateConfig, - }, - openRecordsRef.current[1], - openRecordsRef.current[0], - ); - - // ============================= Open ============================== - const [mergedOpen, triggerInnerOpen] = useMergedState(false, { - value: open, - defaultValue: defaultOpen, - postState: postOpen => (mergedDisabled[mergedActivePickerIndex] ? false : postOpen), - onChange: newOpen => { - if (onOpenChange) { - onOpenChange(newOpen); - } - - if (!newOpen && operationRef.current && operationRef.current.onClose) { - operationRef.current.onClose(); - } - }, - }); - - const startOpen = mergedOpen && mergedActivePickerIndex === 0; - const endOpen = mergedOpen && mergedActivePickerIndex === 1; - - // ============================= Popup ============================= - // Popup min width - const [popupMinWidth, setPopupMinWidth] = useState(0); - useEffect(() => { - if (!mergedOpen && containerRef.current) { - setPopupMinWidth(containerRef.current.offsetWidth); - } - }, [mergedOpen]); - - // ============================ Trigger ============================ - const triggerRef = React.useRef(); - - function triggerOpen(newOpen: boolean, index: 0 | 1) { - if (newOpen) { - clearTimeout(triggerRef.current); - openRecordsRef.current[index] = true; - - setMergedActivePickerIndex(index); - triggerInnerOpen(newOpen); - - // Open to reset view date - if (!mergedOpen) { - setViewDate(null, index); - } - } else if (mergedActivePickerIndex === index) { - triggerInnerOpen(newOpen); - - // Clean up async - // This makes ref not quick refresh in case user open another input with blur trigger - const openRecords = openRecordsRef.current; - triggerRef.current = setTimeout(() => { - if (openRecords === openRecordsRef.current) { - openRecordsRef.current = {}; - } + // Active picker + const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState<0 | 1>(0, { + value: toRef(props, 'activePickerIndex'), }); - } - } - function triggerOpenAndFocus(index: 0 | 1) { - triggerOpen(true, index); - // Use setTimeout to make sure panel DOM exists - setTimeout(() => { - const inputRef = [startInputRef, endInputRef][index]; - if (inputRef.current) { - inputRef.current.focus(); - } - }, 0); - } + // Operation ref + const operationRef = ref(null); - function triggerChange(newValue: RangeValue, sourceIndex: 0 | 1) { - let values = newValue; - let startValue = getValue(values, 0); - let endValue = getValue(values, 1); - - // >>>>> Format start & end values - if (startValue && endValue && generateConfig.isAfter(startValue, endValue)) { - if ( - // WeekPicker only compare week - (picker === 'week' && !isSameWeek(generateConfig, locale.locale, startValue, endValue)) || - // QuotaPicker only compare week - (picker === 'quarter' && !isSameQuarter(generateConfig, startValue, endValue)) || - // Other non-TimePicker compare date - (picker !== 'week' && - picker !== 'quarter' && - picker !== 'time' && - !isSameDate(generateConfig, startValue, endValue)) - ) { - // Clean up end date when start date is after end date - if (sourceIndex === 0) { - values = [startValue, null]; - endValue = null; - } else { - startValue = null; - values = [null, endValue]; + const mergedDisabled = computed<[boolean, boolean]>(() => { + const { disabled } = props; + if (Array.isArray(disabled)) { + return disabled; } - // Clean up cache since invalidate - openRecordsRef.current = { - [sourceIndex]: true, - }; - } else if (picker !== 'time' || order !== false) { - // Reorder when in same date - values = reorderValues(values, generateConfig); - } - } + return [disabled || false, disabled || false]; + }); - setSelectedValue(values); + // ============================= Value ============================= + const [mergedValue, setInnerValue] = useMergedState>(null, { + value: toRef(props, 'value'), + defaultValue: props.defaultValue, + postState: values => + props.picker === 'time' && !props.order + ? values + : reorderValues(values, props.generateConfig), + }); - const startStr = - values && values[0] - ? formatValue(values[0], { generateConfig, locale, format: formatList[0] }) - : ''; - const endStr = - values && values[1] - ? formatValue(values[1], { generateConfig, locale, format: formatList[0] }) - : ''; + // =========================== View Date =========================== + // Config view panel + const [startViewDate, endViewDate, setViewDate] = useRangeViewDates({ + values: mergedValue, + picker: toRef(props, 'picker'), + defaultDates: props.defaultPickerValue, + generateConfig: toRef(props, 'generateConfig'), + }); - if (onCalendarChange) { - const info: RangeInfo = { range: sourceIndex === 0 ? 'start' : 'end' }; + // ========================= Select Values ========================= + const [selectedValue, setSelectedValue] = useMergedState(mergedValue.value, { + postState: values => { + let postValues = values; - onCalendarChange(values, [startStr, endStr], info); - } + if (mergedDisabled[0] && mergedDisabled[1]) { + return postValues; + } - // >>>>> Trigger `onChange` event - const canStartValueTrigger = canValueTrigger(startValue, 0, mergedDisabled, allowEmpty); - const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled, allowEmpty); + // Fill disabled unit + for (let i = 0; i < 2; i += 1) { + if (mergedDisabled[i] && !getValue(postValues, i) && !getValue(props.allowEmpty, i)) { + postValues = updateValues(postValues, props.generateConfig.getNow(), i); + } + } + return postValues; + }, + }); - const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); - - if (canTrigger) { - // Trigger onChange only when value is validate - setInnerValue(values); - - if ( - onChange && - (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) || - !isEqual(generateConfig, getValue(mergedValue, 1), endValue)) - ) { - onChange(values, [startStr, endStr]); - } - } - - // >>>>> Open picker when - - // Always open another picker if possible - let nextOpenIndex: 0 | 1 = null; - if (sourceIndex === 0 && !mergedDisabled[1]) { - nextOpenIndex = 1; - } else if (sourceIndex === 1 && !mergedDisabled[0]) { - nextOpenIndex = 0; - } - - if ( - nextOpenIndex !== null && - nextOpenIndex !== mergedActivePickerIndex && - (!openRecordsRef.current[nextOpenIndex] || !getValue(values, nextOpenIndex)) && - getValue(values, sourceIndex) - ) { - // Delay to focus to avoid input blur trigger expired selectedValues - triggerOpenAndFocus(nextOpenIndex); - } else { - triggerOpen(false, sourceIndex); - } - } - - const forwardKeyDown = (e: React.KeyboardEvent) => { - if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { - // Let popup panel handle keyboard - return operationRef.current.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.', + // ============================= Modes ============================= + const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>( + [props.picker, props.picker], + { + value: toRef(props, 'mode'), + }, ); - return false; - } - }; - // ============================= Text ============================== - const sharedTextHooksProps = { - formatList, - generateConfig, - locale, - }; - - const [startValueTexts, firstStartValueText] = useValueTexts( - getValue(selectedValue, 0), - sharedTextHooksProps, - ); - - const [endValueTexts, firstEndValueText] = useValueTexts( - getValue(selectedValue, 1), - sharedTextHooksProps, - ); - - const onTextChange = (newText: string, index: 0 | 1) => { - const inputDate = parseValue(newText, { - locale, - formatList, - generateConfig, - }); - - const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; - - if (inputDate && !disabledFunc(inputDate)) { - setSelectedValue(updateValues(selectedValue, inputDate, index)); - setViewDate(inputDate, index); - } - }; - - const [startText, triggerStartTextChange, resetStartText] = useTextValueMapping({ - valueTexts: startValueTexts, - onTextChange: newText => onTextChange(newText, 0), - }); - - const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping({ - valueTexts: endValueTexts, - onTextChange: newText => onTextChange(newText, 1), - }); - - const [rangeHoverValue, setRangeHoverValue] = useState>(null); - - // ========================== Hover Range ========================== - const [hoverRangedValue, setHoverRangedValue] = useState>(null); - - const [startHoverValue, onStartEnter, onStartLeave] = useHoverValue(startText, { - formatList, - generateConfig, - locale, - }); - - const [endHoverValue, onEndEnter, onEndLeave] = useHoverValue(endText, { - formatList, - generateConfig, - locale, - }); - - const onDateMouseEnter = (date: DateType) => { - setHoverRangedValue(updateValues(selectedValue, date, mergedActivePickerIndex)); - if (mergedActivePickerIndex === 0) { - onStartEnter(date); - } else { - onEndEnter(date); - } - }; - - const onDateMouseLeave = () => { - setHoverRangedValue(updateValues(selectedValue, null, mergedActivePickerIndex)); - if (mergedActivePickerIndex === 0) { - onStartLeave(); - } else { - onEndLeave(); - } - }; - - // ============================= Input ============================= - const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({ - blurToCancel: needConfirmButton, - forwardKeyDown, - onBlur, - isClickOutside: (target: EventTarget | null) => - !elementsContains( - [panelDivRef.current, startInputDivRef.current, endInputDivRef.current], - target as HTMLElement, - ), - onFocus: (e: React.FocusEvent) => { - setMergedActivePickerIndex(index); - if (onFocus) { - onFocus(e); - } - }, - triggerOpen: (newOpen: boolean) => { - triggerOpen(newOpen, index); - }, - onSubmit: () => { - triggerChange(selectedValue, index); - resetText(); - }, - onCancel: () => { - triggerOpen(false, index); - setSelectedValue(mergedValue); - resetText(); - }, - }); - - const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ - ...getSharedInputHookProps(0, resetStartText), - open: startOpen, - value: startText, - onKeyDown: (e, preventDefault) => { - onKeyDown?.(e, preventDefault); - }, - }); - - const [endInputProps, { focused: endFocused, typing: endTyping }] = usePickerInput({ - ...getSharedInputHookProps(1, resetEndText), - open: endOpen, - value: endText, - onKeyDown: (e, preventDefault) => { - onKeyDown?.(e, preventDefault); - }, - }); - - // ========================== Click Picker ========================== - const onPickerClick = (e: MouseEvent) => { - // When click inside the picker & outside the picker's input elements - // the panel should still be opened - if ( - !mergedOpen && - !startInputRef.current.contains(e.target as Node) && - !endInputRef.current.contains(e.target as Node) - ) { - if (!mergedDisabled[0]) { - triggerOpenAndFocus(0); - } else if (!mergedDisabled[1]) { - triggerOpenAndFocus(1); - } - } - }; - - const onPickerMouseDown = (e: MouseEvent) => { - // shouldn't affect input elements if picker is active - if ( - mergedOpen && - (startFocused || endFocused) && - !startInputRef.current.contains(e.target as Node) && - !endInputRef.current.contains(e.target as Node) - ) { - e.preventDefault(); - } - }; - - // ============================= Sync ============================== - // Close should sync back with text value - const startStr = - mergedValue && mergedValue[0] - ? formatValue(mergedValue[0], { - locale, - format: 'YYYYMMDDHHmmss', - generateConfig, - }) - : ''; - const endStr = - mergedValue && mergedValue[1] - ? formatValue(mergedValue[1], { - locale, - format: 'YYYYMMDDHHmmss', - generateConfig, - }) - : ''; - - useEffect(() => { - if (!mergedOpen) { - setSelectedValue(mergedValue); - - if (!startValueTexts.length || startValueTexts[0] === '') { - triggerStartTextChange(''); - } else if (firstStartValueText !== startText) { - resetStartText(); - } - if (!endValueTexts.length || endValueTexts[0] === '') { - triggerEndTextChange(''); - } else if (firstEndValueText !== endText) { - resetEndText(); - } - } - }, [mergedOpen, startValueTexts, endValueTexts]); - - // Sync innerValue with control mode - useEffect(() => { - setSelectedValue(mergedValue); - }, [startStr, endStr]); - - // ============================ Warning ============================ - if (process.env.NODE_ENV !== 'production') { - if ( - value && - Array.isArray(disabled) && - ((getValue(disabled, 0) && !getValue(value, 0)) || - (getValue(disabled, 1) && !getValue(value, 1))) - ) { - warning( - false, - '`disabled` should not set with empty `value`. You should set `allowEmpty` or `value` instead.', + watch( + () => props.picker, + () => { + setInnerModes([props.picker, props.picker]); + }, ); - } - } - // ============================ Private ============================ - if (pickerRef) { - pickerRef.current = { - focus: () => { - if (startInputRef.current) { - startInputRef.current.focus(); - } - }, - blur: () => { - if (startInputRef.current) { - startInputRef.current.blur(); - } - if (endInputRef.current) { - endInputRef.current.blur(); - } - }, - }; - } - - // ============================ Ranges ============================= - const rangeLabels = Object.keys(ranges || {}); - - const rangeList = rangeLabels.map(label => { - const range = ranges![label]; - const newValues = typeof range === 'function' ? range() : range; - - return { - label, - onClick: () => { - triggerChange(newValues, null); - triggerOpen(false, mergedActivePickerIndex); - }, - onMouseEnter: () => { - setRangeHoverValue(newValues); - }, - onMouseLeave: () => { - setRangeHoverValue(null); - }, - }; - }); - - // ============================= Panel ============================= - function renderPanel( - panelPosition: 'left' | 'right' | false = false, - panelProps: Partial> = {}, - ) { - let panelHoverRangedValue: RangeValue = null; - if ( - mergedOpen && - hoverRangedValue && - hoverRangedValue[0] && - hoverRangedValue[1] && - generateConfig.isAfter(hoverRangedValue[1], hoverRangedValue[0]) - ) { - panelHoverRangedValue = hoverRangedValue; - } - - let panelShowTime: - | boolean - | SharedTimeProps - | undefined = showTime as SharedTimeProps; - if (showTime && typeof showTime === 'object' && showTime.defaultValue) { - const timeDefaultValues: DateType[] = showTime.defaultValue!; - panelShowTime = { - ...showTime, - defaultValue: getValue(timeDefaultValues, mergedActivePickerIndex) || undefined, + const triggerModesChange = (modes: [PanelMode, PanelMode], values: RangeValue) => { + setInnerModes(modes); + props.onPanelChange?.(values, modes); }; - } - let panelDateRender: DateRender | null = null; - if (dateRender) { - panelDateRender = (date, today) => - dateRender(date, today, { - range: mergedActivePickerIndex ? 'end' : 'start', - }); - } + // ========================= Disable Date ========================== + const [disabledStartDate, disabledEndDate] = useRangeDisabled( + { + picker: toRef(props, 'picker'), + selectedValue, + locale: toRef(props, 'locale'), + disabled: mergedDisabled, + disabledDate: toRef(props, 'disabledDate'), + generateConfig: toRef(props, 'generateConfig'), + }, + openRecordsRef, + ); - return ( - - - {...(props as any)} - {...panelProps} - dateRender={panelDateRender} - showTime={panelShowTime} - mode={mergedModes[mergedActivePickerIndex]} - generateConfig={generateConfig} - style={undefined} - direction={direction} - disabledDate={mergedActivePickerIndex === 0 ? disabledStartDate : disabledEndDate} - disabledTime={date => { - if (disabledTime) { - return disabledTime(date, mergedActivePickerIndex === 0 ? 'start' : 'end'); - } - return false; - }} - class={classNames({ - [`${prefixCls}-panel-focused`]: - mergedActivePickerIndex === 0 ? !startTyping : !endTyping, - })} - value={getValue(selectedValue, mergedActivePickerIndex)} - locale={locale} - tabIndex={-1} - onPanelChange={(date, newMode) => { - // clear hover value when panel change - if (mergedActivePickerIndex === 0) { - onStartLeave(true); - } - if (mergedActivePickerIndex === 1) { - onEndLeave(true); - } - triggerModesChange( - updateValues(mergedModes, newMode, mergedActivePickerIndex), - updateValues(selectedValue, date, mergedActivePickerIndex), - ); + // ============================= Open ============================== + const [mergedOpen, triggerInnerOpen] = useMergedState(false, { + value: toRef(props, 'open'), + defaultValue: props.defaultOpen, + postState: postOpen => + mergedDisabled.value[mergedActivePickerIndex.value] ? false : postOpen, + onChange: newOpen => { + props.onOpenChange?.(newOpen); - let viewDate = date; - if (panelPosition === 'right' && mergedModes[mergedActivePickerIndex] === newMode) { - viewDate = getClosingViewDate(viewDate, newMode as any, generateConfig, -1); - } - setViewDate(viewDate, mergedActivePickerIndex); - }} - onOk={null} - onSelect={undefined} - onChange={undefined} - defaultValue={ - mergedActivePickerIndex === 0 ? getValue(selectedValue, 1) : getValue(selectedValue, 0) + if (!newOpen && operationRef.value && operationRef.value.onClose) { + operationRef.value.onClose(); } - defaultPickerValue={undefined} - /> - - ); - } + }, + }); - let arrowLeft: number = 0; - let panelLeft: number = 0; - if ( - mergedActivePickerIndex && - startInputDivRef.current && - separatorRef.current && - panelDivRef.current - ) { - // Arrow offset - arrowLeft = startInputDivRef.current.offsetWidth + separatorRef.current.offsetWidth; + const startOpen = computed(() => mergedOpen.value && mergedActivePickerIndex.value === 0); + const endOpen = computed(() => mergedOpen.value && mergedActivePickerIndex.value === 1); - if (panelDivRef.current.offsetWidth && arrowLeft > panelDivRef.current.offsetWidth) { - panelLeft = arrowLeft; - } - } + // ============================= Popup ============================= + // Popup min width + const popupMinWidth = ref(0); + watch(mergedOpen, () => { + if (!mergedOpen.value && containerRef.value) { + popupMinWidth.value = containerRef.value.offsetWidth; + } + }); - const arrowPositionStyle = direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; + // ============================ Trigger ============================ + const triggerRef = ref(); - function renderPanels() { - let panels: React.ReactNode; - const extraNode = getExtraFooter( - prefixCls, - mergedModes[mergedActivePickerIndex], - renderExtraFooter, - ); + function triggerOpen(newOpen: boolean, index: 0 | 1) { + if (newOpen) { + clearTimeout(triggerRef.value); + openRecordsRef.value[index] = true; - const rangesNode = getRanges({ - prefixCls, - components, - needConfirmButton, - okDisabled: - !getValue(selectedValue, mergedActivePickerIndex) || - (disabledDate && disabledDate(selectedValue[mergedActivePickerIndex])), - locale, - rangeList, - onOk: () => { - if (getValue(selectedValue, mergedActivePickerIndex)) { - // triggerChangeOld(selectedValue); - triggerChange(selectedValue, mergedActivePickerIndex); - if (onOk) { - onOk(selectedValue); + setMergedActivePickerIndex(index); + triggerInnerOpen(newOpen); + + // Open to reset view date + if (!mergedOpen.value) { + setViewDate(null, index); + } + } else if (mergedActivePickerIndex.value === index) { + triggerInnerOpen(newOpen); + + // Clean up async + // This makes ref not quick refresh in case user open another input with blur trigger + const openRecords = openRecordsRef.value; + triggerRef.value = setTimeout(() => { + if (openRecords === openRecordsRef.value) { + openRecordsRef.value = {}; + } + }); + } + } + + function triggerOpenAndFocus(index: 0 | 1) { + triggerOpen(true, index); + // Use setTimeout to make sure panel DOM exists + window.setTimeout(() => { + const inputRef = [startInputRef, endInputRef][index]; + if (inputRef.value) { + inputRef.value.focus(); + } + }, 0); + } + + function triggerChange(newValue: RangeValue, sourceIndex: 0 | 1) { + let values = newValue; + let startValue = getValue(values, 0); + let endValue = getValue(values, 1); + const { generateConfig, locale, picker, order, onCalendarChange, allowEmpty, onChange } = + props; + + // >>>>> Format start & end values + if (startValue && endValue && generateConfig.isAfter(startValue, endValue)) { + if ( + // WeekPicker only compare week + (picker === 'week' && + !isSameWeek(generateConfig, locale.locale, startValue, endValue)) || + // QuotaPicker only compare week + (picker === 'quarter' && !isSameQuarter(generateConfig, startValue, endValue)) || + // Other non-TimePicker compare date + (picker !== 'week' && + picker !== 'quarter' && + picker !== 'time' && + !isSameDate(generateConfig, startValue, endValue)) + ) { + // Clean up end date when start date is after end date + if (sourceIndex === 0) { + values = [startValue, null]; + endValue = null; + } else { + startValue = null; + values = [null, endValue]; + } + + // Clean up cache since invalidate + openRecordsRef.value = { + [sourceIndex]: true, + }; + } else if (picker !== 'time' || order !== false) { + // Reorder when in same date + values = reorderValues(values, generateConfig); } } - }, - }); - if (picker !== 'time' && !showTime) { - const viewDate = getViewDate(mergedActivePickerIndex); - const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); - const currentMode = mergedModes[mergedActivePickerIndex]; + setSelectedValue(values); - const showDoublePanel = currentMode === picker; - const leftPanel = renderPanel(showDoublePanel ? 'left' : false, { - pickerValue: viewDate, - onPickerValueChange: newViewDate => { - setViewDate(newViewDate, mergedActivePickerIndex); - }, - }); - const rightPanel = renderPanel('right', { - pickerValue: nextViewDate, - onPickerValueChange: newViewDate => { - setViewDate( - getClosingViewDate(newViewDate, picker, generateConfig, -1), - mergedActivePickerIndex, + const startStr = + values && values[0] + ? formatValue(values[0], { generateConfig, locale, format: formatList[0] }) + : ''; + const endStr = + values && values[1] + ? formatValue(values[1], { generateConfig, locale, format: formatList[0] }) + : ''; + + if (onCalendarChange) { + const info: RangeInfo = { range: sourceIndex === 0 ? 'start' : 'end' }; + + onCalendarChange(values, [startStr, endStr], info); + } + + // >>>>> Trigger `onChange` event + const canStartValueTrigger = canValueTrigger( + startValue, + 0, + mergedDisabled.value, + allowEmpty, + ); + const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled.value, allowEmpty); + + const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); + + if (canTrigger) { + // Trigger onChange only when value is validate + setInnerValue(values); + + if ( + onChange && + (!isEqual(generateConfig, getValue(mergedValue.value, 0), startValue) || + !isEqual(generateConfig, getValue(mergedValue.value, 1), endValue)) + ) { + onChange(values, [startStr, endStr]); + } + } + + // >>>>> Open picker when + + // Always open another picker if possible + let nextOpenIndex: 0 | 1 = null; + if (sourceIndex === 0 && !mergedDisabled.value[1]) { + nextOpenIndex = 1; + } else if (sourceIndex === 1 && !mergedDisabled.value[0]) { + nextOpenIndex = 0; + } + + if ( + nextOpenIndex !== null && + nextOpenIndex !== mergedActivePickerIndex.value && + (!openRecordsRef.value[nextOpenIndex] || !getValue(values, nextOpenIndex)) && + getValue(values, sourceIndex) + ) { + // Delay to focus to avoid input blur trigger expired selectedValues + triggerOpenAndFocus(nextOpenIndex); + } else { + triggerOpen(false, sourceIndex); + } + } + + const forwardKeyDown = (e: KeyboardEvent) => { + if (mergedOpen && 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; + } + }; + + // ============================= Text ============================== + const sharedTextHooksProps = { + formatList, + generateConfig: toRef(props, 'generateConfig'), + locale: toRef(props, 'locale'), + }; + + const [startValueTexts, firstStartValueText] = useValueTexts( + computed(() => getValue(selectedValue.value, 0)), + sharedTextHooksProps, + ); + + const [endValueTexts, firstEndValueText] = useValueTexts( + computed(() => getValue(selectedValue.value, 1)), + sharedTextHooksProps, + ); + + const onTextChange = (newText: string, index: 0 | 1) => { + const inputDate = parseValue(newText, { + locale: props.locale, + formatList: formatList.value, + generateConfig: props.generateConfig, + }); + + const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; + + if (inputDate && !disabledFunc(inputDate)) { + setSelectedValue(updateValues(selectedValue.value, inputDate, index)); + setViewDate(inputDate, index); + } + }; + + const [startText, triggerStartTextChange, resetStartText] = useTextValueMapping({ + valueTexts: startValueTexts, + onTextChange: newText => onTextChange(newText, 0), + }); + + const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping({ + valueTexts: endValueTexts, + onTextChange: newText => onTextChange(newText, 1), + }); + + const [rangeHoverValue, setRangeHoverValue] = useState>(null); + + // ========================== Hover Range ========================== + const [hoverRangedValue, setHoverRangedValue] = useState>(null); + + const [startHoverValue, onStartEnter, onStartLeave] = useHoverValue( + startText, + sharedTextHooksProps, + ); + + const [endHoverValue, onEndEnter, onEndLeave] = useHoverValue(endText, sharedTextHooksProps); + + const onDateMouseEnter = (date: DateType) => { + setHoverRangedValue(updateValues(selectedValue.value, date, mergedActivePickerIndex.value)); + if (mergedActivePickerIndex.value === 0) { + onStartEnter(date); + } else { + onEndEnter(date); + } + }; + + const onDateMouseLeave = () => { + setHoverRangedValue(updateValues(selectedValue.value, null, mergedActivePickerIndex.value)); + if (mergedActivePickerIndex.value === 0) { + onStartLeave(); + } else { + onEndLeave(); + } + }; + + // ============================= Input ============================= + const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({ + forwardKeyDown, + onBlur: (e: FocusEvent) => { + props.onBlur?.(e); + }, + isClickOutside: (target: EventTarget | null) => + !elementsContains( + [panelDivRef.value, startInputDivRef.value, endInputDivRef.value], + target as HTMLElement, + ), + onFocus: (e: FocusEvent) => { + setMergedActivePickerIndex(index); + props.onFocus?.(e); + }, + triggerOpen: (newOpen: boolean) => { + triggerOpen(newOpen, index); + }, + onSubmit: () => { + triggerChange(selectedValue.value, index); + resetText(); + }, + onCancel: () => { + triggerOpen(false, index); + setSelectedValue(mergedValue.value); + resetText(); }, }); - if (direction === 'rtl') { - panels = ( - <> - {rightPanel} - {showDoublePanel && leftPanel} - - ); - } else { - panels = ( - <> - {leftPanel} - {showDoublePanel && rightPanel} - + const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ + ...getSharedInputHookProps(0, resetStartText), + blurToCancel: needConfirmButton, + open: startOpen, + value: startText, + onKeyDown: (e, preventDefault) => { + props.onKeyDown?.(e, preventDefault); + }, + }); + + const [endInputProps, { focused: endFocused, typing: endTyping }] = usePickerInput({ + ...getSharedInputHookProps(1, resetEndText), + blurToCancel: needConfirmButton, + open: endOpen, + value: endText, + onKeyDown: (e, preventDefault) => { + props.onKeyDown?.(e, preventDefault); + }, + }); + + // ========================== Click Picker ========================== + const onPickerClick = (e: MouseEvent) => { + // When click inside the picker & outside the picker's input elements + // the panel should still be opened + if ( + !mergedOpen.value && + !startInputRef.value.contains(e.target as Node) && + !endInputRef.value.contains(e.target as Node) + ) { + if (!mergedDisabled.value[0]) { + triggerOpenAndFocus(0); + } else if (!mergedDisabled.value[1]) { + triggerOpenAndFocus(1); + } + } + }; + + const onPickerMouseDown = (e: MouseEvent) => { + // shouldn't affect input elements if picker is active + if ( + mergedOpen.value && + (startFocused.value || endFocused.value) && + !startInputRef.value.contains(e.target as Node) && + !endInputRef.value.contains(e.target as Node) + ) { + e.preventDefault(); + } + }; + + // ============================= Sync ============================== + // Close should sync back with text value + const startStr = computed(() => + mergedValue.value?.[0] + ? formatValue(mergedValue.value[0], { + locale: props.locale, + format: 'YYYYMMDDHHmmss', + generateConfig: props.generateConfig, + }) + : '', + ); + const endStr = computed(() => + mergedValue.value?.[1] + ? formatValue(mergedValue.value[1], { + locale: props.locale, + format: 'YYYYMMDDHHmmss', + generateConfig: props.generateConfig, + }) + : '', + ); + + watch([mergedOpen, startValueTexts, endValueTexts], () => { + if (!mergedOpen.value) { + setSelectedValue(mergedValue.value); + + if (!startValueTexts.value.length || startValueTexts.value[0] === '') { + triggerStartTextChange(''); + } else if (firstStartValueText.value !== startText.value) { + resetStartText(); + } + if (!endValueTexts.value.length || endValueTexts.value[0] === '') { + triggerEndTextChange(''); + } else if (firstEndValueText.value !== endText.value) { + resetEndText(); + } + } + }); + + // Sync innerValue with control mode + watch([startStr, endStr], () => { + setSelectedValue(mergedValue.value); + }); + + // ============================ Warning ============================ + if (process.env.NODE_ENV !== 'production') { + watchEffect(() => { + const { value, disabled } = props; + if ( + value && + Array.isArray(disabled) && + ((getValue(disabled, 0) && !getValue(value, 0)) || + (getValue(disabled, 1) && !getValue(value, 1))) + ) { + warning( + false, + '`disabled` should not set with empty `value`. You should set `allowEmpty` or `value` instead.', + ); + } + }); + } + + expose({ + focus: () => { + if (startInputRef.value) { + startInputRef.value.focus(); + } + }, + blur: () => { + if (startInputRef.value) { + startInputRef.value.blur(); + } + if (endInputRef.value) { + endInputRef.value.blur(); + } + }, + }); + + // ============================ Ranges ============================= + + const rangeList = computed(() => + Object.keys(props.ranges || {}).map(label => { + const range = props.ranges![label]; + const newValues = typeof range === 'function' ? range() : range; + + return { + label, + onClick: () => { + triggerChange(newValues, null); + triggerOpen(false, mergedActivePickerIndex.value); + }, + onMouseEnter: () => { + setRangeHoverValue(newValues); + }, + onMouseLeave: () => { + setRangeHoverValue(null); + }, + }; + }), + ); + // ============================= Panel ============================= + const panelHoverRangedValue = computed(() => { + if ( + mergedOpen.value && + hoverRangedValue.value && + hoverRangedValue.value[0] && + hoverRangedValue.value[1] && + props.generateConfig.isAfter(hoverRangedValue.value[1], hoverRangedValue.value[0]) + ) { + return hoverRangedValue.value; + } else { + return null; + } + }); + function renderPanel( + panelPosition: 'left' | 'right' | false = false, + panelProps: Partial> = {}, + ) { + const { generateConfig, showTime, dateRender, direction, disabledTime, prefixCls, locale } = + props; + + let panelShowTime: boolean | SharedTimeProps | undefined = + showTime as SharedTimeProps; + if (showTime && typeof showTime === 'object' && showTime.defaultValue) { + const timeDefaultValues: DateType[] = showTime.defaultValue!; + panelShowTime = { + ...showTime, + defaultValue: getValue(timeDefaultValues, mergedActivePickerIndex.value) || undefined, + }; + } + + let panelDateRender: DateRender | null = null; + if (dateRender) { + panelDateRender = (date, today) => + dateRender(date, today, { + range: mergedActivePickerIndex ? 'end' : 'start', + }); + } + + return ( + + + {...(props as any)} + {...panelProps} + dateRender={panelDateRender} + showTime={panelShowTime} + mode={mergedModes[mergedActivePickerIndex.value]} + generateConfig={generateConfig} + style={undefined} + direction={direction} + disabledDate={ + mergedActivePickerIndex.value === 0 ? disabledStartDate : disabledEndDate + } + disabledTime={date => { + if (disabledTime) { + return disabledTime(date, mergedActivePickerIndex.value === 0 ? 'start' : 'end'); + } + return false; + }} + class={classNames({ + [`${prefixCls}-panel-focused`]: + mergedActivePickerIndex.value === 0 ? !startTyping.value : !endTyping.value, + })} + value={getValue(selectedValue.value, mergedActivePickerIndex.value)} + locale={locale} + tabIndex={-1} + onPanelChange={(date, newMode) => { + // clear hover value when panel change + if (mergedActivePickerIndex.value === 0) { + onStartLeave(true); + } + if (mergedActivePickerIndex.value === 1) { + onEndLeave(true); + } + triggerModesChange( + updateValues(mergedModes.value, newMode, mergedActivePickerIndex.value), + updateValues(selectedValue.value, date, mergedActivePickerIndex.value), + ); + + let viewDate = date; + if ( + panelPosition === 'right' && + mergedModes[mergedActivePickerIndex.value] === newMode + ) { + viewDate = getClosingViewDate(viewDate, newMode as any, generateConfig, -1); + } + setViewDate(viewDate, mergedActivePickerIndex.value); + }} + onOk={null} + onSelect={undefined} + onChange={undefined} + defaultValue={ + mergedActivePickerIndex.value === 0 + ? getValue(selectedValue.value, 1) + : getValue(selectedValue.value, 0) + } + defaultPickerValue={undefined} + /> + ); } - } else { - panels = renderPanel(); - } - let mergedNodes: React.ReactNode = ( - <> -
{panels}
- {(extraNode || rangesNode) && ( -
- {extraNode} - {rangesNode} -
- )} - - ); + const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { + const values = updateValues(selectedValue.value, date, mergedActivePickerIndex.value); - if (panelRender) { - mergedNodes = panelRender(mergedNodes); - } - - return ( -
{ - e.preventDefault(); - }} - > - {mergedNodes} -
- ); - } - - const rangePanel = ( -
-
- - {renderPanels()} -
- ); - - // ============================= Icons ============================= - let suffixNode: React.ReactNode; - if (suffixIcon) { - suffixNode = {suffixIcon}; - } - - let clearNode: React.ReactNode; - if ( - allowClear && - ((getValue(mergedValue, 0) && !mergedDisabled[0]) || - (getValue(mergedValue, 1) && !mergedDisabled[1])) - ) { - clearNode = ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onMouseUp={e => { - e.preventDefault(); - e.stopPropagation(); - let values = mergedValue; - - if (!mergedDisabled[0]) { - values = updateValues(values, null, 0); - } - if (!mergedDisabled[1]) { - values = updateValues(values, null, 1); + if (type === 'submit' || (type !== 'key' && !needConfirmButton.value)) { + // triggerChange will also update selected values + triggerChange(values, mergedActivePickerIndex.value); + // clear hover value style + if (mergedActivePickerIndex.value === 0) { + onStartLeave(); + } else { + onEndLeave(); } + } else { + setSelectedValue(values); + } + }; - triggerChange(values, null); - triggerOpen(false, mergedActivePickerIndex); - }} - class={`${prefixCls}-clear`} - > - {clearIcon || } - - ); - } - - const inputSharedProps = { - size: getInputSize(picker, formatList[0], generateConfig), - }; - - let activeBarLeft: number = 0; - let activeBarWidth: number = 0; - if (startInputDivRef.current && endInputDivRef.current && separatorRef.current) { - if (mergedActivePickerIndex === 0) { - activeBarWidth = startInputDivRef.current.offsetWidth; - } else { - activeBarLeft = arrowLeft; - activeBarWidth = endInputDivRef.current.offsetWidth; - } - } - const activeBarPositionStyle = - direction === 'rtl' ? { right: activeBarLeft } : { left: activeBarLeft }; - // ============================ Return ============================= - const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { - const values = updateValues(selectedValue, date, mergedActivePickerIndex); - - if (type === 'submit' || (type !== 'key' && !needConfirmButton)) { - // triggerChange will also update selected values - triggerChange(values, mergedActivePickerIndex); - // clear hover value style - if (mergedActivePickerIndex === 0) { - onStartLeave(); - } else { - onEndLeave(); - } - } else { - setSelectedValue(values); - } - }; - - return ( - props.picker === 'time'), onDateMouseEnter, onDateMouseLeave, - hideRanges: true, + hideRanges: computed(() => true), onSelect: onContextSelect, open: mergedOpen, - }} - > - -
-
- { - triggerStartTextChange(e.target.value); + }); + + return () => { + const { + prefixCls = 'rc-picker', + id, + popupStyle, + dropdownClassName, + transitionName, + dropdownAlign, + getPopupContainer, + generateConfig, + locale, + placeholder, + autofocus, + picker = 'date', + showTime, + separator = '~', + disabledDate, + panelRender, + allowClear, + suffixIcon, + clearIcon, + inputReadOnly, + renderExtraFooter, + onMouseEnter, + onMouseLeave, + onOk, + components, + direction, + autocomplete = 'off', + } = props; + let arrowLeft: number = 0; + let panelLeft: number = 0; + if ( + mergedActivePickerIndex.value && + startInputDivRef.value && + separatorRef.value && + panelDivRef.value + ) { + // Arrow offset + arrowLeft = startInputDivRef.value.offsetWidth + separatorRef.value.offsetWidth; + + if (panelDivRef.value.offsetWidth && arrowLeft > panelDivRef.value.offsetWidth) { + panelLeft = arrowLeft; + } + } + + const arrowPositionStyle = direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; + + function renderPanels() { + let panels: VueNode; + const extraNode = getExtraFooter( + prefixCls, + mergedModes[mergedActivePickerIndex.value], + renderExtraFooter, + ); + + const rangesNode = getRanges({ + prefixCls, + components, + needConfirmButton: needConfirmButton.value, + okDisabled: + !getValue(selectedValue.value, mergedActivePickerIndex.value) || + (disabledDate && disabledDate(selectedValue.value[mergedActivePickerIndex.value])), + locale, + rangeList: rangeList.value, + onOk: () => { + if (getValue(selectedValue.value, mergedActivePickerIndex.value)) { + // triggerChangeOld(selectedValue.value); + triggerChange(selectedValue.value, mergedActivePickerIndex.value); + if (onOk) { + onOk(selectedValue.value); + } + } + }, + }); + + if (picker !== 'time' && !showTime) { + const viewDate = mergedActivePickerIndex.value + ? startViewDate.value + : endViewDate.value; + const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); + const currentMode = mergedModes[mergedActivePickerIndex.value]; + + const showDoublePanel = currentMode === picker; + const leftPanel = renderPanel(showDoublePanel ? 'left' : false, { + pickerValue: viewDate, + onPickerValueChange: newViewDate => { + setViewDate(newViewDate, mergedActivePickerIndex.value); + }, + }); + const rightPanel = renderPanel('right', { + pickerValue: nextViewDate, + onPickerValueChange: newViewDate => { + setViewDate( + getClosingViewDate(newViewDate, picker, generateConfig, -1), + mergedActivePickerIndex.value, + ); + }, + }); + + if (direction === 'rtl') { + panels = ( + <> + {rightPanel} + {showDoublePanel && leftPanel} + + ); + } else { + panels = ( + <> + {leftPanel} + {showDoublePanel && rightPanel} + + ); + } + } else { + panels = renderPanel(); + } + + let mergedNodes: VueNode = ( + <> +
{panels}
+ {(extraNode || rangesNode) && ( +
+ {extraNode} + {rangesNode} +
+ )} + + ); + + if (panelRender) { + mergedNodes = panelRender(mergedNodes); + } + + return ( +
{ + e.preventDefault(); }} - autoFocus={autoFocus} - placeholder={getValue(placeholder, 0) || ''} - ref={startInputRef} - {...startInputProps} - {...inputSharedProps} - autoComplete={autoComplete} - /> -
-
- {separator} -
+ > + {mergedNodes} +
+ ); + } + + const rangePanel = (
- { - triggerEndTextChange(e.target.value); - }} - placeholder={getValue(placeholder, 1) || ''} - ref={endInputRef} - {...endInputProps} - {...inputSharedProps} - autoComplete={autoComplete} - /> +
+ + {renderPanels()}
-
- {suffixNode} - {clearNode} -
- - - ); + ); + + // ============================= Icons ============================= + let suffixNode: VueNode; + if (suffixIcon) { + suffixNode = {suffixIcon}; + } + + let clearNode: VueNode; + if ( + allowClear && + ((getValue(mergedValue.value, 0) && !mergedDisabled.value[0]) || + (getValue(mergedValue.value, 1) && !mergedDisabled.value[1])) + ) { + clearNode = ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onMouseup={e => { + e.preventDefault(); + e.stopPropagation(); + let values = mergedValue.value; + + if (!mergedDisabled[0]) { + values = updateValues(values, null, 0); + } + if (!mergedDisabled[1]) { + values = updateValues(values, null, 1); + } + + triggerChange(values, null); + triggerOpen(false, mergedActivePickerIndex.value); + }} + class={`${prefixCls}-clear`} + > + {clearIcon || } + + ); + } + + const inputSharedProps = { + size: getInputSize(picker, formatList[0], generateConfig), + }; + + let activeBarLeft: number = 0; + let activeBarWidth: number = 0; + if (startInputDivRef.value && endInputDivRef.value && separatorRef.value) { + if (mergedActivePickerIndex.value === 0) { + activeBarWidth = startInputDivRef.value.offsetWidth; + } else { + activeBarLeft = arrowLeft; + activeBarWidth = endInputDivRef.value.offsetWidth; + } + } + const activeBarPositionStyle = + direction === 'rtl' ? { right: `${activeBarLeft}px` } : { left: `${activeBarLeft}px` }; + // ============================ Return ============================= + + return ( + +
+
+ { + triggerStartTextChange(e.target.value); + }} + autofocus={autofocus} + placeholder={getValue(placeholder, 0) || ''} + ref={startInputRef} + {...startInputProps.value} + {...inputSharedProps} + autocomplete={autocomplete} + /> +
+
+ {separator} +
+
+ { + triggerEndTextChange(e.target.value); + }} + placeholder={getValue(placeholder, 1) || ''} + ref={endInputRef} + {...endInputProps.value} + {...inputSharedProps} + autocomplete={autocomplete} + /> +
+
+ {suffixNode} + {clearNode} +
+ + ); + }; + }, + }); } -// Wrap with class component to enable pass generic with instance method -class RangePicker extends React.Component> { - pickerRef = React.createRef(); - - focus = () => { - if (this.pickerRef.current) { - this.pickerRef.current.focus(); - } - }; - - blur = () => { - if (this.pickerRef.current) { - this.pickerRef.current.blur(); - } - }; - - render() { - return ( - - {...this.props} - pickerRef={this.pickerRef as React.MutableRefObject} - /> - ); - } -} - -export default RangePicker; +const InterRangerPicker = RangerPicker(); +export default ( + props: MergedRangePickerProps, + { slots }, +): JSX.Element => createVNode(InterRangerPicker, props, slots); diff --git a/components/vc-picker/hooks/useHoverValue.ts b/components/vc-picker/hooks/useHoverValue.ts index 345afa277..2081c95da 100644 --- a/components/vc-picker/hooks/useHoverValue.ts +++ b/components/vc-picker/hooks/useHoverValue.ts @@ -1,31 +1,31 @@ -import { useState, useEffect, useRef } from 'react'; +import type { ComputedRef, Ref, UnwrapRef } from 'vue'; +import { ref, onBeforeUnmount, watch } from 'vue'; import type { ValueTextConfig } from './useValueTexts'; import useValueTexts from './useValueTexts'; export default function useHoverValue( - valueText: string, + valueText: Ref, { formatList, generateConfig, locale }: ValueTextConfig, -): [string, (date: DateType) => void, (immediately?: boolean) => void] { - const [value, internalSetValue] = useState(null); - const raf = useRef(null); +): [ComputedRef, (date: DateType) => void, (immediately?: boolean) => void] { + const innerValue = ref(null); + const raf = ref(null); function setValue(val: DateType, immediately = false) { - cancelAnimationFrame(raf.current); + cancelAnimationFrame(raf.value); if (immediately) { - internalSetValue(val); + innerValue.value = val as UnwrapRef; return; } - raf.current = requestAnimationFrame(() => { - internalSetValue(val); + raf.value = requestAnimationFrame(() => { + innerValue.value = val as UnwrapRef; }); } - const [, firstText] = useValueTexts(value, { + const [, firstText] = useValueTexts(innerValue as Ref, { formatList, generateConfig, locale, }); - function onEnter(date: DateType) { setValue(date); } @@ -34,11 +34,12 @@ export default function useHoverValue( setValue(null, immediately); } - useEffect(() => { + watch(valueText, () => { onLeave(true); - }, [valueText]); - - useEffect(() => () => cancelAnimationFrame(raf.current), []); + }); + onBeforeUnmount(() => { + cancelAnimationFrame(raf.value); + }); return [firstText, onEnter, onLeave]; } diff --git a/components/vc-picker/hooks/useRangeDisabled.ts b/components/vc-picker/hooks/useRangeDisabled.ts index c01b81673..f1d883860 100644 --- a/components/vc-picker/hooks/useRangeDisabled.ts +++ b/components/vc-picker/hooks/useRangeDisabled.ts @@ -1,8 +1,9 @@ -import * as React from 'react'; import type { RangeValue, PickerMode, Locale } from '../interface'; import { getValue } from '../utils/miscUtil'; import type { GenerateConfig } from '../generate'; import { isSameDate, getQuarter } from '../utils/dateUtil'; +import type { ComputedRef, Ref } from 'vue'; +import { computed } from 'vue'; export default function useRangeDisabled( { @@ -13,101 +14,101 @@ export default function useRangeDisabled( disabled, generateConfig, }: { - picker: PickerMode; - selectedValue: RangeValue; - disabledDate?: (date: DateType) => boolean; - disabled: [boolean, boolean]; - locale: Locale; - generateConfig: GenerateConfig; + picker: Ref; + selectedValue: Ref>; + disabledDate?: Ref<(date: DateType) => boolean>; + disabled: ComputedRef<[boolean, boolean]>; + locale: Ref; + generateConfig: Ref>; }, - disabledStart: boolean, - disabledEnd: boolean, + openRecordsRef: Ref<{ + [x: number]: boolean; + }>, ) { - const startDate = getValue(selectedValue, 0); - const endDate = getValue(selectedValue, 1); + const startDate = computed(() => getValue(selectedValue.value, 0)); + const endDate = computed(() => getValue(selectedValue.value, 1)); function weekFirstDate(date: DateType) { - return generateConfig.locale.getWeekFirstDate(locale.locale, date); + return generateConfig.value.locale.getWeekFirstDate(locale.value.locale, date); } function monthNumber(date: DateType) { - const year = generateConfig.getYear(date); - const month = generateConfig.getMonth(date); + const year = generateConfig.value.getYear(date); + const month = generateConfig.value.getMonth(date); return year * 100 + month; } function quarterNumber(date: DateType) { - const year = generateConfig.getYear(date); - const quarter = getQuarter(generateConfig, date); + const year = generateConfig.value.getYear(date); + const quarter = getQuarter(generateConfig.value, date); return year * 10 + quarter; } - const disabledStartDate = React.useCallback( - (date: DateType) => { - if (disabledDate && disabledDate(date)) { - return true; + const disabledStartDate = (date: DateType) => { + if (disabledDate && disabledDate.value(date)) { + return true; + } + + // Disabled range + if (disabled[1] && endDate) { + return ( + !isSameDate(generateConfig.value, date, endDate.value) && + generateConfig.value.isAfter(date, endDate.value) + ); + } + + // Disabled part + if (openRecordsRef.value[1] && endDate.value) { + switch (picker.value) { + case 'quarter': + return quarterNumber(date) > quarterNumber(endDate.value); + case 'month': + return monthNumber(date) > monthNumber(endDate.value); + case 'week': + return weekFirstDate(date) > weekFirstDate(endDate.value); + default: + return ( + !isSameDate(generateConfig.value, date, endDate.value) && + generateConfig.value.isAfter(date, endDate.value) + ); } + } - // Disabled range - if (disabled[1] && endDate) { - return !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(date, endDate); + return false; + }; + + const disabledEndDate = (date: DateType) => { + if (disabledDate.value?.(date)) { + return true; + } + + // Disabled range + if (disabled[0] && startDate) { + return ( + !isSameDate(generateConfig.value, date, endDate.value) && + generateConfig.value.isAfter(startDate.value, date) + ); + } + + // Disabled part + if (openRecordsRef.value[0] && startDate.value) { + switch (picker.value) { + case 'quarter': + return quarterNumber(date) < quarterNumber(startDate.value); + case 'month': + return monthNumber(date) < monthNumber(startDate.value); + case 'week': + return weekFirstDate(date) < weekFirstDate(startDate.value); + default: + return ( + !isSameDate(generateConfig.value, date, startDate.value) && + generateConfig.value.isAfter(startDate.value, date) + ); } + } - // Disabled part - if (disabledStart && endDate) { - switch (picker) { - case 'quarter': - return quarterNumber(date) > quarterNumber(endDate); - case 'month': - return monthNumber(date) > monthNumber(endDate); - case 'week': - return weekFirstDate(date) > weekFirstDate(endDate); - default: - return ( - !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(date, endDate) - ); - } - } - - return false; - }, - [disabledDate, disabled[1], endDate, disabledStart], - ); - - const disabledEndDate = React.useCallback( - (date: DateType) => { - if (disabledDate && disabledDate(date)) { - return true; - } - - // Disabled range - if (disabled[0] && startDate) { - return ( - !isSameDate(generateConfig, date, endDate) && generateConfig.isAfter(startDate, date) - ); - } - - // Disabled part - if (disabledEnd && startDate) { - switch (picker) { - case 'quarter': - return quarterNumber(date) < quarterNumber(startDate); - case 'month': - return monthNumber(date) < monthNumber(startDate); - case 'week': - return weekFirstDate(date) < weekFirstDate(startDate); - default: - return ( - !isSameDate(generateConfig, date, startDate) && - generateConfig.isAfter(startDate, date) - ); - } - } - - return false; - }, - [disabledDate, disabled[0], startDate, disabledEnd], - ); + return false; + }; return [disabledStartDate, disabledEndDate]; } diff --git a/components/vc-picker/hooks/useRangeViewDates.ts b/components/vc-picker/hooks/useRangeViewDates.ts index df6e63453..14112bb4f 100644 --- a/components/vc-picker/hooks/useRangeViewDates.ts +++ b/components/vc-picker/hooks/useRangeViewDates.ts @@ -1,8 +1,10 @@ -import * as React from 'react'; import type { RangeValue, PickerMode } from '../interface'; import type { GenerateConfig } from '../generate'; import { getValue, updateValues } from '../utils/miscUtil'; import { getClosingViewDate, isSameYear, isSameMonth, isSameDecade } from '../utils/dateUtil'; +import type { ComputedRef, Ref } from 'vue'; +import { computed } from 'vue'; +import { ref } from 'vue'; function getStartEndDistance( startDate: DateType, @@ -67,55 +69,64 @@ export default function useRangeViewDates({ defaultDates, generateConfig, }: { - values: RangeValue; - picker: PickerMode; + values: Ref>; + picker: Ref; defaultDates: RangeValue | undefined; - generateConfig: GenerateConfig; -}): [(activePickerIndex: 0 | 1) => DateType, (viewDate: DateType | null, index: 0 | 1) => void] { - const [defaultViewDates, setDefaultViewDates] = React.useState< - [DateType | null, DateType | null] - >(() => [getValue(defaultDates, 0), getValue(defaultDates, 1)]); - const [viewDates, setInternalViewDates] = React.useState>(null); - - const startDate = getValue(values, 0); - const endDate = getValue(values, 1); + generateConfig: Ref>; +}): [ + ComputedRef, + ComputedRef, + (viewDate: DateType | null, index: 0 | 1) => void, +] { + const defaultViewDates = ref<[DateType | null, DateType | null]>([ + getValue(defaultDates, 0), + getValue(defaultDates, 1), + ]); + const viewDates = ref>(null); + const startDate = computed(() => getValue(values.value, 0)); + const endDate = computed(() => getValue(values.value, 1)); function getViewDate(index: 0 | 1): DateType { // If set default view date, use it - if (defaultViewDates[index]) { - return defaultViewDates[index]!; + if (defaultViewDates.value[index]) { + return defaultViewDates.value[index]! as DateType; } return ( - getValue(viewDates, index) || - getRangeViewDate(values, index, picker, generateConfig) || - startDate || - endDate || - generateConfig.getNow() + (getValue(viewDates.value, index) as any) || + getRangeViewDate(values.value, index, picker.value, generateConfig.value) || + startDate.value || + endDate.value || + generateConfig.value.getNow() ); } + const startViewDate = computed(() => { + return getViewDate(0); + }); + + const endViewDate = computed(() => { + return getViewDate(1); + }); function setViewDate(viewDate: DateType | null, index: 0 | 1) { if (viewDate) { - let newViewDates = updateValues(viewDates, viewDate, index); + let newViewDates = updateValues(viewDates.value, viewDate as any, index); // Set view date will clean up default one - setDefaultViewDates( - // Should always be an array - updateValues(defaultViewDates, null, index) || [null, null], - ); + // Should always be an array + defaultViewDates.value = updateValues(defaultViewDates.value, null, index) || [null, null]; // Reset another one when not have value const anotherIndex = (index + 1) % 2; - if (!getValue(values, anotherIndex)) { + if (!getValue(values.value, anotherIndex)) { newViewDates = updateValues(newViewDates, viewDate, anotherIndex); } - setInternalViewDates(newViewDates); - } else if (startDate || endDate) { + viewDates.value = newViewDates; + } else if (startDate.value || endDate.value) { // Reset all when has values when `viewDate` is `null` which means from open trigger - setInternalViewDates(null); + viewDates.value = null; } } - return [getViewDate, setViewDate]; + return [startViewDate, endViewDate, setViewDate]; } diff --git a/components/vc-picker/hooks/useValueTexts.ts b/components/vc-picker/hooks/useValueTexts.ts index b836ba93f..645574775 100644 --- a/components/vc-picker/hooks/useValueTexts.ts +++ b/components/vc-picker/hooks/useValueTexts.ts @@ -1,4 +1,5 @@ import type { ComputedRef, Ref } from 'vue'; +import { computed } from 'vue'; import useMemo from '../../_util/hooks/useMemo'; import shallowequal from '../../_util/shallowequal'; import type { GenerateConfig } from '../generate'; @@ -14,8 +15,8 @@ export type ValueTextConfig = { export default function useValueTexts( value: Ref, { formatList, generateConfig, locale }: ValueTextConfig, -) { - return useMemo<[string[], string]>( +): [ComputedRef, ComputedRef] { + const texts = useMemo<[string[], string]>( () => { if (!value.value) { return [[''], '']; @@ -44,4 +45,7 @@ export default function useValueTexts( [value, formatList], (next, prev) => prev[0] !== next[0] || !shallowequal(prev[1], next[1]), ); + const fullValueTexts = computed(() => texts.value[0]); + const firstValueText = computed(() => texts.value[1]); + return [fullValueTexts, firstValueText]; }