Browse Source

fix: [datepicker] unexpected behavior & style when needConfirm is true

1、Fixed DatePicker input value is back to confirmed value bug when needConfirm is true, #457
2、Optimize DatePicker interaction details in needConfirm mode, click outside will no longer close the panel, you need to click cancel to close the panel
3、(style) Fixed DatePicker needConfirm button margin bug in footer
4、(style) Fixed DatePicker year button direction bug when direction="rtl"
走鹃 3 years ago
parent
commit
5ca3e367b5

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

@@ -0,0 +1,62 @@
+// datePicker.spec.js created with Cypress
+//
+// Start writing your Cypress tests below!
+// If you're unfamiliar with how Cypress works,
+// check out the link below and learn how to write your first test:
+// https://on.cypress.io/writing-first-test
+
+describe('DatePicker', () => {
+    it('dateTime needConfirm cancel', () => {
+        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.get('[data-cy=1] .semi-input-wrapper').click();
+        cy.get('.semi-datepicker-footer > .semi-button-borderless').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+    });
+
+    it('dateTime needConfirm confirm', () => {
+        cy.visit('http://127.0.0.1:6009/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').click();
+        cy.get('.semi-datepicker-footer > button:nth-child(2)').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+        cy.get('[data-cy=1] .semi-input').should('have.value', '2021-12-15 10:37:13');
+    });
+
+    it('dateTime needConfirm select+cancel', () => {
+        cy.visit('http://127.0.0.1:6009/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').click();
+        cy.get('.semi-datepicker-footer > button:nth-child(1)').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+        cy.get('[data-cy=1] .semi-input').should('have.value', '2021-12-27 10:37:13');
+    });
+
+    it('dateTimeRange needConfirm cancel', () => {
+        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
+        cy.get('.semi-datepicker-footer > .semi-button-borderless').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+    });
+
+    it('dateTimeRange needConfirm select+cancel', () => {
+        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
+        cy.get('.semi-datepicker-month-grid-left .semi-datepicker-day').contains('15').click();
+        cy.get('.semi-datepicker-month-grid-right .semi-datepicker-day').contains('20').click();
+        cy.get('.semi-datepicker-footer > button:nth-child(1)').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input').should('have.value', '2021-12-27 10:37:13');
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-end .semi-input').should('have.value', '2022-01-28 10:37:13');
+    });
+
+    it('dateTimeRange needConfirm confirm', () => {
+        cy.visit('http://127.0.0.1:6009/iframe.html?id=datepicker--fix-need-confirm&args=&viewMode=story');
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input-wrapper').click();
+        cy.get('.semi-datepicker-month-grid-left .semi-datepicker-day').contains('15').click();
+        cy.get('.semi-datepicker-month-grid-right .semi-datepicker-day').contains('20').click();
+        cy.get('.semi-datepicker-footer > button:nth-child(2)').click();
+        cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-start .semi-input').should('have.value', '2021-12-15 10:37:13');
+        cy.get('[data-cy=3] .semi-datepicker-range-input-wrapper-end .semi-input').should('have.value', '2022-01-20 10:37:13');
+    });
+});

+ 11 - 0
packages/semi-foundation/datePicker/datePicker.scss

@@ -140,6 +140,17 @@ $module: #{$prefix}-datepicker;
         padding-bottom: $spacing-datepicker_footer-paddingBottom;
         text-align: right;
         background-color: $color-datepicker_footer-bg-default;
+
+        .#{$prefix}-button {
+            // cancel button
+            &:first-of-type {
+                margin-right: $spacing-datepicker_footer_cancel_button-marginRight;
+            }
+            // confirm button
+            &:nth-of-type(2) {
+                margin-right: $spacing-datepicker_footer_confirm_button-marginRight;
+            }
+        }
     }
 
     // 年月选择

+ 31 - 11
packages/semi-foundation/datePicker/foundation.ts

@@ -323,10 +323,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      *   2. set cachedSelectedValue using given dates(in needConfirm mode)
      *      - directly closePanel without click confirm will set cachedSelectedValue to state value
      *      - select one date(which means that the selection value is incomplete) and click confirm also set cachedSelectedValue to state value
-     * @param {String} inputValue
-     * @param {Date[]} dates
      */
-    rangeTypeSideEffectsWhenClosePanel(inputValue: string, dates: Date[]) {
+    rangeTypeSideEffectsWhenClosePanel(inputValue: string, willUpdateDates: Date[]) {
         if (this._isRangeType()) {
             this._adapter.setRangeInputFocus(false);
             /**
@@ -334,11 +332,29 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
              * when inputValue is null, picker value will back to last selected value
              */
             this.handleInputBlur(inputValue);
-            const { value, cachedSelectedValue } = this._adapter.getStates();
-            const newCachedSelectedValue = Array.isArray(dates) && dates.length ? dates : value;
-            if (!isEqual(newCachedSelectedValue, cachedSelectedValue)) {
-                this._adapter.updateCachedSelectedValue(newCachedSelectedValue);
-            }
+            this.resetCachedSelectedValue(willUpdateDates);
+        }
+    }
+
+    /**
+     * clear input value when selected date is not confirmed
+     */
+    needConfirmSideEffectsWhenClosePanel(willUpdateDates: Date[] | null | undefined) {
+        if (this._adapter.needConfirm() && !this._isRangeType()) {
+            /**
+             * if `null` input element will show `cachedSelectedValue` formatted value(format in DateInput render)
+             * if `` input element will show `` directly
+             */
+            this._adapter.updateInputValue(null);
+            this.resetCachedSelectedValue(willUpdateDates);
+        }
+    }
+
+    resetCachedSelectedValue(willUpdateDates?: Date[]) {
+        const { value, cachedSelectedValue } = this._adapter.getStates();
+        const newCachedSelectedValue = Array.isArray(willUpdateDates) ? willUpdateDates : value;
+        if (!isEqual(newCachedSelectedValue, cachedSelectedValue)) {
+            this._adapter.updateCachedSelectedValue(newCachedSelectedValue);
         }
     }
 
@@ -354,13 +370,16 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * @param {String} inputValue
      * @param {Date[]} dates
      */
-    closePanel(e?: any, inputValue: string = null, dates: Date[] = []) {
+    closePanel(e?: any, inputValue: string = null, dates?: Date[]) {
+        const { value, cachedSelectedValue } = this._adapter.getStates();
+        const willUpdateDates = isNullOrUndefined(dates) ? this._adapter.needConfirm() ? value : cachedSelectedValue : dates;
         if (!this._isControlledComponent('open')) {
             this._adapter.togglePanel(false);
             this._adapter.unregisterClickOutSide();
         }
         // range type picker, closing panel requires the following side effects
-        this.rangeTypeSideEffectsWhenClosePanel(inputValue, dates);
+        this.rangeTypeSideEffectsWhenClosePanel(inputValue, willUpdateDates as Date[]);
+        this.needConfirmSideEffectsWhenClosePanel(willUpdateDates as Date[]);
         this._adapter.notifyOpenChange(false);
         this._adapter.notifyBlur(e);
     }
@@ -416,7 +435,8 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         if (parsedResult && parsedResult.length) {
             this._updateValueAndInput(parsedResult, input === '');
         } else if (input === '') {
-            this._updateValueAndInput('' as any, true);
+            // if clear input, set input to `''`
+            this._updateValueAndInput('' as any, true, '');
         } else {
             this._updateValueAndInput(stateValue);
         }

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

@@ -89,6 +89,8 @@ export default class InputFoundation extends BaseFoundation<DateInputAdapter> {
     }
 
     handleRangeInputClear(e: any) {
+        // prevent trigger click outside
+        this.stopPropagation(e);
         this._adapter.notifyRangeInputClear(e);
     }
 

+ 15 - 1
packages/semi-foundation/datePicker/rtl.scss

@@ -9,6 +9,18 @@ $module: #{$prefix}-datepicker;
             padding-right: 0;
             padding-left: $spacing-datepicker_footer-paddingRight;
             text-align: left;
+
+            .#{$prefix}-button {
+                &:first-of-type {
+                    margin-left: 0;
+                    margin-right: 0;
+                }
+                // confirm button
+                &:nth-of-type(2) {
+                    margin-right: $spacing-datepicker_footer_cancel_button-marginRight;
+                    margin-left: 0;
+                }
+            }
         }
 
         &-day {
@@ -66,7 +78,9 @@ $module: #{$prefix}-datepicker;
         &-yam {
             // rtl 对箭头进行翻转
             .#{$prefix}-icon-chevron_left,
-            .#{$prefix}-icon-chevron_right {
+            .#{$prefix}-icon-chevron_right,
+            .#{$prefix}-icon-double_chevron_left,
+            .#{$prefix}-icon-double_chevron_right {
                 transform: scaleX(-1);
             }
         }

+ 2 - 0
packages/semi-foundation/datePicker/variables.scss

@@ -33,6 +33,8 @@ $spacing-datepicker_scrolllist_body-padding: 0; // 时间选择滚动菜单内
 $spacing-datepicker_footer-paddingTop: 10px; // 确认选择 footer 顶部内边距
 $spacing-datepicker_footer-paddingBottom: 10px; // 确认选择 footer 底部内边距
 $spacing-datepicker_footer-paddingRight: 8px; // 确认选择 footer 右侧内边距
+$spacing-datepicker_footer_cancel_button-marginRight: 12px; // 确认选择 footer 取消按钮右外边距
+$spacing-datepicker_footer_confirm_button-marginRight: 8px; // 确认选择 footer 确认按钮右外边距
 $spacing-datepicker_navigation-paddingY: $spacing-base-tight; // 年月切换 header 垂直内边距
 $spacing-datepicker_navigation-paddingX: $spacing-base; // 年月切换 header 水平内边距
 $spacing-datepicker_month-padding: $spacing-base; 

+ 15 - 0
packages/semi-ui/datePicker/__test__/datePicker.test.js

@@ -191,12 +191,27 @@ describe(`DatePicker`, () => {
         btns[0].click();
         await sleep();
         expect(_.first(elem.state('value')).getDate() === currentValue.getDate()).toBeTruthy();
+        expect(_.isEqual(elem.state('cachedSelectedValue'), [currentValue])).toBe(true);
 
         /**
          * click ensure button
          */
         btns[1].click();
         await sleep();
+        expect(_.first(elem.state('value')).getDate() === currentValue.getDate()).toBe(true);
+
+        /**
+         * re click next day
+         */
+        nextOffsetDayElem.click();
+        await sleep();
+        expect(_.first(elem.state('value')).getDate() === currentValue.getDate()).toBeTruthy();
+
+        /**
+         * re click ensure button
+         */
+        btns[1].click();
+        await sleep();
         expect(_.first(elem.state('value')).getDate() - currentValue.getDate()).toBe(dayOffset);
 
         demo.unmount();

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

@@ -661,4 +661,65 @@ export const FixParseISOBug = () => (
 FixParseISOBug.storyName = '修复 parseISO bug';
 FixParseISOBug.parameters = {
   chromatic: { disableSnapshot: false },
-};
+};
+
+export const FixNeedConfirm = () => {
+  const defaultDate = '2021-12-27 10:37:13';
+  const defaultDateRange = ['2021-12-27 10:37:13', '2022-01-28 10:37:13' ];
+  const props = {
+    needConfirm: true,
+    onConfirm: (...args) => {
+      console.log('Confirmed: ', ...args);
+    },
+    onChange: (...args) => {
+      console.log('Changed: ', ...args);
+    },
+    onCancel: (...args) => {
+      console.log('Canceled: ', ...args);
+    },
+  };
+
+  return (
+    <div>
+      <div data-cy="1">
+        <span>dateTime + needConfirm + defaultValue</span>
+        <div>
+          <DatePicker
+            type="dateTime"
+            defaultValue={defaultDate}
+            {...props}
+          />
+        </div>
+      </div>
+      <div data-cy="2">
+        <span>dateTime + needConfirm</span>
+        <div>
+          <DatePicker
+            type="dateTime"
+            {...props}
+          />
+        </div>
+      </div>
+      <div data-cy="3">
+        <span>dateTimeRange + needConfirm + defaultValue</span>
+        <div>
+          <DatePicker
+            type="dateTimeRange"
+            defaultValue={defaultDateRange}
+            {...props}
+          />
+        </div>
+      </div>
+      <div data-cy="4">
+        <span>dateTimeRange + needConfirm</span>
+        <div>
+          <DatePicker
+            type="dateTimeRange"
+            {...props}
+          />
+        </div>
+      </div>
+    </div>
+  )
+}
+FixNeedConfirm.storyName = '修复 needConfirm 取消后输入框显示错误';

+ 3 - 0
packages/semi-ui/datePicker/datePicker.tsx

@@ -206,6 +206,9 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
                     this.clickOutSideHandler = null;
                 }
                 this.clickOutSideHandler = e => {
+                    if (this.adapter.needConfirm()) {
+                        return;
+                    }
                     const triggerEl = this.triggerElRef && this.triggerElRef.current;
                     const panelEl = this.panelRef && this.panelRef.current;
                     const isInTrigger = triggerEl && triggerEl.contains(e.target as Node);