diff --git a/src/components/Calendar/Calendar.jsx b/src/components/Calendar/Calendar.jsx index e63795c1..80bac9f3 100644 --- a/src/components/Calendar/Calendar.jsx +++ b/src/components/Calendar/Calendar.jsx @@ -13,7 +13,7 @@ moment() .locale('zh-cn') .utcOffset(8); -@localeConsumerDecorator({ defaultLocale: LOCALE, localeName: 'Calendar' }) +@localeConsumerDecorator({ defaultLocale: LOCALE, localeName: 'Calendar', publicFn: ['focus'] }) class Calendar extends Component { static propTypes = { /** 选中的时间,受控,Moment 类型 */ @@ -44,11 +44,21 @@ class Calendar extends Component { onSelect(value); } }; + focus = () => { + this.calendar && this.calendar.focus(); + }; render() { /* eslint-disable-next-line no-unused-vars */ const { rules = {}, onSelect, ...rest } = this.props; - return ; + return ( + (this.calendar = ref)} + onSelect={this.onSelect} + {...calCalendarProps({ rules })} + {...rest} + /> + ); } } diff --git a/src/components/Calendar/Calendar.md b/src/components/Calendar/Calendar.md index 75b4ba32..b05691f8 100644 --- a/src/components/Calendar/Calendar.md +++ b/src/components/Calendar/Calendar.md @@ -1,6 +1,6 @@ ### 说明 -* 这是 Calendar 组件 +* 这是 日历 组件 ### 演示 diff --git a/src/components/Calendar/Month.jsx b/src/components/Calendar/Month.jsx new file mode 100644 index 00000000..08943daf --- /dev/null +++ b/src/components/Calendar/Month.jsx @@ -0,0 +1,66 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +import localeConsumerDecorator from 'src/components/LocaleProvider/localeConsumerDecorator'; + +import { getValidDate } from './utils'; +import { MonthCalendarWrap } from './style'; +import LOCALE from './locale/zh_CN'; +import { calCalendarProps } from './Calendar'; + +import 'moment/locale/zh-cn'; +moment() + .locale('zh-cn') + .utcOffset(8); + +@localeConsumerDecorator({ defaultLocale: LOCALE, localeName: 'Calendar', publicFn: ['focus'] }) +class Month extends Component { + static propTypes = { + /** 选中的时间,受控,Moment 类型 */ + value: PropTypes.object, + /** 默认选中的时间,非受控,Moment 类型 */ + defaultValue: PropTypes.object, + /** 实际选中时间修改的回调 */ + onSelect: PropTypes.func, + /** 变化时的回调,键盘操作快速切换等操作时触发 */ + onChange: PropTypes.func, + /** 自定义规则 */ + rules: PropTypes.shape({ + /** 可选时间范围,格式为[start, end],数据格式可以为moment实例、Date实例、时间戳 */ + range: PropTypes.array, + /** + * 自定义禁用日期函数,返回true时该日期不可选 + * @param current - 日期 + * @param value - 日历当前选中值 + */ + custom: PropTypes.func + }) + }; + static defaultProps = {}; + onSelect = value => { + const { rules, onSelect } = this.props; + value = getValidDate(value, rules); + if (onSelect) { + onSelect(value); + } + }; + focus = () => { + this.calendar && this.calendar.focus(); + }; + render() { + /* eslint-disable-next-line no-unused-vars */ + const { rules = {}, onSelect, ...rest } = this.props; + + return ( + (this.calendar = ref)} + onSelect={this.onSelect} + {...calCalendarProps({ rules })} + {...rest} + /> + ); + } +} + +export default Month; diff --git a/src/components/Calendar/__demo__/month.jsx b/src/components/Calendar/__demo__/month.jsx new file mode 100644 index 00000000..6b67da0c --- /dev/null +++ b/src/components/Calendar/__demo__/month.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import Calendar from 'components/Calendar'; + +// demo start +const Demo = () => ( +
+ console.log('select', v)} + onChange={v => console.log('change', v)} + rules={{ range: [Date.now() - 3 * 30 * 24 * 60 * 60 * 1000, Date.now() + 3 * 30 * 24 * 60 * 60 * 1000] }} + /> +
+); +// demo end + +export default Demo; diff --git a/src/components/Calendar/__tests__/__snapshots__/demo.test.js.snap b/src/components/Calendar/__tests__/__snapshots__/demo.test.js.snap index dabe530a..2ed30c1c 100644 --- a/src/components/Calendar/__tests__/__snapshots__/demo.test.js.snap +++ b/src/components/Calendar/__tests__/__snapshots__/demo.test.js.snap @@ -2632,6 +2632,478 @@ exports[`Calendar demo -- controlled 1`] = ` `; +exports[`Calendar demo -- month 1`] = ` +.c0.uc-fe-calendar, +.c0 .uc-fe-calendar { + outline: none; + position: relative; + box-shadow: 0 0 8px rgba(0,0,0,0.2); +} + +.c0 .uc-fe-calendar-month-panel, +.c0 .uc-fe-calendar-year-panel, +.c0 .uc-fe-calendar-decade-panel { + background: #FFF; +} + +.c0 .uc-fe-calendar-header, +.c0 .uc-fe-calendar-month-panel-header, +.c0 .uc-fe-calendar-year-panel-header, +.c0 .uc-fe-calendar-decade-panel-header { + width: 100%; + height: 38px; + line-height: 38px; + background-color: #4683e6; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c0 .uc-fe-calendar-month-header-wrap { + height: 180px; +} + +.c0 .uc-fe-calendar-month-panel-body { + height: 140px; +} + +.c0 .uc-fe-calendar-prev-year-btn, +.c0 .uc-fe-calendar-prev-month-btn, +.c0 .uc-fe-calendar-next-year-btn, +.c0 .uc-fe-calendar-next-month-btn, +.c0 .uc-fe-calendar-month-panel-prev-year-btn, +.c0 .uc-fe-calendar-month-panel-next-year-btn, +.c0 .uc-fe-calendar-year-panel-prev-decade-btn, +.c0 .uc-fe-calendar-year-panel-next-decade-btn, +.c0 .uc-fe-calendar-decade-panel-prev-century-btn, +.c0 .uc-fe-calendar-decade-panel-next-century-btn { + cursor: pointer; + padding: 0 8px; + display: inline-block; + color: #FFF; + box-sizing: border-box; + position: absolute; + font-family: Arial,'Hiragino Sans GB','Microsoft Yahei','Microsoft Sans Serif',sans-serif; + font-size: 16px; +} + +.c0 .uc-fe-calendar-prev-year-btn, +.c0 .uc-fe-calendar-month-panel-prev-year-btn, +.c0 .uc-fe-calendar-year-panel-prev-decade-btn, +.c0 .uc-fe-calendar-decade-panel-prev-century-btn { + left: 0; +} + +.c0 .uc-fe-calendar-prev-month-btn { + left: 30px; +} + +.c0 .uc-fe-calendar-next-year-btn, +.c0 .uc-fe-calendar-month-panel-next-year-btn, +.c0 .uc-fe-calendar-year-panel-next-decade-btn, +.c0 .uc-fe-calendar-decade-panel-next-century-btn { + right: 0; +} + +.c0 .uc-fe-calendar-next-month-btn { + right: 30px; +} + +.c0 .uc-fe-calendar-prev-year-btn::before, +.c0 .uc-fe-calendar-month-panel-prev-year-btn::before, +.c0 .uc-fe-calendar-year-panel-prev-decade-btn::before, +.c0 .uc-fe-calendar-decade-panel-prev-century-btn::before { + content: '\\AB'; +} + +.c0 .uc-fe-calendar-prev-month-btn::before { + content: '\\2039'; +} + +.c0 .uc-fe-calendar-next-year-btn::before, +.c0 .uc-fe-calendar-month-panel-next-year-btn::before, +.c0 .uc-fe-calendar-year-panel-next-decade-btn::before, +.c0 .uc-fe-calendar-decade-panel-next-century-btn::before { + content: '\\BB'; +} + +.c0 .uc-fe-calendar-next-month-btn::before { + content: '\\203A'; +} + +.c0 .uc-fe-calendar-year-select, +.c0 .uc-fe-calendar-month-select, +.c0 .uc-fe-calendar-day-select { + margin-right: 8px; +} + +.c0 .uc-fe-calendar-my-select, +.c0 .uc-fe-calendar-ym-select, +.c0 .uc-fe-calendar-month-panel-year-select, +.c0 .uc-fe-calendar-year-panel-decade-select, +.c0 .uc-fe-calendar-decade-panel-century { + position: absolute; + top: 0; + left: 50%; + margin-left: -75px; + width: 150px; + text-align: center; + display: block; + color: #FFF; +} + +.c0 .uc-fe-calendar-body { + width: 100%; + padding: 0 5px 5px; + background-color: #fff; + box-sizing: border-box; +} + +.c0 .uc-fe-calendar-table, +.c0 .uc-fe-calendar-month-panel-table, +.c0 .uc-fe-calendar-year-panel-table, +.c0 .uc-fe-calendar-decade-panel-table { + width: 100%; +} + +.c0 .uc-fe-calendar-table > thead, +.c0 .uc-fe-calendar-month-panel-table > thead { + border-bottom: 1px solid #c3cad9; +} + +.c0 .uc-fe-calendar-column-header, +.c0 .uc-fe-calendar-cell, +.c0 .uc-fe-calendar-month-panel-cell, +.c0 .uc-fe-calendar-year-panel-cell, +.c0 .uc-fe-calendar-decade-panel-cell { + cursor: pointer; + height: 35px; + line-height: 35px; + text-align: center; +} + +.c0 .uc-fe-calendar-last-month-cell, +.c0 .uc-fe-calendar-month-panel-last-year-cell, +.c0 .uc-fe-calendar-year-panel-last-decade-cell, +.c0 .uc-fe-calendar-decade-panel-last-century-cell, +.c0 .uc-fe-calendar-next-month-btn-day, +.c0 .uc-fe-calendar-month-panel-next-year-cell, +.c0 .uc-fe-calendar-year-panel-next-decade-cell, +.c0 .uc-fe-calendar-decade-panel-next-century-cell { + color: #bbb; +} + +.c0 .uc-fe-calendar-disabled-cell, +.c0 .uc-fe-calendar-month-panel-cell-disabled { + color: #bbb; +} + +.c0 .uc-fe-calendar-month-panel, +.c0 .uc-fe-calendar-year-panel, +.c0 .uc-fe-calendar-decade-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.c0 .uc-fe-calendar-today, +.c0 .uc-fe-calendar-month-panel-current-cell { + position: relative; + color: #4074e1; +} + +.c0 .uc-fe-calendar-today::after, +.c0 .uc-fe-calendar-month-panel-current-cell::after { + content: '•'; + position: absolute; + bottom: 0px; + left: 0; + right: 0; + height: 10px; + line-height: 10px; + margin: auto; +} + +.c0 .uc-fe-calendar-selected-day { + background: #eaf3fd; +} + +.c0 .uc-fe-calendar-selected-date, +.c0 .uc-fe-calendar-month-panel-selected-cell, +.c0 .uc-fe-calendar-year-panel-selected-cell, +.c0 .uc-fe-calendar-decade-panel-selected-cell { + color: #FFF; + background: #4683e6; +} + +.c0 .uc-fe-calendar-month-panel-year-select-arrow, +.c0 .uc-fe-calendar-year-panel-decade-select-arrow { + display: none; +} + +
+ +
+`; + exports[`Calendar demo -- rules 1`] = ` .c0.uc-fe-calendar, .c0 .uc-fe-calendar { diff --git a/src/components/Calendar/__tests__/index.test.js b/src/components/Calendar/__tests__/index.test.js index 60ee13c3..868b7a8e 100644 --- a/src/components/Calendar/__tests__/index.test.js +++ b/src/components/Calendar/__tests__/index.test.js @@ -13,6 +13,7 @@ describe('Calendar', () => { const now = moment.now(); const wrapper = mount(); + wrapper.instance().focus(); wrapper.simulate('focus'); wrapper.simulate('keydown', { keyCode: KEYCODE['ARROW_DOWN'] }); expect(onChange).toHaveBeenCalledTimes(1); @@ -31,6 +32,32 @@ describe('Calendar', () => { }); }); +describe('Month', () => { + test('month keyboard action', () => { + const onSelect = jest.fn(); + const onChange = jest.fn(); + const now = moment.now(); + const wrapper = mount(); + + wrapper.instance().focus(); + wrapper.simulate('focus'); + wrapper.simulate('keydown', { keyCode: KEYCODE['ARROW_DOWN'] }); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].toString()).toBe( + moment(now) + .add({ month: 3 }) + .toString() + ); + wrapper.simulate('keydown', { keyCode: KEYCODE['ENTER'] }); + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onSelect.mock.calls[0][0].toString()).toBe( + moment(now) + .add({ month: 3 }) + .toString() + ); + }); +}); + describe('utils', () => { test('isDateDisabled', () => { const value = moment.now(); diff --git a/src/components/Calendar/index.jsx b/src/components/Calendar/index.jsx index 315cefb7..0bb8ca06 100644 --- a/src/components/Calendar/index.jsx +++ b/src/components/Calendar/index.jsx @@ -1,2 +1,5 @@ import Calendar from './Calendar'; export default Calendar; + +import MonthCalendar from './Month'; +Calendar.Month = MonthCalendar; diff --git a/src/components/Calendar/style/index.js b/src/components/Calendar/style/index.js index e15672e1..ec3141d1 100644 --- a/src/components/Calendar/style/index.js +++ b/src/components/Calendar/style/index.js @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components'; import RcCalendar from 'rc-calendar'; +import RcMonthCalendar from 'rc-calendar/lib/MonthCalendar'; import { Color } from 'src/style'; @@ -194,3 +195,7 @@ export const calendarMixin = css` export const CalendarWrap = styled(RcCalendar)` ${calendarMixin}; `; + +export const MonthCalendarWrap = styled(RcMonthCalendar)` + ${calendarMixin}; +`; diff --git a/src/components/LocaleProvider/localeConsumerDecorator.jsx b/src/components/LocaleProvider/localeConsumerDecorator.jsx index 58d9cef6..4a0ea174 100644 --- a/src/components/LocaleProvider/localeConsumerDecorator.jsx +++ b/src/components/LocaleProvider/localeConsumerDecorator.jsx @@ -1,8 +1,14 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -const localeConsumerDecorator = ({ defaultLocale = {}, localeName }) => Child => { +const localeConsumerDecorator = ({ defaultLocale = {}, localeName, publicFn = [] }) => Child => { class LocalConsumerWrappedComponent extends Component { + constructor(...args) { + super(...args); + publicFn.forEach(fnName => { + this[fnName] = (...args) => this.child && this.child[fnName](...args); + }); + } static propTypes = { locale: PropTypes.object }; @@ -15,7 +21,13 @@ const localeConsumerDecorator = ({ defaultLocale = {}, localeName }) => Child => render() { const { locale, ...rest } = this.props; const context = this.context.UC_FE_LOCALE || {}; - return ; + return ( + (this.child = ref)} + locale={{ ...defaultLocale, ...context[localeName], ...locale }} + {...rest} + /> + ); } } return LocalConsumerWrappedComponent;