Feat(DatePicker): increase presets prop (#6387)

* feat(date-picker): add PresetDate type

* feat(date-picker): add usePresets hook

* feat(date-picker): add PresetPanel Component

* feat(date-picker): add PresetPanel Component

* feat(demo): update Preset Ranges Examples

* feat(docs): add new prop presets

* feat(docs): add new prop presets with english

* fix(RangePicker): footer is not managed by panels

* chore(Picker): prefixCls default rc-picker

* chore(date-picker): update presetted-ranges demo

* chore(date-picker): update rangePickerProps'presets

* feat(date-picker): presets reactively processing

* chore(date-picker): update type

* refactor(RangePicker): deprecated ranges prop

* chore(date-picker): update type

* chore(PickerPanel): del notuse panelRef

---------

Co-authored-by: tangjinzhou <415800467@qq.com>
pull/6423/head
Cherry7 2023-04-05 10:56:07 +08:00 committed by GitHub
parent 48ab5a2f99
commit f6daa8d28f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 86 deletions

View File

@ -18,13 +18,14 @@ We can set presetted ranges to RangePicker to improve user experience.
<template>
<a-space direction="vertical" :size="12">
<a-range-picker v-model:value="value1" :ranges="ranges" />
<a-date-picker :presets="presets" @change="onChange" />
<a-range-picker :presets="rangePresets" @change="onRangeChange" />
<a-range-picker
v-model:value="value2"
style="width: 400px"
:ranges="ranges"
show-time
format="YYYY/MM/DD HH:mm:ss"
:presets="rangePresets"
@change="onRangeChange"
/>
</a-space>
</template>
@ -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<RangeValue>(),
value2: ref<RangeValue>(),
ranges: {
Today: [dayjs(), dayjs()] as RangeValue,
'This Month': [dayjs(), dayjs().endOf('month')] as RangeValue,
},
presets,
rangePresets,
onChange,
onRangeChange,
};
},
});

View File

@ -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<DateType> {
* @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<DateType = any>() {
defaultPickerValue: someType<DateType | string>([Object, String]),
defaultValue: someType<DateType | string>([Object, String]),
value: someType<DateType | string>([Object, String]),
presets: arrayType<PresetDate<DateType>[]>(),
disabledTime: functionType<DisabledTime<DateType>>(),
renderExtraFooter: functionType<(mode: PanelMode) => VueNode>(),
showNow: booleanType(),
@ -189,6 +192,7 @@ export interface DatePickerProps<DateType> {
defaultPickerValue?: DateType | string;
defaultValue?: DateType | string;
value?: DateType | string;
presets?: PresetDate<DateType>[];
disabledTime?: DisabledTime<DateType>;
renderExtraFooter?: (mode: PanelMode) => VueNode;
showNow?: boolean;
@ -204,6 +208,7 @@ function rangePickerProps<DateType>() {
defaultPickerValue: arrayType<RangeValue<DateType> | RangeValue<string>>(),
defaultValue: arrayType<RangeValue<DateType> | RangeValue<string>>(),
value: arrayType<RangeValue<DateType> | RangeValue<string>>(),
presets: arrayType<PresetDate<Array<DateType>>[]>(),
disabledTime: functionType<(date: EventValue<DateType>, type: RangeType) => DisabledTimes>(),
disabled: someType<boolean | [boolean, boolean]>([Boolean, Array]),
renderExtraFooter: functionType<() => VueNode>(),
@ -249,6 +254,7 @@ export interface RangePickerProps<DateType> {
defaultPickerValue?: RangeValue<DateType> | RangeValue<string>;
defaultValue?: RangeValue<DateType> | RangeValue<string>;
value?: RangeValue<DateType> | RangeValue<string>;
presets?: PresetDate<RangeValue<DateType>>[];
disabledTime?: (date: EventValue<DateType>, type: RangeType) => DisabledTimes;
disabled?: [boolean, boolean];
renderExtraFooter?: () => VueNode;

View File

@ -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 | `<SwapRightOutlined />` | |

View File

@ -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 | `<SwapRightOutlined />` | |

View File

@ -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<DateType> = {
inputReadOnly?: boolean;
id?: string;
presets?: PresetDate<DateType>[];
// Value
format?: string | CustomFormat<DateType> | (string | CustomFormat<DateType>)[];
@ -163,6 +167,7 @@ function Picker<DateType>() {
'defaultOpen',
'defaultOpenValue',
'suffixIcon',
'presets',
'clearIcon',
'disabled',
'disabledDate',
@ -203,6 +208,8 @@ function Picker<DateType>() {
// ],
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<DateType>() {
useProvidePanel({
operationRef,
hideHeader: computed(() => picker.value === 'time'),
panelRef: panelDivRef,
onSelect: onContextSelect,
open: mergedOpen,
defaultOpenValue: toRef(props, 'defaultOpenValue'),
@ -477,23 +483,33 @@ function Picker<DateType>() {
};
let panelNode: VueNode = (
<PickerPanel
{...panelProps}
generateConfig={generateConfig}
value={selectedValue.value}
locale={locale}
tabindex={-1}
onSelect={date => {
onSelect?.(date);
setSelectedValue(date);
}}
direction={direction}
onPanelChange={(viewDate, mode) => {
const { onPanelChange } = props;
onLeave(true);
onPanelChange?.(viewDate, mode);
}}
/>
<div class={`${prefixCls}-panel-layout`}>
<PresetPanel
prefixCls={prefixCls}
presets={presetList.value}
onClick={nextValue => {
triggerChange(nextValue);
triggerOpen(false);
}}
/>
<PickerPanel
{...panelProps}
generateConfig={generateConfig}
value={selectedValue.value}
locale={locale}
tabindex={-1}
onSelect={date => {
onSelect?.(date);
setSelectedValue(date);
}}
direction={direction}
onPanelChange={(viewDate, mode) => {
const { onPanelChange } = props;
onLeave(true);
onPanelChange?.(viewDate, mode);
}}
/>
</div>
);
if (panelRender) {
@ -503,6 +519,7 @@ function Picker<DateType>() {
const panel = (
<div
class={`${prefixCls}-panel-container`}
ref={panelDivRef}
onMousedown={e => {
e.preventDefault();
}}

View File

@ -186,7 +186,6 @@ function PickerPanel<DateType>() {
const panelContext = useInjectPanel();
const {
operationRef,
panelRef: panelDivRef,
onSelect: onContextSelect,
hideRanges,
defaultOpenValue,
@ -601,7 +600,6 @@ function PickerPanel<DateType>() {
onKeydown={onInternalKeydown}
onBlur={onInternalBlur}
onMousedown={onMousedown}
ref={panelDivRef}
>
{panelNode}
{extraFooter || rangesNode || todayNode ? (

View File

@ -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 (
<div class={`${props.prefixCls}-presets`}>
<ul>
{props.presets.map(({ label, value }, index) => (
<li
key={index}
onClick={() => {
props.onClick(value);
}}
onMouseenter={() => {
props.onHover?.(value);
}}
onMouseleave={() => {
props.onHover?.(null);
}}
>
{label}
</li>
))}
</ul>
</div>
);
};
},
});

View File

@ -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<DateType> = {
placeholder?: [string, string];
disabled?: boolean | [boolean, boolean];
disabledTime?: (date: EventValue<DateType>, type: RangeType) => DisabledTimes;
presets?: PresetDate<RangeValue<DateType>>[];
/** @deprecated Please use `presets` instead */
ranges?: Record<
string,
Exclude<RangeValue<DateType>, null> | (() => Exclude<RangeValue<DateType>, null>)
@ -139,6 +150,7 @@ type OmitPickerProps<Props> = Omit<
| 'onPickerValueChange'
| 'onOk'
| 'dateRender'
| 'presets'
>;
type RangeShowTimeObject<DateType> = Omit<SharedTimeProps<DateType>, 'defaultValue'> & {
@ -238,13 +250,17 @@ function RangerPicker<DateType>() {
'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<Record<number, boolean>>({});
const containerRef = ref<HTMLDivElement>(null);
@ -830,28 +846,6 @@ function RangerPicker<DateType>() {
},
});
// ============================ 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<DateType>() {
!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<DateType>() {
}
let mergedNodes: VueNode = (
<>
<div class={`${prefixCls}-panels`}>{panels}</div>
{(extraNode || rangesNode) && (
<div class={`${prefixCls}-footer`}>
{extraNode}
{rangesNode}
</div>
)}
</>
<div class={`${prefixCls}-panel-layout`}>
<PresetPanel
prefixCls={prefixCls}
presets={presetList.value}
onClick={nextValue => {
triggerChange(nextValue, null);
triggerOpen(false, mergedActivePickerIndex.value);
}}
onHover={hoverValue => {
setRangeHoverValue(hoverValue);
}}
/>
<div>
<div class={`${prefixCls}-panels`}>{panels}</div>
{(extraNode || rangesNode) && (
<div class={`${prefixCls}-footer`}>
{extraNode}
{rangesNode}
</div>
)}
</div>
</div>
);
if (panelRender) {

View File

@ -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<T>(
presets?: ComputedRef<PresetDate<T>[]>,
legacyRanges?: ComputedRef<Record<string, T | (() => T)>>,
): ComputedRef<PresetDate<T>[]> {
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<PresetDate<T>[]>;
}

View File

@ -98,14 +98,18 @@ export type RangeValue<DateType> = [EventValue<DateType>, EventValue<DateType>]
export type Components = {
button?: any;
rangeItem?: any;
};
export type RangeList = {
label: string;
label: VueNode;
onClick: () => void;
onMouseenter: () => void;
onMouseleave: () => void;
}[];
export type CustomFormat<DateType> = (value: DateType) => string;
export interface PresetDate<T> {
label: VueNode;
value: T;
}

View File

@ -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 }) => (
<li key={label} class={`${prefixCls}-preset`}>
<Item onClick={onClick} onMouseenter={onMouseenter} onMouseleave={onMouseleave}>
{label}
</Item>
</li>
))}
</>
);
}
if (needConfirmButton) {
const Button = (components.button || 'button') as any;
if (onNow && !presetNode && showNow !== false) {
if (onNow && showNow !== false) {
presetNode = (
<li class={`${prefixCls}-now`}>
<a class={`${prefixCls}-now-btn`} onClick={onNow}>