浏览代码

fix: datePicker triggerRender disabled bug #676 (#967)

* fix: datePicker triggerRender disabled bug #676

* docs: update semi-eslint-plugin spell
走鹃 3 年之前
父节点
当前提交
ad07bf6d54

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

@@ -629,6 +629,8 @@ class App extends React.Component {
 
 By default we use the `Input` component as the trigger for the `DatePicker` component. You can customize this trigger by passing the `triggerRender` method.
 
+The custom trigger is a complete customization of the trigger, the default clear button will not take effect, if you need clear function, please customize a clear button.
+
 ```jsx live=true
 import React, { useState, useCallback, useMemo } from 'react';
 import * as dateFns from 'date-fns';
@@ -665,6 +667,55 @@ function Demo() {
 }
 ```
 
+<Notice type="primary" title="Note">
+    <div>When DatePicker is range type, the default date selected after the panel is opened is the start date, and it will switch to the end date selection after selection. The focus is reset when the panel is closed.</div>
+    <div>We recommend providing a clear button, when you pass null value to DatePicker, DatePicker will also reset focus internally. This allows the user to reselect the date range after clearing. (from v2.14)</div>
+</Notice>
+
+```jsx live=true hideInDSM
+import React, { useState, useCallback, useMemo } from 'react';
+import { DatePicker, Button, Icon } from '@douyinfe/semi-ui';
+import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
+
+function Demo() {
+    const [date, setDate] = useState();
+    const formatToken = 'yyyy-MM-dd HH:mm:ss';
+    const onChange = useCallback(date => {
+        setDate(date);
+        console.log(date);
+    }, []);
+    const onClear = useCallback(e => {
+        e && e.stopPropagation();
+        setDate();
+    }, []);
+
+    const closeIcon = useMemo(() => {
+        return date ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+    }, [date]);
+
+    const triggerContent = (placeholder) => {
+        if (Array.isArray(date) && date.length) {
+            return `${dateFns.format(date[0], formatToken)} ~ ${dateFns.format(date[1], formatToken)}`;
+        } else {
+            return 'Please select a date range';
+        }
+    };
+
+    return (
+        <DatePicker
+            type='dateTimeRange'
+            onChange={onChange}
+            value={date}
+            triggerRender={({ placeholder }) => (
+                <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
+                    {triggerContent(placeholder)}
+                </Button>
+            )}
+        />
+    );
+}
+```
+
 ### Custom Render Date Content
 
 **Version:**>=1.4.0

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

@@ -595,6 +595,8 @@ import { DatePicker } from '@douyinfe/semi-ui';
 
 默认情况下我们使用 `Input` 组件作为 `DatePicker` 组件的触发器,通过传递 `triggerRender` 方法你可以自定义这个触发器。
 
+自定义触发器是对触发器的完全自定义,默认的清除按钮将不生效,如果你需要清除功能,请自定义一个清除按钮。
+
 ```jsx live=true hideInDSM
 import React, { useState, useCallback, useMemo } from 'react';
 import * as dateFns from 'date-fns';
@@ -631,6 +633,56 @@ function Demo() {
 }
 ```
 
+<Notice type="primary" title="注意事项">
+    <div>范围选择时,面板打开后默认选择的日期为开始日期,选择后会切到结束日期选择。面板关闭后焦点会重置。</div>
+    <div>我们建议提供一个清除按钮,当你给 DatePicker 传入空值时,DatePicker 内部也会重置焦点。这样用户可以在清除后重新选择日期范围。(from v2.14)</div>
+</Notice>
+
+```jsx live=true hideInDSM
+import React, { useState, useCallback, useMemo } from 'react';
+import { DatePicker, Button, Icon } from '@douyinfe/semi-ui';
+import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
+
+
+function Demo() {
+    const [date, setDate] = useState();
+    const formatToken = 'yyyy-MM-dd HH:mm:ss';
+    const onChange = useCallback(date => {
+        setDate(date);
+        console.log(date);
+    }, []);
+    const onClear = useCallback(e => {
+        e && e.stopPropagation();
+        setDate();
+    }, []);
+
+    const closeIcon = useMemo(() => {
+        return date ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+    }, [date]);
+
+    const triggerContent = (placeholder) => {
+        if (Array.isArray(date) && date.length) {
+            return `${dateFns.format(date[0], formatToken)} ~ ${dateFns.format(date[1], formatToken)}`;
+        } else {
+            return '请选择日期时间范围';
+        }
+    };
+
+    return (
+        <DatePicker
+            type='dateTimeRange'
+            onChange={onChange}
+            value={date}
+            triggerRender={({ placeholder }) => (
+                <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
+                    {triggerContent(placeholder)}
+                </Button>
+            )}
+        />
+    );
+}
+```
+
 ### 自定义日期显示内容
 
 **版本:**>=1.4.0

+ 1 - 1
packages/semi-eslint-plugin/README-zh_CN.md

@@ -20,7 +20,7 @@ semi-ui 不应该作为 semi-foundation 的依赖。
 
 点击查看[详情](https://github.com/vercel/next.js/issues/2259)。
 
-### ✅ 不能在 semi-ui 或 semi-foundation 使用相对路径引用 pacakges 下的包
+### ✅ 不能在 semi-ui 或 semi-foundation 使用相对路径引用 packages 下的包
 
 monorepo 下各个包之间的 import 请使用包名而不是相对路径。
 

+ 1 - 1
packages/semi-eslint-plugin/README.md

@@ -20,7 +20,7 @@ Why: In order to be compatible with next, lodash-es only provides the product of
 
 See more [here](https://github.com/vercel/next.js/issues/2259)。
 
-### ✅ Should not use relative paths to import a package under pacakges in semi-ui or semi-foundation
+### ✅ Should not use relative paths to import a package under packages in semi-ui or semi-foundation
 
 For imports between packages under monorepo, use package names instead of relative paths.
 

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

@@ -1,7 +1,7 @@
 /* eslint-disable no-nested-ternary */
 /* eslint-disable max-len, max-depth,  */
 import { format, isValid, isSameSecond, isEqual as isDateEqual, isDate } from 'date-fns';
-import { get, isObject, isString, isEqual } from 'lodash';
+import { get, isObject, isString, isEqual, isFunction } from 'lodash';
 
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import { isValidDate, isTimestamp } from './_utils/index';
@@ -245,6 +245,23 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
         this._adapter.updateInputValue(null);
         this._adapter.updateValue(result);
         this.resetCachedSelectedValue(result);
+        this.initRangeInputFocus(result);
+
+        if (this._adapter.needConfirm()) {
+            this._adapter.updateCachedSelectedValue(result);
+        }
+    }
+
+    /**
+     * 如果用户传了一个空的 value,需要把 range input focus 设置为 rangeStart,这样用户可以清除完之后继续从开始选择
+     * 
+     * If the user passes an empty value, you need to set the range input focus to rangeStart, so that the user can continue to select from the beginning after clearing
+     */
+    initRangeInputFocus(result: Date[]) {
+        const { triggerRender } = this.getProps();
+        if (this._isRangeType() && isFunction(triggerRender) && result.length === 0) {
+            this._adapter.setRangeInputFocus('rangeStart');
+        }
     }
 
     parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number) {
@@ -1285,7 +1302,7 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
      * @returns
      */
     handleTriggerWrapperClick(e: any) {
-        const { disabled } = this._adapter.getProps();
+        const { disabled, triggerRender } = this._adapter.getProps();
         const { rangeInputFocus } = this._adapter.getStates();
         if (disabled) {
             return;
@@ -1297,12 +1314,18 @@ export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapt
          * - When type is not range type, Input component will automatically focus in the same case
          * - isEventTarget is used to judge whether the event is a bubbling event
          */
-        if (this._isRangeType() && !rangeInputFocus && this._adapter.isEventTarget(e)) {
-            setTimeout(() => {
-                // using setTimeout get correct state value 'rangeInputFocus'
-                this.handleInputFocus(e, 'rangeStart');
-                this.openPanel();
-            }, 0);
+        if (this._isRangeType() && !rangeInputFocus) {
+            if (this._adapter.isEventTarget(e)) {
+                setTimeout(() => {
+                    // using setTimeout get correct state value 'rangeInputFocus'
+                    this.handleInputFocus(e, 'rangeStart');
+                }, 0);
+            } else if (isFunction(triggerRender)) {
+                // 如果是 triggerRender 场景,因为没有 input,因此打开面板时默认 focus 在 rangeStart
+                // If it is a triggerRender scene, because there is no input, the default focus is rangeStart when the panel is opened
+                this._adapter.setRangeInputFocus('rangeStart');
+            }
+            this.openPanel();
         } else {
             this.openPanel();
         }

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

@@ -724,9 +724,8 @@ export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapt
             /**
              * no need to check focus then
              *  - dateRange and isDateRangeAndHasOffset
-             *  - dateRange and triggerRender
              */
-            const needCheckFocusRecord = !(type === 'dateRange' && (isDateRangeAndHasOffset || isFunction(triggerRender)));
+            const needCheckFocusRecord = !(type === 'dateRange' && isDateRangeAndHasOffset);
             this._adapter.notifySelectedChange(date, { needCheckFocusRecord });
         }
     }

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

@@ -1056,6 +1056,7 @@ describe(`DatePicker`, () => {
         const rightSecondWeek = rightPanel.querySelectorAll(`.${BASE_CLASS_PREFIX}-datepicker-week`)[1];
         const rightSecondWeekDays = rightSecondWeek.querySelectorAll(`.${BASE_CLASS_PREFIX}-datepicker-day`);
         leftSecondWeekDays[0].click();
+        await sleep();
         rightSecondWeekDays[0].click();
 
         const baseElem = elem.find(BaseDatePicker);

+ 36 - 0
packages/semi-ui/datePicker/_story/v2/FixTriggerRender.tsx

@@ -0,0 +1,36 @@
+import React, { useState, useCallback, useMemo } from 'react';
+import { DatePicker, Button, Icon, Space } from '../../../index';
+
+export default function Demo() {
+    const [date, setDate] = useState([]);
+    const onChange = useCallback(date => {
+        setDate(date);
+        console.log(date);
+    }, []);
+    const onClear = useCallback(e => {
+        e && e.stopPropagation();
+        setDate(null);
+    }, []);
+
+    const closeIcon = useMemo(() => (date ? <Icon type="close" onClick={onClear} /> : <Icon type="chevron_down" />), [date]);
+
+    return (
+        <Space>
+            <DatePicker
+                type="dateTimeRange"
+                onChange={onChange}
+                value={date}
+                triggerRender={({ placeholder }) => (
+                    <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
+                        {(date && String(date)) || placeholder}
+                    </Button>
+                )}
+            />
+            <DatePicker
+                type="dateRange"
+                motion={false}
+                triggerRender={({ placeholder }) => <Button theme={'light'}>{placeholder}</Button>}
+            />
+        </Space>
+    );
+}

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

@@ -8,3 +8,4 @@ export { default as InputFormat  } from './InputFormat';
 export { default as InputFormatDisabled  } from './InputFormatDisabled';
 export { default as AutoFillTime  } from './AutoFillTime';
 export { default as InputFormatConfirm  } from './InputFormatConfirm';
+export { default as FixedTriggerRender } from './FixTriggerRender';