diff --git a/docs/demo/limitation.md b/docs/demo/limitation.md new file mode 100644 index 000000000..8697fd98f --- /dev/null +++ b/docs/demo/limitation.md @@ -0,0 +1,8 @@ +--- +title: limitation +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/demo/multiple.md b/docs/demo/multiple.md new file mode 100644 index 000000000..8cbb6d647 --- /dev/null +++ b/docs/demo/multiple.md @@ -0,0 +1,8 @@ +--- +title: multiple +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/limitation.tsx b/docs/examples/limitation.tsx new file mode 100644 index 000000000..6fe3b06d2 --- /dev/null +++ b/docs/examples/limitation.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import '../../assets/index.less'; +import SinglePicker from '../../src/PickerInput/SinglePicker'; + +import dayjs from 'dayjs'; +import 'dayjs/locale/ar'; +import 'dayjs/locale/zh-cn'; +import LocalizedFormat from 'dayjs/plugin/localizedFormat'; +import dayjsGenerateConfig from '../../src/generate/dayjs'; +import zhCN from '../../src/locale/zh_CN'; + +dayjs.locale('zh-cn'); +dayjs.extend(LocalizedFormat); + +console.clear(); + +(window as any).dayjs = dayjs; + +const sharedLocale = { + locale: zhCN, + generateConfig: dayjsGenerateConfig, + style: { width: 300 }, +}; + +export default () => { + return ( +
+ + + +
+ ); +}; diff --git a/docs/examples/multiple.tsx b/docs/examples/multiple.tsx new file mode 100644 index 000000000..b07c956d1 --- /dev/null +++ b/docs/examples/multiple.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import '../../assets/index.less'; +import type { PickerRef } from '../../src/interface'; +import SinglePicker from '../../src/PickerInput/SinglePicker'; + +import dayjs from 'dayjs'; +import 'dayjs/locale/ar'; +import 'dayjs/locale/zh-cn'; +import LocalizedFormat from 'dayjs/plugin/localizedFormat'; +import dayjsGenerateConfig from '../../src/generate/dayjs'; +import zhCN from '../../src/locale/zh_CN'; + +dayjs.locale('zh-cn'); +dayjs.extend(LocalizedFormat); + +console.clear(); + +(window as any).dayjs = dayjs; + +const sharedLocale = { + locale: zhCN, + generateConfig: dayjsGenerateConfig, + style: { width: 300 }, +}; + +export default () => { + const singleRef = React.useRef(null); + + return ( +
+ + +
+ ); +}; diff --git a/src/PickerInput/Popup/PopupPanel.tsx b/src/PickerInput/Popup/PopupPanel.tsx index 94cfccc4f..0d056e33b 100644 --- a/src/PickerInput/Popup/PopupPanel.tsx +++ b/src/PickerInput/Popup/PopupPanel.tsx @@ -13,8 +13,6 @@ export type PopupPanelProps = MustProp Omit, 'onPickerValueChange' | 'showTime'> & FooterProps & { multiplePanel?: boolean; - minDate?: DateType; - maxDate?: DateType; range?: boolean; onPickerValueChange: (date: DateType) => void; @@ -23,17 +21,8 @@ export type PopupPanelProps = MustProp export default function PopupPanel( props: PopupPanelProps, ) { - const { - picker, - multiplePanel, - pickerValue, - onPickerValueChange, - onSubmit, - minDate, - maxDate, - range, - hoverValue, - } = props; + const { picker, multiplePanel, pickerValue, onPickerValueChange, onSubmit, range, hoverValue } = + props; const { prefixCls, generateConfig } = React.useContext(PickerContext); // ======================== Offset ======================== @@ -63,36 +52,6 @@ export default function PopupPanel( const hideHeader = picker === 'time'; - // ====================== Limitation ====================== - const needLimit = React.useCallback( - (currentPickerValue: DateType) => { - let hidePrev = false; - let hideNext = false; - - const dateBeforePickerValue = internalOffsetDate(currentPickerValue, -1); - if (minDate && generateConfig.isAfter(minDate, dateBeforePickerValue)) { - hidePrev = true; - } - - const dateAfterPickerValue = internalOffsetDate(currentPickerValue, 1); - if (maxDate && generateConfig.isAfter(dateAfterPickerValue, maxDate)) { - hideNext = true; - } - - return { - hidePrev, - hideNext, - }; - }, - [minDate, maxDate, internalOffsetDate, generateConfig], - ); - - const firstPanelNeedLimit = React.useMemo(() => needLimit(pickerValue), [pickerValue, needLimit]); - const secondPanelNeedLimit = React.useMemo( - () => needLimit(nextPickerValue), - [nextPickerValue, needLimit], - ); - // ======================== Props ========================= const pickerProps = { ...props, @@ -113,12 +72,18 @@ export default function PopupPanel( return (
{...pickerProps} /> {...pickerProps} @@ -135,7 +100,6 @@ export default function PopupPanel( diff --git a/src/PickerInput/hooks/useFilledProps.ts b/src/PickerInput/hooks/useFilledProps.ts index f65198364..e87e23bb4 100644 --- a/src/PickerInput/hooks/useFilledProps.ts +++ b/src/PickerInput/hooks/useFilledProps.ts @@ -128,8 +128,9 @@ export default function useFilledProps< const internalPicker: InternalMode = picker === 'date' && showTime ? 'datetime' : picker; /** The picker is `datetime` or `time` */ - const complexPicker = internalPicker === 'time' || internalPicker === 'datetime' || multiple; - const mergedNeedConfirm = needConfirm ?? complexPicker; + const multipleInteractivePicker = internalPicker === 'time' || internalPicker === 'datetime'; + const complexPicker = multipleInteractivePicker || multiple; + const mergedNeedConfirm = needConfirm ?? multipleInteractivePicker; // ========================== Time ========================== // Auto `format` need to check `showTime.showXXX` first. diff --git a/src/PickerInput/hooks/useRangeValue.ts b/src/PickerInput/hooks/useRangeValue.ts index 68a855e15..8e1c8a52a 100644 --- a/src/PickerInput/hooks/useRangeValue.ts +++ b/src/PickerInput/hooks/useRangeValue.ts @@ -137,7 +137,7 @@ export function useInnerValue date) as ValueType, generateConfig); } // Update merged value diff --git a/src/PickerPanel/DatePanel/index.tsx b/src/PickerPanel/DatePanel/index.tsx index fe6fe2a62..7b24b9e2e 100644 --- a/src/PickerPanel/DatePanel/index.tsx +++ b/src/PickerPanel/DatePanel/index.tsx @@ -46,7 +46,8 @@ export default function DatePanel(props: DatePane // ========================== Base ========================== const [info, now] = useInfo(props, mode); const weekFirstDay = generateConfig.locale.getWeekFirstDay(locale.locale); - const baseDate = getWeekStartDate(locale.locale, generateConfig, pickerValue); + const monthStartDate = generateConfig.setDate(pickerValue, 1); + const baseDate = getWeekStartDate(locale.locale, generateConfig, monthStartDate); const month = generateConfig.getMonth(pickerValue); // =========================== PrefixColumn =========================== @@ -176,12 +177,16 @@ export default function DatePanel(props: DatePane
{/* Header */} - { - onPickerValueChange(generateConfig.addMonth(pickerValue, offset)); - }} - onSuperOffset={(offset) => { - onPickerValueChange(generateConfig.addYear(pickerValue, offset)); + + offset={(distance) => generateConfig.addMonth(pickerValue, distance)} + superOffset={(distance) => generateConfig.addYear(pickerValue, distance)} + onChange={onPickerValueChange} + // Limitation + getStart={(date) => generateConfig.setDate(date, 1)} + getEnd={(date) => { + let clone = generateConfig.setDate(date, 1); + clone = generateConfig.addMonth(clone, 1); + return generateConfig.addDate(clone, -1); }} > {monthYearNodes} diff --git a/src/PickerPanel/DecadePanel/index.tsx b/src/PickerPanel/DecadePanel/index.tsx index d57a067d5..7c9c374f7 100644 --- a/src/PickerPanel/DecadePanel/index.tsx +++ b/src/PickerPanel/DecadePanel/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import type { DisabledDate, SharedPanelProps } from '../../interface'; -import { formatValue } from '../../utils/dateUtil'; +import { formatValue, isInRange, isSameDecade } from '../../utils/dateUtil'; import { PanelContext, useInfo } from '../context'; import PanelBody from '../PanelBody'; import PanelHeader from '../PanelHeader'; @@ -15,13 +15,20 @@ export default function DecadePanel( // ========================== Base ========================== const [info] = useInfo(props, 'decade'); - const startYear = Math.floor(generateConfig.getYear(pickerValue) / 100) * 100; - const endYear = startYear + 99; - const baseDate = generateConfig.setYear(pickerValue, startYear - 10); + const getStartYear = (date: DateType) => { + const startYear = Math.floor(generateConfig.getYear(pickerValue) / 100) * 100; + return generateConfig.setYear(date, startYear); + }; + const getEndYear = (date: DateType) => { + const startYear = getStartYear(date); + return generateConfig.addYear(startYear, 99); + }; + + const startYearDate = getStartYear(pickerValue); + const endYearDate = getEndYear(pickerValue); - const startYearDate = generateConfig.setYear(baseDate, startYear); - const endYearDate = generateConfig.setYear(startYearDate, endYear); + const baseDate = generateConfig.addYear(startYearDate, -10); // ========================= Cells ========================== const getCellDate = (date: DateType, offset: number) => { @@ -46,9 +53,11 @@ export default function DecadePanel( }; const getCellClassName = (date: DateType) => { - const dateYear = generateConfig.getYear(date); return { - [`${prefixCls}-cell-in-view`]: startYear <= dateYear && dateYear <= endYear, + [`${prefixCls}-cell-in-view`]: + isSameDecade(generateConfig, date, startYearDate) || + isSameDecade(generateConfig, date, endYearDate) || + isInRange(generateConfig, startYearDate, endYearDate, date), }; }; @@ -88,9 +97,11 @@ export default function DecadePanel(
{/* Header */} { - onPickerValueChange(generateConfig.addYear(pickerValue, offset * 100)); - }} + superOffset={(distance) => generateConfig.addYear(pickerValue, distance * 100)} + onChange={onPickerValueChange} + // Limitation + getStart={getStartYear} + getEnd={getEndYear} > {yearNode} diff --git a/src/PickerPanel/MonthPanel/index.tsx b/src/PickerPanel/MonthPanel/index.tsx index 26421b2b6..2d18e1801 100644 --- a/src/PickerPanel/MonthPanel/index.tsx +++ b/src/PickerPanel/MonthPanel/index.tsx @@ -91,9 +91,11 @@ export default function MonthPanel(
{/* Header */} { - onPickerValueChange(generateConfig.addYear(pickerValue, offset)); - }} + superOffset={(distance) => generateConfig.addYear(pickerValue, distance)} + onChange={onPickerValueChange} + // Limitation + getStart={(date) => generateConfig.setMonth(date, 0)} + getEnd={(date) => generateConfig.setMonth(date, 11)} > {yearNode} diff --git a/src/PickerPanel/PanelHeader.tsx b/src/PickerPanel/PanelHeader.tsx index 6233ec4dd..794962bf6 100644 --- a/src/PickerPanel/PanelHeader.tsx +++ b/src/PickerPanel/PanelHeader.tsx @@ -1,21 +1,32 @@ +import classNames from 'classnames'; import * as React from 'react'; +import { isSameOrAfter } from '../utils/dateUtil'; import { PickerHackContext, usePanelContext } from './context'; const HIDDEN_STYLE: React.CSSProperties = { visibility: 'hidden', }; -export interface HeaderProps { - onOffset?: (offset: number) => void; - onSuperOffset?: (offset: number) => void; +export interface HeaderProps { + offset?: (distance: number, date: DateType) => DateType; + superOffset?: (distance: number, date: DateType) => DateType; + onChange?: (date: DateType) => void; + + // Limitation + getStart?: (date: DateType) => DateType; + getEnd?: (date: DateType) => DateType; children?: React.ReactNode; } -function PanelHeader(props: HeaderProps) { +function PanelHeader(props: HeaderProps) { const { - onOffset, - onSuperOffset, + offset, + superOffset, + onChange, + + getStart, + getEnd, children, } = props; @@ -28,59 +39,136 @@ function PanelHeader(props: HeaderProps) { nextIcon = '\u203A', superPrevIcon = '\u00AB', superNextIcon = '\u00BB', + + // Limitation + minDate, + maxDate, + generateConfig, + locale, + pickerValue, + panelType: type, } = usePanelContext(); const headerPrefixCls = `${prefixCls}-header`; const { hidePrev, hideNext, hideHeader } = React.useContext(PickerHackContext); + // ======================= Limitation ======================= + const disabledOffsetPrev = React.useMemo(() => { + if (!minDate || !offset || !getEnd) { + return false; + } + + const prevPanelLimitDate = getEnd(offset(-1, pickerValue)); + + return !isSameOrAfter(generateConfig, locale, prevPanelLimitDate, minDate, type); + }, [minDate, offset, pickerValue, getEnd, generateConfig, locale, type]); + + const disabledSuperOffsetPrev = React.useMemo(() => { + if (!minDate || !superOffset || !getEnd) { + return false; + } + + const prevPanelLimitDate = getEnd(superOffset(-1, pickerValue)); + + return !isSameOrAfter(generateConfig, locale, prevPanelLimitDate, minDate, type); + }, [minDate, superOffset, pickerValue, getEnd, generateConfig, locale, type]); + + const disabledOffsetNext = React.useMemo(() => { + if (!maxDate || !offset || !getStart) { + return false; + } + + const nextPanelLimitDate = getStart(offset(1, pickerValue)); + + return !isSameOrAfter(generateConfig, locale, maxDate, nextPanelLimitDate, type); + }, [maxDate, offset, pickerValue, getStart, generateConfig, locale, type]); + + const disabledSuperOffsetNext = React.useMemo(() => { + if (!maxDate || !superOffset || !getStart) { + return false; + } + + const nextPanelLimitDate = getStart(superOffset(1, pickerValue)); + + return !isSameOrAfter(generateConfig, locale, maxDate, nextPanelLimitDate, type); + }, [maxDate, superOffset, pickerValue, getStart, generateConfig, locale, type]); + + // ========================= Offset ========================= + const onOffset = (distance: number) => { + if (offset) { + onChange(offset(distance, pickerValue)); + } + }; + + const onSuperOffset = (distance: number) => { + if (superOffset) { + onChange(superOffset(distance, pickerValue)); + } + }; + + // ========================= Render ========================= if (hideHeader) { return null; } - // ========================= Render ========================= + const prevBtnCls = `${headerPrefixCls}-prev-btn`; + const nextBtnCls = `${headerPrefixCls}-next-btn`; + const superPrevBtnCls = `${headerPrefixCls}-super-prev-btn`; + const superNextBtnCls = `${headerPrefixCls}-super-next-btn`; + return (
- {onSuperOffset && ( + {superOffset && ( )} - {onOffset && ( + {offset && ( )}
{children}
- {onOffset && ( + {offset && ( )} - {onSuperOffset && ( + {superOffset && (