Browse Source

feat: add type monthRange to datePicker (#1504)

* feat: add type monthRange to datePicker

* docs: improve doc

* fix: Change the selector priority to make the border of the month type panel correct

* feat: add monthRange-input class to fix wrong bg color in monthRange intpu

* Update cypress/integration/datePicker.spec.js

Co-authored-by: 走鹃 <[email protected]>

* test: remove only

* fix: set both year and month in same time in autoSelectMonth

* Revert "test: remove only"

This reverts commit dafa3cd0f96795962b9d06ba25a426da07ef8f64.

* fix: use lodash.isequal to compare

---------

Co-authored-by: 走鹃 <[email protected]>
YannLynn 2 years ago
parent
commit
add9838d71
40 changed files with 459 additions and 103 deletions
  1. 18 2
      content/input/datepicker/index-en-US.md
  2. 18 2
      content/input/datepicker/index.md
  3. 11 0
      cypress/integration/datePicker.spec.js
  4. 1 0
      packages/semi-foundation/datePicker/_utils/getDefaultFormatToken.ts
  5. 1 0
      packages/semi-foundation/datePicker/_utils/getInsetInputFormatToken.ts
  6. 1 0
      packages/semi-foundation/datePicker/_utils/getInsetInputValueFromInsetInputStr.ts
  7. 1 1
      packages/semi-foundation/datePicker/constants.ts
  8. 33 6
      packages/semi-foundation/datePicker/datePicker.scss
  9. 15 5
      packages/semi-foundation/datePicker/foundation.ts
  10. 8 2
      packages/semi-foundation/datePicker/inputFoundation.ts
  11. 6 4
      packages/semi-foundation/datePicker/variables.scss
  12. 65 18
      packages/semi-foundation/datePicker/yearAndMonthFoundation.ts
  13. 122 1
      packages/semi-ui/datePicker/_story/datePicker.stories.jsx
  14. 10 5
      packages/semi-ui/datePicker/dateInput.tsx
  15. 20 10
      packages/semi-ui/datePicker/datePicker.tsx
  16. 5 3
      packages/semi-ui/datePicker/monthsGrid.tsx
  17. 100 43
      packages/semi-ui/datePicker/yearAndMonth.tsx
  18. 2 1
      packages/semi-ui/locale/interface.ts
  19. 1 0
      packages/semi-ui/locale/source/ar.ts
  20. 1 0
      packages/semi-ui/locale/source/de.ts
  21. 1 0
      packages/semi-ui/locale/source/en_GB.ts
  22. 1 0
      packages/semi-ui/locale/source/en_US.ts
  23. 1 0
      packages/semi-ui/locale/source/es.ts
  24. 1 0
      packages/semi-ui/locale/source/fr.ts
  25. 1 0
      packages/semi-ui/locale/source/id_ID.ts
  26. 1 0
      packages/semi-ui/locale/source/it.ts
  27. 1 0
      packages/semi-ui/locale/source/ja_JP.ts
  28. 1 0
      packages/semi-ui/locale/source/ko_KR.ts
  29. 1 0
      packages/semi-ui/locale/source/ms_MY.ts
  30. 1 0
      packages/semi-ui/locale/source/nl_NL.ts
  31. 1 0
      packages/semi-ui/locale/source/pl_PL.ts
  32. 1 0
      packages/semi-ui/locale/source/pt_BR.ts
  33. 1 0
      packages/semi-ui/locale/source/ro.ts
  34. 1 0
      packages/semi-ui/locale/source/ru_RU.ts
  35. 1 0
      packages/semi-ui/locale/source/sv_SE.ts
  36. 1 0
      packages/semi-ui/locale/source/th_TH.ts
  37. 1 0
      packages/semi-ui/locale/source/tr_TR.ts
  38. 1 0
      packages/semi-ui/locale/source/vi_VN.ts
  39. 1 0
      packages/semi-ui/locale/source/zh_CN.ts
  40. 1 0
      packages/semi-ui/locale/source/zh_TW.ts

+ 18 - 2
content/input/datepicker/index-en-US.md

@@ -175,6 +175,9 @@ function Demo() {
             <DatePicker type="month" placeholder="please input month" insetInput style={{ width: 140 }} />
             <br />
             <br />
+            <DatePicker type="monthRange" placeholder="please input month Range" insetInput style={{ width: 200 }} />
+            <br />
+            <br />
             <DatePicker type="dateTime" format="yyyy-MM-dd HH:mm" insetInput />
         </div>
     );
@@ -299,6 +302,19 @@ class App extends React.Component {
 }
 ```
 
+### Year and Month Range Selection
+
+**version:** >= 2.32.0
+
+Set `type` to `monthRange` to select the year and month range, small size and quick panel are not supported yet.
+
+```jsx live=true
+import React from 'react';
+import { DatePicker } from '@douyinfe/semi-ui';
+
+() => <DatePicker type="monthRange" style={{ width: 200 }} />;
+```
+
 ### Confirm Date and Time Selection
 
 **Version: > = 0.18.0**
@@ -895,7 +911,7 @@ function Demo() {
 | multiple           | Whether you can choose multiple, only type = "date" is supported                                                                                                                          | boolean                                                                                                                                                                                                   | false                                                                                 |                           |
 | needConfirm        | Do you need to "confirm selection", only `type= "dateTime"\| "dateTimeRange"` works.       | boolean                                                                                                                                                                                                   |                                                                                       | **0.18.0**                |
 | open               | Controlled properties displayed or hidden by panels                                                                                                                                       | boolean                                                                                                                                                                                                   |                                                                                       |                           |
-| placeholder        | Input box prompts text                                                                                                                                                                    | string                                                                                                                                                                                                    | 'Select date'                                                                         |                           |
+| placeholder        | Input box prompts text                                                                                                                                                                    | string\|string[]                                                                                                                                                                                                    | 'Select date'                                                                         |                           |
 | position           | Floating layer position, optional value with [Popover #API Reference · position](/en-US/show/popover#API%20Reference)                                                                 | string                                                                                                                                                                                                    | 'bottomLeft'                                                                          |                           |
 | prefix             | Prefix content                                                                                                                                                                            | string\|ReactNode                                                                                                                                                                                         |                                                                                       |                           |
 | presets            | Date Time Shortcut     |  <ApiType detail='Array< { start: BaseValueType, end :BaseValueType, text: string } \| () => { start:B aseValueType, end: BaseValueType, text: string }>'>Array</ApiType>                                  | []                                                                                    |                           |
@@ -912,7 +928,7 @@ function Demo() {
 | timePickerOpts     | For other parameters that can be transparently passed to the time selector, see [TimePicker·API Reference](/en-US/input/timepicker#API%20Reference)                                    |                                                                                                                                                                                                           | object                                                                                | **1.1.0**                 |
 | topSlot            | Render the top extra area                                                                                 | ReactNode                                                                                                                                                                                                 |                                                | **1.22.0**                   |
 | triggerRender      | Custom trigger rendering method                                                                                                                                                           | (TriggerRenderProps) => ReactNode                                                                                                                                                                    |                                                                                       | **0.34.0**                |
-| type               | Type, optional value: "date", "dateRange", "dateTime", "dateTimeRange", "month"                                                                                                           | string                                                                                                                                                                                                    | 'date'                                                                                |  |
+| type               | Type, optional value: "date", "dateRange", "dateTime", "dateTimeRange", "month", "monthRange"                                                                                                           | string                                                                                                                                                                                                    | 'date'                                                                                |  |
 | value              | Controlled value                                                                                                                                                                          | ValueType                                                                                                                                                     |                                                                                       |                           |
 | weekStartsOn       | Take the day of the week as the first day of the week, 0 for Sunday, 1 for Monday, and so on.                                                                                             | number                                                                                                                                                                                                    | 0                                                                                     |                           |
 | onBlur | Callback when focus is lost. It is not recommended to use this API in range selection | (event) => void | () => {} | **1.0.0** |

+ 18 - 2
content/input/datepicker/index.md

@@ -172,6 +172,9 @@ function Demo() {
             <DatePicker type="month" placeholder="请选择年月" insetInput style={{ width: 140 }} />
             <br />
             <br />
+            <DatePicker type="monthRange" placeholder="请选择年月范围" insetInput style={{ width: 200 }} />
+            <br />
+            <br />
             <DatePicker type="date" position="bottomLeft" insetInput />
             <br />
             <br />
@@ -288,6 +291,19 @@ import { DatePicker } from '@douyinfe/semi-ui';
 () => <DatePicker defaultValue={new Date()} type="month" style={{ width: 140 }} />;
 ```
 
+### 年月范围选择
+
+**版本:** >= 2.32.0
+
+将 `type` 设定为 `monthRange`,可以进行年月范围选择。暂不支持小尺寸与快捷面板。
+
+```jsx live=true
+import React from 'react';
+import { DatePicker } from '@douyinfe/semi-ui';
+
+() => <DatePicker type="monthRange" style={{ width: 200 }} />;
+```
+
 ### 确认日期时间选择
 
 **版本:** >= 0.18.0
@@ -860,7 +876,7 @@ function Demo() {
 | multiple | 是否可以选择多个,仅支持 type="date" | boolean | false |  |
 | needConfirm | 是否需要“确认选择”,仅 type="dateTime"\|"dateTimeRange" 时有效 | boolean |  | **0.18.0** |
 | open | 面板显示或隐藏的受控属性 | boolean |  |  |
-| placeholder | 输入框提示文字 | string | 'Select date' |  |
+| placeholder | 输入框提示文字 | string\|string[] | 'Select date' |  |
 | position | 浮层位置,可选值同[Popover#API 参考·position 参数](/zh-CN/show/popover#API参考) | string | 'bottomLeft' |  |
 | prefix | 前缀内容 | string\|ReactNode |  |  |
 | presets | 日期时间快捷方式 |  <ApiType detail='Array< { start: BaseValueType, end :BaseValueType, text: string } \| () => { start:B aseValueType, end: BaseValueType, text: string }>'>Array</ApiType> | [] |  |
@@ -879,7 +895,7 @@ function Demo() {
 | timePickerOpts | 其他可以透传给时间选择器的参数,详见 [TimePicker·API 参考](/zh-CN/input/timepicker#API_参考) |  | object | **1.1.0** |
 | topSlot | 渲染顶部额外区域 | ReactNode |  | **1.22.0** |
 | triggerRender | 自定义触发器渲染方法,第一个参数是个 Object,详情看下方类型定义 | (props) => ReactNode| | **0.34.0** |
-| type | 类型,可选值:"date", "dateRange", "dateTime", "dateTimeRange", "month" | string | 'date' |  |
+| type | 类型,可选值:"date", "dateRange", "dateTime", "dateTimeRange", "month", "monthRange" | string | 'date' |  |
 | validateStatus | 校验状态,可选值 default、error、warning,默认 default。仅影响展示样式 | string |  |  |
 | value | 受控的值 | ValueType |  |  |
 | weekStartsOn | 以周几作为每周第一天,0 代表周日,1 代表周一,以此类推 | number | 0 |  |

+ 11 - 0
cypress/integration/datePicker.spec.js

@@ -145,6 +145,17 @@ describe('DatePicker', () => {
         cy.get('[data-cy=month] .semi-input').should("have.value", "2021-11");
     });
 
+    it('insetInput + monthRange', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--month-range-picker&args=&viewMode=story');
+        cy.get('[data-cy=monthRange] .semi-input').click();
+        cy.get('.semi-popover .semi-input-wrapper-focus');
+        cy.get('.semi-popover .semi-input').should("have.value", "2023年03月 到 2023年04月");
+        cy.get('.semi-popover .semi-input-wrapper-focus').clear();
+        cy.get('.semi-scrolllist').eq(1).contains('2025').click();
+        cy.get('.semi-scrolllist .semi-scrolllist-item').eq(3).contains(6).click();
+        cy.get('[data-cy=monthRange] .semi-input').should("have.value", "2023年03月 到 2025年06月");
+    });
+
     it('insetInput + dateTime', () => {
         cy.visit('http://localhost:6006/iframe.html?id=datepicker--inset-input-e-2-e&args=&viewMode=story');   
         cy.get('[data-cy=dateTime] .semi-input').click();

+ 1 - 0
packages/semi-foundation/datePicker/_utils/getDefaultFormatToken.ts

@@ -6,6 +6,7 @@ const defaultFormatTokens = {
     dateRange: strings.FORMAT_FULL_DATE,
     dateTimeRange: strings.FORMAT_DATE_TIME,
     month: strings.FORMAT_YEAR_MONTH,
+    monthRange: strings.FORMAT_YEAR_MONTH,
 } as const;
 
 const getDefaultFormatToken = (type: string) => defaultFormatTokens;

+ 1 - 0
packages/semi-foundation/datePicker/_utils/getInsetInputFormatToken.ts

@@ -31,6 +31,7 @@ export default function getInsetInputFormatToken(options: { format: string; type
             break;
         case 'date':
         case 'month':
+        case 'monthRange':
         case 'dateRange':
         default:
             const dateResult = dateReg.exec(format);

+ 1 - 0
packages/semi-foundation/datePicker/_utils/getInsetInputValueFromInsetInputStr.ts

@@ -34,6 +34,7 @@ export default function getInsetInputValueFromInsetInputStr(options: { inputValu
     switch (type) {
         case 'date':
         case 'month':
+        case 'monthRange':
             insetInputValue.monthLeft.dateInput = inputValue;
             break;
         case 'dateRange':

+ 1 - 1
packages/semi-foundation/datePicker/constants.ts

@@ -46,7 +46,7 @@ const strings = {
     DEFAULT_SEPARATOR_MULTIPLE: ',',
     DEFAULT_SEPARATOR_RANGE: ' ~ ',
     SIZE_SET: ['small', 'default', 'large'],
-    TYPE_SET: ['date', 'dateRange', 'year', 'month', 'dateTime', 'dateTimeRange'],
+    TYPE_SET: ['date', 'dateRange', 'year', 'month', 'monthRange', 'dateTime', 'dateTimeRange'],
     PRESET_POSITION_SET: ['left', 'right', 'top', 'bottom'],
     DENSITY_SET: ['default', 'compact'],
     PANEL_TYPE_LEFT: 'left',

+ 33 - 6
packages/semi-foundation/datePicker/datePicker.scss

@@ -169,11 +169,23 @@ $module-list: #{$prefix}-scrolllist;
     }
 
     // 年月选择器
-
     &-panel-yam {
         // add left or right preset panel to panel yam, max-width will be bigger
         max-width: $width-datepicker_monthPanel_max + $width-datepicker_presetPanel_left_and_right;
 
+        &[x-type="monthRange"] {
+            max-width: $width-datepicker_monthRangePanel_max + $width-datepicker_presetPanel_left_and_right;
+        }
+
+        .#{$module}-yearmonth-body {
+            display: flex;
+
+            .#{$prefix}-scrolllist:nth-child(2) {
+                border-left: 1px solid var(--semi-color-border);
+
+            }
+        }
+
         .#{$prefix}-scrolllist {
             box-shadow: none;
             height: $height-datepicker_panel_yam_scrolllist;
@@ -196,13 +208,14 @@ $module-list: #{$prefix}-scrolllist;
                 padding: 0;
                 overflow: hidden;
 
-                .#{$prefix}-scrolllist-item-wheel {
+                .#{$prefix}-scrolllist-item-wheel:not(#neverExistElement) {
+                    // equal to #{$prefix}-scrolllist-item-wheel, add [:not] selector only to increase selector priority
                     border: none;
                 }
 
-                .#{$prefix}-scrolllist-item {
-                    border: none;
-                }
+                // .#{$prefix}-scrolllist-item {
+                //     border: none;
+                // }
             }
         }
     }
@@ -832,6 +845,9 @@ $module-list: #{$prefix}-scrolllist;
             &[x-type=month] {
                 width: $width-datepicker_insetInput_month_type_wrapper;
             }
+            &[x-type=monthRange] {
+                width: $width-datepicker_insetInput_month_range_type_wrapper;
+            }
 
             .#{$prefix}-input-wrapper {
                 flex: 1;
@@ -869,7 +885,9 @@ $module-list: #{$prefix}-scrolllist;
                 border-color: $color-datepicker_range_trigger-border-active ;
             }
 
-
+            .#{$module}-monthRange-input {
+                background-color: transparent;
+            }
 
             &-wrapper {
                 box-sizing: border-box;
@@ -1108,6 +1126,15 @@ $module-list: #{$prefix}-scrolllist;
             padding: $spacing-datepicker_yam_panel_header_compact-padding;
         }
 
+        .#{$module}-yearmonth-body {
+            display: flex;
+
+            .#{$prefix}-scrolllist:nth-child(2){
+                border: 1px solid var(--semi-color-border);
+
+            }
+        }
+
         .#{$prefix}-scrolllist {
             @include font-size-small;
             line-height: $lineHeight-datepicker_compact;

+ 15 - 5
packages/semi-foundation/datePicker/foundation.ts

@@ -713,6 +713,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                     break;
                 case 'dateRange':
                 case 'dateTimeRange':
+                case 'monthRange':
                     const separator = rangeSeparator;
                     const values = input.split(separator);
                     parsedResult =
@@ -906,6 +907,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
 
                 case 'dateRange':
                 case 'dateTimeRange':
+                case 'monthRange':
                     const startIsTruthy = !isNullOrUndefined(dates[0]);
                     const endIsTruthy = !isNullOrUndefined(dates[1]);
                     if (startIsTruthy && endIsTruthy) {
@@ -945,6 +947,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                     break;
                 case 'dateRange':
                 case 'dateTimeRange':
+                case 'monthRange':
                     for (let i = 0; i < dates.length; i += 2) {
                         strs.push(this.formatDates(dates.slice(i, i + 2), customFormat));
                     }
@@ -1054,18 +1057,24 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     }
 
     /**
-     * when changing the year and month through the panel when the type is year or month
+     * when changing the year and month through the panel when the type is year or month or monthRange
      * @param {*} item
      */
-    handleYMSelectedChange(item: { currentMonth?: number; currentYear?: number } = {}) {
+    handleYMSelectedChange(item: { currentMonth?: { left: number; right: number }; currentYear?: { left: number; right: number } } = {}) {
         // console.log(item);
         const { currentMonth, currentYear } = item;
+        const { type } = this.getProps();
 
-        if (typeof currentMonth === 'number' && typeof currentYear === 'number') {
-            // Strings with only dates (e.g. "1970-01-01") will be treated as UTC instead of local time #1460
-            const date = new Date(currentYear, currentMonth - 1);
+        if (type === 'month') {
+            const date = new Date(currentYear['left'], currentMonth['left'] - 1);
 
             this.handleSelectedChange([date]);
+        } else {
+            const dateLeft = new Date(currentYear['left'], currentMonth['left'] - 1);
+            const dateRight = new Date(currentYear['right'], currentMonth['right'] - 1);
+
+            this.handleSelectedChange([dateLeft, dateRight]);
+
         }
     }
 
@@ -1170,6 +1179,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
                 break;
             case 'dateRange':
             case 'dateTimeRange':
+            case 'monthRange':
                 notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
                 notifyDate = [..._value];
                 break;

+ 8 - 2
packages/semi-foundation/datePicker/inputFoundation.ts

@@ -17,7 +17,7 @@ const KEY_CODE_ENTER = 'Enter';
 const KEY_CODE_TAB = 'Tab';
 
 
-export type Type = 'date' | 'dateRange' | 'year' | 'month' | 'dateTime' | 'dateTimeRange';
+export type Type = 'date' | 'dateRange' | 'year' | 'month' | 'dateTime' | 'dateTimeRange' | 'monthRange';
 export type RangeType = 'rangeStart' | 'rangeEnd';
 export type PanelType = 'left' | 'right';
 
@@ -180,6 +180,9 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
             case 'month':
                 text = formatDateValues(value, formatToken, undefined, dateFnsLocale);
                 break;
+            case 'monthRange':
+                text = formatDateValues(value, formatToken, { groupSize: 2, groupInnerSeparator: rangeSeparator }, dateFnsLocale);
+                break;
             default:
                 break;
         }
@@ -244,7 +247,7 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
      * Otherwise the default format will be used as placeholder
      */
     getInsetInputPlaceholder() {
-        const { type, format } = this._adapter.getProps();
+        const { type, format, rangeSeparator } = this._adapter.getProps();
         const insetInputFormat = getInsetInputFormatToken({ type, format });
         let datePlaceholder, timePlaceholder;
 
@@ -257,6 +260,8 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
             case 'dateTime':
             case 'dateTimeRange':
                 [datePlaceholder, timePlaceholder] = insetInputFormat.split(' ');
+            case 'monthRange':
+                datePlaceholder = insetInputFormat + rangeSeparator + insetInputFormat;
                 break;
         }
 
@@ -302,6 +307,7 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
         switch (type) {
             case 'date':
             case 'month':
+            case 'monthRange':
                 inputValue = insetInputValue.monthLeft.dateInput;
                 break;
             case 'dateRange':

+ 6 - 4
packages/semi-foundation/datePicker/variables.scss

@@ -9,6 +9,7 @@ $height-datepicker_timeType_tpk: calc(100% - 54px); // 时间面板高度
 $height-datepicker_panel_yam_scrolllist:  266px; // 时间滚动内容高度
 
 $width-datepicker_monthPanel_max: 284px; // 年月选择器最大宽度
+$width-datepicker_monthRangePanel_max: 384px; // 年月选择器最大宽度
 $height-datepicker_timepicker_header_min: 24px; // 年月选择 header 最小高度
 $width-datepicker_navigation_button_min: 32px; // header 按钮最小宽度
 $height-datepicker_yamShowing_min: 378px; // 日期时间选择器菜单最小高度
@@ -30,7 +31,8 @@ $height-datepicker_timeType_insetInput_yam: 100%; // 时间面板高度 - 内嵌
 $height-datepicker_timeType_insetInput_tpk: 100%; // 时间面板高度 - 内嵌输入框
 $width-datepicker_insetInput_date_type_wrapper: 284px; // 日期类型内嵌输入框宽度
 $width-datepicker_insetInput_date_range_type_wrapper: $width-datepicker_insetInput_date_type_wrapper * 2; // 范围选择内嵌输入框宽度
-$width-datepicker_insetInput_month_type_wrapper: 204px; // 月份类型内嵌输入框宽度
+$width-datepicker_insetInput_month_type_wrapper: 165px; // 月份类型内嵌输入框宽度
+$width-datepicker_insetInput_month_range_type_wrapper: 331px; // 年月范围类型内嵌输入框宽度
 $height-datepicker_insetInput_separator: 32px;
 $height-datepicker_month_grid_yearType_insetInput: 317px;
 $height-datepicker_month_grid_timeType_insetInput: 317px;
@@ -222,7 +224,7 @@ $transition-datepicker_range_input: background-color .16s ease-in-out;
 $width-datepicker_presetPanel_left_and_right_content: $width-datepicker_presetPanel_left_and_right - $spacing-datepicker_quick_control_content-paddingX * 2; // 左右方位快捷选择面板,内容宽度
 $width-datepicker_presetPanel_top_and_bottom_content_date: $width-datepicker_day * 7 + $spacing-datepicker_month-padding * 2 - $spacing-datepicker_quick_control_top_and_bottom_content-paddingX * 2; // date/dateTime下, 上下方位快捷选择面板内容宽度, 默认(284 - 40)px
 $width-datepicker_presetPanel_top_and_bottom_content_range: ($width-datepicker_day * 7 + $spacing-datepicker_month-padding * 2) * 2 - $spacing-datepicker_quick_control_top_and_bottom_content-paddingX * 2; // dateRange/dateTimeRange下, 上下方位快捷选择内容面板宽度,默认528px
-$width-datepicker_presetPanel_top_and_bottom_content_month: 194px - $spacing-datepicker_quick_control_top_and_bottom_content-paddingX * 2; // month下,上下方位快捷选择内容面板宽度, 默认154px
+$width-datepicker_presetPanel_top_and_bottom_content_month: 165px - $spacing-datepicker_quick_control_top_and_bottom_content-paddingX * 2; // month下,上下方位快捷选择内容面板宽度, 默认154px
 
 $height-datepicker_month_max: $width-datepicker_day * 7 + 1px; // 年月面板最大高度, 最多6 + 1行,再加上一个border宽度, 默认253px
 $height-datepicker_month_max_compact: $width-datepicker_day_compact * 7 + $spacing-datepicker_weeks_compact-paddingTop + $spacing-datepicker_weeks_compact-padding + $spacing-datepicker_weekday_compact-paddingBottom; // 年月面板最大高度, 最多6 + 1行,再加上padding,默认220px
@@ -233,7 +235,7 @@ $height-datepicker_presetPanel_left_and_right_except_content: 20px + $spacing-da
 // compact
 $width-datepicker_presetPanel_top_and_bottom_content_date_compact: $width-datepicker_day_compact * 7 + $spacing-datepicker_weeks_compact-padding * 2 - $spacing-datepicker_quick_control_top_and_bottom_content_compact-paddingX * 2; // date/dateTime下, 上下方位快捷选择面板内容宽度, 默认(216 - 20)px
 $width-datepicker_presetPanel_top_and_bottom_content_range_compact: ($width-datepicker_day_compact * 7 + $spacing-datepicker_weeks_compact-padding * 2) * 2 - $spacing-datepicker_quick_control_top_and_bottom_content_compact-paddingX * 2; // dateRange/dateTimeRange下, 上下方位快捷选择内容面板宽度,默认412px
-$width-datepicker_presetPanel_top_and_bottom_content_month_compact: 194px - $spacing-datepicker_quick_control_top_and_bottom_content_compact-paddingX  * 2; // month下,上下方位快捷选择内容面板宽度, 默认174px
+$width-datepicker_presetPanel_top_and_bottom_content_month_compact: 165px - $spacing-datepicker_quick_control_top_and_bottom_content_compact-paddingX  * 2; // month下,上下方位快捷选择内容面板宽度, 默认174px
 
 $height-datepicker_date_panel_compact: $height-datepicker_month_max_compact + $width-datepicker_nav_compact + $spacing-datepicker_nav_compact-padding; // compact,date/dateRange,面板渲染最大高度,默认256px
 $height-datepicker_date_time_panel_compact: $height-datepicker_date_panel_compact + $height-datepicker_switch_compact; // compact,dateTime/dateTImeRange,面板渲染最大高度,默认288px
@@ -242,7 +244,7 @@ $height-datepicker_presetPanel_left_and_right_except_content_compact: 20px + $sp
 $width-datepicker_presetPanel_left_and_right_two_col_button: ($width-datepicker_presetPanel_left_and_right_content - $spacing-datepicker_quick_control_item-margin) * 0.5; // 左右方位快捷选择面板,固定两列,按钮宽度
 $width-datepicker_presetPanel_top_and_bottom_three_col_button: ($width-datepicker_presetPanel_top_and_bottom_content_date - $spacing-datepicker_quick_control_item-margin * 2) * 0.333; // 上下方位快捷选择面板,固定三列,按钮宽度
 $width-datepicker_presetPanel_top_and_bottom_five_col_button: ($width-datepicker_presetPanel_top_and_bottom_content_range - $spacing-datepicker_quick_control_item-margin * 4) * 0.2; // 上下方位快捷选择面板,固定五列,按钮宽度
-$width-datepicker_presetPanel_top_and_bottom_two_col_button:($width-datepicker_presetPanel_top_and_bottom_content_month - $spacing-datepicker_quick_control_item-margin) * 0.5; // 上下方位快捷选择面板,固定两列,按钮宽度
+$width-datepicker_presetPanel_top_and_bottom_two_col_button: ($width-datepicker_presetPanel_top_and_bottom_content_month - $spacing-datepicker_quick_control_item-margin) * 0.5; // 上下方位快捷选择面板,固定两列,按钮宽度
 
 // compact
 $width-datepicker_presetPanel_top_and_bottom_three_col_button_compact: ($width-datepicker_presetPanel_top_and_bottom_content_date_compact - $spacing-datepicker_quick_control_item-margin * 2) * 0.333; // 上下方位快捷选择面板,固定三列,按钮宽度

+ 65 - 18
packages/semi-foundation/datePicker/yearAndMonthFoundation.ts

@@ -1,11 +1,17 @@
 import { setMonth, setYear } from 'date-fns';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import { PresetPosition } from './foundation';
+import { ArrayElement } from '../utils/type';
+import { strings } from './constants';
+import { PanelType } from './monthsGridFoundation';
+import { cloneDeep } from 'lodash';
+
+type Type = ArrayElement<typeof strings.TYPE_SET>;
 
 export interface YearAndMonthFoundationProps {
-    currentYear?: number;
-    currentMonth?: number;
-    onSelect?: (obj: { currentMonth: number; currentYear: number }) => void;
+    currentYear: { left: number; right: number };
+    currentMonth: { left: number; right: number };
+    onSelect?: (obj: { currentMonth: { left: number; right: number }; currentYear: { left: number; right: number } }) => void;
     onBackToMain?: () => void;
     locale?: any;
     localeCode?: string;
@@ -17,20 +23,23 @@ export interface YearAndMonthFoundationProps {
     presetPosition?: PresetPosition;
     renderQuickControls?: any;
     renderDateInput?: any;
+    type?: Type;
     yearAndMonthOpts?: any
 }
 
 export interface YearAndMonthFoundationState {
     years: Array<{ value: number; year: number }>;
     months: Array<{ value: number; month: number }>;
-    currentYear: number;
-    currentMonth: number
+    currentYear: { left: number; right: number };
+    currentMonth: { left: number; right: number }
 }
 export interface YearAndMonthAdapter extends DefaultAdapter<YearAndMonthFoundationProps, YearAndMonthFoundationState> {
-    setCurrentYear: (currentYear: number, cb?: () => void) => void;
-    setCurrentMonth: (currentMonth: number) => void;
-    notifySelectYear: (year: number) => void;
-    notifySelectMonth: (month: number) => void;
+    setCurrentYear: (currentYear: { left: number; right: number }, cb?: () => void) => void;
+    setCurrentMonth: (currentMonth: { left: number; right: number }) => void;
+    setCurrentYearAndMonth: (currentYear: { left: number; right: number }, currentMonth: { left: number; right: number }) => void;
+    notifySelectYear: (year: { left: number; right: number }) => void;
+    notifySelectMonth: (month: { left: number; right: number }) => void;
+    notifySelectYearAndMonth: (year: { left: number; right: number }, month: { left: number; right: number }) => void;
     notifyBackToMain: () => void
 }
 
@@ -60,14 +69,47 @@ export default class YearAndMonthFoundation extends BaseFoundation<YearAndMonthA
     // eslint-disable-next-line @typescript-eslint/no-empty-function
     destroy() {}
 
-    selectYear(item: YearScrollItem) {
-        const year = item.value;
-        this._adapter.setCurrentYear(year, () => this.autoSelectMonth(item));
+    selectYear(item: YearScrollItem, panelType?: PanelType) {
+        // const year = item.value;
+        const { currentYear, currentMonth } = this.getStates();
+        const { type } = this.getProps();
+        const left = strings.PANEL_TYPE_LEFT;
+        const right = strings.PANEL_TYPE_RIGHT;
+
+        const year = cloneDeep(currentYear);
+        year[panelType] = item.value;
+
+        // make sure the right panel time is always less than the left panel time
+        if (type === 'monthRange') {
+            const isSameYearIllegalDate = year[left] === year[right] && currentMonth[left] > currentMonth[right];
+            if ((panelType === left && item.value > year[right]) || (panelType === left && isSameYearIllegalDate)) {
+                // 1. select left year and left year > right year
+                // 2. select left year, left year = right year, but left date > right date
+                year[right] = item.value + 1;
+            } else if (panelType === right && isSameYearIllegalDate) {
+                // 1. select right year, left year = right year, but left date > right date
+                year[left] = item.value - 1;
+            }
+        }
+
+        this._adapter.setCurrentYear(year, () => this.autoSelectMonth(item, panelType, year));
         this._adapter.notifySelectYear(year);
     }
 
-    selectMonth(item: MonthScrollItem) {
-        const { month } = item;
+    selectMonth(item: MonthScrollItem, panelType?: PanelType) {
+        const { currentMonth, currentYear } = this.getStates();
+        const { type } = this.getProps();
+        const left = strings.PANEL_TYPE_LEFT;
+        const right = strings.PANEL_TYPE_RIGHT;
+
+        const month = cloneDeep(currentMonth);
+        month[panelType] = item.month;
+
+        // make sure the right panel time is always less than the left panel time
+        if (type === 'monthRange' && panelType === left && currentYear[left] === currentYear[right] && item.value > month[right]) {
+            month[right] = item.month + 1;
+        } 
+
         this._adapter.setCurrentMonth(month);
         this._adapter.notifySelectMonth(month);
     }
@@ -75,14 +117,14 @@ export default class YearAndMonthFoundation extends BaseFoundation<YearAndMonthA
     /**
      * After selecting a year, if the currentMonth is disabled, automatically select a non-disabled month
      */
-    autoSelectMonth(item: YearScrollItem) {
+    autoSelectMonth(item: YearScrollItem, panelType: PanelType, year: { left: number; right: number }) {
         const { disabledDate, locale } = this._adapter.getProps();
         const { months, currentMonth } = this._adapter.getStates();
 
         const currentDate = setYear(Date.now(), item.year);
-        const isCurrentMonthDisabled = disabledDate(setMonth(currentDate, currentMonth - 1));
+        const isCurrentMonthDisabled = disabledDate(setMonth(currentDate, currentMonth[panelType] - 1));
         if (isCurrentMonthDisabled) {
-            const currentIndex = months.findIndex(({ month }) => month === currentMonth);
+            const currentIndex = months.findIndex(({ month }) => month === currentMonth[panelType]);
             let validMonth: typeof months[number];
             // First look in the back, if you can't find it in the back, then look in the front
             validMonth = months.slice(currentIndex).find(({ month }) => !disabledDate(setMonth(currentDate, month - 1)));
@@ -90,7 +132,12 @@ export default class YearAndMonthFoundation extends BaseFoundation<YearAndMonthA
                 validMonth = months.slice(0, currentIndex).find(({ month }) => !disabledDate(setMonth(currentDate, month - 1)));
             }
             if (validMonth) {
-                this.selectMonth({ month: validMonth.month, value: locale.fullMonths[validMonth.month], disabled: false });
+                const month = cloneDeep(currentMonth);
+                month[panelType] = validMonth.month;
+
+                // change year and month same time
+                this._adapter.setCurrentYearAndMonth(year, month);
+                this._adapter.notifySelectYearAndMonth(year, month);
             }
         }
     }

+ 122 - 1
packages/semi-ui/datePicker/_story/datePicker.stories.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, useMemo, useCallback } from 'react';
 import {
   addDays,
   addWeeks,
@@ -37,7 +37,11 @@ import DatePickerTimeZone from './DatePickerTimeZone';
 import BetterRangePicker from './BetterRangePicker';
 import SyncSwitchMonth from './SyncSwitchMonth';
 import { Checkbox } from '../../checkbox';
+import Typography from '../../typography/typography';
+import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
 export * from './v2';
+import * as dateFns from 'date-fns';
+
 
 export default {
   title: 'DatePicker',
@@ -311,6 +315,20 @@ export const DatePickerWithPresets = () => {
         onPresetClick={onPresetClick}
         onChange={(...args) => console.log(...args)}
       />
+      {/* <br/>
+      <br/>
+      <div>type="monthRange"</div>
+      <DatePicker
+        type="monthRange"
+        insetInput={insetInput}
+        presetPosition={presetPosition}
+        presets={presetArr.map(preset => ({
+          text: preset.text,
+          start: preset.start,
+        }))}
+        onPresetClick={onPresetClick}
+        onChange={(...args) => console.log(...args)}
+      /> */}
       <br/>
       <br/>
       <div>type="date" density="compact"</div>
@@ -479,6 +497,109 @@ export const MonthPicker = () => {
   return <Demo />;
 };
 
+export const MonthRangePicker = () => {
+    const { Text } = Typography;
+    const formatToken = 'yyyy-MM';
+    const [controlledValue, setControlledValue] = useState(['2023-03', '2023-04']);
+    const [triggerValue, setTriggerValue] = useState();
+
+    const _setControlledValue = value => setControlledValue(value);
+
+    const onChange = useCallback(date => {
+      setTriggerValue(date);
+    }, []);
+
+    const onClear = useCallback(e => {
+        e && e.stopPropagation();
+        setTriggerValue(null);
+    }, []);
+
+    const closeIcon = useMemo(() => {
+        return triggerValue ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+    }, [triggerValue]);
+
+    const triggerContent = (placeholder) => {
+        if (Array.isArray(triggerValue) && triggerValue.length) {
+            return `${dateFns.format(triggerValue[0], formatToken)} ~ ${dateFns.format(triggerValue[1], formatToken)}`;
+        } else {
+            return '请选择年月时间范围';
+        }
+    };
+
+    const TopSlot = function(props) {
+      const { style } = props;
+      return (
+          <Space style={{ padding: '12px 20px', ...style }}>
+              <Text strong style={{ color: 'var(--semi-color-text-2)' }}>
+                  请选择月份范围
+              </Text>
+          </Space>
+      );
+    };
+
+    const BottomSlot = function(props) {
+      const { style } = props;
+      return (
+          <Space style={{ padding: '12px 20px', ...style }}>
+              <Text strong style={{ color: 'var(--semi-color-text-2)' }}>
+                  定版前请阅读
+              </Text>
+              <Text link={{ href: 'https://semi.design/', target: '_blank' }}>发版须知</Text>
+          </Space>
+      );
+    };
+
+    const disabledDate = date => {
+        const deadDate = new Date('2023/3/1 00:00:00');
+        return date.getTime() < deadDate.getTime(); 
+    };
+
+    return (
+      <>
+        <div>
+          <div>default</div>
+          <DatePicker type="monthRange" />
+          <br />
+          <br />
+          <div>rangeSeparator 与 placeholder</div>
+          <DatePicker type="monthRange" rangeSeparator={'➡️'} placeholder={['开始', '结束']} insetLabel='月份范围'/>
+          <br />
+          <br />
+          <div>受控</div>
+          <DatePicker type="monthRange" bottomSlot={<BottomSlot />} topSlot={<TopSlot />} value={controlledValue} onChange={_setControlledValue}/>
+          <br />
+          <br />
+          <div>insetInput ➕ format</div>
+          <div data-cy="monthRange">
+            <DatePicker type="monthRange" insetInput format={'yyyy年MM月'} rangeSeparator={'到'} defaultValue={['2023-03', '2023-04']} style={{ width: 400 }}/>
+          </div>
+          <br />
+          <div>triggerRender</div>
+          <DatePicker 
+            type="monthRange" 
+            value={triggerValue} 
+            onChange={onChange}
+            triggerRender={({ placeholder }) => (
+                <Button theme={'light'} icon={closeIcon} iconPosition='right'>
+                    {triggerContent(placeholder)}
+                </Button>
+            )}
+          />
+          <br />
+          <br />
+          <div>年月禁用:禁用2023年3月前的所有年月</div>
+          <DatePicker type="monthRange" disabledDate={disabledDate}/>
+          <br />
+          <br />
+          <div>validateStatus</div>
+          <DatePicker type="monthRange" validateStatus='warning' />
+          <br />
+          <DatePicker type="monthRange" validateStatus='error' />
+        </div>
+      </>
+    );
+};
+
 export const PropTypesAndDefaultProps = () => (
   <div>
     <article>

+ 10 - 5
packages/semi-ui/datePicker/dateInput.tsx

@@ -307,6 +307,12 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
         );
     }
 
+    isRenderMultipleInputs() {
+        const { type } = this.props;
+        // isRange and not monthRange render multiple inputs
+        return type.includes('Range') && type !== 'monthRange';
+    }
+
     renderInputInset() {
         const {
             type,
@@ -321,7 +327,6 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
             insetInput,
         } = this.props;
 
-        const _isRangeType = type.includes('Range');
         const newInsetInputValue = this.foundation.getInsetInputValue({ value, insetInputValue });
         const { dateStart, dateEnd, timeStart, timeEnd } = get(insetInput, 'placeholder', {}) as InsetInputProps['placeholder'];
         const { datePlaceholder, timePlaceholder } = this.foundation.getInsetInputPlaceholder();
@@ -348,7 +353,7 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
                     onChange={this.handleInsetInputChange}
                     onFocus={handleInsetTimeFocus}
                 />
-                {_isRangeType && (
+                { this.isRenderMultipleInputs() && (
                     <>
                         <div className={separatorCls}>{density === 'compact' ? null : '-'}</div>
                         <InsetDateInput
@@ -423,12 +428,12 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
 
         const inputCls = cls({
             [`${prefixCls}-input-readonly`]: inputReadOnly,
+            [`${prefixCls}-monthRange-input`]: type === 'monthRange',
         });
 
-        const isRangeType = /range/i.test(type);
         const rangeProps = { ...this.props, text, suffix, inputCls };
 
-        return isRangeType ? (
+        return this.isRenderMultipleInputs() ? (
             this.renderRangeInput(rangeProps)
         ) : (
             <Input
@@ -440,7 +445,7 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
                 className={inputCls}
                 style={inputStyle}
                 hideSuffix={showClear}
-                placeholder={placeholder}
+                placeholder={type === 'monthRange' && Array.isArray(placeholder) ? placeholder[0] + rangeSeparator + placeholder[1] : placeholder}
                 onEnterPress={this.handleEnterPress}
                 onChange={this.handleChange}
                 onClear={this.handleInputClear}

+ 20 - 10
packages/semi-ui/datePicker/datePicker.tsx

@@ -296,7 +296,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             },
             needConfirm: () =>
                 ['dateTime', 'dateTimeRange'].includes(this.props.type) && this.props.needConfirm === true,
-            typeIsYearOrMonth: () => ['month', 'year'].includes(this.props.type),
+            typeIsYearOrMonth: () => ['month', 'year', 'monthRange'].includes(this.props.type),
             setRangeInputFocus: rangeInputFocus => {
                 const { preventScroll } = this.props;
                 if (rangeInputFocus !== this.state.rangeInputFocus) {
@@ -730,7 +730,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
     };
 
     renderPanel = (locale: Locale['DatePicker'], localeCode: string, dateFnsLocale: Locale['dateFnsLocale']) => {
-        const { dropdownClassName, dropdownStyle, density, topSlot, bottomSlot, presetPosition } = this.props;
+        const { dropdownClassName, dropdownStyle, density, topSlot, bottomSlot, presetPosition, type } = this.props;
         const wrapCls = classnames(
             cssClasses.PREFIX,
             {
@@ -741,17 +741,18 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
         );
 
         return (
-            <div ref={this.panelRef} className={wrapCls} style={dropdownStyle} >
+            <div ref={this.panelRef} className={wrapCls} style={dropdownStyle} x-type={type}>
                 {topSlot && (
                     <div className={`${cssClasses.PREFIX}-topSlot`} x-semi-prop="topSlot">
                         {topSlot}
                     </div>
                 )}
-                {presetPosition === "top" && this.renderQuickControls()}
+                {/* todo: monthRange does not support presetPosition temporarily */}
+                {presetPosition === "top" && type !== 'monthRange' && this.renderQuickControls()}
                 {this.adapter.typeIsYearOrMonth()
                     ? this.renderYearMonthPanel(locale, localeCode)
                     : this.renderMonthGrid(locale, localeCode, dateFnsLocale)}
-                {presetPosition === "bottom" && this.renderQuickControls()}
+                {presetPosition === "bottom" && type !== 'monthRange' && this.renderQuickControls()}
                 {bottomSlot && (
                     <div className={`${cssClasses.PREFIX}-bottomSlot`} x-semi-prop="bottomSlot">
                         {bottomSlot}
@@ -763,15 +764,23 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
     };
 
     renderYearMonthPanel = (locale: Locale['DatePicker'], localeCode: string) => {
-        const { density, presetPosition, yearAndMonthOpts } = this.props;
+        const { density, presetPosition, yearAndMonthOpts, type } = this.props;
 
         const date = this.state.value[0];
-        let year = 0;
-        let month = 0;
+        const year = { left: 0, right: 0 };
+        const month = { left: 0, right: 0 };
 
         if (isDate(date)) {
-            year = date.getFullYear();
-            month = date.getMonth() + 1;
+            year.left = date.getFullYear();
+            month.left = date.getMonth() + 1;
+        }
+
+        if (type === 'monthRange') {
+            const dateRight = this.state.value[1];
+            if (isDate(dateRight)) {
+                year.right = dateRight.getFullYear();
+                month.right = dateRight.getMonth() + 1;
+            }
         }
 
         return (
@@ -788,6 +797,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
                 presetPosition={presetPosition}
                 renderQuickControls={this.renderQuickControls()}
                 renderDateInput={this.renderDateInput()}
+                type={type}
                 yearAndMonthOpts={yearAndMonthOpts}
             />
         );

+ 5 - 3
packages/semi-ui/datePicker/monthsGrid.tsx

@@ -507,10 +507,12 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
                 ref={current => this.cacheRefCurrent(`yam-${panelType}`, current)}
                 locale={locale}
                 localeCode={localeCode}
-                currentYear={y}
-                currentMonth={m}
+                // currentYear={y}
+                // currentMonth={m}
+                currentYear={{ left: y, right: 0 }}
+                currentMonth={{ left: m, right: 0 }}
                 onSelect={item =>
-                    this.foundation.toYearMonth(panelType, new Date(item.currentYear, item.currentMonth - 1))
+                    this.foundation.toYearMonth(panelType, new Date(item.currentYear.left, item.currentMonth.left - 1))
                 }
                 onBackToMain={() => {
                     this.foundation.showDatePanel(panelType);

+ 100 - 43
packages/semi-ui/datePicker/yearAndMonth.tsx

@@ -6,16 +6,16 @@ import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import ScrollList from '../scrollList/index';
 import ScrollItem from '../scrollList/scrollItem';
 import { getYears } from '@douyinfe/semi-foundation/datePicker/_utils/index';
-import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 
 import IconButton from '../iconButton';
 import { IconChevronLeft } from '@douyinfe/semi-icons';
 import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
 
-import { noop, stubFalse } from 'lodash';
+import { noop, stubFalse, isEqual } from 'lodash';
 import { setYear, setMonth, set } from 'date-fns';
 import { Locale } from '../locale/interface';
 import { strings } from '@douyinfe/semi-foundation/datePicker/constants';
+import { PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
 
 
 const prefixCls = `${BASE_CLASS_PREFIX}-datepicker`;
@@ -28,8 +28,8 @@ export type YearAndMonthState = YearAndMonthFoundationState;
 
 class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
     static propTypes = {
-        currentYear: PropTypes.number,
-        currentMonth: PropTypes.number,
+        currentYear: PropTypes.object,
+        currentMonth: PropTypes.object,
         onSelect: PropTypes.func,
         locale: PropTypes.object,
         localeCode: PropTypes.string,
@@ -40,7 +40,8 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         density: PropTypes.string,
         presetPosition: PropTypes.oneOf(strings.PRESET_POSITION_SET),
         renderQuickControls: PropTypes.node,
-        renderDateInput: PropTypes.node
+        renderDateInput: PropTypes.node,
+        type: PropTypes.oneOf(strings.TYPE_SET),
     };
 
     static defaultProps = {
@@ -49,6 +50,7 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         yearCycled: false,
         noBackBtn: false,
         onSelect: noop,
+        type: 'month',
     };
     foundation: YearAndMonthFoundation;
     yearRef: React.RefObject<ScrollItem<YearScrollItem>>;
@@ -59,8 +61,12 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         const now = new Date();
 
         let { currentYear, currentMonth } = props;
-        currentYear = currentYear || now.getFullYear();
-        currentMonth = currentMonth || now.getMonth() + 1;
+
+        const currentLeftYear = currentYear.left || now.getFullYear();
+        const currentLeftMonth = currentMonth.left || now.getMonth() + 1;
+
+        currentYear = { left: currentLeftYear, right: currentLeftYear };
+        currentMonth = { left: currentLeftMonth, right: currentMonth.right || currentLeftMonth + 1 };
 
         this.state = {
             years: getYears().map(year => ({
@@ -89,47 +95,67 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
             // updateMonths: months => this.setState({ months }),
             setCurrentYear: (currentYear, cb) => this.setState({ currentYear }, cb),
             setCurrentMonth: currentMonth => this.setState({ currentMonth }),
-            notifySelectYear: year =>
+            setCurrentYearAndMonth: (currentYear, currentMonth) => this.setState({ currentYear, currentMonth }),
+            notifySelectYear: (year) =>
                 this.props.onSelect({
                     currentMonth: this.state.currentMonth,
                     currentYear: year,
                 }),
-            notifySelectMonth: month =>
+            notifySelectMonth: (month) =>
                 this.props.onSelect({
                     currentYear: this.state.currentYear,
                     currentMonth: month,
                 }),
+            notifySelectYearAndMonth: (year, month) =>
+                this.props.onSelect({
+                    currentYear: year,
+                    currentMonth: month,
+                }),
             notifyBackToMain: () => this.props.onBackToMain(),
         };
     }
 
     static getDerivedStateFromProps(props: YearAndMonthProps, state: YearAndMonthState) {
         const willUpdateStates: Partial<YearAndMonthState> = {};
-        const now = new Date();
 
-        if (!isNullOrUndefined(props.currentMonth) && props.currentMonth !== state.currentMonth && props.currentMonth !== 0) {
-            willUpdateStates.currentMonth = props.currentMonth || now.getMonth() + 1;
+        if (!isEqual(props.currentYear, state.currentYear) && props.currentYear.left !== 0) {
+            willUpdateStates.currentYear = props.currentYear;
         }
 
-        if (isNullOrUndefined(props.currentYear) && props.currentYear !== state.currentYear && props.currentYear !== 0) {
-            willUpdateStates.currentYear = props.currentYear || now.getFullYear();
+        if (!isEqual(props.currentMonth, state.currentMonth) && props.currentMonth.left !== 0) {
+            willUpdateStates.currentMonth = props.currentMonth;
         }
 
         return willUpdateStates;
     }
 
-    renderColYear() {
+    renderColYear(panelType: PanelType) {
         const { years, currentYear, currentMonth, months } = this.state;
         const { disabledDate, localeCode, yearCycled, yearAndMonthOpts } = this.props;
-        const currentDate = setMonth(Date.now(), currentMonth - 1);
+        const currentDate = setMonth(Date.now(), currentMonth[panelType] - 1);
+        const left = strings.PANEL_TYPE_LEFT;
+        const right = strings.PANEL_TYPE_RIGHT;
+
+        const needDisabled = (year) => {
+            if (panelType === right && currentYear[left]) {
+                if (currentMonth[left] <= currentMonth[right]) {
+                    return currentYear[left] > year;
+                } else {
+                    return currentYear[left] >= year;
+                }
+            }
+            return false;
+        };
+
         const list: any[] = years.map(({ value, year }) => {
             const isAllMonthDisabled = months.every(({ month }) => {
                 return disabledDate(set(currentDate, { year, month: month - 1 }));
             });
+            const isRightPanelDisabled = needDisabled(year);
             return ({
                 year,
                 value, // Actual rendered text
-                disabled: isAllMonthDisabled,
+                disabled: isAllMonthDisabled || isRightPanelDisabled,
             });
         });
         let transform = (val: string) => val;
@@ -143,21 +169,21 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
                 cycled={yearCycled}
                 list={list}
                 transform={transform}
-                selectedIndex={years.findIndex(item => item.value === currentYear)}
+                selectedIndex={years.findIndex(item => item.value === currentYear[panelType])}
                 type="year"
-                onSelect={this.selectYear}
+                onSelect={item => this.selectYear(item as YearScrollItem, panelType)}
                 mode="normal"
                 {...yearAndMonthOpts}
             />
         );
     }
 
-    selectYear = (item: YearScrollItem) => {
-        this.foundation.selectYear(item);
+    selectYear = (item: YearScrollItem, panelType?: PanelType) => {
+        this.foundation.selectYear(item, panelType);
     };
 
-    selectMonth = (item: MonthScrollItem) => {
-        this.foundation.selectMonth(item);
+    selectMonth = (item: MonthScrollItem, panelType?: PanelType) => {
+        this.foundation.selectMonth(item, panelType);
     };
 
     reselect = () => {
@@ -172,22 +198,29 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         });
     };
 
-    renderColMonth() {
+    renderColMonth(panelType: PanelType) {
         const { months, currentMonth, currentYear } = this.state;
         const { locale, localeCode, monthCycled, disabledDate, yearAndMonthOpts } = this.props;
         let transform = (val: string) => val;
-        const currentDate = setYear(Date.now(), currentYear);
+        const currentDate = setYear(Date.now(), currentYear[panelType]);
+        const left = strings.PANEL_TYPE_LEFT;
+        const right = strings.PANEL_TYPE_RIGHT;
+
         if (localeCode === 'zh-CN' || localeCode === 'zh-TW') {
             // Only Chinese needs to add [month] after the selected month
             transform = val => `${val}月`;
         }
         // i18n
-        const list: MonthScrollItem[] = months.map(({ value, month }) => ({
-            month,
-            disabled: disabledDate(setMonth(currentDate, month - 1)),
-            value: locale.fullMonths[value], // Actual rendered text
-        }));
-        const selectedIndex = list.findIndex(item => item.month === currentMonth);
+        const list: MonthScrollItem[] = months.map(({ value, month }) => {
+            const isRightPanelDisabled = panelType === right && currentMonth[left] && currentYear[left] === currentYear[right] && currentMonth[left] > month;
+
+            return ({
+                month,
+                disabled: disabledDate(setMonth(currentDate, month - 1)) || isRightPanelDisabled,
+                value: locale.fullMonths[value], // Actual rendered text
+            });
+        });
+        const selectedIndex = list.findIndex(item => item.month === currentMonth[panelType]);
         return (
             <ScrollItem
                 ref={this.monthRef}
@@ -196,7 +229,7 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
                 transform={transform}
                 selectedIndex={selectedIndex}
                 type="month"
-                onSelect={this.selectMonth}
+                onSelect={item => this.selectMonth(item as MonthScrollItem, panelType)}
                 mode='normal'
                 {...yearAndMonthOpts}
             />
@@ -208,13 +241,41 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
         this.foundation.backToMain();
     };
 
+    renderPanel(panelType: PanelType) {
+
+        return (
+            <>
+                <ScrollList>
+                    {this.renderColYear(panelType)}
+                    {this.renderColMonth(panelType)}
+                </ScrollList>
+            </>
+        );
+    }
+
     render() {
-        const { locale, noBackBtn, density, presetPosition, renderQuickControls, renderDateInput } = this.props;
+        const { locale, noBackBtn, density, presetPosition, renderQuickControls, renderDateInput, type } = this.props;
         const prefix = `${prefixCls}-yearmonth-header`;
+        const bodyCls = `${prefixCls}-yearmonth-body`;
+
         // i18n
         const selectDateText = locale.selectDate;
         const iconSize = density === 'compact' ? 'default' : 'large';
         const buttonSize = density === 'compact' ? 'small' : 'default';
+        const panelTypeLeft = strings.PANEL_TYPE_LEFT;
+        const panelTypeRight = strings.PANEL_TYPE_RIGHT;
+
+        let content = null;
+        if (type === 'month') {
+            content = this.renderPanel(panelTypeLeft);
+        } else {
+            content = (
+                <div className={bodyCls}>
+                    {this.renderPanel(panelTypeLeft)}
+                    {this.renderPanel(panelTypeRight)}
+                </div>
+            );
+        }
 
         return (
             <React.Fragment>
@@ -233,23 +294,19 @@ class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
                 {
                     presetPosition ? (
                         <div style={{ display: 'flex' }}>
-                            {presetPosition === "left" && renderQuickControls}
+                            {/* todo: monthRange does not support presetPosition temporarily */}
+                            {presetPosition === "left" && type !== 'monthRange' && renderQuickControls}
                             <div>
                                 {renderDateInput}
-                                <ScrollList>
-                                    {this.renderColYear()}
-                                    {this.renderColMonth()}
-                                </ScrollList>
+                                {content} 
                             </div>
-                            {presetPosition === "right" && renderQuickControls}
+                            {/* todo: monthRange does not support presetPosition temporarily */}
+                            {presetPosition === "right" && type !== 'monthRange' && renderQuickControls}
                         </div>
                     ) :
                         <>
                             {renderDateInput}
-                            <ScrollList>
-                                {this.renderColYear()}
-                                {this.renderColMonth()}
-                            </ScrollList>
+                            {content} 
                         </>
                 }
             </React.Fragment>

+ 2 - 1
packages/semi-ui/locale/interface.ts

@@ -31,7 +31,8 @@ export interface Locale {
             date: string;
             dateTime: string;
             dateRange: [string, string];
-            dateTimeRange: [string, string]
+            dateTimeRange: [string, string];
+            monthRange: [string, string]
         };
         footer: {
             confirm: string;

+ 1 - 0
packages/semi-ui/locale/source/ar.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'حدد التاريخ والوقت',
             dateRange: ['تاريخ البدء', 'تاريخ النهاية'],
             dateTimeRange: ['تاريخ البدء', 'تاريخ النهاية'],
+            monthRange: ['الشهر الأول', 'الشهر الأخير'],
         },
         footer: {
             confirm: 'تؤكد',

+ 1 - 0
packages/semi-ui/locale/source/de.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Datum und Uhrzeit auswählen',
             dateRange: ['Startdatum', 'Enddatum'],
             dateTimeRange: ['Startdatum', 'Enddatum'],
+            monthRange: ['Startmonat', 'Endmonat'],
         },
         footer: {
             confirm: 'Bestätigen',

+ 1 - 0
packages/semi-ui/locale/source/en_GB.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Select date and time',
             dateRange: ['Start date', 'End date'],
             dateTimeRange: ['Start date', 'End date'],
+            monthRange: ['Start month', 'End month'],
         },
         footer: {
             confirm: 'Confirm',

+ 1 - 0
packages/semi-ui/locale/source/en_US.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Select date and time',
             dateRange: ['Start date', 'End date'],
             dateTimeRange: ['Start date', 'End date'],
+            monthRange: ['Start month', 'End month'],
         },
         footer: {
             confirm: 'Confirm',

+ 1 - 0
packages/semi-ui/locale/source/es.ts

@@ -38,6 +38,7 @@ const locale: Locale = {
             dateTime: 'Seleccionar hora y fecha',
             dateRange: ['Fecha inicial', 'Fecha final'],
             dateTimeRange: ['Fecha inicial', 'Fecha final'],
+            monthRange: ['Mes inicial', 'Mes final'],
         },
         footer: {
             confirm: 'Aceptar',

+ 1 - 0
packages/semi-ui/locale/source/fr.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Sélectionner date et temps',
             dateRange: ['Date de début', 'Date de fin'],
             dateTimeRange: ['Date de début', 'Date de fin'],
+            monthRange: ['Mois de début', 'Mois de fin'],
         },
         footer: {
             confirm: 'Confirmer',

+ 1 - 0
packages/semi-ui/locale/source/id_ID.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Pilih tanggal dan waktu',
             dateRange: ['Tanggal mulai', 'Tanggal akhir'],
             dateTimeRange: ['Tanggal mulai', 'Tanggal akhir'],
+            monthRange: ['Bulan pertama', 'Bulan terakhir'],
         },
         footer: {
             confirm: 'Konfirmasi',

+ 1 - 0
packages/semi-ui/locale/source/it.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Seleziona data e ora',
             dateRange: ['Data inizio', 'Data fine'],
             dateTimeRange: ['Data inizio', 'Data fine'],
+            monthRange: ['Mese inizio', 'Mese fine'],
         },
         footer: {
             confirm: 'Conferma',

+ 1 - 0
packages/semi-ui/locale/source/ja_JP.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: '日時を選択してください',
             dateRange: ['開始日', '終了日'],
             dateTimeRange: ['開始日', '終了日'],
+            monthRange: ['開始月', '終了月'],
         },
         footer: {
             confirm: '確認する',

+ 1 - 0
packages/semi-ui/locale/source/ko_KR.ts

@@ -34,6 +34,7 @@ const local: Locale = {
             dateTime: '날짜 및 시간 선택',
             dateRange: ['시작 날짜', '종료일'],
             dateTimeRange: ['시작 날짜', '종료일'],
+            monthRange: ['시작 월', '종료 월'],
         },
         footer: {
             confirm: '확인',

+ 1 - 0
packages/semi-ui/locale/source/ms_MY.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Pilih tarikh dan masa',
             dateRange: ['Tarikh mula', 'Tarikh akhir'],
             dateTimeRange: ['Tarikh mula', 'Tarikh akhir'],
+            monthRange: ['Bulan mula', 'Bulan akhir'],
         },
         footer: {
             confirm: 'Sahkan',

+ 1 - 0
packages/semi-ui/locale/source/nl_NL.ts

@@ -40,6 +40,7 @@ const local: Locale = {
             dateTime: 'Datum en tijd selecteren',
             dateRange: ['Begindatum', 'Einddatum'],
             dateTimeRange: ['Begindatum', 'Einddatum'],
+            monthRange: ['Begindatum', 'Einddatum'],
         },
         footer: {
             confirm: 'Bevestigen',

+ 1 - 0
packages/semi-ui/locale/source/pl_PL.ts

@@ -41,6 +41,7 @@ const local: Locale = {
             dateTime: 'Wybierz datę i godzinę',
             dateRange: ['Data rozpoczęcia', 'Data zakończenia'],
             dateTimeRange: ['Data rozpoczęcia', 'Data zakończenia'],
+            monthRange: ['Miesiąc rozpoczęcia', 'Miesiąc zakończenia'],
         },
         footer: {
             confirm: 'Potwierdź',

+ 1 - 0
packages/semi-ui/locale/source/pt_BR.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: 'Selecione a data e hora',
             dateRange: ['Data de início', 'Data de fim'],
             dateTimeRange: ['Data de início', 'Data de fim'],
+            monthRange: ['Mês de início', 'Mês de fim'],
         },
         footer: {
             confirm: 'OK',

+ 1 - 0
packages/semi-ui/locale/source/ro.ts

@@ -35,6 +35,7 @@ export default {
             dateTime: 'Selectează data și ora',
             dateRange: ['Data de început', 'Data de sfârșit'],
             dateTimeRange: ['Data de început', 'Data de sfârșit'],
+            monthRange: ['Luna de început', 'Luna de sfârșit'],
         },
         footer: {
             confirm: 'Confirmă',

+ 1 - 0
packages/semi-ui/locale/source/ru_RU.ts

@@ -36,6 +36,7 @@ const local: Locale = {
             dateTime: 'Выбрать дату и время',
             dateRange: ['Дата начала', 'Дата окончания'],
             dateTimeRange: ['Дата начала', 'Дата окончания'],
+            monthRange: ['Начальный месяц', 'Конечный месяц'],
         },
         footer: {
             confirm: 'подтвердить',

+ 1 - 0
packages/semi-ui/locale/source/sv_SE.ts

@@ -38,6 +38,7 @@ const local: Locale = {
             dateTime: 'Välj datum och tid',
             dateRange: ['Startdatum', 'Slutdatum'],
             dateTimeRange: ['Startdatum', 'Slutdatum'],
+            monthRange: ['Startmånad', 'Slutmånad'],
         },
         footer: {
             confirm: 'Bekräfta',

+ 1 - 0
packages/semi-ui/locale/source/th_TH.ts

@@ -36,6 +36,7 @@ const local: Locale = {
             dateTime: 'โปรดเลือกวันที่และเวลา',
             dateRange: ['วันที่เริ่มต้น', 'วันที่สิ้นสุด'],
             dateTimeRange: ['วันที่เริ่มต้น', 'วันที่สิ้นสุด'],
+            monthRange: ['เดือนเริ่มต้น', 'เดือนสิ้นสุด'],
         },
         footer: {
             confirm: 'ตกลง',

+ 1 - 0
packages/semi-ui/locale/source/tr_TR.ts

@@ -36,6 +36,7 @@ const local: Locale = {
             dateTime: 'Lütfen bir tarih ve saat seçin',
             dateRange: ['Başlangıç tarihi', 'Bitiş tarihi'],
             dateTimeRange: ['Başlangıç tarihi', 'Bitiş tarihi'],
+            monthRange: ['Başlangıç ​​ayı', 'Bitiş ayı']
         },
         footer: {
             confirm: 'Tamam',

+ 1 - 0
packages/semi-ui/locale/source/vi_VN.ts

@@ -36,6 +36,7 @@ const local: Locale = {
             dateTime: 'Chọn ngày và giờ',
             dateRange: ['Ngày bắt đầu', 'Ngày kết thúc'],
             dateTimeRange: ['Ngày bắt đầu', 'Ngày kết thúc'],
+            monthRange: ['Tháng bắt đầu', 'Tháng kết thúc'],
         },
         footer: {
             confirm: 'Xác nhận',

+ 1 - 0
packages/semi-ui/locale/source/zh_CN.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: '请选择日期及时间',
             dateRange: ['开始日期', '结束日期'],
             dateTimeRange: ['开始日期', '结束日期'],
+            monthRange: ['开始月份', '结束月份'],
         },
         footer: {
             confirm: '确定',

+ 1 - 0
packages/semi-ui/locale/source/zh_TW.ts

@@ -33,6 +33,7 @@ const local: Locale = {
             dateTime: '請選擇日期及時間',
             dateRange: ['開始日期', '結束日期'],
             dateTimeRange: ['開始日期', '結束日期'],
+            monthRange: ['開始月份', '結束月份'],
         },
         footer: {
             confirm: '確定',