1
0
Эх сурвалжийг харах

feat(datepicker): add prev/next year buttons (#361)

* feat(icon): add double_chevron_left,double_chevron_right icons #260

* feat(datepicker): add year button #260
走鹃 3 жил өмнө
parent
commit
f1f0753975

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

@@ -143,6 +143,8 @@ version:>= 1.28.0
 
 In the scenario of range selection, turning on `syncSwitchMonth` means to switch the two panels simultaneously. The default is false.
 
+> Note: Clicking the year button will also switch the two panels synchronously. Switching the year and month from the scroll wheel will not switch the panels synchronously. This ensures the user's ability to select months at non-fixed intervals.
+
 ```jsx live=true
 import React from 'react';
 import { DatePicker } from '@douyinfe/semi-ui';

+ 3 - 0
content/input/datepicker/index.md

@@ -15,6 +15,7 @@ brief: 日期选择器用于帮助用户选择一个符合要求的、格式化
 
 ### 如何引入
 
+
 ```jsx import
 import { DatePicker } from '@douyinfe/semi-ui';
 ```
@@ -128,6 +129,8 @@ version: >= 1.28.0
 
 在范围选择的场景中, 开启 `syncSwitchMonth` 则允许双面板同步切换。默认为 false。
 
+> Note:点击年份按钮也会同步切换两个面板,从滚轮里面切换年月不会同步切换面板,这保证了用户选择非固定间隔月份的能力。
+
 ```jsx live=true
 import React from 'react';
 import { DatePicker } from '@douyinfe/semi-ui';

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

@@ -94,6 +94,15 @@ $module: #{$prefix}-datepicker;
                 min-height: $height-datepicker_timepicker_header_min;
             }
         }
+
+        // 为了防止 scrollList 因为 weeks 变化高度发生变化导致年月可能发生滚动
+        // In order to prevent scrollList from scrolling due to changes in the height of weeks, the year and month may be scrolled
+        &[x-panel-yearandmonth-open-type="left"],
+        &[x-panel-yearandmonth-open-type="right"] {
+            .#{$module}-weeks {
+                min-height: 6 * $width-datepicker_day;
+            }
+        }
     }
 
     // 年月选择器
@@ -833,6 +842,15 @@ $module: #{$prefix}-datepicker;
                 }
             }
         }
+
+        // 为了防止 scrollList 因为 weeks 变化高度发生变化导致年月可能发生滚动
+        // In order to prevent scrollList from scrolling due to changes in the height of weeks, the year and month may be scrolled
+        &[x-panel-yearandmonth-open-type="left"],
+        &[x-panel-yearandmonth-open-type="right"] {
+            .#{$module}-weeks {
+                min-height: 6 * $width-datepicker_day_compact;
+            }
+        }
     }
 
     // 年月选择器

+ 101 - 8
packages/semi-foundation/datePicker/monthsGridFoundation.ts

@@ -47,6 +47,7 @@ interface MonthsGridElementProps {
 }
 
 export type PanelType = 'left' | 'right';
+export type YearMonthChangeType = 'prevMonth' | 'nextMonth' | 'prevYear' | 'nextYear';
 
 export interface MonthsGridFoundationProps extends MonthsGridElementProps {
     type?: Type;
@@ -268,9 +269,65 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     // eslint-disable-next-line @typescript-eslint/no-empty-function
     destroy() { }
 
+    /**
+     * sync change another panel month when change months from the else yam panel
+     * call it when
+     *  - current change panel targe date month is same with another panel date
+     * 
+     * @example
+     *  - panelType=right, target=new Date('2022-09-01') and left panel is in '2022-09' => call it, left panel minus one month to '2022-08'
+     *  - panelType=left, target=new Date('2021-12-01') and right panel is in '2021-12' => call it, right panel add one month to '2021-01'
+     */
+    handleSyncChangeMonths(options: { panelType: PanelType, target: Date }) {
+        const { panelType, target } = options;
+        const { type } = this._adapter.getProps();
+        const { monthLeft, monthRight } = this._adapter.getStates();
+        if (this.isRangeType(type)) {
+            if (panelType === 'right' && differenceInCalendarMonths(target, monthLeft.pickerDate) === 0) {
+                this.handleYearOrMonthChange('prevMonth', 'left', 1, true);
+            } else if (panelType === 'left' && differenceInCalendarMonths(monthRight.pickerDate, target) === 0) {
+                this.handleYearOrMonthChange('nextMonth', 'right', 1, true);
+            }
+        }
+    }
+
+    /**
+     * Get the target date based on the panel type and switch type
+     */
+    getTargetChangeDate(options: { panelType: PanelType, switchType: YearMonthChangeType }) {
+        const { panelType, switchType } = options;
+        const { monthRight, monthLeft } = this._adapter.getStates();
+        const currentDate = panelType === 'left' ? monthLeft.pickerDate : monthRight.pickerDate;
+        let target: Date;
+        
+        switch (switchType) {
+            case 'prevMonth':
+                target = addMonths(currentDate, -1);
+                break;
+            case 'nextMonth':
+                target = addMonths(currentDate, 1);
+                break;
+            case 'prevYear':
+                target = addYears(currentDate, -1);
+                break;
+            case 'nextYear':
+                target = addYears(currentDate, 1);
+                break;
+        }
+        return target;
+    }
+
+    /**
+     * Change month by yam panel
+     */
     toMonth(panelType: PanelType, target: Date) {
+        const { type } = this._adapter.getProps();
         const diff = this._getDiff('month', target, panelType);
         this.handleYearOrMonthChange(diff < 0 ? 'prevMonth' : 'nextMonth', panelType, Math.abs(diff), false);
+    
+        if (this.isRangeType(type)) {
+            this.handleSyncChangeMonths({ panelType, target });
+        }
     }
 
     toYear(panelType: PanelType, target: Date) {
@@ -289,30 +346,43 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
         return typeof realType === 'string' && /range/i.test(realType);
     }
 
-    handleSwitchMonth(switchType: 'prevMonth' | 'nextMonth', panelType: PanelType) {
+    handleSwitchMonthOrYear(switchType: YearMonthChangeType, panelType: PanelType) {
         const { type, syncSwitchMonth } = this.getProps();
-        if (this.isRangeType(type) && syncSwitchMonth) {
+        const rangeType = this.isRangeType(type);
+
+        // range type and syncSwitchMonth, we should change panels at same time
+        if (rangeType && syncSwitchMonth) {
             this.handleYearOrMonthChange(switchType, 'left', 1, true);
             this.handleYearOrMonthChange(switchType, 'right', 1, true);
         } else {
             this.handleYearOrMonthChange(switchType, panelType);
+
+            /**
+             * default behavior (v2.2.0)
+             * In order to prevent the two panels from being the same month, this will confuse the user when selecting the range
+             * https://github.com/DouyinFE/semi-design/issues/260
+             */
+            if (rangeType) {
+                const target = this.getTargetChangeDate({ panelType, switchType });
+                this.handleSyncChangeMonths({ panelType, target });
+            }
         }
     }
 
     prevMonth(panelType: PanelType) {
-        this.handleSwitchMonth('prevMonth', panelType);
+        this.handleSwitchMonthOrYear('prevMonth', panelType);
     }
 
     nextMonth(panelType: PanelType) {
-        this.handleSwitchMonth('nextMonth', panelType);
+        this.handleSwitchMonthOrYear('nextMonth', panelType);
     }
 
     prevYear(panelType: PanelType) {
-        this.handleYearOrMonthChange('prevYear', panelType);
+        this.handleSwitchMonthOrYear('prevYear', panelType);
     }
 
     nextYear(panelType: PanelType) {
-        this.handleYearOrMonthChange('nextYear', panelType);
+        this.handleSwitchMonthOrYear('nextYear', panelType);
     }
 
     /**
@@ -414,7 +484,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     }
 
     handleYearOrMonthChange(
-        type: 'prevMonth' | 'nextMonth' | 'prevYear' | 'nextYear',
+        type: YearMonthChangeType,
         panelType: PanelType = strings.PANEL_TYPE_LEFT,
         step = 1,
         notSeparateInRange = false
@@ -458,7 +528,7 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
      * @param {*} targetDate
      */
     updateDateAfterChangeYM(
-        type: 'prevMonth' | 'nextMonth' | 'prevYear' | 'nextYear',
+        type: YearMonthChangeType,
         targetDate: Date
     ) {
         const { multiple, disabledDate } = this.getProps();
@@ -839,4 +909,27 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
     showDatePanel(panelType: PanelType) {
         this._updatePanelDetail(panelType, { isTimePickerOpen: false, isYearPickerOpen: false });
     }
+
+    /**
+     * Get year and month panel open type
+     * 
+     * It is useful info to set minHeight of weeks.
+     *  - When yam open type is 'left' or 'right', weeks minHeight should be set
+     *    If the minHeight is not set, the change of the number of weeks will cause the scrollList to be unstable
+     */
+    getYAMOpenType() {
+        const { monthLeft, monthRight } = this._adapter.getStates();
+        const leftYearPickerOpen = monthLeft.isYearPickerOpen;
+        const rightYearPickerOpen = monthRight.isYearPickerOpen;
+
+        if (leftYearPickerOpen && rightYearPickerOpen) {
+            return 'both';
+        } else if (leftYearPickerOpen) {
+            return 'left';
+        } else if (rightYearPickerOpen) {
+            return 'right';
+        } else {
+            return 'none';
+        }
+    }
 }

+ 67 - 2
packages/semi-ui/datePicker/__test__/datePicker.test.js

@@ -280,10 +280,12 @@ describe(`DatePicker`, () => {
         const rightPanel = document.querySelector(`.${BASE_CLASS_PREFIX}-datepicker-month-grid-right`);
         const rightNavBtns = rightPanel.querySelectorAll(`.${BASE_CLASS_PREFIX}-datepicker-navigation .${BASE_CLASS_PREFIX}-button`);
 
-        _.get(rightNavBtns, 1).click();
+        // 点击右边面板下一月
+        _.get(rightNavBtns, 2).click();
         await sleep();
 
-        _.times(leftPrevClickTimes).forEach(() => _.first(leftNavBtns).click());
+        // 点击左边面板上一月
+        _.times(leftPrevClickTimes).forEach(() => _.get(leftNavBtns, 1).click());
 
         const leftSecondWeek = leftPanel.querySelectorAll(`.${BASE_CLASS_PREFIX}-datepicker-week`)[1];
         const leftSecondWeekDays = leftSecondWeek.querySelectorAll(`.${BASE_CLASS_PREFIX}-datepicker-day`);
@@ -870,4 +872,67 @@ describe(`DatePicker`, () => {
         expect(allSeparators[0].textContent.trim()).toBe(rangeSeparator);
         expect(allSeparators[1].textContent.trim()).toBe(rangeSeparator);
     });
+
+    it('test click next/prev year buttons', () => {
+        let props = {
+          type: 'dateRange',
+          motion: false,
+          style: { width: 300 },
+          defaultPickerValue: new Date('2021-12-01'),
+          defaultOpen: true,
+        };
+        const elem = mount(<DatePicker {...props} />);
+
+        const leftPanel = document.querySelector(`.semi-datepicker-month-grid-left`);
+        const leftNavBtns = leftPanel.querySelector(`.semi-datepicker-navigation`).children;
+        const rightPanel = document.querySelector(`.semi-datepicker-month-grid-right`);
+        const rightNavBtns = rightPanel.querySelector(`.semi-datepicker-navigation`).children;
+
+        // 点击左边面板上一年
+        _.get(leftNavBtns, 0).click();
+        expect(document.querySelector(`.semi-datepicker-month-grid-left .semi-datepicker-navigation-month`).textContent).toBe('2020年 12月');
+        // 点击左边面板下一年
+        _.get(leftNavBtns, 4).click();
+        expect(document.querySelector(`.semi-datepicker-month-grid-left .semi-datepicker-navigation-month`).textContent).toBe('2021年 12月');
+
+        // 点击右边面板下一年
+        _.get(rightNavBtns, 4).click();
+        expect(document.querySelector(`.semi-datepicker-month-grid-right .semi-datepicker-navigation-month`).textContent).toBe('2023年 1月');
+        // 点击右边面板上一年
+        _.get(rightNavBtns, 0).click();
+        expect(document.querySelector(`.semi-datepicker-month-grid-right .semi-datepicker-navigation-month`).textContent).toBe('2022年 1月');
+    });
+
+    const testMonthSyncChange = type => {
+        let props = {
+            type,
+            motion: false,
+            style: { width: 300 },
+            defaultPickerValue: new Date('2021-12-01'),
+            defaultOpen: true,
+          };
+          const elem = mount(<DatePicker {...props} />);
+  
+          const leftPanel = document.querySelector(`.semi-datepicker-month-grid-left`);
+          const leftNavBtns = leftPanel.querySelector(`.semi-datepicker-navigation`).children;
+          const rightPanel = document.querySelector(`.semi-datepicker-month-grid-right`);
+          const rightNavBtns = rightPanel.querySelector(`.semi-datepicker-navigation`).children;
+  
+          // 点击左边面板下一月,自动切换右面板
+          _.get(leftNavBtns, 3).click();
+          expect(document.querySelector(`.semi-datepicker-month-grid-left .semi-datepicker-navigation-month`).textContent).toBe('2022年 1月');
+          expect(document.querySelector(`.semi-datepicker-month-grid-right .semi-datepicker-navigation-month`).textContent).toBe('2022年 2月');
+          // 点击右边面板上一月,自动切换左面板
+          _.get(rightNavBtns, 1).click();
+          expect(document.querySelector(`.semi-datepicker-month-grid-left .semi-datepicker-navigation-month`).textContent).toBe('2021年 12月');
+          expect(document.querySelector(`.semi-datepicker-month-grid-right .semi-datepicker-navigation-month`).textContent).toBe('2022年 1月');
+  
+          // 点击左边面板上一月,不需要自动切换右面板
+          _.get(leftNavBtns, 1).click();
+          expect(document.querySelector(`.semi-datepicker-month-grid-left .semi-datepicker-navigation-month`).textContent).toBe('2021年 11月');
+          elem.unmount();
+    }
+
+    it('test month sync change dateRange type', () => { testMonthSyncChange('dateRange') });
+    it('test month sync change dateTimeRange type', () => { testMonthSyncChange('dateTimeRange')});
 });

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

@@ -36,6 +36,7 @@ import DatePickerSlot from './DatePickerSlot';
 import DatePickerTimeZone from './DatePickerTimeZone';
 import BetterRangePicker from './BetterRangePicker';
 import SyncSwitchMonth from './SyncSwitchMonth';
+import { YearButton } from './v2';
 
 export default {
   title: 'DatePicker',
@@ -65,7 +66,8 @@ export {
   DatePickerSlot,
   DatePickerTimeZone,
   BetterRangePicker,
-  SyncSwitchMonth
+  SyncSwitchMonth,
+  YearButton
 }
 
 const demoDiv = {

+ 17 - 0
packages/semi-ui/datePicker/_story/v2/YearButton.jsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import { DatePicker } from '../../../index';
+
+export default function App() {
+    return (
+        <div>
+            <h4>type=date</h4>
+            <DatePicker />
+            <h4>type=dateRange</h4>
+            <DatePicker type="dateRange" defaultPickerValue="2021-12" />
+            <h4>type=dateTimeRange</h4>
+            <DatePicker type="dateTimeRange" />
+            <h4>type=dateRange + compact</h4>
+            <DatePicker type="dateRange" density="compact" />
+        </div>
+    );
+}

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

@@ -0,0 +1 @@
+export { default as YearButton } from './YearButton';

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

@@ -446,6 +446,10 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         });
     };
 
+    getYAMOpenType = () => {
+        return this.foundation.getYAMOpenType();
+    }
+
     renderTimePicker(panelType: PanelType, panelDetail: MonthInfo) {
         const { type, locale, format, hideDisabledOptions, timePickerOpts, dateFnsLocale } = this.props;
         const { pickerDate } = panelDetail;
@@ -606,8 +610,15 @@ export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGri
         } else if (type === 'year' || type === 'month') {
             content = 'year month';
         }
+        const yearOpenType = this.getYAMOpenType();
+
         return (
-            <div className={monthGridCls} x-type={type} ref={current => this.cacheRefCurrent('monthGrid', current)}>
+            <div
+                className={monthGridCls}
+                x-type={type}
+                x-panel-yearandmonth-open-type={yearOpenType}
+                ref={current => this.cacheRefCurrent('monthGrid', current)}
+            >
                 {content}
             </div>
         );

+ 55 - 29
packages/semi-ui/datePicker/navigation.tsx

@@ -6,7 +6,7 @@ import { noop } from 'lodash';
 import IconButton from '../iconButton';
 import Button from '../button';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/datePicker/constants';
-import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+import { IconChevronLeft, IconChevronRight, IconDoubleChevronLeft, IconDoubleChevronRight } from '@douyinfe/semi-icons';
 import { PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
 
 const prefixCls = cssClasses.NAVIGATION;
@@ -68,6 +68,8 @@ export default class Navigation extends PureComponent<NavigationProps> {
             onMonthClick,
             onNextMonth,
             onPrevMonth,
+            onPrevYear,
+            onNextYear,
             density,
             shouldBimonthSwitch,
             panelType
@@ -77,43 +79,67 @@ export default class Navigation extends PureComponent<NavigationProps> {
         const iconBtnSize = density === 'compact' ? 'default' : 'large';
         const btnNoHorizontalPadding = true;
         const buttonSize = density === 'compact' ? 'small' : 'default';
-        // Enable dual-panel synchronous switching, and the current panel is the left panel
-        const bimonthSwitchWithLeftPanel = shouldBimonthSwitch && panelType === strings.PANEL_TYPE_LEFT;
-        // Enable dual-panel synchronous switching, and the current panel is the right panel
-        const bimonthSwitchWithRightPanel = shouldBimonthSwitch && panelType === strings.PANEL_TYPE_RIGHT;
+        const isLeftPanel = panelType === strings.PANEL_TYPE_LEFT;
+        const isRightPanel =  panelType === strings.PANEL_TYPE_RIGHT;
+
+        // syncSwitchMonth and the current panel is the left
+        const hiddenLeftPanelRightButtons = shouldBimonthSwitch && isLeftPanel;
+        // syncSwitchMonth and the current panel is the right
+        const hiddenRightPanelLeftButtons = shouldBimonthSwitch && isRightPanel;
+        // `visibility: hidden` will keep the icon in position
+        const leftButtonStyle: React.CSSProperties = {};
+        const rightButtonStyle: React.CSSProperties = {};
+        if (hiddenRightPanelLeftButtons) {
+            leftButtonStyle.visibility = 'hidden';
+        }
+        if (hiddenLeftPanelRightButtons) {
+            rightButtonStyle.visibility = 'hidden';
+        }
 
         const ref = forwardRef || this.navRef;
         return (
             <div className={prefixCls} ref={ref}>
-                {
-                    !bimonthSwitchWithRightPanel &&
-                    (
-                        <IconButton
-                            icon={<IconChevronLeft size={iconBtnSize} />}
-                            size={buttonSize}
-                            onClick={onPrevMonth}
-                            theme={btnTheme}
-                            noHorizontalPadding={btnNoHorizontalPadding}
-                        />
-                    )
-                }
+                <IconButton
+                    key="double-chevron-left"
+                    icon={<IconDoubleChevronLeft size={iconBtnSize}/>} 
+                    size={buttonSize}
+                    theme={btnTheme}
+                    noHorizontalPadding={btnNoHorizontalPadding}
+                    onClick={onPrevYear}
+                    style={leftButtonStyle}
+                />
+                <IconButton
+                    key="chevron-left"
+                    icon={<IconChevronLeft size={iconBtnSize} />}
+                    size={buttonSize}
+                    onClick={onPrevMonth}
+                    theme={btnTheme}
+                    noHorizontalPadding={btnNoHorizontalPadding}
+                    style={leftButtonStyle}
+                />
                 <div className={`${prefixCls}-month`}>
                     <Button onClick={onMonthClick} theme={btnTheme} size={buttonSize}>
                         <span>{monthText}</span>
                     </Button>
                 </div>
-                {
-                    !bimonthSwitchWithLeftPanel &&
-                    (
-                        <IconButton
-                            icon={<IconChevronRight size={iconBtnSize} />}
-                            size={buttonSize}
-                            onClick={onNextMonth}
-                            theme={btnTheme}
-                            noHorizontalPadding={btnNoHorizontalPadding}
-                        />
-                    )
-                }
+                <IconButton
+                    key="chevron-right"
+                    icon={<IconChevronRight size={iconBtnSize} />}
+                    size={buttonSize}
+                    onClick={onNextMonth}
+                    theme={btnTheme}
+                    noHorizontalPadding={btnNoHorizontalPadding}
+                    style={rightButtonStyle}
+                />
+                <IconButton
+                    key="double-chevron-right"
+                    icon={<IconDoubleChevronRight size={iconBtnSize}/>} 
+                    size={buttonSize}
+                    theme={btnTheme}
+                    noHorizontalPadding={btnNoHorizontalPadding}
+                    onClick={onNextYear}
+                    style={rightButtonStyle}
+                />
             </div>
         );
     }