diff --git a/components/date-picker/demo/presetted-ranges.vue b/components/date-picker/demo/presetted-ranges.vue index a825bebb5..e646769b1 100644 --- a/components/date-picker/demo/presetted-ranges.vue +++ b/components/date-picker/demo/presetted-ranges.vue @@ -18,13 +18,14 @@ We can set presetted ranges to RangePicker to improve user experience. @@ -34,13 +35,40 @@ import { defineComponent, ref } from 'vue'; type RangeValue = [Dayjs, Dayjs]; export default defineComponent({ setup() { + const onChange = (date: Dayjs) => { + if (date) { + console.log('Date: ', date); + } else { + console.log('Clear'); + } + }; + const onRangeChange = (dates: RangeValue, dateStrings: string[]) => { + if (dates) { + console.log('From: ', dates[0], ', to: ', dates[1]); + console.log('From: ', dateStrings[0], ', to: ', dateStrings[1]); + } else { + console.log('Clear'); + } + }; + + const presets = ref([ + { label: 'Yesterday', value: dayjs().add(-1, 'd') }, + { label: 'Last Week', value: dayjs().add(-7, 'd') }, + { label: 'Last Month', value: dayjs().add(-1, 'month') }, + ]); + + const rangePresets = ref([ + { label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()] }, + { label: 'Last 14 Days', value: [dayjs().add(-14, 'd'), dayjs()] }, + { label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()] }, + { label: 'Last 90 Days', value: [dayjs().add(-90, 'd'), dayjs()] }, + ]); return { - value1: ref(), - value2: ref(), - ranges: { - Today: [dayjs(), dayjs()] as RangeValue, - 'This Month': [dayjs(), dayjs().endOf('month')] as RangeValue, - }, + presets, + rangePresets, + + onChange, + onRangeChange, }; }, }); diff --git a/components/date-picker/generatePicker/props.ts b/components/date-picker/generatePicker/props.ts index 834537bff..1af653de5 100644 --- a/components/date-picker/generatePicker/props.ts +++ b/components/date-picker/generatePicker/props.ts @@ -3,6 +3,7 @@ import type { CSSProperties } from 'vue'; import type { PickerLocale } from '.'; import type { SizeType } from '../../config-provider'; import type { + PresetDate, CustomFormat, DisabledTime, DisabledTimes, @@ -118,6 +119,7 @@ export interface CommonProps { * @deprecated `dropdownClassName` is deprecated which will be removed in next major * version.Please use `popupClassName` instead. */ + dropdownClassName?: string; popupClassName?: string; popupStyle?: CSSProperties; @@ -176,6 +178,7 @@ function datePickerProps() { defaultPickerValue: someType([Object, String]), defaultValue: someType([Object, String]), value: someType([Object, String]), + presets: arrayType[]>(), disabledTime: functionType>(), renderExtraFooter: functionType<(mode: PanelMode) => VueNode>(), showNow: booleanType(), @@ -189,6 +192,7 @@ export interface DatePickerProps { defaultPickerValue?: DateType | string; defaultValue?: DateType | string; value?: DateType | string; + presets?: PresetDate[]; disabledTime?: DisabledTime; renderExtraFooter?: (mode: PanelMode) => VueNode; showNow?: boolean; @@ -204,6 +208,7 @@ function rangePickerProps() { defaultPickerValue: arrayType | RangeValue>(), defaultValue: arrayType | RangeValue>(), value: arrayType | RangeValue>(), + presets: arrayType>[]>(), disabledTime: functionType<(date: EventValue, type: RangeType) => DisabledTimes>(), disabled: someType([Boolean, Array]), renderExtraFooter: functionType<() => VueNode>(), @@ -249,6 +254,7 @@ export interface RangePickerProps { defaultPickerValue?: RangeValue | RangeValue; defaultValue?: RangeValue | RangeValue; value?: RangeValue | RangeValue; + presets?: PresetDate>[]; disabledTime?: (date: EventValue, type: RangeType) => DisabledTimes; disabled?: [boolean, boolean]; renderExtraFooter?: () => VueNode; diff --git a/components/date-picker/index.en-US.md b/components/date-picker/index.en-US.md index 49fedbb57..78ad10be7 100644 --- a/components/date-picker/index.en-US.md +++ b/components/date-picker/index.en-US.md @@ -96,6 +96,7 @@ The following APIs are shared by DatePicker, RangePicker. | placeholder | The placeholder of date input | string \| \[string,string] | - | | | placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | popupStyle | To customize the style of the popup calendar | CSSProperties | {} | | +| presets | The preset ranges for quick selection | { label: slot, value: [dayjs](https://day.js.org/) }[] | - | | | prevIcon | The custom prev icon | slot | - | 3.0 | | size | To determine the size of the input box, the height of `large` and `small`, are 40px and 24px respectively, while default size is 32px | `large` \| `middle` \| `small` | - | | | status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | @@ -174,6 +175,7 @@ The following APIs are shared by DatePicker, RangePicker. | disabled | If disable start or end | \[boolean, boolean] | - | | | disabledTime | To specify the time that cannot be selected | function(date: dayjs, partial: `start` \| `end`) | - | | | format | To set the date format, refer to [dayjs](https://day.js.org/) | [formatType](#formatType) | `YYYY-MM-DD HH:mm:ss` | | +| presets | The preset ranges for quick selection | { label: slot, value: [dayjs](https://day.js.org/)\[] }[] | - | | | ranges | The preseted ranges for quick selection | { \[range: string]: [dayjs](https://day.js.org/)\[] } \| { \[range: string]: () => [dayjs](https://day.js.org/)\[] } | - | | | renderExtraFooter | Render extra footer in panel | v-slot:renderExtraFooter="mode" | - | | | separator | Set separator between inputs | string \| v-slot:separator | `` | | diff --git a/components/date-picker/index.zh-CN.md b/components/date-picker/index.zh-CN.md index ef8a0af12..9b948ecf0 100644 --- a/components/date-picker/index.zh-CN.md +++ b/components/date-picker/index.zh-CN.md @@ -98,6 +98,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3OpRQKcygo8AAA | placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | popupStyle | 额外的弹出日历样式 | CSSProperties | {} | | | prevIcon | 自定义上一个图标 | slot | - | 3.0 | +| presets | 预设时间范围快捷选择 | { label: slot, value: [dayjs](https://day.js.org/) }[] | - | | | size | 输入框大小,`large` 高度为 40px,`small` 为 24px,默认是 32px | `large` \| `middle` \| `small` | - | | | status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | 自定义的选择框后缀图标 | v-slot:suffixIcon | - | | @@ -175,6 +176,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3OpRQKcygo8AAA | disabled | 禁用起始项 | \[boolean, boolean] | - | | | disabledTime | 不可选择的时间 | function(date: dayjs, partial: `start` \| `end`) | - | | | format | 展示的日期格式 | [formatType](#formatType) | `YYYY-MM-DD HH:mm:ss` | | +| presets | 预设时间范围快捷选择 | { label: slot, value: [dayjs](https://day.js.org/)\[] }[] | - | | | ranges | 预设时间范围快捷选择 | { \[range: string]: [dayjs](https://day.js.org/)\[] } \| { \[range: string]: () => [dayjs](https://day.js.org/)\[] } | - | | | renderExtraFooter | 在面板中添加额外的页脚 | v-slot:renderExtraFooter="mode" | - | | | separator | 设置分隔符 | string \| v-slot:separator | `` | | diff --git a/components/vc-picker/Picker.tsx b/components/vc-picker/Picker.tsx index 1754d5586..5879251e9 100644 --- a/components/vc-picker/Picker.tsx +++ b/components/vc-picker/Picker.tsx @@ -18,16 +18,18 @@ import type { } from './PickerPanel'; import PickerPanel from './PickerPanel'; import PickerTrigger from './PickerTrigger'; +import PresetPanel from './PresetPanel'; import { formatValue, isEqual, parseValue } from './utils/dateUtil'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import type { ContextOperationRefProps } from './PanelContext'; import { useProvidePanel } from './PanelContext'; -import type { CustomFormat, PickerMode } from './interface'; +import type { CustomFormat, PickerMode, PresetDate } from './interface'; import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; import usePickerInput from './hooks/usePickerInput'; import useTextValueMapping from './hooks/useTextValueMapping'; import useValueTexts from './hooks/useValueTexts'; import useHoverValue from './hooks/useHoverValue'; +import usePresets from './hooks/usePresets'; import type { CSSProperties, HTMLAttributes, Ref } from 'vue'; import { computed, defineComponent, ref, toRef, watch } from 'vue'; import type { ChangeEvent, FocusEventHandler, MouseEventHandler } from '../_util/EventInterface'; @@ -61,6 +63,8 @@ export type PickerSharedProps = { inputReadOnly?: boolean; id?: string; + presets?: PresetDate[]; + // Value format?: string | CustomFormat | (string | CustomFormat)[]; @@ -163,6 +167,7 @@ function Picker() { 'defaultOpen', 'defaultOpenValue', 'suffixIcon', + 'presets', 'clearIcon', 'disabled', 'disabledDate', @@ -203,6 +208,8 @@ function Picker() { // ], setup(props, { attrs, expose }) { const inputRef = ref(null); + const presets = computed(() => props.presets); + const presetList = usePresets(presets); const picker = computed(() => props.picker ?? 'date'); const needConfirmButton = computed( () => (picker.value === 'date' && !!props.showTime) || picker.value === 'time', @@ -408,7 +415,6 @@ function Picker() { useProvidePanel({ operationRef, hideHeader: computed(() => picker.value === 'time'), - panelRef: panelDivRef, onSelect: onContextSelect, open: mergedOpen, defaultOpenValue: toRef(props, 'defaultOpenValue'), @@ -477,23 +483,33 @@ function Picker() { }; let panelNode: VueNode = ( - { - onSelect?.(date); - setSelectedValue(date); - }} - direction={direction} - onPanelChange={(viewDate, mode) => { - const { onPanelChange } = props; - onLeave(true); - onPanelChange?.(viewDate, mode); - }} - /> +
+ { + triggerChange(nextValue); + triggerOpen(false); + }} + /> + { + onSelect?.(date); + setSelectedValue(date); + }} + direction={direction} + onPanelChange={(viewDate, mode) => { + const { onPanelChange } = props; + onLeave(true); + onPanelChange?.(viewDate, mode); + }} + /> +
); if (panelRender) { @@ -503,6 +519,7 @@ function Picker() { const panel = (
{ e.preventDefault(); }} diff --git a/components/vc-picker/PickerPanel.tsx b/components/vc-picker/PickerPanel.tsx index 1ce896e68..8677db201 100644 --- a/components/vc-picker/PickerPanel.tsx +++ b/components/vc-picker/PickerPanel.tsx @@ -186,7 +186,6 @@ function PickerPanel() { const panelContext = useInjectPanel(); const { operationRef, - panelRef: panelDivRef, onSelect: onContextSelect, hideRanges, defaultOpenValue, @@ -601,7 +600,6 @@ function PickerPanel() { onKeydown={onInternalKeydown} onBlur={onInternalBlur} onMousedown={onMousedown} - ref={panelDivRef} > {panelNode} {extraFooter || rangesNode || todayNode ? ( diff --git a/components/vc-picker/PresetPanel.tsx b/components/vc-picker/PresetPanel.tsx new file mode 100644 index 000000000..7ee01bef8 --- /dev/null +++ b/components/vc-picker/PresetPanel.tsx @@ -0,0 +1,43 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + name: 'PresetPanel', + props: { + prefixCls: String, + presets: { + type: Array, + default: () => [], + }, + onClick: Function, + onHover: Function, + }, + setup(props) { + return () => { + if (!props.presets.length) { + return null; + } + return ( +
+
    + {props.presets.map(({ label, value }, index) => ( +
  • { + props.onClick(value); + }} + onMouseenter={() => { + props.onHover?.(value); + }} + onMouseleave={() => { + props.onHover?.(null); + }} + > + {label} +
  • + ))} +
+
+ ); + }; + }, +}); diff --git a/components/vc-picker/RangePicker.tsx b/components/vc-picker/RangePicker.tsx index 4f8b11cc0..d497b9511 100644 --- a/components/vc-picker/RangePicker.tsx +++ b/components/vc-picker/RangePicker.tsx @@ -1,9 +1,17 @@ -import type { DisabledTimes, PanelMode, PickerMode, RangeValue, EventValue } from './interface'; +import type { + DisabledTimes, + PanelMode, + PickerMode, + RangeValue, + EventValue, + PresetDate, +} from './interface'; 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 PresetPanel from './PresetPanel'; import getDataOrAriaProps, { toArray, getValue, updateValues } from './utils/miscUtil'; import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; import type { ContextOperationRefProps } from './PanelContext'; @@ -19,6 +27,7 @@ import { } from './utils/dateUtil'; import useValueTexts from './hooks/useValueTexts'; import useTextValueMapping from './hooks/useTextValueMapping'; +import usePresets from './hooks/usePresets'; import type { GenerateConfig } from './generate'; import type { PickerPanelProps } from '.'; import { RangeContextProvider } from './RangeContext'; @@ -91,6 +100,8 @@ export type RangePickerSharedProps = { placeholder?: [string, string]; disabled?: boolean | [boolean, boolean]; disabledTime?: (date: EventValue, type: RangeType) => DisabledTimes; + presets?: PresetDate>[]; + /** @deprecated Please use `presets` instead */ ranges?: Record< string, Exclude, null> | (() => Exclude, null>) @@ -139,6 +150,7 @@ type OmitPickerProps = Omit< | 'onPickerValueChange' | 'onOk' | 'dateRender' + | 'presets' >; type RangeShowTimeObject = Omit, 'defaultValue'> & { @@ -238,13 +250,17 @@ function RangerPicker() { 'secondStep', 'hideDisabledOptions', 'disabledMinutes', + 'presets', ] as any, setup(props, { attrs, expose }) { const needConfirmButton = computed( () => (props.picker === 'date' && !!props.showTime) || props.picker === 'time', ); const getPortal = useProviderTrigger(); - // We record opened status here in case repeat open with picker + const presets = computed(() => props.presets); + const ranges = computed(() => props.ranges); + const presetList = usePresets(presets, ranges); + // We record oqqpened status here in case repeat open with picker const openRecordsRef = ref>({}); const containerRef = ref(null); @@ -830,28 +846,6 @@ function RangerPicker() { }, }); - // ============================ 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 ( @@ -1044,7 +1038,6 @@ function RangerPicker() { !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); @@ -1099,15 +1092,28 @@ function RangerPicker() { } let mergedNodes: VueNode = ( - <> -
{panels}
- {(extraNode || rangesNode) && ( -
- {extraNode} - {rangesNode} -
- )} - +
+ { + triggerChange(nextValue, null); + triggerOpen(false, mergedActivePickerIndex.value); + }} + onHover={hoverValue => { + setRangeHoverValue(hoverValue); + }} + /> +
+
{panels}
+ {(extraNode || rangesNode) && ( +
+ {extraNode} + {rangesNode} +
+ )} +
+
); if (panelRender) { diff --git a/components/vc-picker/hooks/usePresets.ts b/components/vc-picker/hooks/usePresets.ts new file mode 100644 index 000000000..9f2c4861c --- /dev/null +++ b/components/vc-picker/hooks/usePresets.ts @@ -0,0 +1,30 @@ +import type { ComputedRef } from 'vue'; +import { computed } from 'vue'; + +import warning from 'ant-design-vue/es/vc-util/warning'; +import type { PresetDate } from '../interface'; + +export default function usePresets( + presets?: ComputedRef[]>, + legacyRanges?: ComputedRef T)>>, +): ComputedRef[]> { + if (presets.value) { + return presets; + } + if (legacyRanges && legacyRanges.value) { + warning(false, '`ranges` is deprecated. Please use `presets` instead.'); + + return computed(() => { + const rangeLabels = Object.keys(legacyRanges.value); + return rangeLabels.map(label => { + const range = legacyRanges.value[label]; + const newValues = typeof range === 'function' ? (range as any)() : range; + return { + label, + value: newValues, + }; + }); + }); + } + return [] as unknown as ComputedRef[]>; +} diff --git a/components/vc-picker/interface.ts b/components/vc-picker/interface.ts index fc710d812..58b7106da 100644 --- a/components/vc-picker/interface.ts +++ b/components/vc-picker/interface.ts @@ -98,14 +98,18 @@ export type RangeValue = [EventValue, EventValue] export type Components = { button?: any; - rangeItem?: any; }; export type RangeList = { - label: string; + label: VueNode; onClick: () => void; onMouseenter: () => void; onMouseleave: () => void; }[]; export type CustomFormat = (value: DateType) => string; + +export interface PresetDate { + label: VueNode; + value: T; +} diff --git a/components/vc-picker/utils/getRanges.tsx b/components/vc-picker/utils/getRanges.tsx index 5f1e68ce4..e6a200a72 100644 --- a/components/vc-picker/utils/getRanges.tsx +++ b/components/vc-picker/utils/getRanges.tsx @@ -1,9 +1,8 @@ import type { VueNode } from '../../_util/type'; -import type { Components, RangeList, Locale } from '../interface'; +import type { Components, Locale } from '../interface'; export type RangesProps = { prefixCls: string; - rangeList?: RangeList; components?: Components; needConfirmButton: boolean; onNow?: null | (() => void) | false; @@ -15,7 +14,6 @@ export type RangesProps = { export default function getRanges({ prefixCls, - rangeList = [], components = {}, needConfirmButton, onNow, @@ -27,26 +25,10 @@ export default function getRanges({ let presetNode: VueNode; let okNode: VueNode; - if (rangeList.length) { - const Item = (components.rangeItem || 'span') as any; - - presetNode = ( - <> - {rangeList.map(({ label, onClick, onMouseenter, onMouseleave }) => ( -
  • - - {label} - -
  • - ))} - - ); - } - if (needConfirmButton) { const Button = (components.button || 'button') as any; - if (onNow && !presetNode && showNow !== false) { + if (onNow && showNow !== false) { presetNode = (