diff --git a/src/NewPicker/PickerInput/Popup/PopupPanel.tsx b/src/NewPicker/PickerInput/Popup/PopupPanel.tsx index e47011cae..f7e176612 100644 --- a/src/NewPicker/PickerInput/Popup/PopupPanel.tsx +++ b/src/NewPicker/PickerInput/Popup/PopupPanel.tsx @@ -12,19 +12,56 @@ export type PopupPanelProps = MustProp & }; export default function PopupPanel(props: PopupPanelProps) { - const { internalMode, picker, multiple } = props; - const { prefixCls } = React.useContext(PickerContext); + const { internalMode, picker, multiple, pickerValue, onPickerValueChange } = props; + const { prefixCls, generateConfig } = React.useContext(PickerContext); - console.log('????', internalMode, picker); + const offsetDate = React.useCallback( + (date: DateType, offset: number) => { + switch (picker) { + case 'date': + return generateConfig.addMonth(date, offset); + case 'month': + case 'quarter': + return generateConfig.addYear(date, offset); + + case 'year': + return generateConfig.addYear(date, offset * 10); + + case 'decade': + return generateConfig.addYear(date, offset * 100); + + default: + return date; + } + }, + [generateConfig, picker], + ); + + const nextPickerValue = React.useMemo( + () => offsetDate(pickerValue, 1), + [pickerValue, offsetDate], + ); + + const onNextPickerValueChange = (nextDate: DateType) => { + onPickerValueChange(offsetDate(nextDate, -1)); + }; + + // ======================== Render ======================== + // Multiple if (multiple) { return (
- +
); } + // Single return ; } diff --git a/src/NewPicker/PickerInput/RangePicker.tsx b/src/NewPicker/PickerInput/RangePicker.tsx index 92735fc65..facee7ae0 100644 --- a/src/NewPicker/PickerInput/RangePicker.tsx +++ b/src/NewPicker/PickerInput/RangePicker.tsx @@ -1,6 +1,5 @@ import { useMergedState } from 'rc-util'; import * as React from 'react'; -import { isSameTimestamp } from '../../utils/dateUtil'; import type { InternalMode, OnOpenChange, @@ -14,6 +13,7 @@ import PickerTrigger from '../PickerTrigger'; import PickerContext from './context'; import { useFieldFormat } from './hooks/useFieldFormat'; import useOpen from './hooks/useOpen'; +import useRangeValue from './hooks/useRangeValue'; import useShowNow from './hooks/useShowNow'; import Popup from './Popup'; import RangeSelector from './Selector/RangeSelector'; @@ -43,6 +43,12 @@ export interface RangePickerProps extends SharedPickerProps }, ) => void; + // Picker Value + defaultPickerValue?: [DateType, DateType] | null; + pickerValue?: [DateType, DateType] | null; + onPickerValueChange?: (date: [DateType, DateType]) => void; + + // MISC order?: boolean; disabled?: boolean | [boolean, boolean]; @@ -87,6 +93,11 @@ export default function Picker(props: RangePickerProps showNow, showToday, + // Picker Value + defaultPickerValue, + pickerValue, + onPickerValueChange, + // Format format, @@ -161,81 +172,44 @@ export default function Picker(props: RangePickerProps const mergedAllowEmpty = separateConfig(allowEmpty, true); // ======================== Value ========================= - const valueConfig = { + const [mergedValue, setMergedValue, submitValue, setSubmitValue, triggerChange] = useRangeValue( value, - postState: (valList: RangeValueType): RangeValueType => - valList || [null, null], - }; - - // Used for internal value management. - // It should always use `mergedValue` in render logic - const [mergedValue, setMergedValue] = useMergedState(defaultValue, valueConfig); - - // Used for trigger `onChange` event. - // Record current submitted value. - const [submitValue, setSubmitValue] = useMergedState(defaultValue, valueConfig); - - // ======================== Change ======================== - const getDateTexts = (dateList: RangeValueType) => { - return dateList.map((date) => - date ? generateConfig.locale.format(locale.locale, date, formatList[0]) : '', - ) as [string, string]; - }; - - const isSameDates = (source: RangeValueType, target: RangeValueType) => { - const [prevSubmitStart, prevSubmitEnd] = source; - - const isSameStart = isSameTimestamp(generateConfig, prevSubmitStart, target[0]); - const isSameEnd = isSameTimestamp(generateConfig, prevSubmitEnd, target[1]); - - return [isSameStart && isSameEnd, isSameStart, isSameEnd]; - }; - - const triggerChange = ([start, end]: RangeValueType, source?: 'submit') => { - const clone: RangeValueType = [start, end]; - - // Only when exist value to sort - if (order && source === 'submit' && clone[0] && clone[1]) { - clone.sort((a, b) => (generateConfig.isAfter(a, b) ? 1 : -1)); - } + defaultValue, + generateConfig, + locale, + formatList, + mergedAllowEmpty, + order, + onCalendarChange, + onChange, + ); - // Update merged value - const [isSameMergedDates, isSameStart] = isSameDates(mergedValue, clone); + // ===================== Picker Value ===================== + const [mergedStartPickerValue, setStartPickerValue] = useMergedState( + () => defaultPickerValue?.[0] || mergedValue?.[0] || generateConfig.getNow(), + { + value: pickerValue?.[0], + }, + ); - if (!isSameMergedDates) { - setMergedValue(clone); + const [mergedEndPickerValue, setEndPickerValue] = useMergedState( + () => defaultPickerValue?.[1] || mergedValue?.[1] || generateConfig.getNow(), + { + value: pickerValue?.[1], + }, + ); - // Trigger calendar change event - if (onCalendarChange) { - onCalendarChange(clone, getDateTexts(clone), { - range: isSameStart ? 'end' : 'start', - }); - } - } + const currentPickerValue = [mergedStartPickerValue, mergedEndPickerValue][activeIndex]; + const setCurrentPickerValue = (nextPickerValue: DateType) => { + const updater = [setStartPickerValue, setEndPickerValue][activeIndex]; + updater(nextPickerValue); - // Update `submitValue` to trigger event by effect - if (source === 'submit') { - setSubmitValue(clone); - - // Trigger `onChange` if needed - const [isSameSubmitDates] = isSameDates(submitValue, clone); - - const startEmpty = !clone[0]; - const endEmpty = !clone[1]; - - if ( - onChange && - !isSameSubmitDates && - // Validate start - (!startEmpty || mergedAllowEmpty[0]) && - // Validate end - (!endEmpty || mergedAllowEmpty[1]) - ) { - onChange(clone, getDateTexts(clone)); - } - } + const clone: [DateType, DateType] = [mergedStartPickerValue, mergedEndPickerValue]; + clone[activeIndex] = nextPickerValue; + onPickerValueChange?.(clone); }; + // ======================== Change ======================== const fillMergedValue = (date: DateType, index: number) => { // Trigger change only when date changed const [prevStart, prevEnd] = mergedValue; @@ -349,6 +323,9 @@ export default function Picker(props: RangePickerProps value={panelValue} onChange={null} onCalendarChange={onPanelCalendarChange} + // PickerValue + pickerValue={currentPickerValue} + onPickerValueChange={setCurrentPickerValue} // Submit needConfirm={needConfirm} onSubmit={submitAndFocusNext} diff --git a/src/NewPicker/PickerInput/hooks/useRangeValue.ts b/src/NewPicker/PickerInput/hooks/useRangeValue.ts new file mode 100644 index 000000000..3f1f93838 --- /dev/null +++ b/src/NewPicker/PickerInput/hooks/useRangeValue.ts @@ -0,0 +1,110 @@ +import { useMergedState } from 'rc-util'; +import type { GenerateConfig } from '../../../generate'; +import { isSameTimestamp } from '../../../utils/dateUtil'; +import type { Locale } from '../../interface'; +import type { RangePickerProps, RangeValueType } from '../RangePicker'; + +// Submit Logic (with order): +// * All the input is filled step by step +// * None of the Picker has focused anymore + +type SetValue = (val: RangeValueType) => void; + +type TriggerChange = ([start, end]: RangeValueType, source?: 'submit') => void; + +export default function useRangeValue( + value: RangeValueType, + defaultValue: RangeValueType, + generateConfig: GenerateConfig, + locale: Locale, + formatList: string[], + allowEmpty: [boolean | undefined, boolean | undefined], + order: boolean, + onCalendarChange?: RangePickerProps['onCalendarChange'], + onChange?: RangePickerProps['onChange'], +): [ + mergedValue: RangeValueType, + setMergedValue: SetValue, + submitValue: RangeValueType, + setSubmitValue: SetValue, + triggerChange: TriggerChange, +] { + // ============================ Values ============================ + const valueConfig = { + value, + postState: (valList: RangeValueType): RangeValueType => + valList || [null, null], + }; + + // Used for internal value management. + // It should always use `mergedValue` in render logic + const [mergedValue, setMergedValue] = useMergedState(defaultValue, valueConfig); + + // Used for trigger `onChange` event. + // Record current submitted value. + const [submitValue, setSubmitValue] = useMergedState(defaultValue, valueConfig); + + // ============================ Change ============================ + const getDateTexts = (dateList: RangeValueType) => { + return dateList.map((date) => + date ? generateConfig.locale.format(locale.locale, date, formatList[0]) : '', + ) as [string, string]; + }; + + const isSameDates = (source: RangeValueType, target: RangeValueType) => { + const [prevSubmitStart, prevSubmitEnd] = source; + + const isSameStart = isSameTimestamp(generateConfig, prevSubmitStart, target[0]); + const isSameEnd = isSameTimestamp(generateConfig, prevSubmitEnd, target[1]); + + return [isSameStart && isSameEnd, isSameStart, isSameEnd]; + }; + + const triggerChange = ([start, end]: RangeValueType, source?: 'submit') => { + const clone: RangeValueType = [start, end]; + + // Only when exist value to sort + if (order && source === 'submit' && clone[0] && clone[1]) { + clone.sort((a, b) => (generateConfig.isAfter(a, b) ? 1 : -1)); + } + + // Update merged value + const [isSameMergedDates, isSameStart] = isSameDates(mergedValue, clone); + + if (!isSameMergedDates) { + setMergedValue(clone); + + // Trigger calendar change event + if (onCalendarChange) { + onCalendarChange(clone, getDateTexts(clone), { + range: isSameStart ? 'end' : 'start', + }); + } + } + + // Update `submitValue` to trigger event by effect + if (source === 'submit') { + setSubmitValue(clone); + + // Trigger `onChange` if needed + const [isSameSubmitDates] = isSameDates(submitValue, clone); + + const startEmpty = !clone[0]; + const endEmpty = !clone[1]; + + if ( + onChange && + !isSameSubmitDates && + // Validate start + (!startEmpty || allowEmpty[0]) && + // Validate end + (!endEmpty || allowEmpty[1]) + ) { + onChange(clone, getDateTexts(clone)); + } + } + }; + + // ============================ Return ============================ + return [mergedValue, setMergedValue, submitValue, setSubmitValue, triggerChange]; +}