From 42de085a90acdff1d40ab5c0c2475b67184fdc4e Mon Sep 17 00:00:00 2001 From: myron Date: Wed, 20 Nov 2019 15:45:23 +0800 Subject: [PATCH] feat(DatePicker): Add WeekPicker (#1284) * feat(Calendar): export Calendar.RangeCalendar * feat(DatePicker): Add WeekPicker --- docs/date-picker/demo/basic.md | 5 +- docs/date-picker/demo/default.md | 2 + docs/date-picker/index.en-us.md | 28 ++ docs/date-picker/index.md | 36 +- src/calendar/index.jsx | 2 + src/calendar/scss/table.scss | 54 +++ src/date-picker/index.jsx | 5 + src/date-picker/main.scss | 14 +- src/date-picker/range-picker.jsx | 129 +++++-- src/date-picker/scss/date-picker.scss | 6 +- src/date-picker/scss/variable.scss | 2 + src/date-picker/util/index.js | 10 +- src/date-picker/week-picker.jsx | 464 ++++++++++++++++++++++++++ src/locale/en-us.js | 1 + src/locale/ja-jp.js | 1 + src/locale/zh-cn.js | 1 + src/locale/zh-tw.js | 1 + test/date-picker/index-spec.js | 154 ++++++++- types/date-picker/index.d.ts | 2 + types/locale/default.d.ts | 1 + 20 files changed, 886 insertions(+), 32 deletions(-) create mode 100644 src/date-picker/week-picker.jsx diff --git a/docs/date-picker/demo/basic.md b/docs/date-picker/demo/basic.md index 33b54c5373..0da0c08570 100644 --- a/docs/date-picker/demo/basic.md +++ b/docs/date-picker/demo/basic.md @@ -18,13 +18,16 @@ A basic usage case. ````jsx import { DatePicker } from '@alifd/next'; -const { RangePicker, MonthPicker, YearPicker } = DatePicker; +const { RangePicker, MonthPicker, YearPicker, WeekPicker } = DatePicker; const onChange = val => console.log(val); ReactDOM.render(






+

+

+

, mountNode); ```` diff --git a/docs/date-picker/demo/default.md b/docs/date-picker/demo/default.md index e65a1feb43..2f9bdaf7f2 100644 --- a/docs/date-picker/demo/default.md +++ b/docs/date-picker/demo/default.md @@ -28,6 +28,8 @@ ReactDOM.render(






+

+

, mountNode); ```` diff --git a/docs/date-picker/index.en-us.md b/docs/date-picker/index.en-us.md index 40b792ecf7..70822f772f 100644 --- a/docs/date-picker/index.en-us.md +++ b/docs/date-picker/index.en-us.md @@ -50,6 +50,7 @@ DatePicker are used to select a single date for an input. | Param | Description | Type | Default Value | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------ | | size | Size of input

**option**:
'small', 'medium', 'large' | Enum | 'medium' | +| type (v1.19.0+) | Select date range type

**option**:
'date', 'month', 'year' | Enum | 'date' | | | | defaultVisibleMonth | Default visible month

**signature**:
Function() => MomentObject
**return**:
{MomentObject} moment instance with specified month
| Function | - | | value | Range value `[moment, moment]` | Array | - | | defaultValue | Default range value `[moment, moment]` | Array | - | @@ -78,6 +79,33 @@ DatePicker are used to select a single date for an input. | endDateInputAriaLabel | End date input `aria-label` attribute | String | - | | | | endTimeInputAriaLabel | End time input `aria-label` attribute | String | - | | | +### DatePicker.WeekPicker v1.19.0+ + +| 参数 | 说明 | 类型 | 默认值 | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | +| label | Inset label of input | ReactNode | - | +| size | Size of input

**option**:
'small', 'medium', 'large' | Enum | 'medium' | +| state | State of input

**option**:
'success', 'error' | Enum | - | +| placeholder | Placeholder of input | String | - | +| defaultVisibleMonth | Default visible month

**signature**:
Function() => MomentObject
**return**:
{MomentObject} moment instance with specified month
| Function | - | +| value | Value of date-picker | moment | - | +| defaultValue | Default value of date-picker | moment | - | +| format | Format of date value (it will also effect user input) | String | 'YYYY-wo' | + +| disabledDate | Function to disable date

**signature**:
Function(dateValue: MomentObject) => Boolean
**parameter**:
_dateValue_: {MomentObject} null
_view_: {Enum} current view type: 'year', 'month', 'date'
**return**:
{Boolean} if disable current date
| Function | () => false | +| footerRender | Template render for custom footer

**signature**:
Function() => Node
**return**:
{Node} Custom footer
| Function | () => null | +| onChange | Callback when date changes

**signature**:
Function() => MomentObject
**return**:
{MomentObject} dateValue
| Function | func.noop | +| disabled | Disable the picker | Boolean | - | +| hasClear | Has clear icon | Boolean | true | +| visible | Visible state of popup | Boolean | - | +| defaultVisible | Default visible state of popup | Boolean | - | +| onVisibleChange | Callback when visible state changes

**signature**:
Function(visible: Boolean, reason: String) => void
**parameter**:
_visible_: {Boolean} if popup visible
_reason_: {String} reason to change visible | Function | func.noop | +| popupTriggerType | Trigger type of popup

**option**:
'click', 'hover' | Enum | 'click' | +| popupAlign | Align of popup | String | 'tl tl' | +| popupContainer | Container of a popup

**signature**:
Function(target: Element) => Element
**option**:
_target_: {Element} target element
**return**:
{Element} coninter element of popup
| Function | - | +| popupStyle | Custom style of popup | Object | - | +| popupClassName | Custom className of popup | String | - | +| popupProps | Props of popup | Object | - | ## ARIA and KeyBoard diff --git a/docs/date-picker/index.md b/docs/date-picker/index.md index 9ac52c52e0..6b9704fb99 100644 --- a/docs/date-picker/index.md +++ b/docs/date-picker/index.md @@ -158,11 +158,12 @@ DatePicker 默认情况下接收和返回的数据类型都是 Moment 对象。 | 参数 | 说明 | 类型 | 默认值 | | | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- | -------- | --------- | -| defaultVisibleMonth | 默认展示的起始月份

**签名**:
Function() => MomentObject
**返回值**:
{MomentObject} 返回包含指定月份的 moment 对象实例
| Function | - | | | | size | 输入框尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | | | +| type (v1.19.0+) | 日期范围类型

**可选值**:
'date', 'month', 'year' | Enum | 'date' | | | +| defaultVisibleMonth | 默认展示的起始月份

**签名**:
Function() => MomentObject
**返回值**:
{MomentObject} 返回包含指定月份的 moment 对象实例
| Function | - | | | | value | 日期范围值数组 [moment, moment] | Array | - | | | | defaultValue | 初始的日期范围值数组 [moment, moment] | Array | - | | | -| format | 日期格式 | String | 'YYYY-MM-DD' | | | +| format | 日期格式 | String | - | | | | showTime | 是否使用时间控件,支持传入 TimePicker 的属性 | Object/Boolean | false | | | | resetTime | 每次选择是否重置时间(仅在 showTime 开启时有效) | Boolean | false | | | | disabledDate | 禁用日期函数

**签名**:
Function(日期值: MomentObject, view: String) => Boolean
**参数**:
_日期值_: {MomentObject} null
_view_: {String} 当前视图类型,year: 年, month: 月, date: 日
**返回值**:
{Boolean} 是否禁用
| Function | () => false | | | @@ -191,6 +192,37 @@ DatePicker 默认情况下接收和返回的数据类型都是 Moment 对象。 | endDateInputAriaLabel | 结束日期输入框的 aria-label 属性 | String | - | | | | endTimeInputAriaLabel | 结束时间输入框的 aria-label 属性 | String | - | | | +### DatePicker.WeekPicker v1.19.0+ + +| 参数 | 说明 | 类型 | 默认值 | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | +| label | 输入框内置标签 | ReactNode | - | +| size | 输入框尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | +| state | 输入框状态

**可选值**:
'success', 'loading', 'error' | Enum | - | +| placeholder | 输入提示 | String | - | +| defaultVisibleMonth | 默认展现的月

**签名**:
Function() => MomentObject
**返回值**:
{MomentObject} 返回包含指定月份的 moment 对象实例
| Function | - | +| value | 日期值(受控)moment 对象 | custom | - | +| defaultValue | 初始日期值,moment 对象 | custom | - | +| format | 日期值的格式(用于限定用户输入和展示) | String | 'YYYY-wo' | +| disabledDate | 禁用日期函数

**签名**:
Function(日期值: MomentObject, view: String) => Boolean
**参数**:
_日期值_: {MomentObject} null
_view_: {String} 当前视图类型,year: 年, month: 月, date: 日
**返回值**:
{Boolean} 是否禁用
| Function | () => false | +| footerRender | 自定义面板页脚

**签名**:
Function() => Node
**返回值**:
{Node} 自定义的面板页脚组件
| Function | () => null | +| onChange | 日期值改变时的回调

**签名**:
Function(value: MomentObject/String) => void
**参数**:
_value_: {MomentObject/String} 日期值 | Function | func.noop | +| disabled | 是否禁用 | Boolean | - | +| hasClear | 是否显示清空按钮 | Boolean | true | +| visible | 弹层显示状态 | Boolean | - | +| defaultVisible | 弹层默认是否显示 | Boolean | false | +| onVisibleChange | 弹层展示状态变化时的回调

**签名**:
Function(visible: Boolean, type: String) => void
**参数**:
_visible_: {Boolean} 弹层是否显示
_type_: {String} 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 | Function | func.noop | +| popupTriggerType | 弹层触发方式

**可选值**:
'click', 'hover' | Enum | 'click' | +| popupAlign | 弹层对齐方式,具体含义见 OverLay文档 | String | 'tl tl' | +| popupContainer | 弹层容器 | String/Function | - | +| popupStyle | 弹层自定义样式 | Object | - | +| popupClassName | 弹层自定义样式类 | String | - | +| popupProps | 弹层其他属性 | Object | - | +| followTrigger | 是否跟随滚动 | Boolean | - | +| inputProps | 输入框其他属性 | Object | - | +| dateCellRender | 自定义日期渲染函数

**签名**:
Function(value: Object) => ReactNode
**参数**:
_value_: {Object} 日期值(moment对象)
**返回值**:
{ReactNode} null
| Function | - | +| monthCellRender | 自定义月份渲染函数

**签名**:
Function(calendarDate: Object) => ReactNode
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
**返回值**:
{ReactNode} null
| Function | - | + ### DatePicker.YearPicker | 参数 | 说明 | 类型 | 默认值 | diff --git a/src/calendar/index.jsx b/src/calendar/index.jsx index 2591affb9e..1e55b5084c 100644 --- a/src/calendar/index.jsx +++ b/src/calendar/index.jsx @@ -1,6 +1,7 @@ import ConfigProvider from '../config-provider'; import { preFormatDateValue } from './utils'; import Calendar from './calendar'; +import RangeCalendar from './range-calendar'; /* istanbul ignore next */ const transform = (props, deprecated) => { @@ -75,6 +76,7 @@ const transform = (props, deprecated) => { return newProps; }; +Calendar.RangeCalendar = RangeCalendar; export default ConfigProvider.config(Calendar, { transform, }); diff --git a/src/calendar/scss/table.scss b/src/calendar/scss/table.scss index 1062cb23c7..db38d847b1 100644 --- a/src/calendar/scss/table.scss +++ b/src/calendar/scss/table.scss @@ -234,3 +234,57 @@ } } } + +#{$calendar-prefix}-panel#{$calendar-prefix}-week { + #{$calendar-prefix}-tbody { + tr { + cursor: pointer; + } + tr:hover { + #{$calendar-prefix}-cell #{$calendar-prefix}-date { + @include calendar-cell-state ( + $calendar-card-table-cell-hover-background, + $calendar-card-table-cell-hover-color, + $calendar-card-table-cell-hover-border-color + ); + } + } + + #{$calendar-prefix}-cell.#{$css-prefix}selected { + #{$calendar-prefix}-date { + font-weight: normal; + background: transparent; + border-color: transparent; + } + } + + #{$calendar-prefix}-week-active-date { + position: relative; + color: $calendar-card-table-cell-inrange-color; + &::before { + content: ''; + position: absolute; + left: -$line-1; + top: -$line-1; + bottom: -$line-1; + right: -$line-1; + border: $line-1 $line-solid; + background: $calendar-card-table-cell-inrange-background; + border-color: $calendar-card-table-cell-inrange-border-color; + border-radius: $calendar-card-table-cell-date-border-radius; + } + > span { + position: relative; + } + } + + #{$calendar-prefix}-week-active-start, + #{$calendar-prefix}-week-active-end { + color: $calendar-card-table-cell-select-color; + &::before { + background: $calendar-card-table-cell-select-background; + border-color: $calendar-card-table-cell-select-border-color; + } + } + } +} diff --git a/src/date-picker/index.jsx b/src/date-picker/index.jsx index 6af78513a0..7040f4c72b 100644 --- a/src/date-picker/index.jsx +++ b/src/date-picker/index.jsx @@ -3,6 +3,7 @@ import DatePicker from './date-picker'; import RangePicker from './range-picker'; import MonthPicker from './month-picker'; import YearPicker from './year-picker'; +import WeekPicker from './week-picker'; /* istanbul ignore next */ const transform = (props, deprecated) => { @@ -69,6 +70,10 @@ DatePicker.YearPicker = ConfigProvider.config(YearPicker, { transform, }); +DatePicker.WeekPicker = ConfigProvider.config(WeekPicker, { + componentName: 'DatePicker', +}); + export default ConfigProvider.config(DatePicker, { transform, }); diff --git a/src/date-picker/main.scss b/src/date-picker/main.scss index 8dd5725b46..23bc519b71 100644 --- a/src/date-picker/main.scss +++ b/src/date-picker/main.scss @@ -10,7 +10,11 @@ @import "scss/range-picker"; @import "./rtl.scss"; -#{$date-picker-prefix}, #{$range-picker-prefix}, #{$month-picker-prefix}, #{$year-picker-prefix} { +#{$date-picker-prefix}, +#{$range-picker-prefix}, +#{$month-picker-prefix}, +#{$year-picker-prefix}, +#{$week-picker-prefix} { @include box-sizing; &-body { @@ -55,3 +59,11 @@ vertical-align: baseline; } } + +#{$range-picker-prefix}-panel-body { + font-size: 0; + .#{$css-prefix}calendar { + display: inline-block; + width: 50%; + } +} diff --git a/src/date-picker/range-picker.jsx b/src/date-picker/range-picker.jsx index 50006b14f8..a82c82500d 100644 --- a/src/date-picker/range-picker.jsx +++ b/src/date-picker/range-picker.jsx @@ -5,6 +5,7 @@ import moment from 'moment'; import ConfigProvider from '../config-provider'; import Overlay from '../overlay'; import Input from '../input'; +import Calendar from '../calendar'; import RangeCalendar from '../calendar/range-calendar'; import TimePickerPanel from '../time-picker/panel'; import nextLocale from '../locale/zh-cn'; @@ -57,6 +58,10 @@ export default class RangePicker extends Component { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, + /** + * 日期范围类型 + */ + type: PropTypes.oneOf(['date', 'month', 'year']), /** * 默认展示的起始月份 * @return {MomentObject} 返回包含指定月份的 moment 对象实例 @@ -220,10 +225,11 @@ export default class RangePicker extends Component { static defaultProps = { prefix: 'next-', rtl: false, - format: 'YYYY-MM-DD', + type: 'date', size: 'medium', showTime: false, resetTime: false, + format: 'YYYY-MM-DD', disabledDate: () => false, footerRender: () => null, hasClear: true, @@ -238,7 +244,11 @@ export default class RangePicker extends Component { constructor(props, context) { super(props, context); - const dateTimeFormat = getDateTimeFormat(props.format, props.showTime); + const dateTimeFormat = getDateTimeFormat( + props.format, + props.showTime, + props.type + ); extend(dateTimeFormat, this); const val = props.value || props.defaultValue; @@ -263,7 +273,8 @@ export default class RangePicker extends Component { if ('showTime' in nextProps) { const dateTimeFormat = getDateTimeFormat( nextProps.format || this.props.format, - nextProps.showTime + nextProps.showTime, + nextProps.type ); extend(dateTimeFormat, this); } @@ -303,20 +314,21 @@ export default class RangePicker extends Component { this.props[handler](ret); }; - onSelectCalendarPanel = value => { + onSelectCalendarPanel = (value, active) => { const { showTime, resetTime } = this.props; const { activeDateInput: prevActiveDateInput, startValue: prevStartValue, endValue: prevEndValue, } = this.state; + const newState = { - activeDateInput: prevActiveDateInput, + activeDateInput: active || prevActiveDateInput, inputing: false, }; let newValue = value; - switch (prevActiveDateInput) { + switch (active || prevActiveDateInput) { case 'startValue': { if ( !prevEndValue || @@ -458,14 +470,18 @@ export default class RangePicker extends Component { }; onDateInputKeyDown = e => { + const { type } = this.props; const { activeDateInput } = this.state; const stateName = mapInputStateName(activeDateInput); const dateInputStr = this.state[stateName]; - const { format } = this.props; const dateStr = onDateKeydown( e, - { format, value: this.state[activeDateInput], dateInputStr }, - 'day' + { + format: this.format, + value: this.state[activeDateInput], + dateInputStr, + }, + type === 'date' ? 'day' : type ); if (!dateStr) return; @@ -700,7 +716,6 @@ export default class RangePicker extends Component { const { prefix, format, className, renderPreview } = this.props; const previewCls = classnames(className, `${prefix}form-preview`); - const startLabel = startValue ? startValue.format(format) : ''; const endLabel = endValue ? endValue.format(format) : ''; @@ -723,6 +738,7 @@ export default class RangePicker extends Component { const { prefix, rtl, + type, defaultVisibleMonth, onVisibleMonthChange, showTime, @@ -844,21 +860,84 @@ export default class RangePicker extends Component { /> ); - const datePanel = ( - - ); + const shareCalendarProps = { + showOtherMonth: true, + dateCellRender: dateCellRender, + monthCellRender: monthCellRender, + yearCellRender: yearCellRender, + format: this.format, + defaultVisibleMonth: defaultVisibleMonth, + onVisibleMonthChange: onVisibleMonthChange, + }; + + const datePanel = + type === 'date' ? ( + + ) : ( +
+ { + return ( + state.endValue && + date.isAfter(state.endValue, type) + ); + }} + onSelect={value => { + const selectedValue = value + .clone() + .date(1) + .hour(0) + .minute(0) + .second(0); + if (type === 'year') { + selectedValue.month(0); + } + this.onSelectCalendarPanel( + selectedValue, + 'startValue' + ); + }} + value={state.startValue} + /> + { + return ( + state.startValue && + date.isBefore(state.startValue, type) + ); + }} + onSelect={value => { + const selectedValue = value + .clone() + .hour(23) + .minute(59) + .second(59); + if (type === 'year') { + selectedValue.month(11).date(31); + } else { + selectedValue.date(selectedValue.daysInMonth()); + } + this.onSelectCalendarPanel( + selectedValue, + 'endValue' + ); + }} + value={state.endValue} + /> +
+ ); let startTimeInput = null; let endTimeInput = null; diff --git a/src/date-picker/scss/date-picker.scss b/src/date-picker/scss/date-picker.scss index a2bdbf1aa1..28bd07d734 100644 --- a/src/date-picker/scss/date-picker.scss +++ b/src/date-picker/scss/date-picker.scss @@ -1,7 +1,10 @@ // // date-picker 样式 // -#{$date-picker-prefix}, #{$month-picker-prefix}, #{$year-picker-prefix} { +#{$date-picker-prefix}, +#{$month-picker-prefix}, +#{$year-picker-prefix}, +#{$week-picker-prefix} { display: inline-block; width: $s-50; @@ -11,6 +14,7 @@ &-body { width: $s-18 * 4; + font-size: 0; } &-panel-input#{$date-picker-input-prefix} { diff --git a/src/date-picker/scss/variable.scss b/src/date-picker/scss/variable.scss index 92a04b050b..77b3cfcaee 100644 --- a/src/date-picker/scss/variable.scss +++ b/src/date-picker/scss/variable.scss @@ -17,6 +17,8 @@ $month-picker-prefix: '.' + $css-prefix + 'month-picker'; $year-picker-prefix: '.' + $css-prefix + 'year-picker'; +$week-picker-prefix: '.' + $css-prefix + 'week-picker'; + $date-picker-btn-prefix: '.' + $css-prefix + 'btn'; $date-picker-input-prefix: '.' + $css-prefix + 'input'; diff --git a/src/date-picker/util/index.js b/src/date-picker/util/index.js index 36bb76e1c6..bbd97ab3d3 100644 --- a/src/date-picker/util/index.js +++ b/src/date-picker/util/index.js @@ -51,7 +51,15 @@ export function checkDateValue(props, propName, componentName) { } } -export function getDateTimeFormat(format, showTime) { +export function getDateTimeFormat(format, showTime, type) { + if (!format && type) { + format = { + date: 'YYYY-MM-DD', + month: 'YYYY-MM', + year: 'YYYY', + time: '', + }[type]; + } const timeFormat = showTime ? showTime.format || DEFAULT_TIME_FORMAT : ''; const dateTimeFormat = timeFormat ? `${format} ${timeFormat}` : format; return { diff --git a/src/date-picker/week-picker.jsx b/src/date-picker/week-picker.jsx new file mode 100644 index 0000000000..1edf8a2322 --- /dev/null +++ b/src/date-picker/week-picker.jsx @@ -0,0 +1,464 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import moment from 'moment'; +import { polyfill } from 'react-lifecycles-compat'; +import Overlay from '../overlay'; +import Input from '../input'; +import Calendar from '../calendar'; +import ConfigProvider from '../config-provider'; +import nextLocale from '../locale/zh-cn'; +import { func, obj, KEYCODE } from '../util'; +import { checkDateValue, formatDateValue } from './util'; + +const { Popup } = Overlay; + +/** + * DatePicker.WeekPicker + */ +class WeekPicker extends Component { + static propTypes = { + ...ConfigProvider.propTypes, + prefix: PropTypes.string, + rtl: PropTypes.bool, + /** + * 输入框内置标签 + */ + label: PropTypes.node, + /** + * 输入框状态 + */ + state: PropTypes.oneOf(['success', 'loading', 'error']), + /** + * 输入提示 + */ + placeholder: PropTypes.string, + /** + * 默认展现的月 + * @return {MomentObject} 返回包含指定月份的 moment 对象实例 + */ + defaultVisibleMonth: PropTypes.func, + onVisibleMonthChange: PropTypes.func, + /** + * 日期值(受控)moment 对象 + */ + value: checkDateValue, + /** + * 初始日期值,moment 对象 + */ + defaultValue: checkDateValue, + /** + * 日期值的格式(用于限定用户输入和展示) + */ + format: PropTypes.string, + /** + * 禁用日期函数 + * @param {MomentObject} 日期值 + * @param {String} view 当前视图类型,year: 年, month: 月, date: 日 + * @return {Boolean} 是否禁用 + */ + disabledDate: PropTypes.func, + /** + * 自定义面板页脚 + * @return {Node} 自定义的面板页脚组件 + */ + footerRender: PropTypes.func, + /** + * 日期值改变时的回调 + * @param {MomentObject|String} value 日期值 + */ + onChange: PropTypes.func, + /** + * 输入框尺寸 + */ + size: PropTypes.oneOf(['small', 'medium', 'large']), + /** + * 是否禁用 + */ + disabled: PropTypes.bool, + /** + * 是否显示清空按钮 + */ + hasClear: PropTypes.bool, + /** + * 弹层显示状态 + */ + visible: PropTypes.bool, + /** + * 弹层默认是否显示 + */ + defaultVisible: PropTypes.bool, + /** + * 弹层展示状态变化时的回调 + * @param {Boolean} visible 弹层是否显示 + * @param {String} type 触发弹层显示和隐藏的来源 calendarSelect 表示由日期表盘的选择触发; okBtnClick 表示由确认按钮触发; fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 + */ + onVisibleChange: PropTypes.func, + /** + * 弹层触发方式 + */ + popupTriggerType: PropTypes.oneOf(['click', 'hover']), + /** + * 弹层对齐方式,具体含义见 OverLay文档 + */ + popupAlign: PropTypes.string, + /** + * 弹层容器 + * @param {Element} target 目标元素 + * @return {Element} 弹层的容器元素 + */ + popupContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** + * 弹层自定义样式 + */ + popupStyle: PropTypes.object, + /** + * 弹层自定义样式类 + */ + popupClassName: PropTypes.string, + /** + * 弹层其他属性 + */ + popupProps: PropTypes.object, + /** + * 是否跟随滚动 + */ + followTrigger: PropTypes.bool, + /** + * 输入框其他属性 + */ + inputProps: PropTypes.object, + /** + * 自定义日期渲染函数 + * @param {Object} value 日期值(moment对象) + * @returns {ReactNode} + */ + dateCellRender: PropTypes.func, + /** + * 自定义月份渲染函数 + * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象 + * @returns {ReactNode} + */ + monthCellRender: PropTypes.func, + /** + * 是否为预览态 + */ + isPreview: PropTypes.bool, + /** + * 预览态模式下渲染的内容 + * @param {MomentObject} value 年份 + */ + renderPreview: PropTypes.func, + yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender + locale: PropTypes.object, + className: PropTypes.string, + name: PropTypes.string, + popupComponent: PropTypes.elementType, + popupContent: PropTypes.node, + }; + + static defaultProps = { + prefix: 'next-', + rtl: false, + format: 'YYYY-wo', + size: 'medium', + disabledDate: () => false, + footerRender: () => null, + hasClear: true, + popupTriggerType: 'click', + popupAlign: 'tl tl', + locale: nextLocale.DatePicker, + defaultVisible: false, + onChange: func.noop, + onVisibleChange: func.noop, + }; + + constructor(props, context) { + super(props, context); + + const value = formatDateValue( + props.value || props.defaultValue, + props.format + ); + + this.state = { + value, + visible: props.visible || props.defaultVisible, + }; + } + + static getDerivedStateFromProps(props) { + const st = {}; + if ('value' in props) { + st.value = formatDateValue(props.value, props.format); + } + + if ('visible' in props) { + st.visible = props.visible; + } + + return st; + } + + handleChange = (newValue, prevValue) => { + if (!('value' in this.props)) { + this.setState({ + value: newValue, + }); + } + + const newValueOf = newValue ? newValue.valueOf() : null; + const preValueOf = prevValue ? prevValue.valueOf() : null; + + if (newValueOf !== preValueOf) { + this.props.onChange(newValue); + } + }; + + onDateInputChange = (value, e, eventType) => { + if (eventType === 'clear' || !value) { + e.stopPropagation(); + this.handleChange(null, this.state.value); + } + }; + + onKeyDown = e => { + if ( + [ + KEYCODE.UP, + KEYCODE.DOWN, + KEYCODE.PAGE_UP, + KEYCODE.PAGE_DOWN, + ].indexOf(e.keyCode) === -1 + ) { + return; + } + + if ( + (e.altKey && + [KEYCODE.PAGE_UP, KEYCODE.PAGE_DOWN].indexOf(e.keyCode) === + -1) || + e.controlKey || + e.shiftKey + ) { + return; + } + + let date = this.state.value; + + if (date && date.isValid()) { + const stepUnit = e.altKey ? 'year' : 'month'; + switch (e.keyCode) { + case KEYCODE.UP: + date.subtract(1, 'w'); + break; + case KEYCODE.DOWN: + date.add(1, 'w'); + break; + case KEYCODE.PAGE_UP: + date.subtract(1, stepUnit); + break; + case KEYCODE.PAGE_DOWN: + date.add(1, stepUnit); + break; + } + } else { + date = moment(); + } + + e.preventDefault(); + + this.handleChange(date, this.state.value); + }; + + onVisibleChange = (visible, type) => { + if (!('visible' in this.props)) { + this.setState({ + visible, + }); + } + this.props.onVisibleChange(visible, type); + }; + + onSelectCalendarPanel = value => { + this.handleChange(value, this.state.value); + this.onVisibleChange(false, 'calendarSelect'); + }; + + renderPreview(others) { + const { prefix, format, className, renderPreview } = this.props; + const { value } = this.state; + const previewCls = classnames(className, `${prefix}form-preview`); + + const label = value ? value.format(format) : ''; + + if (typeof renderPreview === 'function') { + return ( +
+ {renderPreview(value, this.props)} +
+ ); + } + + return ( +

+ {label} +

+ ); + } + + dateRender = value => { + const { prefix, dateCellRender } = this.props; + const selectedValue = this.state.value; + const content = + dateCellRender && typeof dateCellRender === 'function' + ? dateCellRender(value) + : value.dates(); + if ( + selectedValue && + selectedValue.years() === value.years() && + selectedValue.weeks() === value.weeks() + ) { + const firstDay = moment.localeData().firstDayOfWeek(); + const endDay = firstDay - 1 < 0 ? 6 : firstDay - 1; + return ( +
+ {content} +
+ ); + } + + return content; + }; + + render() { + const { + prefix, + rtl, + locale, + label, + state, + format, + defaultVisibleMonth, + onVisibleMonthChange, + disabledDate, + footerRender, + placeholder, + size, + disabled, + hasClear, + popupTriggerType, + popupAlign, + popupContainer, + popupStyle, + popupClassName, + popupProps, + popupComponent, + popupContent, + followTrigger, + className, + inputProps, + monthCellRender, + yearCellRender, + isPreview, + ...others + } = this.props; + const { visible, value } = this.state; + + const sharedInputProps = { + ...inputProps, + size, + disabled, + onChange: this.onDateInputChange, + onKeyDown: this.onKeyDown, + }; + + if (rtl) { + others.dir = 'rtl'; + } + + if (isPreview) { + return this.renderPreview( + obj.pickOthers(others, WeekPicker.PropTypes) + ); + } + + const trigger = ( +
+ +
+ ); + + const PopupComponent = popupComponent ? popupComponent : Popup; + + return ( +
+ + {popupContent ? ( + popupContent + ) : ( +
+ + {footerRender()} +
+ )} +
+
+ ); + } +} + +export default polyfill(WeekPicker); diff --git a/src/locale/en-us.js b/src/locale/en-us.js index 517f2451bd..3d8cbfb10e 100644 --- a/src/locale/en-us.js +++ b/src/locale/en-us.js @@ -32,6 +32,7 @@ export default { datetimePlaceholder: 'Select Date And Time', monthPlaceholder: 'Select Month', yearPlaceholder: 'Select Year', + weekPlaceholder: 'Select week', now: 'Now', selectTime: 'Select Time', selectDate: 'Select Date', diff --git a/src/locale/ja-jp.js b/src/locale/ja-jp.js index f9d52c3cf9..b7f9bb92b8 100644 --- a/src/locale/ja-jp.js +++ b/src/locale/ja-jp.js @@ -32,6 +32,7 @@ export default { datetimePlaceholder: '日時を選択してください', monthPlaceholder: '月を選択してください', yearPlaceholder: '年を選択してください', + weekPlaceholder: '周を選んでください', now: '現在', selectTime: '時間の選択', selectDate: '日付けの選択', diff --git a/src/locale/zh-cn.js b/src/locale/zh-cn.js index 0f0d34a8e8..a377af45db 100644 --- a/src/locale/zh-cn.js +++ b/src/locale/zh-cn.js @@ -32,6 +32,7 @@ export default { datetimePlaceholder: '请选择日期和时间', monthPlaceholder: '请选择月', yearPlaceholder: '请选择年', + weekPlaceholder: '请选择周', now: '此刻', selectTime: '选择时间', selectDate: '选择日期', diff --git a/src/locale/zh-tw.js b/src/locale/zh-tw.js index 490fdba53e..fe465b1c9e 100644 --- a/src/locale/zh-tw.js +++ b/src/locale/zh-tw.js @@ -32,6 +32,7 @@ export default { datetimePlaceholder: '請選擇日期和時間', monthPlaceholder: '請選擇月', yearPlaceholder: '請選擇年', + weekPlaceholder: '請選擇周', now: '此刻', selectTime: '選擇時間', selectDate: '選擇日期', diff --git a/test/date-picker/index-spec.js b/test/date-picker/index-spec.js index bf56cff4d8..5a601ee61f 100644 --- a/test/date-picker/index-spec.js +++ b/test/date-picker/index-spec.js @@ -7,7 +7,7 @@ import DatePicker from '../../src/date-picker/index'; import { KEYCODE } from '../../src/util'; Enzyme.configure({ adapter: new Adapter() }); -const { RangePicker, MonthPicker, YearPicker } = DatePicker; +const { RangePicker, MonthPicker, YearPicker, WeekPicker } = DatePicker; const startValue = moment('2017-11-20', 'YYYY-MM-DD', true); const endValue = moment('2017-12-15', 'YYYY-MM-DD', true); @@ -810,6 +810,129 @@ describe('MonthPicker', () => { }); }); +describe('WeekPicker', () => { + const startWeek = moment('2018-01-08', 'YYYY-MM-DD', true); + const endWeek = moment('2019-10-08', 'YYYY-MM-DD', true); + let wrapper; + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + wrapper = null; + }); + + describe('render with props', () => { + it('should render with defaultValue', () => { + wrapper = mount(); + assert( + wrapper.find('.next-week-picker-input input').instance().value.indexOf('2018-') !== -1 + ); + assert(wrapper.find('.next-icon-delete-filling').length === 1); + }); + + it('should set hasClear to false', () => { + wrapper = mount( + + ); + assert(!wrapper.find('.next-icon-delete-filling').length); + }); + + it('should render controlled value of YearPicker', () => { + wrapper = mount(); + assert( + wrapper.find('.next-week-picker-input input').instance() + .value.indexOf('2018-') !== -1 + ); + wrapper.setProps({ value: endWeek }); + assert( + wrapper.find('.next-week-picker-input input').instance() + .value.indexOf('2019-') !== -1 + ); + }); + + it('should render controlled visible of YearPicker', () => { + wrapper = mount(); + assert(wrapper.find('.next-week-picker-body').length === 0); + wrapper.setProps({ visible: true }); + assert(wrapper.find('.next-week-picker-body').length === 1); + }); + + it('should support preview mode render', () => { + wrapper = mount(); + assert(wrapper.find('.next-form-preview').length > 0); + assert(wrapper.find('.next-form-preview').text() === '2018-48'); + wrapper.setProps({ + renderPreview: (value) => { + assert(value.format('YYYY') === '2018'); + return 'Hello World'; + } + }); + assert(wrapper.find('.next-form-preview').text() === 'Hello World'); + }); + }); + + describe('action', () => { + it('should select', () => { + let ret; + wrapper = mount( + startWeek} + onChange={val => (ret = val)} + /> + ); + wrapper.find('.next-week-picker-input input').simulate('click'); + wrapper.find('td[title="2018-3"] .next-calendar-date').at(0).simulate('click'); + assert(ret.format('YYYY-w') === '2018-3'); + }); + + it('should clear value', () => { + let ret = 'hello'; + wrapper = mount( + (ret = val)} + /> + ); + wrapper.find('.next-icon-delete-filling').simulate('click'); + assert(ret === null); + }); + + it('should keyboard input', () => { + let ret; + wrapper = mount( + (ret = val)} + defaultVisible + /> + ); + const input = wrapper.find('.next-week-picker-input input'); + const instance = wrapper.instance().getInstance(); + input.simulate('keydown', { keyCode: KEYCODE.DOWN }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.LEFT }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.DOWN, altKey: true }); + input.simulate('keydown', { keyCode: KEYCODE.DOWN, shiftKey: true }); + input.simulate('keydown', { keyCode: KEYCODE.DOWN, controlKey: true }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.DOWN }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().add(1, 'w').format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.UP }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.PAGE_DOWN }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().add(1, 'month').format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.PAGE_UP }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.PAGE_DOWN, altKey: true }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().add(1, 'year').format('YYYY-MM-DD')); + input.simulate('keydown', { keyCode: KEYCODE.PAGE_UP, altKey: true }); + assert(instance.state.value.format('YYYY-MM-DD') === moment().format('YYYY-MM-DD')); + }); + }); + +}) + describe('RangePicker', () => { let wrapper; @@ -920,6 +1043,15 @@ describe('RangePicker', () => { // assert(wrapper.instance().getInstance().startValue && wrapper.instance().getInstance().startValue.isSame(start)); }); + it('should render type month', () => { + wrapper = mount(); + assert(wrapper.find('.next-calendar').length === 2); + }); + + it('should render type year', () => { + wrapper = mount(); + assert(wrapper.find('.next-calendar').length === 2); + }); it('should support preview mode render', () => { wrapper = mount(); assert(wrapper.find('.next-form-preview').length > 0); @@ -1008,6 +1140,26 @@ describe('RangePicker', () => { assert(ret[1].format('YYYY-MM-DD') === '2017-12-15'); }); + it('should select in type range', () => { + let ret; + wrapper = mount( + (ret = val)} + /> + ); + + wrapper + .find('.next-range-picker-trigger-input input') + .at(0) + .simulate('click'); + wrapper + .find('td[title="Aug"] .next-calendar-month').at(0) + .simulate('click'); + assert(ret[0].format('YYYY-MM-DD') === '2017-08-01'); + }) + it('should slect range with same day', () => { let ret; wrapper = mount( diff --git a/types/date-picker/index.d.ts b/types/date-picker/index.d.ts index 2f40af1f72..47071cd755 100644 --- a/types/date-picker/index.d.ts +++ b/types/date-picker/index.d.ts @@ -143,6 +143,7 @@ interface HTMLAttributesWeak extends React.HTMLAttributes { } export interface RangePickerProps extends HTMLAttributesWeak, CommonProps { + type?: 'date' | 'month' | 'year', /** * 默认展示的起始月份 */ @@ -580,4 +581,5 @@ export default class DatePicker extends React.Component { static MonthPicker: typeof MonthPicker; static RangePicker: typeof RangePicker; static YearPicker: typeof YearPicker; + static WeekPicker: React.ComponentType; } diff --git a/types/locale/default.d.ts b/types/locale/default.d.ts index 1210603d8c..cab673f477 100644 --- a/types/locale/default.d.ts +++ b/types/locale/default.d.ts @@ -31,6 +31,7 @@ export interface locale { placeholder: string; datetimePlaceholder: string; monthPlaceholder: string; + weekPlaceholder: string; yearPlaceholder: string; now: string; selectTime: string;