fix: DateRange组件关闭面板时写入InValid Date问题; feat: DateRange游标动画支持控制 (#5223)

This commit is contained in:
RUNZE LU 2022-08-25 13:57:42 +08:00 committed by GitHub
parent 6d00e12423
commit 380adba034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 202 additions and 56 deletions

View File

@ -13,6 +13,7 @@ order: 15
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
@ -141,8 +142,8 @@ order: 15
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | ------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | ------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------- |
| format | `string` | `X` | [日期选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `YYYY-MM-DD` | [日期选择器显示格式](./date#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择日期范围"` | 占位文本 |
@ -154,6 +155,7 @@ order: 15
| utc | `boolean` | `false` | [保存 UTC 值](./date#utc) |
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -13,6 +13,7 @@ order: 13
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{

View File

@ -13,6 +13,7 @@ order: 16
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
@ -66,8 +67,8 @@ order: 16
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------- |
| format | `string` | `X` | [日期时间选择器值格式](./input-datetime#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `YYYY-MM-DD` | [日期时间选择器显示格式](./input-datetime#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择日期范围"` | 占位文本 |
@ -76,6 +77,7 @@ order: 16
| maxDate | `string` | | 限制最大日期时间,用法同 [限制范围](./input-datetime#%E9%99%90%E5%88%B6%E8%8C%83%E5%9B%B4) |
| utc | `boolean` | `false` | [保存 UTC 值](./input-datetime#utc) |
| clearable | `boolean` | `true` | 是否可清除 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -13,6 +13,7 @@ order: 14
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{

View File

@ -13,6 +13,7 @@ order: 15
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
@ -46,8 +47,8 @@ order: 15
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- | ------- |
| format | `string` | `X` | [日期选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `YYYY-DD` | [日期选择器显示格式](./date#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择月份范围"` | 占位文本 |
@ -58,6 +59,7 @@ order: 15
| utc | `boolean` | `false` | [保存 UTC 值](./date#utc) |
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -13,6 +13,7 @@ order: 81
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{

View File

@ -46,8 +46,8 @@ order: 15
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- | ------- |
| format | `string` | `X` | [日期选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `YYYY-DD` | [日期选择器显示格式](./date#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择季度范围"` | 占位文本 |
@ -58,6 +58,7 @@ order: 15
| utc | `boolean` | `false` | [保存 UTC 值](./date#utc) |
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -13,6 +13,7 @@ order: 15
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
@ -68,14 +69,15 @@ order: 15
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | --------- | ------------------ | --------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | --------- | ------------------ | --------------------------------------------------------------------- | ------- |
| timeFormat | `string` | `HH:mm` | [时间范围选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| format | `string` | `HH:mm` | [时间范围选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `HH:mm` | [时间范围选择器显示格式](./date#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择时间范围"` | 占位文本 |
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -13,6 +13,7 @@ order: 58
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{

View File

@ -13,6 +13,7 @@ order: 15
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
@ -46,8 +47,8 @@ order: 15
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ----------- | --------- | ------------------ | ---------------------------------------------------------------------------- | ------- |
| format | `string` | `X` | [年份选择器值格式](./date#%E5%80%BC%E6%A0%BC%E5%BC%8F) |
| inputFormat | `string` | `YYYY` | [年份选择器显示格式](./date#%E6%98%BE%E7%A4%BA%E6%A0%BC%E5%BC%8F) |
| placeholder | `string` | `"请选择年份范围"` | 占位文本 |
@ -58,6 +59,7 @@ order: 15
| utc | `boolean` | `false` | [保存 UTC 值](./date#utc) |
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| animation | `boolean` | `true` | 是否启用游标动画 | `2.2.0` |
## 事件表

View File

@ -651,6 +651,8 @@
var(--fontSizeLg) * 8 + var(--DatePicker-paddingX) * 2 +
var(--Form-input-clearBtn-size) * 2
);
--DateRangePicker-activeCursor-color: var(--primary);
--DateRangePicker-activeCursor-height: 2px;
--LocationPicker-borderRadius: var(--Form-input-borderWidth);

View File

@ -52,7 +52,8 @@
}
.#{$ns}DateRangePicker-input.isActive {
border-bottom: 2px solid var(--Form-input-onHover-borderColor);
border-bottom: var(--DateRangePicker-activeCursor-height) solid
var(--DateRangePicker-activeCursor-color);
}
.#{$ns}DateRangePicker-input-separator {
@ -63,7 +64,7 @@
&-line {
width: var(--gap-sm);
height: 1px;
background: #B8BABF;
background: #b8babf;
}
}
@ -98,6 +99,19 @@
@include input-clear();
line-height: 1;
}
&-activeCursor {
bottom: 0;
height: var(--DateRangePicker-activeCursor-height);
background: var(--DateRangePicker-activeCursor-color);
opacity: 0;
transition: all var(--animation-duration) ease-in-out;
pointer-events: none;
&.isFocused {
opacity: 1;
}
}
}
.#{$ns}DateRangePicker-wrap {

View File

@ -7,6 +7,7 @@
import React from 'react';
import moment from 'moment';
import omit from 'lodash/omit';
import kebabCase from 'lodash/kebabCase';
import {findDOMNode} from 'react-dom';
import {Icon} from './icons';
import {Overlay} from 'amis-core';
@ -14,7 +15,7 @@ import {ShortCuts, ShortCutDateRange} from './DatePicker';
import Calendar from './calendar/Calendar';
import {PopOver} from 'amis-core';
import PopUp from './PopUp';
import {ClassNamesFn, themeable, ThemeProps} from 'amis-core';
import {ClassNamesFn, themeable, ThemeProps, getComputedStyle} from 'amis-core';
import type {PlainObject} from 'amis-core';
import {isMobile, noop, ucFirst} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
@ -57,17 +58,26 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps {
type?: string;
onRef?: any;
label?: string | false;
/** 是否开启游标动画 */
animation?: boolean;
}
export interface DateRangePickerState {
isOpened: boolean;
isFocused: boolean;
/** 开始时间 */
startDate?: moment.Moment;
/** 结束时间 */
endDate?: moment.Moment;
/** 最近一次confirm的开始时间 */
oldStartDate?: moment.Moment;
/** 最近一次confirm的结束时间 */
oldEndDate?: moment.Moment;
editState?: 'start' | 'end'; // 编辑开始时间还是结束时间
/** 当前编辑的时间类型:开始时间 结束时间 */
editState?: 'start' | 'end';
/** 开始时间输入值 */
startInputValue?: string;
/** 结束时间输入值 */
endInputValue?: string;
endDateOpenedFirst: boolean;
}
@ -464,6 +474,7 @@ export class DateRangePicker extends React.Component<
return newValue;
}
/* 将日期时间转化为momemnt格式如果输入的内容不合法则返回undefined */
static unFormatValue(
value: any,
format: string,
@ -481,9 +492,17 @@ export class DateRangePicker extends React.Component<
value = value.split(delimiter);
}
const startDate = moment(value?.[0], format);
const endDate = moment(value?.[1], format);
/**
* value输入都丢弃
* undefined被moment认为是合法的输入moment会转化为now
* @reference https://github.com/moment/moment/issues/1639
*/
return {
startDate: value[0] ? moment(value[0], format) : undefined,
endDate: value[1] ? moment(value[1], format) : undefined
startDate: value[0] && startDate.isValid() ? startDate : undefined,
endDate: value[1] && endDate.isValid() ? endDate : undefined
};
}
@ -494,12 +513,14 @@ export class DateRangePicker extends React.Component<
startInputRef: React.RefObject<HTMLInputElement>;
endInputRef: React.RefObject<HTMLInputElement>;
separatorRef: React.RefObject<HTMLSpanElement>;
constructor(props: DateRangePickerProps) {
super(props);
this.startInputRef = React.createRef();
this.endInputRef = React.createRef();
this.separatorRef = React.createRef();
this.calendarRef = React.createRef();
this.open = this.open.bind(this);
this.openStart = this.openStart.bind(this);
@ -587,8 +608,12 @@ export class DateRangePicker extends React.Component<
this.setState({
startDate,
endDate,
startInputValue: startDate?.format(inputFormat),
endInputValue: endDate?.format(inputFormat)
startInputValue:
startDate && startDate?.isValid()
? startDate?.format(inputFormat)
: '',
endInputValue:
endDate && endDate?.isValid() ? endDate?.format(inputFormat) : ''
});
}
}
@ -658,13 +683,27 @@ export class DateRangePicker extends React.Component<
close(isConfirm: boolean = false) {
if (!isConfirm) {
const {oldEndDate, oldStartDate} = this.state;
const {inputFormat} = this.props;
/** 未点击确认关闭时,将日期恢复至未做任何选择的状态 */
const {value, format, joinValues, delimiter, inputFormat} = this.props;
const {startDate, endDate} = DateRangePicker.unFormatValue(
value,
format,
joinValues,
delimiter
);
this.setState({
endDate: oldEndDate,
endInputValue: oldEndDate ? oldEndDate.format(inputFormat) : '',
startDate: oldStartDate,
startInputValue: oldStartDate ? oldStartDate.format(inputFormat) : ''
startDate,
endDate,
oldStartDate: startDate,
oldEndDate: endDate,
startInputValue:
startDate && moment(startDate).isValid()
? startDate.format(inputFormat)
: '',
endInputValue:
endDate && moment(endDate).isValid()
? endDate.format(inputFormat)
: ''
});
} else {
this.setState({
@ -699,28 +738,25 @@ export class DateRangePicker extends React.Component<
}
confirm() {
if (!this.state.startDate && !this.state.endDate) {
const {format, joinValues, delimiter, utc} = this.props;
const {startDate, endDate} = this.state;
if (!startDate && !endDate) {
return;
} else if (
this.state.endDate &&
this.state.startDate?.isAfter(this.state.endDate)
) {
} else if (endDate && startDate?.isAfter(this.state.endDate)) {
return;
}
this.props.onChange(
DateRangePicker.formatValue(
{
startDate: this.state.startDate,
endDate: this.state.endDate
},
this.props.format,
this.props.joinValues,
this.props.delimiter,
this.props.utc
{startDate, endDate},
format,
joinValues,
delimiter,
utc
)
);
if (this.state.startDate && !this.state.endDate) {
if (startDate && !endDate) {
this.setState({editState: 'end', endDateOpenedFirst: false});
} else {
this.close(true);
@ -1280,12 +1316,21 @@ export class DateRangePicker extends React.Component<
viewMode = 'days'
} = this.props;
const __ = this.props.translate;
const {startDate, endDate, editState} = this.state;
const isDateTimeRange = type === 'input-datetime-range';
const isDateRange = type === 'input-date-range';
// timeRange需要单独选择范围
const isTimeRange = isDateTimeRange || viewMode === 'time';
const isConfirmBtnDisbaled =
(isTimeRange && editState === 'start' && !startDate) ||
(isTimeRange && editState === 'end' && !endDate) ||
(startDate && endDate?.isBefore(this.state.startDate)) ||
/** 日期范围选择之后会立即切换面板,所以开始/结束日期任意一个不合法就不允许更新数据 */
(isDateRange &&
(!startDate ||
!endDate ||
!startDate?.isValid() ||
!endDate?.isValid()));
return (
<div className={cx(`${ns}DateRangePicker-wrap`)} ref={this.calendarRef}>
@ -1361,13 +1406,7 @@ export class DateRangePicker extends React.Component<
</a>
<a
className={cx('Button', 'Button--primary', 'm-l-sm', {
'is-disabled':
(!this.state.startDate &&
isTimeRange &&
editState === 'start') ||
(!this.state.endDate && isTimeRange && editState === 'end') ||
(this.state.startDate &&
this.state.endDate?.isBefore(this.state.startDate))
'is-disabled': isConfirmBtnDisbaled
})}
onClick={this.confirm}
>
@ -1396,6 +1435,63 @@ export class DateRangePicker extends React.Component<
};
}
/** 获取宽度类型变量的值 */
getValidWidthValue(element: HTMLElement, propsName: string): number {
if (!element || !propsName) {
return 0;
}
const propsValue = parseInt(
getComputedStyle(element, kebabCase(propsName)),
10
);
return isNaN(propsValue) ? 0 : propsValue;
}
renderActiveCursor() {
const {classnames: cx} = this.props;
const {editState, isFocused} = this.state;
let cursorWidth: number = 0;
let cursorLeft: number = 0;
const parentNode = this?.dom?.current;
const startInputNode = this?.startInputRef?.current;
const endInputNode = this?.endInputRef?.current;
const separatorNode = this?.separatorRef?.current;
if (parentNode && startInputNode && endInputNode && separatorNode) {
if (editState === 'start') {
const paddingWidth = this.getValidWidthValue(parentNode, 'paddingLeft');
cursorLeft = paddingWidth;
cursorWidth = startInputNode.offsetWidth;
} else if (editState === 'end') {
const separatorWidth =
separatorNode.offsetWidth +
this.getValidWidthValue(parentNode, 'paddingLeft') +
this.getValidWidthValue(parentNode, 'marginLeft') +
this.getValidWidthValue(parentNode, 'paddingRight') +
this.getValidWidthValue(parentNode, 'marginRight');
cursorLeft = startInputNode.offsetWidth + separatorWidth;
cursorWidth = endInputNode.offsetWidth;
} else {
cursorWidth = 0;
}
}
return (
<div
className={cx('DateRangePicker-activeCursor', {isFocused})}
style={{
position: 'absolute',
left: cursorLeft,
width: cursorWidth
}}
/>
);
}
render() {
const {
className,
@ -1424,15 +1520,14 @@ export class DateRangePicker extends React.Component<
dateFormat,
viewMode = 'days',
ranges,
label
label,
animation
} = this.props;
const useCalendarMobile =
useMobileUI &&
isMobile() &&
['days', 'months', 'quarters'].indexOf(viewMode) > -1;
const {isOpened, isFocused, startDate, endDate} = this.state;
const __ = this.props.translate;
const calendarMobile = (
@ -1479,6 +1574,8 @@ export class DateRangePicker extends React.Component<
{label && typeof label === 'string' ? label : __('Calendar.datepicker')}
</div>
);
/** 是否启用游标动画 */
const useAnimation = animation !== false;
return (
<div
@ -1500,7 +1597,8 @@ export class DateRangePicker extends React.Component<
>
<Input
className={cx('DateRangePicker-input', {
isActive: this.state.editState === 'start' && isOpened
isActive:
!useAnimation && this.state.editState === 'start' && isOpened
})}
onChange={this.startInputChange}
onClick={this.openStart}
@ -1510,12 +1608,16 @@ export class DateRangePicker extends React.Component<
value={this.state.startInputValue || ''}
disabled={disabled}
/>
<span className={cx('DateRangePicker-input-separator')}>
<span
className={cx('DateRangePicker-input-separator')}
ref={this.separatorRef}
>
<span className={cx('DateRangePicker-input-separator-line')}></span>
</span>
<Input
className={cx('DateRangePicker-input', {
isActive: this.state.editState === 'end' && isOpened
isActive:
!useAnimation && this.state.editState === 'end' && isOpened
})}
onChange={this.endInputChange}
onClick={this.openEnd}
@ -1526,6 +1628,9 @@ export class DateRangePicker extends React.Component<
disabled={disabled}
/>
{/* 指示游标 */}
{useAnimation ? this.renderActiveCursor() : null}
{clearable && !disabled && value ? (
<a className={`${ns}DateRangePicker-clear`} onClick={this.clearValue}>
<Icon icon="input-clear" className="icon" />

View File

@ -77,6 +77,10 @@ exports[`Renderer:dateRange 1`] = `
type="text"
value="2019-06-26"
/>
<div
class="cxd-DateRangePicker-activeCursor"
style="position: absolute; left: 0px; width: 0px;"
/>
<a
class="cxd-DateRangePicker-clear"
>

View File

@ -87,6 +87,11 @@ export interface DateRangeControlSchema extends FormBaseControlSchema {
* -
*/
endPlaceholder?: string;
/**
*
*/
animation?: boolean;
}
export interface DateRangeProps
@ -104,7 +109,8 @@ export default class DateRangeControl extends React.Component<DateRangeProps> {
static defaultProps = {
format: 'X',
joinValues: true,
delimiter: ','
delimiter: ',',
animation: true
};
dateRef?: any;