Browse Source

feat(datepicker): input range start and show it on panel #294 (#742)

* feat(datepicker): fixed DatePicker defaultPickerValue given number panel render error #735 (#739)

* fix(datepicker): input props error and closePanel willUpdateDates bug #294

* fix(datepicker): fix closePanel willUpdates and add test cases

Co-authored-by: shijia.me <[email protected]>
走鹃 3 years ago
parent
commit
c5d42ebbf4

+ 1 - 1
content/input/datepicker/index-en-US.md

@@ -784,7 +784,7 @@ function Demo() {
 |--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------|------------|
 | autoAdjustOverflow | Whether the floating layer automatically adjusts its direction when it is blocked                                                                                                         | boolean                                          | true    | **0.34.0** |
 | autoFocus          | Automatic access to focus                                                                                                                                                                 | boolean                                          | false   | **1.10.0** |
-| autoSwitchDate     | When false is passed in, the date will not be automatically switched when the year and year are changed through the left and right buttons on the top of the panel and the drop-down menu | boolean                                          | true    | **1.13.0** |
+| autoSwitchDate     | When the year and month are changed through the left and right buttons and the drop-down menu at the top of the panel, the date is automatically switched. Only valid for `date` type. | boolean                                          | true    | **1.13.0** |
 | bottomSlot         | Render the bottom extra area                                                                                                                                                              | ReactNode                                        |         | **1.22.0** |
 | className          | Class name                                                                                                                                                                                | string                                           | -       |            |
 | defaultOpen        | Panel displays or hides by default                                                                                                                                                        | boolean                                          | false   |            |

+ 1 - 1
content/input/datepicker/index.md

@@ -746,7 +746,7 @@ function Demo() {
 | --- | --- | --- | --- | --- |
 | autoAdjustOverflow | 浮层被遮挡时是否自动调整方向 | boolean | true | **0.34.0** |
 | autoFocus | 自动获取焦点 | boolean | false | **1.10.0** |
-| autoSwitchDate | 传入 false 时,通过面板上方左右按钮、下拉菜单更改年月时,不会自动切换日期 | boolean | true | **1.13.0** |
+| autoSwitchDate | 通过面板上方左右按钮、下拉菜单更改年月时,自动切换日期。仅对 date type 生效。 | boolean | true | **1.13.0** |
 | bottomSlot | 渲染底部额外区域 | ReactNode |  | **1.22.0** |
 | className | 类名 | string | - |  |
 | defaultOpen | 面板默认显示或隐藏 | boolean | false |  |

+ 271 - 1
cypress/integration/datePicker.spec.js

@@ -35,7 +35,7 @@ describe('DatePicker', () => {
         cy.get('[data-cy=1] .semi-input').should('have.value', '2021-12-15 10:37:13');
     });
 
-    it('dateTime needConfirm select+cancel', () => {
+    it('dateTime needConfirm select + cancel', () => {
         cy.visit('http://localhost:6006/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
         cy.get('[data-cy=1] .semi-input-wrapper').click();
         cy.get('.semi-datepicker-day').contains('15')
@@ -302,4 +302,274 @@ describe('DatePicker', () => {
         cy.get('[data-cy=dateRange] .semi-datepicker-range-input-wrapper-start .semi-input').should('have.value', '2022-07-10');
         cy.get('[data-cy=dateRange] .semi-datepicker-range-input-wrapper-end .semi-input').should('have.value', '2022-10-11');
     });
+
+    // 输入完整日期,面板需要同步变化
+    it('input a valid date + change panel selected date', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('[data-cy=date] .semi-input').first().type('2021-03-15');
+        cy.get('[x-type=date] .semi-datepicker-navigation-month').contains("2021年 3月");
+        cy.get('[x-type=date] .semi-datepicker-day-selected').contains("15");
+    });
+
+    // 输入结束日期,面板需要同步变化
+    it('input start + change panel selected date', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).type('2021-03-15');
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').contains("2021年 3月");
+        cy.get('[x-type=dateRange] .semi-datepicker-day-selected-end').contains("15");
+    });
+
+    // 输入开始和结束日期后,通过滚轮修改年月,面板和输入框需要同时发生变化
+    // 暂时去掉了这个需求
+    it.skip('input start + end then change year or month from scroll list', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        // input date
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('[data-cy=dateRange] .semi-input').first().type('2021-03-15');
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').first().contains("2021年 3月");
+        cy.get('[x-type=dateRange] .semi-datepicker-day-selected-start').contains("15");
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).type('2021-05-15');
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').last().contains("2021年 5月");
+        cy.get('[x-type=dateRange] .semi-datepicker-day-selected-end').contains("15");
+        // click right scroll list
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').last().click();
+        cy.get('.semi-scrolllist-list-outer li').contains('2022').click();
+        cy.get('.semi-datepicker-yam .semi-datepicker-yearmonth-header .semi-button').click();
+        cy.get('[data-cy=dateRange] .semi-input').last().should('have.value', '2022-05-15');
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').last().contains("2022年 5月");
+        // click left scroll list
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').first().click();
+        cy.get('.semi-scrolllist-list-outer li').contains('2022').click();
+        cy.get('.semi-datepicker-yam .semi-datepicker-yearmonth-header .semi-button').click();
+        cy.get('[data-cy=dateRange] .semi-input').first().should('have.value', '2022-03-15');
+        cy.get('[x-type=dateRange] .semi-datepicker-navigation-month').first().contains("2022年 3月");
+    });
+
+    // 输入开始和结束日期后,输入一个不合法的日期,输入框恢复到上次合法日期
+    it('input invalid in date type + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('[data-cy=date] .semi-input').first().type('2021-03-15');
+        cy.get('[data-cy=date] .semi-input').first().type('123');
+        // 失焦
+        cy.get('[data-cy=container').click({ force: true });
+        // 恢复到上一次选中时间
+        cy.get('[data-cy=date] .semi-input').first().should('have.value', '2021-03-15');
+    });
+
+    // 输入开始和结束日期后,输入一个不合法的日期,输入框恢复到上次合法日期
+    it('input invalid in dateRange + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        // input date
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('[data-cy=dateRange] .semi-input').first().type('2021-03-15');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).type('2021-05-15');
+        // 输入不合法日期
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('[data-cy=dateRange] .semi-input').first().type('abc');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).type('abc');
+        cy.get('[data-cy=container').click({ force: true });
+        cy.get('[data-cy=dateRange] .semi-input').first().should('have.value', '2021-03-15');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).should('have.value', '2021-05-15');
+    });
+
+    // 输入开始和结束日期后,输入一个不合法的日期,输入框恢复到上次合法日期
+    it('input invalid in dateTime + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        // input date
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().type('2021-03-15 00:00:00');
+        // 输入不合法日期
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().type('abc');
+        cy.get('[data-cy=container').click({ force: true });
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '2021-03-15 00:00:00');
+    });
+
+    it('input only invalid in date type + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('[data-cy=date] .semi-input').first().type('abc');
+        cy.get('[data-cy=container').click({ force: true });
+        cy.get('[data-cy=date] .semi-input').first().should('have.value', '');
+    });
+
+    it('input only invalid in dateRange + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('[data-cy=dateRange] .semi-input').first().type('abc');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).type('123');
+        cy.get('[data-cy=container').click({ force: true });
+        cy.get('[data-cy=dateRange] .semi-input').first().should('have.value', '');
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).should('have.value', '');
+    });
+
+    it('input only invalid in dateTime + blur', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format&args=&viewMode=story');
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().type('123');
+        cy.get('[data-cy=container').click({ force: true });
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '');
+    });
+
+    // 输入禁用日期,面板不需要同步变化
+    it('input a disabled date + change panel selected date', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-disabled&args=&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('[data-cy=date] .semi-input').first().type('2021-03-15');
+        cy.get('[x-type=date] .semi-datepicker-day-selected').should('not.exist');
+    });
+
+    it('auto fill time + dateTime', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--auto-fill-time&viewMode=story');
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[x-type=dateTime] .semi-input').first().type('2021-03-15');
+        cy.get('[x-type=dateTime] .semi-input').eq(1).should('have.value', '14:00');
+    });
+
+    it('auto fill time invalid + dateTime', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--auto-fill-time&viewMode=story');
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[x-type=dateTime] .semi-input').first().type('2021-03-');
+        cy.get('[x-type=dateTime] .semi-input').eq(1).should('have.value', '');
+    });
+
+    it('auto fill time + dateTimeRange', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--auto-fill-time&viewMode=story');
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click();
+        cy.get('[x-type=dateTimeRange] .semi-input').first().type('2021-01-01');
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(1).should('have.value', '00:01');
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(2).type('2021-03-01');
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(3).should('have.value', '23:59');
+    });
+
+    it('input date + needConfirm + cancel', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().type('2021-03-15 14:00');
+        cy.get('.semi-datepicker-footer .semi-button').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '');
+    });
+
+    it('input date + needConfirm + confirm', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime] .semi-input').first().type('2021-03-15 14:00');
+        cy.get('.semi-datepicker-footer .semi-button').eq(1).click();
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '2021-03-15 14:00');
+    });
+
+    it('input date range + needConfirm + cancel', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click().type('2021-03-15 14:00');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).click().type('2021-03-20 23:59');
+        cy.get('.semi-datepicker-footer .semi-button').eq(0).click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(0).should('have.value', '');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).should('have.value', '');
+    });
+
+    it('input date range + needConfirm + confirm', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click().type('2021-03-15 14:00');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).click().type('2021-03-20 23:59');
+        cy.get('.semi-datepicker-footer .semi-button').eq(1).click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(0).should('have.value', '2021-03-15 14:00');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).should('have.value', '2021-03-20 23:59');
+    });
+
+    it('input date + needConfirm + cancel', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=inset-switch]').click();
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[x-type=dateTime] .semi-input').eq(0).type('2021-03-15');
+        cy.get('.semi-datepicker-footer .semi-button').eq(0).click();
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '');
+    });
+
+    it('input date + needConfirm + confirm', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=inset-switch]').click();
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[x-type=dateTime] .semi-input').eq(0).type('2021-03-15');
+        cy.get('.semi-datepicker-footer .semi-button').eq(1).click();
+        cy.get('[data-cy=dateTime] .semi-input').first().should('have.value', '2021-03-15 14:00');
+    });
+
+    it('input date range + needConfirm + cancel', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=inset-switch]').click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click();
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(0).type('2021-03-15');
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(2).type('2021-03-20');
+        cy.get('.semi-datepicker-footer .semi-button').eq(0).click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(0).should('have.value', '');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).should('have.value', '');
+    });
+
+    it('input date range + needConfirm + confirm', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--input-format-confirm&args=&viewMode=story');
+        cy.get('[data-cy=inset-switch]').click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click();
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(0).type('2021-03-15');
+        cy.get('[x-type=dateTimeRange] .semi-input').eq(2).type('2021-03-20');
+        cy.get('.semi-datepicker-footer .semi-button').eq(1).click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(0).should('have.value', '2021-03-15 00:01');
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(1).should('have.value', '2021-03-20 23:59');
+    });
+
+    it('cashedSelectedValue return to last selected when needConfirm & input invalid', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--need-confirm-delete&args=&viewMode=story');
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').eq(0).clear().type('2021-0');
+        cy.get('.semi-datepicker-footer .semi-button').eq(0).click();
+        cy.get('[data-cy=dateTimeRange] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected-start').contains('8');
+        cy.get('.semi-popover .semi-datepicker-day-selected-end').contains('9');
+    });
+
+    it('cashedSelectedValue after selected date', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--cashed-selected-value&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('.semi-datepicker-day').contains("5").click();
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected').contains('5');
+
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('.semi-datepicker-day').contains("5").click();
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected').contains('5');
+
+        cy.get('[data-cy=dateRange] .semi-input').eq(0).click();
+        cy.get('.semi-datepicker-day').contains("5").click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(1).click();
+        cy.get('.semi-datepicker-day').contains("20").click();
+        cy.get('[data-cy=dateRange] .semi-input').eq(0).click();
+        cy.get('.semi-popover .semi-datepicker-day-selected-start').contains('5');
+        cy.get('.semi-popover .semi-datepicker-day-selected-end').contains('20');
+    });
+
+    it('cashedSelectedValue after click outside', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=datepicker--cashed-selected-value&viewMode=story');
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('[data-cy=date]').click({ force: true });
+        cy.get('[data-cy=date] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected').contains('8');
+
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('[data-cy=dateTime]').click({ force: true });
+        cy.get('[data-cy=dateTime] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected').contains('8');
+
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('[data-cy=dateRange]').click({ force: true });
+        cy.get('[data-cy=dateRange] .semi-input').first().click();
+        cy.get('.semi-popover .semi-datepicker-day-selected-start').contains('8');
+        cy.get('.semi-popover .semi-datepicker-day-selected-end').contains('9');
+    });
 });

+ 21 - 0
packages/semi-foundation/datePicker/_utils/parser.ts

@@ -32,3 +32,24 @@ export function compatibleParse(
     }
     return result;
 }
+
+
+/**
+ * whether value can be parsed with date-fns `parse`
+ * 
+ * @example
+ * isValueParseValid({ value: '2021-01-01', formatToken: 'yyyy-MM-dd' }); // true
+ * isValueParseValid({ value: '2021-01-0', formatToken: 'yyyy-MM-dd' }); // false
+ * isValueParseValid({ value: '2021-01', formatToken: 'yyyy-MM-dd' }); // false
+ */
+export function isValueParseValid(options: {
+    value: string;
+    formatToken: string;
+    baseDate?: Date;
+    locale?: Locale;
+}) {
+    const { value, locale, formatToken } = options;
+    const baseDate = options.baseDate || new Date();
+    const result = parse(value, formatToken, baseDate, { locale });
+    return isValid(result);
+}

+ 144 - 20
packages/semi-foundation/datePicker/foundation.ts

@@ -244,10 +244,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         this._adapter.updatePrevTimezone(prevTimeZone);
         this._adapter.updateInputValue(null);
         this._adapter.updateValue(result);
-
-        if (this._adapter.needConfirm()) {
-            this._adapter.updateCachedSelectedValue(result);
-        }
+        this.resetCachedSelectedValue(result);
     }
 
     parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number) {
@@ -370,6 +367,9 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         }
     }
 
+    /**
+     * call it when change state value or input value
+     */
     resetCachedSelectedValue(willUpdateDates?: Date[]) {
         const { value, cachedSelectedValue } = this._adapter.getStates();
         const newCachedSelectedValue = Array.isArray(willUpdateDates) ? willUpdateDates : value;
@@ -391,8 +391,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * @param {Date[]} dates
      */
     closePanel(e?: any, inputValue: string = null, dates?: Date[]) {
-        const { value, cachedSelectedValue } = this._adapter.getStates();
-        const willUpdateDates = isNullOrUndefined(dates) ? this._adapter.needConfirm() ? value : cachedSelectedValue : dates;
+        const { value } = this._adapter.getStates();
+        const willUpdateDates = isNullOrUndefined(dates) ? value : dates;
         if (!this._isControlledComponent('open')) {
             this._adapter.togglePanel(false);
             this._adapter.unregisterClickOutSide();
@@ -426,6 +426,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
     handleInputChange(input: string, e: any) {
         const result = this._isMultiple() ? this.parseMultipleInput(input) : this.parseInput(input);
         const { value: stateValue } = this.getStates();
+        this._updateCachedSelectedValueFromInput(input);
         // Enter a valid date or empty
         if ((result && result.length) || input === '') {
             // If you click the clear button
@@ -437,9 +438,6 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
             // Updates the selected value when entering a valid date
             const changedDates = this._getChangedDates(result);
             if (!this._someDateDisabled(changedDates)) {
-                if (this._adapter.needConfirm()) {
-                    this._adapter.updateCachedSelectedValue(result);
-                }
                 if (!isEqual(result, stateValue)) {
                     this._notifyChange(result);
                 }
@@ -460,15 +458,13 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         const _isMultiple = this._isMultiple();
         const result = _isMultiple ? this.parseMultipleInput(insetInputStr, format) : this.parseInput(insetInputStr, format);
         const { value: stateValue } = this.getStates();
+        this._updateCachedSelectedValueFromInput(insetInputStr);
 
         if ((result && result.length)) {
             const changedDates = this._getChangedDates(result);
             if (!this._someDateDisabled(changedDates)) {
-                if (this._adapter.needConfirm()) {
-                    this._adapter.updateCachedSelectedValue(result);
-                }
                 if (!isEqual(result, stateValue)) {
-                    if (!this._isControlledComponent()) {
+                    if (!this._isControlledComponent() && !this._adapter.needConfirm()) {
                         this._adapter.updateValue(result);
                     }
                     this._notifyChange(result);
@@ -480,6 +476,17 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         this._adapter.updateInsetInputValue(insetInputValue);
     }
 
+    /**
+     * when input change we reset cached selected value
+     */
+    _updateCachedSelectedValueFromInput(input: string) {
+        const looseResult = this.getLooseDateFromInput(input);
+        const changedLooseResult = this._getChangedDates(looseResult);
+        if (!this._someDateDisabled(changedLooseResult)) {
+            this.resetCachedSelectedValue(looseResult);
+        }
+    }
+
     /**
      * Input box blur
      * @param {String} input
@@ -504,6 +511,19 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         } else {
             this._updateValueAndInput(stateValue);
         }
+
+        /**
+         * 当不是范围类型且不需要确认时,使用 stateValue 重置 cachedSelectedValue
+         * 这样做的目的是,在输入非法值时,使用上次选中的值作为已选值
+         * needConfirm 或者 range type 时,我们在 close panel 时调用 resetCachedSelectedValue,这里不用重复调用
+         * 
+         * Use stateValue to reset cachedSelectedValue when it is not a range type and does not require confirmation
+         * The purpose of this is to use the last selected value as the selected value when an invalid value is entered
+         * When needConfirm or range type, we call resetCachedSelectedValue when close panel, no need to call repeatedly here
+         */
+        if (!this._adapter.needConfirm() && !this._isRangeType()) {
+            this.resetCachedSelectedValue(stateValue);
+        }
     }
 
     /**
@@ -551,9 +571,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         const inputValue = '';
         if (!this._isControlledComponent('value')) {
             this._updateValueAndInput(value, true, inputValue);
-            if (this._adapter.needConfirm()) {
-                this._adapter.updateCachedSelectedValue(value);
-            }
+            this.resetCachedSelectedValue(value);
         }
         this._notifyChange(value);
         this._adapter.notifyClear(e);
@@ -646,6 +664,115 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         return result;
     }
 
+    /**
+     * get date which may include null from input
+     */
+    getLooseDateFromInput(input: string): Array<Date | null> {
+        const value = this._isMultiple() ? this.parseMultipleInputLoose(input) : this.parseInputLoose(input);
+        return value;
+    }
+
+    /**
+     * parse input into `Array<Date|null>`, loose means return value includes `null`
+     * 
+     * @example
+     * ```javascript
+     * parseInputLoose('2022-03-15 ~ '); // [Date, null]
+     * parseInputLoose(' ~ 2022-03-15 '); // [null, Date]
+     * parseInputLoose(''); // []
+     * parseInputLoose('2022-03- ~ 2022-0'); // [null, null]
+     * ```
+     */
+    parseInputLoose(input = ''): Array<Date | null> {
+        let result: Array<Date | null> = [];
+        const { dateFnsLocale, rangeSeparator, type, format } = this.getProps();
+
+        if (input && input.length) {
+            const formatToken = format || getDefaultFormatTokenByType(type);
+            let parsedResult, formatedInput;
+            const nowDate = new Date();
+            switch (type) {
+                case 'date':
+                case 'dateTime':
+                case 'month':
+                    const _parsedResult = compatibleParse(input, formatToken, nowDate, dateFnsLocale);
+                    if (isValidDate(_parsedResult)) {
+                        formatedInput = this.localeFormat(_parsedResult as Date, formatToken);
+                        if (formatedInput === input) {
+                            parsedResult = _parsedResult;
+                        }
+                    } else {
+                        parsedResult = null;
+                    }
+                    result = [parsedResult];
+                    break;
+                case 'dateRange':
+                case 'dateTimeRange':
+                    const separator = rangeSeparator;
+                    const values = input.split(separator);
+                    parsedResult =
+                        values &&
+                        values.reduce((arr, cur) => {
+                            let parsedVal = null;
+                            const _parsedResult = compatibleParse(cur, formatToken, nowDate, dateFnsLocale);
+                            if (isValidDate(_parsedResult)) {
+                                formatedInput = this.localeFormat(_parsedResult as Date, formatToken);
+                                if (formatedInput === cur) {
+                                    parsedVal = _parsedResult;
+                                }
+                            }
+                            arr.push(parsedVal);
+                            return arr;
+                        }, []);
+                    if (Array.isArray(parsedResult) && parsedResult.every(item => isValid(item))) {
+                        parsedResult.sort((d1, d2) => d1.getTime() - d2.getTime());
+                    }
+                    result = parsedResult;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * parse multiple into `Array<Date|null>`, loose means return value includes `null`
+     * 
+     * @example
+     * ```javascript
+     * parseMultipleInputLoose('2021-01-01,2021-10-15'); // [Date, Date];
+     * parseMultipleInputLoose('2021-01-01,2021-10-'); // [Date, null];
+     * parseMultipleInputLoose(''); // [];
+     * ```
+     */
+    parseMultipleInputLoose(input = '', separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, needDedupe = false) {
+        const max = this.getProp('max');
+        const inputArr = input.split(separator);
+        const result: Date[] = [];
+
+        for (const curInput of inputArr) {
+            let tmpParsed = curInput && this.parseInputLoose(curInput);
+            tmpParsed = Array.isArray(tmpParsed) ? tmpParsed : tmpParsed && [tmpParsed];
+            if (tmpParsed && tmpParsed.length) {
+                if (needDedupe) {
+                    !result.filter(r => Boolean(tmpParsed.find(tp => isSameSecond(r, tp)))) && result.push(...tmpParsed);
+                } else {
+                    result.push(...tmpParsed);
+                }
+            } else {
+                return [];
+            }
+
+            if (max && max > 0 && result.length > max) {
+                return [];
+            }
+        }
+
+        return result;
+    }
+
     /**
      * Parses the input when multiple is true, if valid,
      *  returns a list of time objects, otherwise returns an array
@@ -799,15 +926,12 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
          */
         const needCheckFocusRecord = get(options, 'needCheckFocusRecord', true);
 
-        if (this._adapter.needConfirm()) {
-            this._adapter.updateCachedSelectedValue(value);
-        }
-
         const dates = Array.isArray(value) ? [...value] : value ? [value] : [];
         const changedDates = this._getChangedDates(dates);
 
         let inputValue, insetInputValue;
         if (!this._someDateDisabled(changedDates)) {
+            this.resetCachedSelectedValue(dates);
             inputValue = this._isMultiple() ? this.formatMultipleDates(dates) : this.formatDates(dates);
             if (insetInput) {
                 const insetInputFormatToken = getInsetInputFormatToken({ format, type });

+ 49 - 3
packages/semi-foundation/datePicker/inputFoundation.ts

@@ -1,13 +1,17 @@
 /* eslint-disable max-len */
-import { cloneDeep, isObject, set } from 'lodash';
+import { cloneDeep, isObject, set, get } from 'lodash';
+import { format as formatFn } from 'date-fns';
 
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
-import { BaseValueType, ValidateStatus } from './foundation';
+import { BaseValueType, ValidateStatus, ValueType } from './foundation';
 import { formatDateValues } from './_utils/formatter';
 import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken';
 import getInsetInputFormatToken from './_utils/getInsetInputFormatToken';
 import getInsetInputValueFromInsetInputStr from './_utils/getInsetInputValueFromInsetInputStr';
 import { strings } from './constants';
+import getDefaultPickerDate from './_utils/getDefaultPickerDate';
+import { compatibleParse } from '@douyinfe/semi-foundation/datePicker/_utils/parser';
+import { isValidDate } from './_utils';
 
 const KEY_CODE_ENTER = 'Enter';
 const KEY_CODE_TAB = 'Tab';
@@ -50,6 +54,7 @@ export interface DateInputFoundationProps extends DateInputElementProps, DateInp
     insetInput?: boolean;
     insetInputValue?: InsetInputValue;
     density?: typeof strings.DENSITY_SET[number];
+    defaultPickerValue?: ValueType;
 }
 
 export interface InsetInputValue {
@@ -175,11 +180,52 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
         const { value, valuePath, insetInputValue } = options;
         const { format, type } = this._adapter.getProps();
         const insetFormatToken = getInsetInputFormatToken({ type, format });
-        const newInsetInputValue = set(cloneDeep(insetInputValue), valuePath, value);
+        let newInsetInputValue = set(cloneDeep(insetInputValue), valuePath, value);
+        newInsetInputValue = this._autoFillTimeToInsetInputValue({ insetInputValue: newInsetInputValue, valuePath, format: insetFormatToken });
         const newInputValue = this.concatInsetInputValue({ insetInputValue: newInsetInputValue });
         this._adapter.notifyInsetInputChange({ insetInputValue: newInsetInputValue, format: insetFormatToken, insetInputStr: newInputValue });
     }
 
+    _autoFillTimeToInsetInputValue(options: { insetInputValue: InsetInputValue; format: string; valuePath: string;}) {
+        const { valuePath, insetInputValue, format } = options;
+        const { type, defaultPickerValue, dateFnsLocale } = this._adapter.getProps();
+        const insetInputValueWithTime = cloneDeep(insetInputValue);
+        const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue, format, dateFnsLocale  });
+
+        if (type.includes('Time')) {
+            let timeStr = '';
+            const dateFormatToken = get(format.split(' '), '0', strings.FORMAT_FULL_DATE);
+            const timeFormatToken = get(format.split(' '), '1', strings.FORMAT_TIME_PICKER);
+            
+            switch (valuePath) {
+                case 'monthLeft.dateInput':
+                    const dateLeftStr = insetInputValueWithTime.monthLeft.dateInput;
+                    if (!insetInputValueWithTime.monthLeft.timeInput && dateLeftStr.length === dateFormatToken.length) {
+                        const dateLeftParsed = compatibleParse(insetInputValueWithTime.monthLeft.dateInput, dateFormatToken);
+                        if (isValidDate(dateLeftParsed)) {
+                            timeStr = formatFn(nowDate, timeFormatToken);
+                            insetInputValueWithTime.monthLeft.timeInput = timeStr;
+                        }
+                    }
+                    break;
+                case 'monthRight.dateInput':
+                    const dateRightStr = insetInputValueWithTime.monthRight.dateInput;
+                    if (!insetInputValueWithTime.monthRight.timeInput && dateRightStr.length === dateFormatToken.length) {
+                        const dateRightParsed = compatibleParse(dateRightStr, dateFormatToken);
+                        if (isValidDate(dateRightParsed)) {
+                            timeStr = formatFn(nextDate, timeFormatToken);
+                            insetInputValueWithTime.monthRight.timeInput = timeStr;
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return insetInputValueWithTime;
+    }
+
     /**
      * 只有传入的 format 符合 formatReg 时,才会使用用户传入的 format
      * 否则会使用默认的 format 作为 placeholder

+ 2 - 2
packages/semi-foundation/datePicker/monthsGridFoundation.ts

@@ -223,10 +223,10 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         this._adapter.updateMonthOnLeft(newMonthLeft);
         const newSelected = new Set<string>();
         if (!this._isMultiple()) {
-            newSelected.add(format(values[0] as Date, strings.FORMAT_FULL_DATE));
+            values[0] && newSelected.add(format(values[0] as Date, strings.FORMAT_FULL_DATE));
         } else {
             values.forEach(date => {
-                newSelected.add(format(date as Date, strings.FORMAT_FULL_DATE));
+                date && newSelected.add(format(date as Date, strings.FORMAT_FULL_DATE));
             });
         }
         if (refreshPicker) {

+ 48 - 1
packages/semi-ui/datePicker/_story/datePicker.stories.js

@@ -849,5 +849,52 @@ export const A11yKeyboardDemo = () => {
   );
 };
 
-A11yKeyboardDemo.storyName = "a11y keyboard demo"
+A11yKeyboardDemo.storyName = "a11y keyboard demo";
 
+/**
+ * test with cypress
+ */
+export const NeedConfirmDelete = () => {
+  return (
+    <div data-cy="dateTimeRange">
+      <DatePicker
+        value={[new Date('2022-08-08 00:00'), new Date('2022-08-09 12:00')]}
+        type="dateTimeRange"
+        needConfirm
+      />
+    </div>
+  );
+};
+NeedConfirmDelete.storyName = "cashedSelectedValue return to last selected when needConfirm & input invalid";
+
+/**
+ * test with cypress
+ */
+ export const CashedSelectedValue = () => {
+  return (
+    <Space>
+      <div data-cy="date">
+        <DatePicker
+          defaultValue={new Date('2022-08-08')}
+          type="date"
+          motion={false}
+        />
+      </div>
+      <div data-cy="dateTime">
+        <DatePicker
+          defaultValue={new Date('2022-08-08 19:11:00')}
+          type="dateTime"
+          motion={false}
+        />
+      </div>
+      <div data-cy="dateRange">
+        <DatePicker
+          defaultValue={[new Date('2022-08-08'), new Date('2022-08-09')]}
+          type="dateRange"
+          motion={false}
+        />
+      </div>
+    </Space>
+  );
+};
+CashedSelectedValue.storyName = "cashedSelectedValue";

+ 37 - 0
packages/semi-ui/datePicker/_story/v2/AutoFillTime.jsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import { DatePicker, Space, Button } from '../../../index';
+
+AutoFillTime.storyName = '自动填充时间';
+
+/**
+ * 输入开始日期后,自动填充一个时间
+ */
+export default function AutoFillTime() {
+    const format = 'yyyy-MM-dd HH:mm';
+    const defaultPickerValue = '2021-03-15 14:00';
+    const defaultPickerValue2 = ['2021-01-10 00:01', '2021-03-15 23:59'];
+    
+    const handleChange = (...args) => {
+        console.log('change', ...args);
+    };
+
+    const props = {
+        format,
+        insetInput: true,
+        onChange: handleChange,
+        motion: false,
+    };
+
+    return (
+        <div data-cy="container">
+            <Space>
+                <div data-cy="dateTime">
+                    <DatePicker {...props} type="dateTime" defaultPickerValue={defaultPickerValue} />
+                </div>
+                <div data-cy="dateTimeRange">
+                    <DatePicker {...props} type="dateTimeRange" defaultPickerValue={defaultPickerValue2} />
+                </div>
+            </Space>
+        </div>
+    );
+}

+ 29 - 0
packages/semi-ui/datePicker/_story/v2/InputFormat.jsx

@@ -0,0 +1,29 @@
+import React from 'react';
+import { DatePicker, Space, Button } from '../../../index';
+
+InputFormat.storyName = '输入部分日期,回显在面板上';
+
+/**
+ * 优化 input format
+ */
+export default function InputFormat() {
+    const handleChange = (...args) => {
+        console.log('change', ...args);
+    };
+
+    return (
+        <div data-cy="container">
+            <Space>
+                <div data-cy="date">
+                    <DatePicker onChange={handleChange} />
+                </div>
+                <div data-cy="dateRange">
+                    <DatePicker onChange={handleChange} type="dateRange" />
+                </div>
+                <div data-cy="dateTime">
+                    <DatePicker onChange={handleChange} type="dateTime" />
+                </div>
+            </Space>
+        </div>
+    );
+}

+ 44 - 0
packages/semi-ui/datePicker/_story/v2/InputFormatConfirm.jsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import { DatePicker, Space, Button } from '../../../index';
+
+InputFormatConfirm.storyName = '输入时间 + needConfirm';
+
+export default function InputFormatConfirm() {
+    const [insetInput, setInputInput] = React.useState(false);
+    const format = 'yyyy-MM-dd HH:mm';
+    const defaultPickerValue = '2021-03-15 14:00';
+    const defaultPickerValue2 = ['2021-01-10 00:01', '2021-03-15 23:59'];
+    
+    const handleChange = (...args) => {
+        console.log('change', ...args);
+    };
+        
+    const handleConfirm = (...args) => {
+        console.log('confirm', ...args);
+    };
+
+    const props = {
+        format,
+        onChange: handleChange,
+        onConfirm: handleConfirm,
+        motion: false,
+        needConfirm: true,
+        insetInput
+    };
+
+    return (
+        <div data-cy="container">
+            <Space>
+                <Button data-cy="inset-switch" onClick={() => setInputInput(!insetInput)}>{`insetInput=${insetInput}`}</Button>
+                <Space>
+                    <div data-cy="dateTime">
+                        <DatePicker {...props} type="dateTime" defaultPickerValue={defaultPickerValue} />
+                    </div>
+                    <div data-cy="dateTimeRange">
+                        <DatePicker {...props} type="dateTimeRange" defaultPickerValue={defaultPickerValue2} />
+                    </div>
+                </Space>
+            </Space>
+        </div>
+    );
+}

+ 27 - 0
packages/semi-ui/datePicker/_story/v2/InputFormatDisabled.jsx

@@ -0,0 +1,27 @@
+import React from 'react';
+import { DatePicker, Space, Button } from '../../../index';
+
+InputFormatDisabled.storyName = '输入禁用日期,不回显在面板上';
+
+/**
+ * 优化 input format
+ */
+export default function InputFormatDisabled() {
+    const handleChange = (...args) => {
+        console.log('change', ...args);
+    };
+
+    const disabledDate = (date) => {
+        return date.getDate() === 15;
+    };
+
+    return (
+        <div data-cy="container">
+            <Space>
+                <div data-cy="date">
+                    <DatePicker disabledDate={disabledDate} onChange={handleChange} />
+                </div>
+            </Space>
+        </div>
+    );
+}

+ 4 - 0
packages/semi-ui/datePicker/_story/v2/index.js

@@ -4,3 +4,7 @@ export { default as FixInputRangeFocus } from './FixInputRangeFocus';
 export { default as InsetInput  } from './InsetInput';
 export { default as InsetInputE2E  } from './InsetInputE2E';
 export { default as FixDefaultPickerValue } from './FixDefaultPickerValue';
+export { default as InputFormat  } from './InputFormat';
+export { default as InputFormatDisabled  } from './InputFormatDisabled';
+export { default as AutoFillTime  } from './AutoFillTime';
+export { default as InputFormatConfirm  } from './InputFormatConfirm';

+ 7 - 0
packages/semi-ui/datePicker/dateInput.tsx

@@ -64,6 +64,12 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
         rangeSeparator: PropTypes.string,
         insetInput: PropTypes.bool,
         insetInputValue: PropTypes.object,
+        defaultPickerValue: PropTypes.oneOfType([
+            PropTypes.string,
+            PropTypes.number,
+            PropTypes.object,
+            PropTypes.array,
+        ]),
     };
 
     static defaultProps = {
@@ -392,6 +398,7 @@ export default class DateInput extends BaseComponent<DateInputProps, {}> {
             rangeSeparator,
             insetInput,
             insetInputValue,
+            defaultPickerValue,
             ...rest
         } = this.props;
         const dateIcon = <IconCalendar aria-hidden />;

+ 7 - 11
packages/semi-ui/datePicker/datePicker.tsx

@@ -184,7 +184,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             isRange: false,
             inputValue: null, // Staging input values
             value: [], // The currently selected date, each date is a Date object
-            cachedSelectedValue: null, // Save last selected date
+            cachedSelectedValue: null, // Save last selected date, maybe include null
             prevTimeZone: null,
             motionEnd: false, // Monitor if popover animation ends
             rangeInputFocus: undefined, // Optional'rangeStart ',' rangeEnd ', false
@@ -415,16 +415,9 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             triggerRender,
             insetInput
         } = this.props;
-        const { value, cachedSelectedValue, motionEnd, rangeInputFocus } = this.state;
-
-        // const cachedSelectedValue = this.adapter.getCache('cachedSelectedValue');
-
-        let defaultValue = value;
-
-        if (this.adapter.needConfirm()) {
-            defaultValue = cachedSelectedValue;
-        }
+        const { cachedSelectedValue, motionEnd, rangeInputFocus } = this.state;
 
+        const defaultValue = cachedSelectedValue;
         return (
             <MonthsGrid
                 ref={this.monthGrid}
@@ -535,6 +528,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             inputReadOnly,
             rangeSeparator,
             insetInput,
+            defaultPickerValue
         } = this.props;
         const { value, inputValue, rangeInputFocus, triggerDisabled } = this.state;
         // This class is not needed when triggerRender is function
@@ -555,6 +549,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             disabled: inputDisabled,
             inputValue,
             value: value as Date[],
+            defaultPickerValue,
             onChange: this.handleInputChange,
             onEnterPress: this.handleInputComplete,
             // TODO: remove in next major version
@@ -629,7 +624,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
     };
 
     renderPanel = (locale: Locale['DatePicker'], localeCode: string, dateFnsLocale: Locale['dateFnsLocale']) => {
-        const { dropdownClassName, dropdownStyle, density, topSlot, bottomSlot, insetInput, type, format, rangeSeparator } = this.props;
+        const { dropdownClassName, dropdownStyle, density, topSlot, bottomSlot, insetInput, type, format, rangeSeparator, defaultPickerValue } = this.props;
         const { insetInputValue, value } = this.state;
         const wrapCls = classnames(
             cssClasses.PREFIX,
@@ -653,6 +648,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             rangeInputStartRef: this.rangeInputStartRef,
             rangeInputEndRef: this.rangeInputEndRef,
             density,
+            defaultPickerValue
         };
 
         return (

+ 2 - 1
packages/semi-ui/datePicker/monthsGrid.tsx

@@ -168,7 +168,8 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
     componentDidUpdate(prevProps: MonthsGridProps, prevState: MonthsGridState) {
         const { defaultValue, defaultPickerValue, motionEnd } = this.props;
         if (prevProps.defaultValue !== defaultValue) {
-            this.foundation.updateSelectedFromProps(defaultValue, false);
+            // we should always update panel state when value changes
+            this.foundation.updateSelectedFromProps(defaultValue);
         }
 
         if (prevProps.defaultPickerValue !== defaultPickerValue) {