feat: datePicker add status & placement

pull/5820/head
tangjinzhou 2022-05-15 14:48:46 +08:00
parent 353f470c11
commit 4eb8088645
21 changed files with 321 additions and 38 deletions

View File

@ -9,6 +9,9 @@ import type { Locale } from '../locale-provider';
type GlobalFormCOntextProps = {
validateMessages?: Ref<ValidateMessages>;
};
export type DirectionType = 'ltr' | 'rtl' | undefined;
export const GlobalFormContextKey: InjectionKey<GlobalFormCOntextProps> =
Symbol('GlobalFormContextKey');

View File

@ -17,7 +17,14 @@ import type { ValidateMessages } from '../form/interface';
import type { ConfigProviderProps, Theme } from './context';
import { configProviderProps, useProvideGlobalForm } from './context';
export type { ConfigProviderProps, Theme, SizeType, Direction, CSPConfig } from './context';
export type {
ConfigProviderProps,
Theme,
SizeType,
Direction,
CSPConfig,
DirectionType,
} from './context';
export const defaultPrefixCls = 'ant';
function getGlobalPrefixCls() {
return globalConfigForApi.prefixCls || defaultPrefixCls;

View File

@ -15,6 +15,8 @@
<Mode />
<Switchable />
<Suffix />
<statusVue />
<placementVue />
</demo-sort>
</template>
<script>
@ -33,6 +35,8 @@ import Time from './time.vue';
import Suffix from './suffix.vue';
import Bordered from './bordered.vue';
import RangePicker from './range-picker.vue';
import placementVue from './placement.vue';
import statusVue from './status.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
@ -41,6 +45,8 @@ export default defineComponent({
CN,
US,
components: {
statusVue,
placementVue,
Basic,
DateRender,
DisabledDate,

View File

@ -0,0 +1,46 @@
<docs>
---
order: 28
title:
zh-CN: 弹出位置
en-US: Popup Placement
---
## zh-CN
可以通过 `placement` 手动指定弹出的位置
## en-US
You can manually specify the position of the popup via `placement`.
</docs>
<template>
<a-radio-group v-model:value="placement">
<a-radio-button value="topLeft">topLeft</a-radio-button>
<a-radio-button value="topRight">topRight</a-radio-button>
<a-radio-button value="bottomLeft">bottomLeft</a-radio-button>
<a-radio-button value="bottomRight">bottomRight</a-radio-button>
</a-radio-group>
<br />
<br />
<a-space direction="vertical" :size="12">
<a-date-picker v-model:value="value1" :placement="placement" />
<a-range-picker v-model:value="value2" :placement="placement" />
</a-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import type { Dayjs } from 'dayjs';
export default defineComponent({
setup() {
const placement = ref('topLeft' as const);
return {
placement,
value1: ref<Dayjs>(),
value2: ref<[Dayjs, Dayjs]>(),
};
},
});
</script>

View File

@ -0,0 +1,35 @@
<docs>
---
order: 19
version: 4.19.0
title:
zh-CN: 自定义状态
en-US: Status
---
## zh-CN
使用 `status` DatePicker 添加状态可选 `error` 或者 `warning`
## en-US
Add status to DatePicker with `status`, which could be `error` or `warning`.
</docs>
<template>
<a-space direction="vertical" style="width: 100%">
<a-date-picker status="error" />
<a-date-picker status="warning" />
<a-range-picker status="error" />
<a-date-picker status="warning" />
</a-space>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>

View File

@ -6,7 +6,7 @@ import { RangePicker as VCRangePicker } from '../../vc-picker';
import type { GenerateConfig } from '../../vc-picker/generate/index';
import enUS from '../locale/en_US';
import { useLocaleReceiver } from '../../locale-provider/LocaleReceiver';
import { getRangePlaceholder } from '../util';
import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';
import { getTimeProps, Components } from '.';
import { computed, defineComponent, nextTick, onMounted, ref } from 'vue';
import useConfigInject from '../../_util/hooks/useConfigInject';
@ -16,8 +16,9 @@ import { commonProps, rangePickerProps } from './props';
import type { PanelMode, RangeValue } from '../../vc-picker/interface';
import type { RangePickerSharedProps } from '../../vc-picker/RangePicker';
import devWarning from '../../vc-util/devWarning';
import { useInjectFormItemContext } from '../../form/FormItemContext';
import { FormItemInputContext, useInjectFormItemContext } from '../../form/FormItemContext';
import omit from '../../_util/omit';
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
export default function generateRangePicker<DateType, ExtraProps = {}>(
generateConfig: GenerateConfig<DateType>,
@ -46,6 +47,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
setup(_props, { expose, slots, attrs, emit }) {
const props = _props as unknown as CommonProps<DateType> & RangePickerProps<DateType>;
const formItemContext = useInjectFormItemContext();
const formItemInputContext = FormItemInputContext.useInject();
devWarning(
!attrs.getCalendarContainer,
'DatePicker',
@ -166,6 +168,12 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
: {}),
};
const pre = prefixCls.value;
const suffixNode = (
<>
{suffixIcon || (picker === 'time' ? <ClockCircleOutlined /> : <CalendarOutlined />)}
{formItemInputContext.hasFeedback && formItemInputContext.feedbackIcon}
</>
);
return (
<VCRangePicker
dateRender={dateRender}
@ -178,10 +186,9 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
)
}
ref={pickerRef}
dropdownAlign={transPlacement2DropdownAlign(direction.value, props.placement)}
placeholder={getRangePlaceholder(picker, locale, placeholder as [string, string])}
suffixIcon={
suffixIcon || (picker === 'time' ? <ClockCircleOutlined /> : <CalendarOutlined />)
}
suffixIcon={suffixNode}
clearIcon={clearIcon || <CloseCircleFilled />}
allowClear={allowClear}
transitionName={transitionName || `${rootPrefixCls.value}-slide-up`}
@ -197,6 +204,11 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
[`${pre}-${size.value}`]: size.value,
[`${pre}-borderless`]: !bordered,
},
getStatusClassNames(
pre,
getMergedStatus(formItemInputContext.status, props.status),
formItemInputContext.hasFeedback,
),
attrs.class,
)}
locale={locale!.lang}

View File

@ -5,7 +5,7 @@ import RCPicker from '../../vc-picker';
import type { PanelMode, PickerMode } from '../../vc-picker/interface';
import type { GenerateConfig } from '../../vc-picker/generate/index';
import enUS from '../locale/en_US';
import { getPlaceholder } from '../util';
import { getPlaceholder, transPlacement2DropdownAlign } from '../util';
import { useLocaleReceiver } from '../../locale-provider/LocaleReceiver';
import { getTimeProps, Components } from '.';
import { computed, defineComponent, nextTick, onMounted, ref } from 'vue';
@ -15,7 +15,8 @@ import type { CommonProps, DatePickerProps } from './props';
import { commonProps, datePickerProps } from './props';
import devWarning from '../../vc-util/devWarning';
import { useInjectFormItemContext } from '../../form/FormItemContext';
import { FormItemInputContext, useInjectFormItemContext } from '../../form/FormItemContext';
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
export default function generateSinglePicker<DateType, ExtraProps = {}>(
generateConfig: GenerateConfig<DateType>,
@ -49,6 +50,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
DatePickerProps<DateType> &
ExtraProps;
const formItemContext = useInjectFormItemContext();
const formItemInputContext = FormItemInputContext.useInject();
devWarning(
!(props.monthCellContentRender || slots.monthCellContentRender),
'DatePicker',
@ -185,6 +187,12 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
: {}),
};
const pre = prefixCls.value;
const suffixNode = (
<>
{suffixIcon || (picker === 'time' ? <ClockCircleOutlined /> : <CalendarOutlined />)}
{formItemInputContext.hasFeedback && formItemInputContext.feedbackIcon}
</>
);
return (
<RCPicker
monthCellRender={monthCellRender}
@ -192,10 +200,8 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
renderExtraFooter={renderExtraFooter}
ref={pickerRef}
placeholder={getPlaceholder(mergedPicker, locale, placeholder)}
suffixIcon={
suffixIcon ||
(mergedPicker === 'time' ? <ClockCircleOutlined /> : <CalendarOutlined />)
}
suffixIcon={suffixNode}
dropdownAlign={transPlacement2DropdownAlign(direction.value, props.placement)}
clearIcon={clearIcon || <CloseCircleFilled />}
allowClear={allowClear}
transitionName={transitionName || `${rootPrefixCls.value}-slide-up`}
@ -213,6 +219,11 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
[`${pre}-${size.value}`]: size.value,
[`${pre}-borderless`]: !bordered,
},
getStatusClassNames(
pre,
getMergedStatus(formItemInputContext.status, props.status),
formItemInputContext.hasFeedback,
),
attrs.class,
)}
prefixCls={pre}

View File

@ -17,13 +17,18 @@ function toArray<T>(list: T | T[]): T[] {
return Array.isArray(list) ? list : [list];
}
export function getTimeProps<DateType>(
props: { format?: string; picker?: PickerMode } & SharedTimeProps<DateType>,
export function getTimeProps<DateType, DisabledTime>(
props: { format?: string; picker?: PickerMode } & Omit<
SharedTimeProps<DateType>,
'disabledTime'
> & {
disabledTime?: DisabledTime;
},
) {
const { format, picker, showHour, showMinute, showSecond, use12Hours } = props;
const firstFormat = toArray(format)[0];
const showTimeObj: SharedTimeProps<DateType> = { ...props };
const showTimeObj = { ...props };
if (firstFormat && typeof firstFormat === 'string') {
if (!firstFormat.includes('s') && showSecond === undefined) {

View File

@ -2,7 +2,6 @@ import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInte
import type { CSSProperties, PropType } from 'vue';
import type { PickerLocale } from '.';
import type { SizeType } from '../../config-provider';
import type { AlignType } from '../../vc-align/interface';
import type {
CustomFormat,
DisabledTime,
@ -17,12 +16,16 @@ import type { MonthCellRender } from '../../vc-picker/panels/MonthPanel/MonthBod
import type { SharedTimeProps } from '../../vc-picker/panels/TimePanel';
import type { RangeDateRender, RangeInfo, RangeType } from '../../vc-picker/RangePicker';
import type { VueNode } from '../../_util/type';
import { tuple } from '../../_util/type';
import type { InputStatus } from '../../_util/statusUtils';
const DataPickerPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight');
type DataPickerPlacement = typeof DataPickerPlacements[number];
function commonProps<DateType = any>() {
return {
id: String,
dropdownClassName: String,
dropdownAlign: { type: Object as PropType<AlignType> },
popupStyle: { type: Object as PropType<CSSProperties> },
transitionName: String,
placeholder: String,
@ -82,6 +85,8 @@ function commonProps<DateType = any>() {
mode: { type: String as PropType<PanelMode> },
picker: { type: String as PropType<PickerMode> },
valueFormat: String,
placement: String as PropType<DataPickerPlacement>,
status: String as PropType<InputStatus>,
};
}
@ -89,7 +94,6 @@ export interface CommonProps<DateType> {
id?: string;
prefixCls?: string;
dropdownClassName?: string;
dropdownAlign?: AlignType;
popupStyle?: CSSProperties;
transitionName?: string;
placeholder?: string;
@ -136,6 +140,8 @@ export interface CommonProps<DateType> {
mode?: PanelMode;
picker?: PickerMode;
valueFormat?: string;
placement?: DataPickerPlacement;
status?: InputStatus;
}
function datePickerProps<DateType = any>() {

View File

@ -92,9 +92,11 @@ The following APIs are shared by DatePicker, RangePicker.
| open | The open state of picker | boolean | - | |
| picker | Set picker type | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter` |
| 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 | {} | |
| 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 |
| suffixIcon | The custom suffix icon | v-slot:suffixIcon | - | |
| superNextIcon | The custom super next icon | slot | - | 3.0 |
| superPrevIcon | The custom super prev icon | slot | - | 3.0 |

View File

@ -93,9 +93,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/RT_USzA48/DatePicker.svg
| open | 控制弹层是否展开 | boolean | - | |
| picker | 设置选择器类型 | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter` |
| placeholder | 输入框提示文字 | string \| \[string, string] | - | |
| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 |
| popupStyle | 额外的弹出日历样式 | CSSProperties | {} | |
| prevIcon | 自定义上一个图标 | slot | - | 3.0 |
| size | 输入框大小,`large` 高度为 40px`small` 为 24px默认是 32px | `large` \| `middle` \| `small` | - | |
| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 |
| suffixIcon | 自定义的选择框后缀图标 | v-slot:suffixIcon | - | |
| superNextIcon | 自定义 `<<` 切换图标 | slot | - | 3.0 |
| superPrevIcon | 自定义 `>>` 切换图标 | slot | - | 3.0 |

View File

@ -1,6 +1,7 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
@import './status';
@picker-prefix-cls: ~'@{ant-prefix}-picker';
@ -13,7 +14,7 @@
}
.@{picker-prefix-cls} {
@arrow-size: 10px;
@arrow-size: @popover-arrow-width;
.reset-component();
.picker-padding(@input-height-base, @font-size-base, @input-padding-horizontal-base);
@ -22,7 +23,7 @@
align-items: center;
background: @picker-bg;
border: @border-width-base @border-style-base @select-border-color;
border-radius: @border-radius-base;
border-radius: @control-border-radius;
transition: border @animation-duration-slow, box-shadow @animation-duration-slow;
&:hover,
@ -106,6 +107,8 @@
}
&-suffix {
display: flex;
flex: none;
align-self: center;
margin-left: (@padding-xs / 2);
color: @disabled-color;
@ -114,6 +117,10 @@
> * {
vertical-align: top;
&:not(:last-child) {
margin-right: 8px;
}
}
}
@ -221,17 +228,17 @@
&-placement-bottomLeft {
.@{picker-prefix-cls}-range-arrow {
top: (@arrow-size / 2) - (@arrow-size / 3);
top: (@arrow-size / 2) - (@arrow-size / 3) + 0.7px;
display: block;
transform: rotate(-45deg);
transform: rotate(-135deg) translateY(1px);
}
}
&-placement-topLeft {
.@{picker-prefix-cls}-range-arrow {
bottom: (@arrow-size / 2) - (@arrow-size / 3);
bottom: (@arrow-size / 2) - (@arrow-size / 3) + 0.7px;
display: block;
transform: rotate(135deg);
transform: rotate(45deg);
}
}
@ -311,19 +318,14 @@
width: @arrow-size;
height: @arrow-size;
margin-left: @input-padding-horizontal-base * 1.5;
box-shadow: 2px -2px 6px fade(@black, 6%);
background: linear-gradient(
135deg,
transparent 40%,
@calendar-bg 40%
); // Use linear-gradient to prevent arrow from covering text
box-shadow: 2px 2px 6px -2px fade(@black, 10%); // use spread radius to hide shadow over popover
transition: left @animation-duration-slow ease-out;
&::after {
position: absolute;
top: @border-width-base;
right: @border-width-base;
width: @arrow-size;
height: @arrow-size;
border: (@arrow-size / 2) solid @border-color-split;
border-color: @calendar-bg @calendar-bg transparent transparent;
content: '';
}
.roundedArrow(@arrow-size, 5px, @calendar-bg);
}
&-panel-container {

View File

@ -3,3 +3,5 @@ import './index.less';
// style dependencies
import '../../tag/style';
import '../../button/style';
// deps-lint-skip: form

View File

@ -665,7 +665,7 @@
// Fix IE11 render bug by css hacks
// https://github.com/ant-design/ant-design/issues/21559
// https://codepen.io/afc163-1472555193/pen/mdJRaNj?editors=0110
/* stylelint-disable-next-line selector-type-no-unknown,selector-no-vendor-prefix */
/* stylelint-disable selector-type-no-unknown,selector-no-vendor-prefix */
_:-ms-fullscreen,
:root {
.@{picker-prefix-cls}-range-wrapper {

View File

@ -0,0 +1,34 @@
@import '../../input/style/mixin';
@picker-prefix-cls: ~'@{ant-prefix}-picker';
.picker-status-color(
@text-color: @input-color;
@border-color: @input-border-color;
@background-color: @input-bg;
@hoverBorderColor: @primary-color-hover;
@outlineColor: @primary-color-outline;
) {
&.@{picker-prefix-cls} {
&,
&:not([disabled]):hover {
background-color: @background-color;
border-color: @border-color;
}
&-focused,
&:focus {
.active(@text-color, @hoverBorderColor, @outlineColor);
}
}
}
.@{picker-prefix-cls} {
&-status-error {
.picker-status-color(@error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
}
&-status-warning {
.picker-status-color(@warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
}
}

View File

@ -1,5 +1,7 @@
import type { PickerMode } from '../vc-picker/interface';
import type { SelectCommonPlacement } from '../_util/transition';
import type { PickerLocale } from './generatePicker';
import type { DirectionType } from '../config-provider';
export function getPlaceholder(
picker: PickerMode | undefined,
@ -51,3 +53,56 @@ export function getRangePlaceholder(
}
return locale.lang.rangePlaceholder;
}
export function transPlacement2DropdownAlign(
direction: DirectionType,
placement?: SelectCommonPlacement,
) {
const overflow = {
adjustX: 1,
adjustY: 1,
};
switch (placement) {
case 'bottomLeft': {
return {
points: ['tl', 'bl'],
offset: [0, 4],
overflow,
};
}
case 'bottomRight': {
return {
points: ['tr', 'br'],
offset: [0, 4],
overflow,
};
}
case 'topLeft': {
return {
points: ['bl', 'tl'],
offset: [0, -4],
overflow,
};
}
case 'topRight': {
return {
points: ['br', 'tr'],
offset: [0, -4],
overflow,
};
}
default: {
return direction === 'rtl'
? {
points: ['tr', 'br'],
offset: [0, 4],
overflow,
}
: {
points: ['tl', 'bl'],
offset: [0, 4],
overflow,
};
}
}
}

View File

@ -11,3 +11,4 @@
@import 'motion';
@import 'reset';
@import 'operation-unit';
@import 'rounded-arrow';

View File

@ -0,0 +1,43 @@
.roundedArrow(@width, @outer-radius, @bg-color: var(--antd-arrow-background-color)) {
@corner-height: unit(((@outer-radius) * (1 - 1 / sqrt(2))));
@width-without-unit: unit(@width);
@outer-radius-without-unit: unit(@outer-radius);
@inner-radius-without-unit: unit(@arrow-border-radius);
@a-x: @width-without-unit - @corner-height;
@a-y: 2 * @width-without-unit + @corner-height;
@b-x: @a-x + @outer-radius-without-unit * (1 / sqrt(2));
@b-y: 2 * @width-without-unit;
@c-x: 2 * @width-without-unit - @inner-radius-without-unit;
@c-y: 2 * @width-without-unit;
@d-x: 2 * @width-without-unit;
@d-y: 2 * @width-without-unit - @inner-radius-without-unit;
@e-x: 2 * @width-without-unit;
@e-y: @f-y + @outer-radius-without-unit * (1 / sqrt(2));
@f-x: 2 * @width-without-unit + @corner-height;
@f-y: @width-without-unit - @corner-height;
@g-x: @f-x - 1;
@g-y: @f-y;
@h-x: @a-x;
@h-y: @a-y - 1;
border-radius: 0 0 @arrow-border-radius;
pointer-events: none;
&::before {
position: absolute;
top: -@width;
left: -@width;
width: @width * 3;
height: @width * 3;
background: @bg-color;
// Hack firefox: https://github.com/ant-design/ant-design/pull/33710#issuecomment-1015287825
background-repeat: no-repeat;
background-position: ceil(-@width + 1px) ceil(-@width + 1px);
content: '';
clip-path: path(
'M @{a-x} @{a-y} A @{outer-radius-without-unit} @{outer-radius-without-unit} 0 0 1 @{b-x} @{b-y} L @{c-x} @{c-y} A @{inner-radius-without-unit} @{inner-radius-without-unit} 0 0 0 @{d-x} @{d-y} L @{e-x} @{e-y} A @{outer-radius-without-unit} @{outer-radius-without-unit} 0 0 1 @{f-x} @{f-y} L @{g-x} @{g-y} L @{h-x} @{h-y} Z'
);
}
}

View File

@ -108,6 +108,12 @@
@border-radius-base: 2px;
@border-radius-sm: @border-radius-base;
// control border
@control-border-radius: @border-radius-base;
// arrow border
@arrow-border-radius: 2px;
// vertical paddings
@padding-lg: 24px; // containers
@padding-md: 16px; // small containers and buttons

View File

@ -163,6 +163,12 @@ html {
@border-radius-base: 2px;
@border-radius-sm: @border-radius-base;
// control border
@control-border-radius: @border-radius-base;
// arrow border
@arrow-border-radius: @border-radius-sm;
// vertical paddings
@padding-lg: 24px; // containers
@padding-md: 16px; // small containers and buttons

View File

@ -458,7 +458,6 @@ function Picker<DateType>() {
direction,
autocomplete = 'off',
} = props;
// ============================= Panel =============================
const panelProps = {
// Remove `picker` & `format` here since TimePicker is little different with other panel