From 632a1e316e8417717ead6ac0f95b3fe81fdf3810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 6 Mar 2020 18:09:20 +0800 Subject: [PATCH] feat: Time RangePicker support endTime before startTime (#33) * test driven * skip reorder when picker is time * support order props * readme update --- .prettierrc | 4 +- README.md | 168 +++++++++++++------------- src/RangePicker.tsx | 270 ++++++++++------------------------------- tests/range.spec.tsx | 279 ++++++++++++++++--------------------------- 4 files changed, 254 insertions(+), 467 deletions(-) diff --git a/.prettierrc b/.prettierrc index 27dd8af..6e59103 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,7 @@ "semi": true, "singleQuote": true, "tabWidth": 2, - "trailingComma": "all" + "trailingComma": "all", + "proseWrap": "never", + "printWidth": 100 } diff --git a/README.md b/README.md index edbae61..a8841b7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # rc-picker -[![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![Codecov][codecov-image]][codecov-url] -[![david deps][david-image]][david-url] -[![david devDeps][david-dev-image]][david-dev-url] -[![npm download][download-image]][download-url] +[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![Codecov][codecov-image]][codecov-url] [![david deps][david-image]][david-url] [![david devDeps][david-dev-image]][david-dev-url] [![npm download][download-image]][download-url] [npm-image]: http://img.shields.io/npm/v/rc-picker.svg?style=flat-square [npm-url]: http://npmjs.org/package/rc-picker @@ -42,92 +37,93 @@ render(, mountNode); ### Picker -| Property | Type | Default | Description | -| ----------------- | ------------------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| prefixCls | String | rc-picker | prefixCls of this component | -| className | String | '' | additional css class of root dom node | -| style | React.CSSProperties | | additional style of root dom node | -| dropdownClassName | String | '' | additional className applied to dropdown | -| dropdownAlign | Object:alignConfig of [dom-align](https://github.com/yiminghe/dom-align) | | value will be merged into placement's dropdownAlign config | -| popupStyle | React.CSSProperties | | customize popup style | -| transitionName | String | '' | css class for animation | -| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | -| inputReadOnly | Boolean | false | set input to read only | -| allowClear | Boolean | false | whether show clear button | -| autoFocus | Boolean | false | whether auto focus | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | -| picker | time \| date \| week \| month \| year | | control which kind of panel should be shown | -| format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | -| use12Hours | Boolean | false | 12 hours display mode | -| value | moment | | current value like input's value | -| defaultValue | moment | | defaultValue like input's defaultValue | -| open | Boolean | false | current open state of picker. controlled prop | -| suffixIcon | ReactNode | | The custom suffix icon | -| clearIcon | ReactNode | | The custom clear icon | -| prevIcon | ReactNode | | The custom prev icon | -| nextIcon | ReactNode | | The custom next icon | -| superPrevIcon | ReactNode | | The custom super prev icon | -| superNextIcon | ReactNode | | The custom super next icon | -| disabled | Boolean | false | whether the picker is disabled | -| placeholder | String | | picker input's placeholder | -| getPopupContainer | function(trigger) | | to set the container of the floating layer, while the default is to create a div element in body | -| onChange | Function(date: moment, dateString: string) | | a callback function, can be executed when the selected time is changing | -| onOpenChange | Function(open:boolean) | | called when open/close picker | -| onFocus | (evnet:React.FocusEventHandler) => void | | called like input's on focus | -| onBlur | (evnet:React.FocusEventHandler) => void | | called like input's on blur | -| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| prefixCls | String | rc-picker | prefixCls of this component | +| className | String | '' | additional css class of root dom node | +| style | React.CSSProperties | | additional style of root dom node | +| dropdownClassName | String | '' | additional className applied to dropdown | +| dropdownAlign | Object:alignConfig of [dom-align](https://github.com/yiminghe/dom-align) | | value will be merged into placement's dropdownAlign config | +| popupStyle | React.CSSProperties | | customize popup style | +| transitionName | String | '' | css class for animation | +| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | +| inputReadOnly | Boolean | false | set input to read only | +| allowClear | Boolean | false | whether show clear button | +| autoFocus | Boolean | false | whether auto focus | +| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| picker | time \| date \| week \| month \| year | | control which kind of panel should be shown | +| format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | +| use12Hours | Boolean | false | 12 hours display mode | +| value | moment | | current value like input's value | +| defaultValue | moment | | defaultValue like input's defaultValue | +| open | Boolean | false | current open state of picker. controlled prop | +| suffixIcon | ReactNode | | The custom suffix icon | +| clearIcon | ReactNode | | The custom clear icon | +| prevIcon | ReactNode | | The custom prev icon | +| nextIcon | ReactNode | | The custom next icon | +| superPrevIcon | ReactNode | | The custom super prev icon | +| superNextIcon | ReactNode | | The custom super next icon | +| disabled | Boolean | false | whether the picker is disabled | +| placeholder | String | | picker input's placeholder | +| getPopupContainer | function(trigger) | | to set the container of the floating layer, while the default is to create a div element in body | +| onChange | Function(date: moment, dateString: string) | | a callback function, can be executed when the selected time is changing | +| onOpenChange | Function(open:boolean) | | called when open/close picker | +| onFocus | (evnet:React.FocusEventHandler) => void | | called like input's on focus | +| onBlur | (evnet:React.FocusEventHandler) => void | | called like input's on blur | +| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | ### PickerPanel -| Property | Type | Default | Description | -| ------------------ | ----------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- | -| prefixCls | String | rc-picker | prefixCls of this component | -| className | String | '' | additional css class of root dom | -| style | React.CSSProperties | | additional style of root dom node | -| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | -| value | moment | | current value like input's value | -| defaultValue | moment | | defaultValue like input's defaultValue | -| defaultPickerValue | moment | | Set default display picker view date | -| mode | time \| datetime \| date \| week \| month \| year \| decade | | control which kind of panel | -| picker | time \| date \| week \| month \| year | | control which kind of panel | -| tabIndex | Number | 0 | view [tabIndex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | -| showToday | Boolean | false | whether to show today button | -| disabledDate | Function(date:moment) => Boolean | | whether to disable select of current date | -| dateRender | Function(currentDate:moment, today:moment) => React.Node | | custom rendering function for date cells | -| monthCellRender | Function(currentDate:moment, locale:Locale) => React.Node | | Custom month cell render method | -| renderExtraFooter | (mode) => React.Node | | extra footer | -| onSelect | Function(date: moment) | | a callback function, can be executed when the selected time | -| onPanelChange | Function(value: moment, mode) | | callback when picker panel mode is changed | -| onMouseDown | (evnet:React.MouseEventHandler) => void | | callback when executed onMouseDown evnent | -| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| prefixCls | String | rc-picker | prefixCls of this component | +| className | String | '' | additional css class of root dom | +| style | React.CSSProperties | | additional style of root dom node | +| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | +| value | moment | | current value like input's value | +| defaultValue | moment | | defaultValue like input's defaultValue | +| defaultPickerValue | moment | | Set default display picker view date | +| mode | time \| datetime \| date \| week \| month \| year \| decade | | control which kind of panel | +| picker | time \| date \| week \| month \| year | | control which kind of panel | +| tabIndex | Number | 0 | view [tabIndex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) | +| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| showToday | Boolean | false | whether to show today button | +| disabledDate | Function(date:moment) => Boolean | | whether to disable select of current date | +| dateRender | Function(currentDate:moment, today:moment) => React.Node | | custom rendering function for date cells | +| monthCellRender | Function(currentDate:moment, locale:Locale) => React.Node | | Custom month cell render method | +| renderExtraFooter | (mode) => React.Node | | extra footer | +| onSelect | Function(date: moment) | | a callback function, can be executed when the selected time | +| onPanelChange | Function(value: moment, mode) | | callback when picker panel mode is changed | +| onMouseDown | (evnet:React.MouseEventHandler) => void | | callback when executed onMouseDown evnent | +| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | ### RangePicker -| Property | Type | Default | Description | -| --------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| prefixCls | String | rc-picker | prefixCls of this component | -| className | String | '' | additional css class of root dom | -| style | React.CSSProperties | | additional style of root dom node | -| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | -| value | moment | | current value like input's value | -| defaultValue | moment | | defaultValue like input's defaultValue | -| defaultPickerValue | moment | | Set default display picker view date | -| separator | String | '~' | set separator between inputs | -| picker | time \| date \| week \| month \| year | | control which kind of panel | -| placeholder | [String, String] | | placeholder of date input | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | -| showTime.defaultValue | [moment, moment] | | to set default time of selected date | -| use12Hours | Boolean | false | 12 hours display mode | -| disabledTime | Function(date: moment, type:'start'\|'end'):Object | | | to specify the time that cannot be selected | -| ranges | { String \| [range: string]: moment[] } \| { [range: string]: () => moment[] } | | preseted ranges for quick selection | -| format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | -| allowEmpty | [Boolean, Boolean] | | allow range picker clearing text | -| selectable | [Boolean, Boolean] | | whether to selected picker | -| disabled | Boolean | false | whether the range picker is disabled | -| onChange | Function(value:[moment], formatString: [string, string]) | | a callback function, can be executed when the selected time is changing | -| onCalendarChange | Function(value:[moment], formatString: [string, string]) | | a callback function, can be executed when the start time or the end time of the range is changing | -| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| prefixCls | String | rc-picker | prefixCls of this component | +| className | String | '' | additional css class of root dom | +| style | React.CSSProperties | | additional style of root dom node | +| locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | +| value | moment | | current value like input's value | +| defaultValue | moment | | defaultValue like input's defaultValue | +| defaultPickerValue | moment | | Set default display picker view date | +| separator | String | '~' | set separator between inputs | +| picker | time \| date \| week \| month \| year | | control which kind of panel | +| placeholder | [String, String] | | placeholder of date input | +| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| showTime.defaultValue | [moment, moment] | | to set default time of selected date | +| use12Hours | Boolean | false | 12 hours display mode | +| disabledTime | Function(date: moment, type:'start'\|'end'):Object | | | to specify the time that cannot be selected | +| ranges | { String \| [range: string]: moment[] } \| { [range: string]: () => moment[] } | | preseted ranges for quick selection | +| format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | +| allowEmpty | [Boolean, Boolean] | | allow range picker clearing text | +| selectable | [Boolean, Boolean] | | whether to selected picker | +| disabled | Boolean | false | whether the range picker is disabled | +| onChange | Function(value:[moment], formatString: [string, string]) | | a callback function, can be executed when the selected time is changing | +| onCalendarChange | Function(value:[moment], formatString: [string, string]) | | a callback function, can be executed when the start time or the end time of the range is changing | +| direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | +| order | Boolean | true | (TimeRangePicker only) `false` to disable auto order | ### showTime-options diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 4b289fe..5001115 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -2,33 +2,14 @@ import * as React from 'react'; import classNames from 'classnames'; import warning from 'rc-util/lib/warning'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import { - DisabledTimes, - PanelMode, - PickerMode, - RangeValue, - EventValue, -} from './interface'; -import { - PickerBaseProps, - PickerDateProps, - PickerTimeProps, - PickerRefConfig, -} from './Picker'; +import { DisabledTimes, PanelMode, PickerMode, RangeValue, EventValue } from './interface'; +import { PickerBaseProps, PickerDateProps, PickerTimeProps, PickerRefConfig } from './Picker'; import { SharedTimeProps } from './panels/TimePanel'; import PickerTrigger from './PickerTrigger'; import PickerPanel from './PickerPanel'; import usePickerInput from './hooks/usePickerInput'; -import getDataOrAriaProps, { - toArray, - getValue, - updateValues, -} from './utils/miscUtil'; -import { - getDefaultFormat, - getInputSize, - elementsContains, -} from './utils/uiUtil'; +import getDataOrAriaProps, { toArray, getValue, updateValues } from './utils/miscUtil'; +import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil'; import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { isEqual, getClosingViewDate, isSameDate } from './utils/dateUtil'; import useValueTexts from './hooks/useValueTexts'; @@ -45,12 +26,7 @@ function reorderValues( values: RangeValue, generateConfig: GenerateConfig, ): RangeValue { - if ( - values && - values[0] && - values[1] && - generateConfig.isAfter(values[0], values[1]) - ) { + if (values && values[0] && values[1] && generateConfig.isAfter(values[0], values[1])) { return [values[1], values[0]]; } @@ -84,30 +60,17 @@ export interface RangePickerSharedProps { defaultPickerValue?: [DateType, DateType]; placeholder?: [string, string]; disabled?: boolean | [boolean, boolean]; - disabledTime?: ( - date: EventValue, - type: 'start' | 'end', - ) => DisabledTimes; + disabledTime?: (date: EventValue, type: 'start' | 'end') => DisabledTimes; ranges?: Record< string, - | Exclude, null> - | (() => Exclude, null>) + Exclude, null> | (() => Exclude, null>) >; separator?: React.ReactNode; allowEmpty?: [boolean, boolean]; mode?: [PanelMode, PanelMode]; - onChange?: ( - values: RangeValue, - formatString: [string, string], - ) => void; - onCalendarChange?: ( - values: RangeValue, - formatString: [string, string], - ) => void; - onPanelChange?: ( - values: RangeValue, - modes: [PanelMode, PanelMode], - ) => void; + onChange?: (values: RangeValue, formatString: [string, string]) => void; + onCalendarChange?: (values: RangeValue, formatString: [string, string]) => void; + onPanelChange?: (values: RangeValue, modes: [PanelMode, PanelMode]) => void; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onOk?: (dates: RangeValue) => void; @@ -133,10 +96,7 @@ type OmitPickerProps = Omit< | 'onOk' >; -type RangeShowTimeObject = Omit< - SharedTimeProps, - 'defaultValue' -> & { +type RangeShowTimeObject = Omit, 'defaultValue'> & { defaultValue?: DateType[]; }; @@ -152,7 +112,9 @@ export interface RangePickerDateProps export interface RangePickerTimeProps extends RangePickerSharedProps, - OmitPickerProps> {} + OmitPickerProps> { + order?: boolean; +} export type RangePickerProps = | RangePickerBaseProps @@ -213,11 +175,11 @@ function InnerRangePicker(props: RangePickerProps) { onBlur, onOk, components, + order, direction, } = props as MergedRangePickerProps; - const needConfirmButton: boolean = - (picker === 'date' && !!showTime) || picker === 'time'; + const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; const containerRef = React.useRef(null); const panelDivRef = React.useRef(null); @@ -228,9 +190,7 @@ function InnerRangePicker(props: RangePickerProps) { const endInputRef = React.useRef(null); // ============================= Misc ============================== - const formatList = toArray( - getDefaultFormat(format, picker, showTime, use12Hours), - ); + const formatList = toArray(getDefaultFormat(format, picker, showTime, use12Hours)); // Active picker const [activePickerIndex, setActivePickerIndex] = React.useState<0 | 1>(0); @@ -249,14 +209,12 @@ function InnerRangePicker(props: RangePickerProps) { }, [disabled]); // ============================= Value ============================= - const [mergedValue, setInnerValue] = useMergedState>( - null, - { - value, - defaultValue, - postState: values => reorderValues(values, generateConfig), - }, - ); + const [mergedValue, setInnerValue] = useMergedState>(null, { + value, + defaultValue, + postState: values => + picker === 'time' && !order ? values : reorderValues(values, generateConfig), + }); // =========================== View Date =========================== // Config view panel @@ -272,11 +230,7 @@ function InnerRangePicker(props: RangePickerProps) { postState: values => { let postValues = values; for (let i = 0; i < 2; i += 1) { - if ( - mergedDisabled[i] && - !getValue(postValues, i) && - !getValue(allowEmpty, i) - ) { + if (mergedDisabled[i] && !getValue(postValues, i) && !getValue(allowEmpty, i)) { postValues = updateValues(postValues, generateConfig.getNow(), i); } } @@ -284,14 +238,10 @@ function InnerRangePicker(props: RangePickerProps) { }, }); - const [rangeHoverValue, setRangeHoverValue] = React.useState< - RangeValue - >(null); + const [rangeHoverValue, setRangeHoverValue] = React.useState>(null); // ========================== Hover Range ========================== - const [hoverRangedValue, setHoverRangedValue] = React.useState< - RangeValue - >(null); + const [hoverRangedValue, setHoverRangedValue] = React.useState>(null); const onDateMouseEnter = (date: DateType) => { setHoverRangedValue(updateValues(selectedValue, date, activePickerIndex)); @@ -301,21 +251,15 @@ function InnerRangePicker(props: RangePickerProps) { }; // ============================= Modes ============================= - const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>( - [picker, picker], - { - value: mode, - }, - ); + const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>([picker, picker], { + value: mode, + }); React.useEffect(() => { setInnerModes([picker, picker]); }, [picker]); - const triggerModesChange = ( - modes: [PanelMode, PanelMode], - values: RangeValue, - ) => { + const triggerModesChange = (modes: [PanelMode, PanelMode], values: RangeValue) => { setInnerModes(modes); if (onPanelChange) { @@ -337,8 +281,7 @@ function InnerRangePicker(props: RangePickerProps) { const [mergedOpen, triggerInnerOpen] = useMergedState(false, { value: open, defaultValue: defaultOpen, - postState: postOpen => - mergedDisabled[activePickerIndex] ? false : postOpen, + postState: postOpen => (mergedDisabled[activePickerIndex] ? false : postOpen), onChange: newOpen => { if (onOpenChange) { onOpenChange(newOpen); @@ -363,11 +306,7 @@ function InnerRangePicker(props: RangePickerProps) { }, [mergedOpen]); // ============================ Trigger ============================ - let triggerOpen: ( - newOpen: boolean, - index: 0 | 1, - preventChangeEvent?: boolean, - ) => void; + let triggerOpen: (newOpen: boolean, index: 0 | 1, preventChangeEvent?: boolean) => void; const triggerChange = ( newValue: RangeValue, @@ -382,18 +321,14 @@ function InnerRangePicker(props: RangePickerProps) { const startValue = getValue(values, 0); let endValue = getValue(values, 1); - if ( - startValue && - endValue && - generateConfig.isAfter(startValue, endValue) - ) { + if (startValue && endValue && generateConfig.isAfter(startValue, endValue)) { if (!isSameDate(generateConfig, startValue, endValue)) { // Clean up end date when start date is after end date values = [startValue, null]; endValue = null; - } else { + } else if (picker !== 'time' || order !== false) { // Reorder when in same date - values = [endValue, startValue]; + values = reorderValues(values, generateConfig); } } @@ -412,21 +347,10 @@ function InnerRangePicker(props: RangePickerProps) { onCalendarChange(values, [startStr, endStr]); } - const canStartValueTrigger = canValueTrigger( - startValue, - 0, - mergedDisabled, - allowEmpty, - ); - const canEndValueTrigger = canValueTrigger( - endValue, - 1, - mergedDisabled, - allowEmpty, - ); + const canStartValueTrigger = canValueTrigger(startValue, 0, mergedDisabled, allowEmpty); + const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled, allowEmpty); - const canTrigger = - values === null || (canStartValueTrigger && canEndValueTrigger); + const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); if (canTrigger) { // Trigger onChange only when value is validate @@ -465,11 +389,7 @@ function InnerRangePicker(props: RangePickerProps) { } }; - triggerOpen = ( - newOpen: boolean, - index: 0 | 1, - preventChangeEvent: boolean = false, - ) => { + triggerOpen = (newOpen: boolean, index: 0 | 1, preventChangeEvent: boolean = false) => { if (newOpen) { setActivePickerIndex(index); triggerInnerOpen(newOpen); @@ -510,22 +430,12 @@ function InnerRangePicker(props: RangePickerProps) { locale, }; - const startValueTexts = useValueTexts( - getValue(selectedValue, 0), - sharedTextHooksProps, - ); + const startValueTexts = useValueTexts(getValue(selectedValue, 0), sharedTextHooksProps); - const endValueTexts = useValueTexts( - getValue(selectedValue, 1), - sharedTextHooksProps, - ); + const endValueTexts = useValueTexts(getValue(selectedValue, 1), sharedTextHooksProps); const onTextChange = (newText: string, index: 0 | 1) => { - const inputDate = generateConfig.locale.parse( - locale.locale, - newText, - formatList, - ); + const inputDate = generateConfig.locale.parse(locale.locale, newText, formatList); const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; @@ -535,18 +445,12 @@ function InnerRangePicker(props: RangePickerProps) { } }; - const [ - startText, - triggerStartTextChange, - resetStartText, - ] = useTextValueMapping({ + const [startText, triggerStartTextChange, resetStartText] = useTextValueMapping({ valueTexts: startValueTexts, onTextChange: newText => onTextChange(newText, 0), }); - const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping< - DateType - >({ + const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping({ valueTexts: endValueTexts, onTextChange: newText => onTextChange(newText, 1), }); @@ -579,18 +483,12 @@ function InnerRangePicker(props: RangePickerProps) { }, }); - const [ - startInputProps, - { focused: startFocused, typing: startTyping }, - ] = usePickerInput({ + const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ ...getSharedInputHookProps(0, resetStartText), open: startOpen, }); - const [ - endInputProps, - { focused: endFocused, typing: endTyping }, - ] = usePickerInput({ + const [endInputProps, { focused: endFocused, typing: endTyping }] = usePickerInput({ ...getSharedInputHookProps(1, resetEndText), open: endOpen, }); @@ -599,19 +497,11 @@ function InnerRangePicker(props: RangePickerProps) { // Close should sync back with text value const startStr = mergedValue && mergedValue[0] - ? generateConfig.locale.format( - locale.locale, - mergedValue[0], - 'YYYYMMDDHHmmss', - ) + ? generateConfig.locale.format(locale.locale, mergedValue[0], 'YYYYMMDDHHmmss') : ''; const endStr = mergedValue && mergedValue[1] - ? generateConfig.locale.format( - locale.locale, - mergedValue[1], - 'YYYYMMDDHHmmss', - ) + ? generateConfig.locale.format(locale.locale, mergedValue[1], 'YYYYMMDDHHmmss') : ''; React.useEffect(() => { @@ -707,16 +597,12 @@ function InnerRangePicker(props: RangePickerProps) { panelHoverRangedValue = hoverRangedValue; } - let panelShowTime: - | boolean - | SharedTimeProps - | undefined = showTime; + let panelShowTime: boolean | SharedTimeProps | undefined = showTime; if (showTime && typeof showTime === 'object' && showTime.defaultValue) { const timeDefaultValues: DateType[] = showTime.defaultValue!; panelShowTime = { ...showTime, - defaultValue: - getValue(timeDefaultValues, activePickerIndex) || undefined, + defaultValue: getValue(timeDefaultValues, activePickerIndex) || undefined, }; } @@ -737,21 +623,15 @@ function InnerRangePicker(props: RangePickerProps) { generateConfig={generateConfig} style={undefined} direction={direction} - disabledDate={ - activePickerIndex === 0 ? disabledStartDate : disabledEndDate - } + disabledDate={activePickerIndex === 0 ? disabledStartDate : disabledEndDate} disabledTime={date => { if (disabledTime) { - return disabledTime( - date, - activePickerIndex === 0 ? 'start' : 'end', - ); + return disabledTime(date, activePickerIndex === 0 ? 'start' : 'end'); } return false; }} className={classNames({ - [`${prefixCls}-panel-focused`]: - activePickerIndex === 0 ? !startTyping : !endTyping, + [`${prefixCls}-panel-focused`]: activePickerIndex === 0 ? !startTyping : !endTyping, })} value={getValue(selectedValue, activePickerIndex)} locale={locale} @@ -783,27 +663,18 @@ function InnerRangePicker(props: RangePickerProps) { panelDivRef.current ) { // Arrow offset - arrowLeft = - startInputDivRef.current.offsetWidth + separatorRef.current.offsetWidth; + arrowLeft = startInputDivRef.current.offsetWidth + separatorRef.current.offsetWidth; - if ( - panelDivRef.current.offsetWidth && - arrowLeft > panelDivRef.current.offsetWidth - ) { + if (panelDivRef.current.offsetWidth && arrowLeft > panelDivRef.current.offsetWidth) { panelLeft = arrowLeft; } } - const arrowPositionStyle = - direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; + const arrowPositionStyle = direction === 'rtl' ? { right: arrowLeft } : { left: arrowLeft }; function renderPanels() { let panels: React.ReactNode; - const extraNode = getExtraFooter( - prefixCls, - mergedModes[activePickerIndex], - renderExtraFooter, - ); + const extraNode = getExtraFooter(prefixCls, mergedModes[activePickerIndex], renderExtraFooter); const rangesNode = getRanges({ prefixCls, @@ -847,9 +718,7 @@ function InnerRangePicker(props: RangePickerProps) { panels = ( <> {direction === 'rtl' ? rightPanel : leftPanel} - {direction === 'rtl' - ? showDoublePanel && leftPanel - : showDoublePanel && rightPanel} + {direction === 'rtl' ? showDoublePanel && leftPanel : showDoublePanel && rightPanel} ); } else { @@ -878,10 +747,7 @@ function InnerRangePicker(props: RangePickerProps) { const rangePanel = (
@@ -931,11 +797,7 @@ function InnerRangePicker(props: RangePickerProps) { let activeBarLeft: number = 0; let activeBarWidth: number = 0; - if ( - startInputDivRef.current && - endInputDivRef.current && - separatorRef.current - ) { + if (startInputDivRef.current && endInputDivRef.current && separatorRef.current) { if (activePickerIndex === 0) { activeBarWidth = startInputDivRef.current.offsetWidth; } else { @@ -946,10 +808,7 @@ function InnerRangePicker(props: RangePickerProps) { const activeBarPositionStyle = direction === 'rtl' ? { right: activeBarLeft } : { left: activeBarLeft }; // ============================ Return ============================= - const onContextSelect = ( - date: DateType, - type: 'key' | 'mouse' | 'submit', - ) => { + const onContextSelect = (date: DateType, type: 'key' | 'mouse' | 'submit') => { const values = updateValues(selectedValue, date, activePickerIndex); if (type === 'submit' || (type !== 'key' && !needConfirmButton)) { @@ -988,8 +847,7 @@ function InnerRangePicker(props: RangePickerProps) { ref={containerRef} className={classNames(prefixCls, `${prefixCls}-range`, className, { [`${prefixCls}-disabled`]: mergedDisabled[0] && mergedDisabled[1], - [`${prefixCls}-focused`]: - activePickerIndex === 0 ? startFocused : endFocused, + [`${prefixCls}-focused`]: activePickerIndex === 0 ? startFocused : endFocused, [`${prefixCls}-rtl`]: direction === 'rtl', })} style={style} @@ -1054,9 +912,7 @@ function InnerRangePicker(props: RangePickerProps) { } // Wrap with class component to enable pass generic with instance method -class RangePicker extends React.Component< - RangePickerProps -> { +class RangePicker extends React.Component> { pickerRef = React.createRef(); focus = () => { diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index b4c16f5..a9bda21 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -4,13 +4,7 @@ import { act } from 'react-dom/test-utils'; import KeyCode from 'rc-util/lib/KeyCode'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import { Moment } from 'moment'; -import { - mount, - getMoment, - isSame, - MomentRangePicker, - Wrapper, -} from './util/commonUtil'; +import { mount, getMoment, isSame, MomentRangePicker, Wrapper } from './util/commonUtil'; import zhCN from '../src/locale/zh_CN'; describe('Picker.Range', () => { @@ -40,9 +34,7 @@ describe('Picker.Range', () => { describe('value', () => { it('defaultValue', () => { const wrapper = mount( - , + , ); matchValues(wrapper, '1989-11-28', '1990-09-03'); @@ -50,9 +42,7 @@ describe('Picker.Range', () => { it('controlled', () => { const wrapper = mount( - , + , ); matchValues(wrapper, '1989-11-28', '1990-09-03'); @@ -69,10 +59,7 @@ describe('Picker.Range', () => { const onChange = jest.fn(); const onCalendarChange = jest.fn(); const wrapper = mount( - , + , ); // Start date @@ -82,9 +69,7 @@ describe('Picker.Range', () => { wrapper.selectCell(13); expect(onChange).not.toHaveBeenCalled(); - expect( - isSame(onCalendarChange.mock.calls[0][0][0], '1990-09-13'), - ).toBeTruthy(); + expect(isSame(onCalendarChange.mock.calls[0][0][0], '1990-09-13')).toBeTruthy(); expect(onCalendarChange.mock.calls[0][0][1]).toBeFalsy(); expect(onCalendarChange.mock.calls[0][1]).toEqual(['1990-09-13', '']); @@ -96,24 +81,15 @@ describe('Picker.Range', () => { expect(isSame(onChange.mock.calls[0][0][1], '1990-09-14')).toBeTruthy(); expect(onChange.mock.calls[0][1]).toEqual(['1990-09-13', '1990-09-14']); - expect( - isSame(onCalendarChange.mock.calls[0][0][0], '1990-09-13'), - ).toBeTruthy(); - expect( - isSame(onCalendarChange.mock.calls[0][0][1], '1990-09-14'), - ).toBeTruthy(); - expect(onCalendarChange.mock.calls[0][1]).toEqual([ - '1990-09-13', - '1990-09-14', - ]); + expect(isSame(onCalendarChange.mock.calls[0][0][0], '1990-09-13')).toBeTruthy(); + expect(isSame(onCalendarChange.mock.calls[0][0][1], '1990-09-14')).toBeTruthy(); + expect(onCalendarChange.mock.calls[0][1]).toEqual(['1990-09-13', '1990-09-14']); }); }); it('exchanged value should re-order', () => { const wrapper = mount( - , + , ); wrapper.update(); @@ -141,10 +117,7 @@ describe('Picker.Range', () => { it('year with footer', () => { const wrapper = mount( -

footer

} - picker="year" - />, +

footer

} picker="year" />, ); wrapper.openPicker(); expect(wrapper.find('.rc-picker-footer').text()).toEqual('footer'); @@ -196,12 +169,8 @@ describe('Picker.Range', () => { wrapper.openPicker(); wrapper.selectCell(11); - expect( - wrapper.findCell(4).hasClass('rc-picker-cell-disabled'), - ).toBeTruthy(); - expect( - wrapper.findCell(11).hasClass('rc-picker-cell-disabled'), - ).toBeFalsy(); + expect(wrapper.findCell(4).hasClass('rc-picker-cell-disabled')).toBeTruthy(); + expect(wrapper.findCell(11).hasClass('rc-picker-cell-disabled')).toBeFalsy(); }); it('Reset when startDate is after endDate', () => { @@ -220,19 +189,12 @@ describe('Picker.Range', () => { it('allowEmpty', () => { const onChange = jest.fn(); const wrapper = mount( - , + , ); wrapper.openPicker(); wrapper.selectCell(11); - expect(onChange).toHaveBeenCalledWith( - [expect.anything(), null], - ['1990-09-11', ''], - ); + expect(onChange).toHaveBeenCalledWith([expect.anything(), null], ['1990-09-11', '']); wrapper.clearValue(); onChange.mockReset(); @@ -291,9 +253,7 @@ describe('Picker.Range', () => { it('null value with disabled', () => { const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mount( - , - ); + mount(); expect(errSpy).toHaveBeenCalledWith( 'Warning: `disabled` should not set with empty `value`. You should set `allowEmpty` or `value` instead.', @@ -363,33 +323,19 @@ describe('Picker.Range', () => { wrapper.openPicker(); wrapper.find('.rc-picker-preset > *').simulate('mouseEnter'); - expect( - wrapper.findCell(11).hasClass('rc-picker-cell-range-start'), - ).toBeTruthy(); - expect( - wrapper.findCell(12).hasClass('rc-picker-cell-in-range'), - ).toBeTruthy(); - expect( - wrapper.findCell(13).hasClass('rc-picker-cell-range-end'), - ).toBeTruthy(); + expect(wrapper.findCell(11).hasClass('rc-picker-cell-range-start')).toBeTruthy(); + expect(wrapper.findCell(12).hasClass('rc-picker-cell-in-range')).toBeTruthy(); + expect(wrapper.findCell(13).hasClass('rc-picker-cell-range-end')).toBeTruthy(); wrapper.find('.rc-picker-preset > *').simulate('mouseLeave'); - expect( - wrapper.findCell(11).hasClass('rc-picker-cell-range-start'), - ).toBeFalsy(); - expect( - wrapper.findCell(12).hasClass('rc-picker-cell-in-range'), - ).toBeFalsy(); - expect( - wrapper.findCell(13).hasClass('rc-picker-cell-range-end'), - ).toBeFalsy(); + expect(wrapper.findCell(11).hasClass('rc-picker-cell-range-start')).toBeFalsy(); + expect(wrapper.findCell(12).hasClass('rc-picker-cell-in-range')).toBeFalsy(); + expect(wrapper.findCell(13).hasClass('rc-picker-cell-range-end')).toBeFalsy(); }); }); it('placeholder', () => { - const wrapper = mount( - , - ); + const wrapper = mount(); expect( wrapper .find('input') @@ -406,9 +352,7 @@ describe('Picker.Range', () => { it('defaultPickerValue', () => { const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -551,10 +495,7 @@ describe('Picker.Range', () => { it('mode', () => { const onPanelChange = jest.fn(); const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -573,9 +514,7 @@ describe('Picker.Range', () => { it('picker', () => { const onPanelChange = jest.fn(); - const wrapper = mount( - , - ); + const wrapper = mount(); // First go to year panel wrapper.openPicker(); @@ -684,57 +623,35 @@ describe('Picker.Range', () => { // Hover it wrapper.findCell(end).simulate('mouseEnter'); - expect( - wrapper.findCell(start).hasClass('rc-picker-cell-range-hover-start'), - ).toBeTruthy(); - expect( - wrapper.findCell(mid).hasClass('rc-picker-cell-range-hover'), - ).toBeTruthy(); - expect( - wrapper.findCell(end).hasClass('rc-picker-cell-range-hover-end'), - ).toBeTruthy(); + expect(wrapper.findCell(start).hasClass('rc-picker-cell-range-hover-start')).toBeTruthy(); + expect(wrapper.findCell(mid).hasClass('rc-picker-cell-range-hover')).toBeTruthy(); + expect(wrapper.findCell(end).hasClass('rc-picker-cell-range-hover-end')).toBeTruthy(); // Leave wrapper.findCell(end).simulate('mouseLeave'); - expect( - wrapper.findCell(start).hasClass('rc-picker-cell-range-hover-start'), - ).toBeFalsy(); - expect( - wrapper.findCell(mid).hasClass('rc-picker-cell-range-hover'), - ).toBeFalsy(); - expect( - wrapper.findCell(end).hasClass('rc-picker-cell-range-hover-end'), - ).toBeFalsy(); + expect(wrapper.findCell(start).hasClass('rc-picker-cell-range-hover-start')).toBeFalsy(); + expect(wrapper.findCell(mid).hasClass('rc-picker-cell-range-hover')).toBeFalsy(); + expect(wrapper.findCell(end).hasClass('rc-picker-cell-range-hover-end')).toBeFalsy(); }); }); it('range edge className', () => { const wrapper = mount( - , + , ); // End edge wrapper.openPicker(); wrapper.findCell(10).simulate('mouseEnter'); - expect( - wrapper.findCell(19).hasClass('rc-picker-cell-range-hover-edge-end'), - ).toBeTruthy(); - expect( - wrapper.findCell(20).hasClass('rc-picker-cell-range-start-near-hover'), - ).toBeTruthy(); + expect(wrapper.findCell(19).hasClass('rc-picker-cell-range-hover-edge-end')).toBeTruthy(); + expect(wrapper.findCell(20).hasClass('rc-picker-cell-range-start-near-hover')).toBeTruthy(); wrapper.findCell(10).simulate('mouseOut'); // Start edge wrapper.openPicker(1); wrapper.findCell(28).simulate('mouseEnter'); - expect( - wrapper.findCell(21).hasClass('rc-picker-cell-range-hover-edge-start'), - ).toBeTruthy(); - expect( - wrapper.findCell(20).hasClass('rc-picker-cell-range-end-near-hover'), - ).toBeTruthy(); + expect(wrapper.findCell(21).hasClass('rc-picker-cell-range-hover-edge-start')).toBeTruthy(); + expect(wrapper.findCell(20).hasClass('rc-picker-cell-range-end-near-hover')).toBeTruthy(); wrapper.findCell(28).simulate('mouseOut'); }); }); @@ -797,10 +714,9 @@ describe('Picker.Range', () => { const wrapper = mount(); wrapper.openPicker(1); wrapper.update(); - expect( - (wrapper.find('.rc-picker-panel-container').props() as any).style - .marginLeft, - ).toEqual(200); + expect((wrapper.find('.rc-picker-panel-container').props() as any).style.marginLeft).toEqual( + 200, + ); }); }); @@ -841,9 +757,7 @@ describe('Picker.Range', () => { it('fixed open need repeat trigger onOpenChange', () => { jest.useFakeTimers(); const onOpenChange = jest.fn(); - const wrapper = mount( - , - ); + const wrapper = mount(); for (let i = 0; i < 10; i += 1) { const clickEvent = new Event('mousedown'); @@ -871,11 +785,7 @@ describe('Picker.Range', () => { const onCalendarChange = jest.fn(); const onOk = jest.fn(); const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -990,10 +900,7 @@ describe('Picker.Range', () => { it('with closing value', () => { const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -1004,10 +911,7 @@ describe('Picker.Range', () => { it('with far value', () => { const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -1018,10 +922,7 @@ describe('Picker.Range', () => { it('no end date', () => { const wrapper = mount( - , + , ); wrapper.openPicker(); @@ -1046,39 +947,71 @@ describe('Picker.Range', () => { expect(wrapper.find('MonthPanel').length).toBeTruthy(); }); - it('datetime should reorder in onChange if start is after end in same date', () => { - const onChange = jest.fn(); + describe('reorder onChange logic', () => { + it('datetime should reorder in onChange if start is after end in same date', () => { + const onChange = jest.fn(); - const wrapper = mount(); - wrapper.openPicker(); - wrapper.selectCell(15); - wrapper - .find('ul') - .first() - .find('li') - .last() - .simulate('click'); - wrapper.find('.rc-picker-ok button').simulate('click'); + const wrapper = mount(); + wrapper.openPicker(); + wrapper.selectCell(15); + wrapper + .find('ul') + .first() + .find('li') + .last() + .simulate('click'); + wrapper.find('.rc-picker-ok button').simulate('click'); - wrapper.selectCell(15); - wrapper - .find('ul') - .first() - .find('li') - .first() - .simulate('click'); - wrapper.find('.rc-picker-ok button').simulate('click'); + wrapper.selectCell(15); + wrapper + .find('ul') + .first() + .find('li') + .first() + .simulate('click'); + wrapper.find('.rc-picker-ok button').simulate('click'); - expect(onChange).toHaveBeenCalledWith(expect.anything(), [ - '1990-09-15 00:00:00', - '1990-09-15 23:00:00', - ]); + expect(onChange).toHaveBeenCalledWith(expect.anything(), [ + '1990-09-15 00:00:00', + '1990-09-15 23:00:00', + ]); - expect( - isSame(onChange.mock.calls[0][0][0], '1990-09-15 00:00:00'), - ).toBeTruthy(); - expect( - isSame(onChange.mock.calls[0][0][1], '1990-09-15 23:00:00'), - ).toBeTruthy(); + expect(isSame(onChange.mock.calls[0][0][0], '1990-09-15 00:00:00')).toBeTruthy(); + expect(isSame(onChange.mock.calls[0][0][1], '1990-09-15 23:00:00')).toBeTruthy(); + }); + + function testOrderOnTime(order: boolean, start: string, end: string) { + it(`order: ${String(order)} when picker is time`, () => { + const onChange = jest.fn(); + + const wrapper = mount( + , + ); + wrapper.openPicker(); + wrapper + .find('ul') + .first() + .find('li') + .last() + .simulate('click'); + wrapper.find('.rc-picker-ok button').simulate('click'); + + wrapper + .find('ul') + .first() + .find('li') + .at(2) + .simulate('click'); + wrapper.find('.rc-picker-ok button').simulate('click'); + + expect(onChange).toHaveBeenCalledWith(expect.anything(), [start, end]); + + expect(isSame(onChange.mock.calls[0][0][0], start)).toBeTruthy(); + expect(isSame(onChange.mock.calls[0][0][1], end)).toBeTruthy(); + }); + } + + testOrderOnTime(false, '23:00:00', '02:00:00'); + testOrderOnTime(true, '02:00:00', '23:00:00'); }); });