diff --git a/__tests__/renderers/Form/__snapshots__/dateRang.test.tsx.snap b/__tests__/renderers/Form/__snapshots__/dateRange.test.tsx.snap similarity index 84% rename from __tests__/renderers/Form/__snapshots__/dateRang.test.tsx.snap rename to __tests__/renderers/Form/__snapshots__/dateRange.test.tsx.snap index 52795eaf9..3d012e9cc 100644 --- a/__tests__/renderers/Form/__snapshots__/dateRang.test.tsx.snap +++ b/__tests__/renderers/Form/__snapshots__/dateRange.test.tsx.snap @@ -56,11 +56,25 @@ exports[`Renderer:dateRange 1`] = ` class="cxd-DateRangePicker" tabindex="0" > + - 2019-06-06 至 2019-06-26 + ~ + diff --git a/__tests__/renderers/Form/dateRang.test.tsx b/__tests__/renderers/Form/dateRange.test.tsx similarity index 78% rename from __tests__/renderers/Form/dateRang.test.tsx rename to __tests__/renderers/Form/dateRange.test.tsx index c52f63792..94490d22c 100644 --- a/__tests__/renderers/Form/dateRang.test.tsx +++ b/__tests__/renderers/Form/dateRange.test.tsx @@ -31,12 +31,12 @@ test('Renderer:dateRange', async () => { ) ); - const input = container.querySelector('.cxd-DateRangePicker-value'); - expect(input?.innerHTML).toEqual( - `${moment(1559750400, 'X').format('YYYY-MM-DD')} 至 ${moment( - 1561564799, - 'X' - ).format('YYYY-MM-DD')}` + const input = container.querySelectorAll('.cxd-DateRangePicker-input'); + expect(input[0].value).toEqual( + `${moment(1559750400, 'X').format('YYYY-MM-DD')}` + ); + expect(input[1].value).toEqual( + `${moment(1561564799, 'X').format('YYYY-MM-DD')}` ); expect(container).toMatchSnapshot(); diff --git a/__tests__/renderers/Form/inputYearRange.test.tsx b/__tests__/renderers/Form/inputYearRange.test.tsx index 9ce0b6a77..6e5db01e3 100644 --- a/__tests__/renderers/Form/inputYearRange.test.tsx +++ b/__tests__/renderers/Form/inputYearRange.test.tsx @@ -8,7 +8,7 @@ import {makeEnv} from '../../helper'; import moment from 'moment'; test('Renderer:inputYearRange click', async () => { - const {container, findByText, getByText} = render( + const {container, findByPlaceholderText, getByText} = render( amisRender( { type: 'form', @@ -28,7 +28,7 @@ test('Renderer:inputYearRange click', async () => { ) ); - const inputDate = await findByText('请选择年份范围'); + const inputDate = await findByPlaceholderText('选择开始时间'); fireEvent.click(inputDate); @@ -51,9 +51,8 @@ test('Renderer:inputYearRange click', async () => { fireEvent.click(confirm); - const value = document.querySelector( - '.cxd-DateRangePicker-value' - ) as HTMLSpanElement; + const value = document.querySelectorAll('.cxd-DateRangePicker-input')!; - expect(value.innerHTML).toEqual(thisYearText + ' 至 ' + nextYearText); + expect((value[0] as HTMLInputElement).value).toEqual(thisYearText); + expect((value[1] as HTMLInputElement).value).toEqual(nextYearText); }); diff --git a/scss/components/form/_date-range.scss b/scss/components/form/_date-range.scss index 123ce543e..90f48a8b6 100644 --- a/scss/components/form/_date-range.scss +++ b/scss/components/form/_date-range.scss @@ -31,6 +31,22 @@ box-shadow: var(--Form-input-boxShadow); } + .#{$ns}DateRangePicker-input { + border: none; + border-bottom: 1px solid transparent; + outline: none; + padding: 0; + background: 0; + } + + .#{$ns}DateRangePicker-input.isActive { + border-bottom: 1px solid var(--DatePicker-onFocused-borderColor); + } + + .#{$ns}DateRangePicker-input-separator { + margin: 0 var(--gap-sm); + } + &.is-disabled { background: $gray200; diff --git a/src/components/DateRangePicker.tsx b/src/components/DateRangePicker.tsx index 0c6af5da9..22c38f5e6 100644 --- a/src/components/DateRangePicker.tsx +++ b/src/components/DateRangePicker.tsx @@ -7,7 +7,6 @@ import React from 'react'; import moment from 'moment'; import {findDOMNode} from 'react-dom'; -import cx from 'classnames'; import {Icon} from './icons'; import Overlay from './Overlay'; import {ShortCuts, ShortCutDateRange} from './DatePicker'; @@ -19,11 +18,13 @@ import {PlainObject} from '../types'; import {isMobile, noop, ucFirst} from '../utils/helper'; import {LocaleProps, localeable} from '../locale'; import CalendarMobile from './CalendarMobile'; +import Input from './Input'; export interface DateRangePickerProps extends ThemeProps, LocaleProps { className?: string; popoverClassName?: string; - placeholder?: string; + startPlaceholder?: string; + endPlaceholder?: string; theme?: any; format: string; utc?: boolean; @@ -52,6 +53,7 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps { useMobileUI?: boolean; onFocus?: Function; onBlur?: Function; + type?: string; } export interface DateRangePickerState { @@ -59,6 +61,9 @@ export interface DateRangePickerState { isFocused: boolean; startDate?: moment.Moment; endDate?: moment.Moment; + editState?: 'start' | 'end'; // 编辑开始时间还是结束时间 + startInputValue?: string; + endInputValue?: string; } export const availableRanges: {[propName: string]: any} = { @@ -390,7 +395,8 @@ export class DateRangePicker extends React.Component< DateRangePickerState > { static defaultProps = { - placeholder: 'DateRange.placeholder', + startPlaceholder: 'Calendar.startPick', + endPlaceholder: 'Calendar.endPick', format: 'X', inputFormat: 'YYYY-MM-DD', joinValues: true, @@ -456,12 +462,21 @@ export class DateRangePicker extends React.Component< dom: React.RefObject; nextMonth = moment().add(1, 'months'); + startInputRef: React.RefObject; + endInputRef: React.RefObject; + constructor(props: DateRangePickerProps) { super(props); + this.startInputRef = React.createRef(); + this.endInputRef = React.createRef(); this.open = this.open.bind(this); + this.openStart = this.openStart.bind(this); + this.openEnd = this.openEnd.bind(this); this.close = this.close.bind(this); - this.handleSelectChange = this.handleSelectChange.bind(this); + this.startInputChange = this.startInputChange.bind(this); + this.endInputChange = this.endInputChange.bind(this); + this.handleDateChange = this.handleDateChange.bind(this); this.handleTimeStartChange = this.handleTimeStartChange.bind(this); this.handleTimeEndChange = this.handleTimeEndChange.bind(this); this.handleFocus = this.handleFocus.bind(this); @@ -477,22 +492,61 @@ export class DateRangePicker extends React.Component< this.renderDay = this.renderDay.bind(this); this.renderQuarter = this.renderQuarter.bind(this); this.handleMobileChange = this.handleMobileChange.bind(this); - const {format, joinValues, delimiter, value} = this.props; - + this.handleOutClick = this.handleOutClick.bind(this); + const {format, joinValues, delimiter, value, inputFormat} = this.props; + const {startDate, endDate} = DateRangePicker.unFormatValue( + value, + format, + joinValues, + delimiter + ); this.state = { isOpened: false, isFocused: false, - ...DateRangePicker.unFormatValue(value, format, joinValues, delimiter) + startDate, + endDate, + startInputValue: startDate?.format(inputFormat), + endInputValue: endDate?.format(inputFormat) }; } + componentDidMount() { + document.body.addEventListener('click', this.handleOutClick, true); + } + + componentWillUnmount() { + document.body.removeEventListener('click', this.handleOutClick, true); + } + + handleOutClick(e: Event) { + if ( + !e.target || + !this.dom.current || + this.dom.current.contains(e.target as HTMLElement) + ) { + return; + } + if (this.state.isOpened) { + e.preventDefault(); + this.close(); + } + } componentDidUpdate(prevProps: DateRangePickerProps) { const props = this.props; - const {value, format, joinValues, delimiter} = props; + const {value, format, joinValues, inputFormat, delimiter} = props; if (prevProps.value !== value) { + const {startDate, endDate} = DateRangePicker.unFormatValue( + value, + format, + joinValues, + delimiter + ); this.setState({ - ...DateRangePicker.unFormatValue(value, format, joinValues, delimiter) + startDate, + endDate, + startInputValue: startDate?.format(inputFormat), + endInputValue: endDate?.format(inputFormat) }); } } @@ -539,10 +593,31 @@ export class DateRangePicker extends React.Component< }); } + openStart() { + if (this.props.disabled) { + return; + } + this.setState({ + isOpened: true, + editState: 'start' + }); + } + + openEnd() { + if (this.props.disabled) { + return; + } + this.setState({ + isOpened: true, + editState: 'end' + }); + } + close() { this.setState( { - isOpened: false + isOpened: false, + editState: undefined }, this.blur ); @@ -612,77 +687,163 @@ export class DateRangePicker extends React.Component< return value; } - handleSelectChange(newValue: moment.Moment) { - const {embed, timeFormat, minDuration, maxDuration, minDate} = this.props; - let {startDate, endDate} = this.state; - - // 第一次点击只标记起始时间,或者点击了开始时间前面的时间 - if (this.isFirstClick || newValue.isBefore(startDate)) { - // 这种情况说明第二次点击点击了前面的时间,这时要标记为第二次点击 - if (newValue.isBefore(startDate)) { - this.isFirstClick = true; - } + handleDateChange(newValue: moment.Moment) { + const { + embed, + timeFormat, + minDuration, + maxDuration, + minDate, + inputFormat, + type + } = this.props; + let {startDate, endDate, editState} = this.state; + if (editState === 'start') { if (minDate && newValue.isBefore(minDate)) { newValue = minDate; } - this.setState({ - startDate: this.filterDate( - newValue, - startDate || minDate, - timeFormat, - 'start' - ), - endDate: undefined - }); - } else { - // 第二次点击作为结束时间 - if (!startDate) { - // 不大可能,但只能作为开始时间了 - startDate = newValue; + const date = this.filterDate( + newValue, + startDate || minDate, + timeFormat, + 'start' + ); + const newState = { + startDate: date, + startInputValue: date.format(inputFormat) + } as any; + // 这些没有时间的选择点第一次后第二次就是选结束时间 + if ( + type === 'input-date-range' || + type === 'input-year-range' || + type === 'input-quarter-range' + ) { + newState.editState = 'end'; + } + this.setState(newState); + } else if (editState === 'end') { + newValue = this.getEndDateByDuration(newValue); + + // 如果结束时间在前面,需要清空开始时间 + if (newValue.isBefore(startDate)) { + this.setState({ + startDate: undefined, + startInputValue: '' + }); } - if (minDuration && newValue.isAfter(startDate.clone().add(minDuration))) { - newValue = startDate.clone().add(minDuration); - } - if ( - maxDuration && - newValue.isBefore(startDate.clone().add(maxDuration)) - ) { - newValue = startDate.clone().add(maxDuration); - } + const date = this.filterDate(newValue, endDate, timeFormat, 'end'); this.setState( { - endDate: this.filterDate(newValue, endDate, timeFormat, 'end') + endDate: date, + endInputValue: date.format(inputFormat) }, () => { embed && this.confirm(); } ); } + } - this.isFirstClick = !this.isFirstClick; + // 手动控制输入时间 + startInputChange(e: React.ChangeEvent) { + const {onChange, inputFormat, format, utc} = this.props; + const value = e.currentTarget.value; + this.setState({startInputValue: value}); + if (value === '') { + onChange(''); + } else { + let newDate = this.getStartDateByDuration(moment(value, inputFormat)); + this.setState({startDate: newDate}); + } + } + + endInputChange(e: React.ChangeEvent) { + const {onChange, inputFormat, format, utc} = this.props; + const value = e.currentTarget.value; + this.setState({endInputValue: value}); + if (value === '') { + onChange(''); + } else { + let newDate = this.getEndDateByDuration(moment(value, inputFormat)); + this.setState({endDate: newDate}); + } + } + + // 根据 duration 修复结束时间 + getEndDateByDuration(newValue: moment.Moment) { + const {minDuration, maxDuration, type} = this.props; + let {startDate, endDate, editState} = this.state; + if (!startDate) { + return newValue; + } + + // 时间范围必须统一成同一天,不然会不一致 + if (type === 'input-time-range' && startDate) { + newValue.set({ + year: startDate.year(), + month: startDate.month(), + date: startDate.date() + }); + } + + if (minDuration && newValue.isBefore(startDate.clone().add(minDuration))) { + newValue = startDate.clone().add(minDuration); + } + + if (maxDuration && newValue.isAfter(startDate.clone().add(maxDuration))) { + newValue = startDate.clone().add(maxDuration); + } + + return newValue; + } + + // 根据 duration 修复起始时间 + getStartDateByDuration(newValue: moment.Moment) { + const {minDuration, maxDuration, type} = this.props; + let {endDate, editState} = this.state; + if (!endDate) { + return newValue; + } + // 时间范围必须统一成同一天,不然会不一致 + if (type === 'input-time-range' && endDate) { + newValue.set({ + year: endDate.year(), + month: endDate.month(), + date: endDate.date() + }); + } + + if ( + minDuration && + newValue.isBefore(endDate.clone().subtract(minDuration)) + ) { + newValue = endDate.clone().subtract(minDuration); + } + + if ( + maxDuration && + newValue.isAfter(endDate.clone().subtract(maxDuration)) + ) { + newValue = endDate.clone().subtract(maxDuration); + } + + return newValue; } // 主要用于处理时间的情况 handleTimeStartChange(newValue: moment.Moment) { - const {embed, timeFormat, minDuration, maxDuration, minDate} = this.props; + const {embed, timeFormat, inputFormat, minDuration, maxDuration, minDate} = + this.props; const {startDate, endDate} = this.state; - if ( - startDate && - (!endDate || (endDate && newValue.isSame(startDate))) && // 没有结束时间,或者新的时间也是开始时间,这时都会将新值当成结束时间 - newValue.isSameOrAfter(startDate) && - (!minDuration || newValue.isAfter(startDate.clone().add(minDuration))) && - (!maxDuration || newValue.isBefore(startDate.clone().add(maxDuration))) - ) { - return this.setState( - { - endDate: this.filterDate(newValue, endDate, timeFormat, 'end') - }, - () => { - embed && this.confirm(); - } - ); + // 时间范围必须统一成同一天,不然会不一致 + if (endDate) { + newValue.set({ + year: endDate.year(), + month: endDate.month(), + date: endDate.date() + }); } if (minDate && newValue && newValue.isBefore(minDate, 'second')) { @@ -691,12 +852,8 @@ export class DateRangePicker extends React.Component< this.setState( { - startDate: this.filterDate( - newValue, - startDate || minDate, - timeFormat, - 'start' - ) + startDate: newValue, + startInputValue: newValue.format(inputFormat) }, () => { embed && this.confirm(); @@ -705,39 +862,40 @@ export class DateRangePicker extends React.Component< } handleTimeEndChange(newValue: moment.Moment) { - const {embed, timeFormat, minDuration, maxDuration, maxDate} = this.props; + const {embed, timeFormat, inputFormat, minDuration, maxDuration, maxDate} = + this.props; const {startDate, endDate} = this.state; - - if ( - endDate && - !startDate && - newValue.isSameOrBefore(endDate) && - (!minDuration || - newValue.isBefore(endDate.clone().subtract(minDuration))) && - (!maxDuration || newValue.isAfter(endDate.clone().subtract(maxDuration))) - ) { - return this.setState( - { - startDate: this.filterDate(newValue, startDate, timeFormat, 'start') - }, - () => { - embed && this.confirm(); - } - ); + if (startDate) { + newValue.set({ + year: startDate.year(), + month: startDate.month(), + date: startDate.date() + }); } if (maxDate && newValue && newValue.isAfter(maxDate, 'second')) { newValue = maxDate; } + if ( + startDate && + minDuration && + newValue.isAfter(startDate.clone().add(minDuration)) + ) { + newValue = startDate.clone().add(minDuration); + } + if ( + startDate && + maxDuration && + newValue.isBefore(startDate.clone().add(maxDuration)) + ) { + newValue = startDate.clone().add(maxDuration); + } + this.setState( { - endDate: this.filterDate( - newValue, - endDate || maxDate, - timeFormat, - 'end' - ) + endDate: newValue, + endInputValue: newValue.format(inputFormat) }, () => { embed && this.confirm(); @@ -836,7 +994,7 @@ export class DateRangePicker extends React.Component< e.preventDefault(); e.stopPropagation(); const {resetValue, onChange} = this.props; - + this.setState({startInputValue: '', endInputValue: ''}); onChange(resetValue); } @@ -950,6 +1108,7 @@ export class DateRangePicker extends React.Component< ranges, locale, embed, + type, viewMode = 'days' } = this.props; const __ = this.props.translate; @@ -959,7 +1118,6 @@ export class DateRangePicker extends React.Component< return (
{this.renderRanges(ranges)} - - @@ -1030,8 +1190,10 @@ export class DateRangePicker extends React.Component< className, popoverClassName, classPrefix: ns, + classnames: cx, value, - placeholder, + startPlaceholder, + endPlaceholder, popOverContainer, inputFormat, format, @@ -1059,21 +1221,6 @@ export class DateRangePicker extends React.Component< const {isOpened, isFocused, startDate, endDate} = this.state; - const selectedDate = DateRangePicker.unFormatValue( - value, - format, - joinValues, - delimiter - ); - const startViewValue = selectedDate.startDate - ? selectedDate.startDate.format(inputFormat) - : ''; - const endViewValue = selectedDate.endDate - ? selectedDate.endDate.format(inputFormat) - : ''; - const arr = []; - startViewValue && arr.push(startViewValue); - endViewValue && arr.push(endViewValue); const __ = this.props.translate; const calendarMobile = ( @@ -1138,17 +1285,32 @@ export class DateRangePicker extends React.Component< className )} ref={this.dom} - onClick={this.handleClick} > - {arr.length ? ( - - {arr.join(__('DateRange.valueConcat'))} - - ) : ( - - {__(placeholder)} - - )} + + ~ + {clearable && !disabled && value ? ( @@ -1185,7 +1347,6 @@ export class DateRangePicker extends React.Component< className={cx(`${ns}DateRangePicker-popover`, popoverClassName)} onHide={this.close} onClick={this.handlePopOverClick} - overlay > {this.renderCalendar()} diff --git a/src/components/calendar/Calendar.tsx b/src/components/calendar/Calendar.tsx index 41a27047b..f75d3fa9f 100644 --- a/src/components/calendar/Calendar.tsx +++ b/src/components/calendar/Calendar.tsx @@ -223,8 +223,7 @@ class BaseDatePicker extends React.Component< updatedState.viewDate = moment(props.viewDate); } - // time-range 下会有问题,先不支持 - if (Object.keys(updatedState).length && props.viewMode !== 'time') { + if (Object.keys(updatedState).length) { this.setState(updatedState); }