Browse Source

Merge branch 'main' into release

pointhalo 3 years ago
parent
commit
d1697c1702

+ 11 - 1
content/navigation/tabs/index-en-US.md

@@ -308,7 +308,7 @@ class App extends React.Component {
             <Tabs style={{ width: '60%', margin: '20px' }} type="card" collapsible>
             <Tabs style={{ width: '60%', margin: '20px' }} type="card" collapsible>
                 {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((item, index) => (
                 {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((item, index) => (
                     <TabPane tab={`Tab-${item}`} itemKey={`Tab-${item}`} key={item}>
                     <TabPane tab={`Tab-${item}`} itemKey={`Tab-${item}`} key={item}>
-                        Content of card tab {i}
+                        Content of card tab {index}
                     </TabPane>
                     </TabPane>
                 ))}
                 ))}
             </Tabs>
             </Tabs>
@@ -578,3 +578,13 @@ closable | whether user can close the tab **>=2.1.0** | boolean | false |
 ## Design Token
 ## Design Token
 
 
 <DesignToken/>
 <DesignToken/>
+
+## FAQ
+
+-   **Why typography with ellipses in Tabs doesn't work?**
+
+    Because when Tabs renders TabPane, the default is to render display: none. At this point these components cannot get the correct width or height values. It is recommended to enable lazyRender in version 1.x, or disable keepDOM. Version 0.x needs to use tabList notation.
+
+-   **Why are the height or width values ​​wrong when using components such as Collapse/Collapsible/Resizable Table in Tabs?**
+
+    The reason is the same as above. In addition, if the collapse does not need animation, you can also turn off the animation effect by setting motion={false}. There is no need to get the height of the component at this point。

+ 10 - 0
content/navigation/tabs/index.md

@@ -602,3 +602,13 @@ closable  | 允许关闭tab **>=2.1.0**| boolean | false |
 ## 设计变量
 ## 设计变量
 
 
 <DesignToken/>
 <DesignToken/>
+
+## FAQ
+
+-   **为什么在Tabs中使用 Typography 的省略 ellipsis 失效?**
+
+    因为Tabs渲染TabPane时,默认是全部渲染display: none。此时这些组件无法获取到正确的宽度或高度值。建议1.x的版本开启lazyRender,或者关闭keepDOM。0.x的版本需要使用tabList的写法。
+
+-   **为什么在Tabs中使用Collapse/Collapsible/Resizable Table等组件的高度或宽度值不对?**
+
+    原因同上,另外如果 collapse 不需要动画,也可以通过设置 motion={false} 来关闭动画效果。此时无需获取组件的高度。

+ 22 - 6
packages/semi-foundation/inputNumber/foundation.ts

@@ -4,7 +4,7 @@
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import keyCode from '../utils/keyCode';
 import keyCode from '../utils/keyCode';
 import { numbers } from './constants';
 import { numbers } from './constants';
-import { toNumber, toString, get } from 'lodash';
+import { toNumber, toString, get, isString } from 'lodash';
 import { minus as numberMinus } from '../utils/number';
 import { minus as numberMinus } from '../utils/number';
 
 
 export interface InputNumberAdapter extends DefaultAdapter {
 export interface InputNumberAdapter extends DefaultAdapter {
@@ -26,6 +26,14 @@ export interface InputNumberAdapter extends DefaultAdapter {
     restoreCursor: (str?: string) => boolean;
     restoreCursor: (str?: string) => boolean;
     fixCaret: (start: number, end: number) => void;
     fixCaret: (start: number, end: number) => void;
     setClickUpOrDown: (clicked: boolean) => void;
     setClickUpOrDown: (clicked: boolean) => void;
+    updateStates: (states: BaseInputNumberState, callback?: () => void) => void;
+}
+
+export interface BaseInputNumberState {
+    value?: number | string;
+    number?: number | null;
+    focusing?: boolean;
+    hovering?: boolean;
 }
 }
 
 
 class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
 class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
@@ -360,17 +368,21 @@ class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
         const { defaultValue, value } = this.getProps();
         const { defaultValue, value } = this.getProps();
 
 
         const propsValue = this._isControlledComponent('value') ? value : defaultValue;
         const propsValue = this._isControlledComponent('value') ? value : defaultValue;
-        const tmpNumer = this.doParse(toString(propsValue), false, true, true);
+        const tmpNumber = this.doParse(toString(propsValue), false, true, true);
 
 
         let number = null;
         let number = null;
-        if (typeof tmpNumer === 'number' && !isNaN(tmpNumer)) {
-            number = tmpNumer;
+        if (typeof tmpNumber === 'number' && !isNaN(tmpNumber)) {
+            number = tmpNumber;
         }
         }
 
 
-        const formatedValue = typeof number === 'number' ? this.doFormat(number, true) : '';
+        const formattedValue = typeof number === 'number' ? this.doFormat(number, true) : '';
 
 
         this._adapter.setNumber(number);
         this._adapter.setNumber(number);
-        this._adapter.setValue(formatedValue);
+        this._adapter.setValue(formattedValue);
+
+        if (isString(formattedValue) && formattedValue !== String(propsValue)) {
+            this.notifyChange(formattedValue, null);
+        }
     }
     }
 
 
     add(step?: number, event?: any): string {
     add(step?: number, event?: any): string {
@@ -616,6 +628,10 @@ class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
             this._adapter.notifyNumberChange(value, e);
             this._adapter.notifyNumberChange(value, e);
         }
         }
     }
     }
+
+    updateStates(states: BaseInputNumberState, callback?: () => void) {
+        this._adapter.updateStates(states, callback);
+    }
 }
 }
 
 
 export default InputNumberFoundation;
 export default InputNumberFoundation;

+ 17 - 5
packages/semi-ui/form/hoc/withField.tsx

@@ -379,12 +379,24 @@ function withField<
                 return () => {};
                 return () => {};
             }
             }
             // log('register: ' + field);
             // log('register: ' + field);
-            updater.register(field, fieldState, {
+
+            // field value may change after field component mounted, we use ref value here to get changed value
+            const refValue = getVal();
+            updater.register(
                 field,
                 field,
-                fieldApi,
-                keepState,
-                allowEmpty: allowEmpty || allowEmptyString,
-            });
+                {
+                    value: refValue,
+                    error,
+                    touched,
+                    status,
+                },
+                {
+                    field,
+                    fieldApi,
+                    keepState,
+                    allowEmpty: allowEmpty || allowEmptyString,
+                }
+            );
             // return unRegister cb
             // return unRegister cb
             return () => {
             return () => {
                 updater.unRegister(field);
                 updater.unRegister(field);

+ 40 - 3
packages/semi-ui/inputNumber/__test__/inputNumber.test.js

@@ -6,7 +6,7 @@ import keyCode from '@douyinfe/semi-foundation/utils/keyCode';
 import * as _ from 'lodash';
 import * as _ from 'lodash';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
 import { numbers } from '@douyinfe/semi-foundation/inputNumber/constants';
 import { numbers } from '@douyinfe/semi-foundation/inputNumber/constants';
-import { Form, withField } from '../../index';
+import { Form, withField, useFormApi } from '../../index';
 
 
 const log = (...args) => console.log(...args);
 const log = (...args) => console.log(...args);
 const times = (n = 0, fn) => {
 const times = (n = 0, fn) => {
@@ -182,8 +182,9 @@ describe(`InputNumber`, () => {
         const inputElem = inputNumber.find('input');
         const inputElem = inputNumber.find('input');
 
 
         inputElem.simulate('change', event);
         inputElem.simulate('change', event);
-        expect(onChange.calledOnce).toBe(true);
-        expect(onChange.calledWithMatch(Number(newValue.toFixed(precision)))).toBe(true);
+        expect(onChange.calledTwice).toBe(true);
+        expect(onChange.getCall(1).args[0]).toEqual(Number(newValue.toFixed(precision)));
+        // expect(onChange.calledWithMatch(Number(newValue.toFixed(precision)))).toBe(true);
         expect(inputElem.instance().value).toBe(formatter(newValue));
         expect(inputElem.instance().value).toBe(formatter(newValue));
 
 
         inputElem.simulate('blur');
         inputElem.simulate('blur');
@@ -395,4 +396,40 @@ describe(`InputNumber`, () => {
         expect(onUpClick.called).toBe(false);
         expect(onUpClick.called).toBe(false);
         expect(onDownClick.called).toBe(false);
         expect(onDownClick.called).toBe(false);
     });
     });
+
+    it('fix controlled min value didMount', () => {
+        const spyChange = sinon.spy();
+        const inputNumber = mount(
+            <InputNumber min={1} value={0} onChange={spyChange} />
+        );
+        expect(spyChange.calledOnce).toBe(true);
+    });
+
+    it('fix controlled min value didUpdate', () => {
+        const spyChange = sinon.spy();
+        const value = undefined;
+        const inputNumber = mount(
+            <InputNumber min={1} value={value} onChange={spyChange} />
+        );
+        inputNumber.setProps({ value: 0 });
+        expect(spyChange.calledTwice).toBe(true);
+        expect(spyChange.getCall(0).args[0]).toEqual('');
+        expect(spyChange.getCall(1).args[0]).toEqual(1);
+    });
+
+    it('fix controlled min value form field', () => {
+        const spyChange = sinon.spy();
+        let formApi = null;
+        let getFormApi = api => {
+            formApi = api;
+        };
+        const inputNumber = mount(
+            <Form initValues={{ minControlled: 0 }} getFormApi={getFormApi}>
+                <Form.InputNumber field="minControlled" min={1} onChange={spyChange} />
+            </Form>
+        );
+        expect(spyChange.calledOnce).toBe(true);
+        expect(spyChange.getCall(0).args[0]).toEqual(1);
+        expect(formApi.getValue('minControlled')).toBe(1);
+    });
 });
 });

+ 43 - 1
packages/semi-ui/inputNumber/_story/inputNumber.stories.js

@@ -4,6 +4,7 @@ import './inputNumber.scss';
 import InputNumber from '../index';
 import InputNumber from '../index';
 import Button from '../../button/index';
 import Button from '../../button/index';
 import { withField, Form } from '../../index';
 import { withField, Form } from '../../index';
+import { useFormApi } from '../../form';
 
 
 export default {
 export default {
   title: 'InputNumber',
   title: 'InputNumber',
@@ -656,4 +657,45 @@ export const FixPrecision = () => {
         <InputNumber keepFocus onBlur={() => console.log('blur')} onChange={v => setValue2(v)} value={value2} style={{ width: 190 }} precision={2} />
         <InputNumber keepFocus onBlur={() => console.log('blur')} onChange={v => setValue2(v)} value={value2} style={{ width: 190 }} precision={2} />
     </div>
     </div>
   );
   );
-}
+}
+
+/**
+ * 受控传超出 min value 的值,需要触发 onChange
+ * 不然在 Form 中使用可能会导致 Form State 与 InputNumber 展示的值不同问题
+ */
+export const FixMinValue = () => {
+  const [value, setValue] = useState();
+  const formRef = useFormApi();
+  return (
+      <div style={{ width: 280 }}>
+          <Button onClick={() => setValue(0)}>min=1, setValue=0</Button>
+          <InputNumber
+            min={1}
+            value={value} 
+            onChange={(v, e) => {
+              console.log('inputNumber1 change', `'${v}'`, e);
+              setValue(v);
+            }} 
+          />
+          <InputNumber
+            min={1}
+            value={0} 
+            onChange={(v, e) => {
+              console.log('inputNumber2 change', v, e);
+            }}
+          />
+          <Form initValues={{ minControlled: 0 }}>
+            <Form.InputNumber
+              field='minControlled'
+              min={1}
+              onChange={(v, e) => {
+                console.log('form inputNumber change', v, e);
+              }}
+            />
+          </Form>
+          <Button onClick={() => formRef.current.setValue('minControlled', 0) }>set form value</Button>
+          <Button onClick={() => { console.log('form value', JSON.stringify(formRef.current.getValues()))}}>get form values</Button>
+      </div>
+  );
+}
+FixMinValue.storyName = 'fix min value';

+ 22 - 14
packages/semi-ui/inputNumber/index.tsx

@@ -10,13 +10,13 @@ import Input, { InputProps } from '../input';
 import { forwardStatics } from '@douyinfe/semi-foundation/utils/object';
 import { forwardStatics } from '@douyinfe/semi-foundation/utils/object';
 import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 import isBothNaN from '@douyinfe/semi-foundation/utils/isBothNaN';
 import isBothNaN from '@douyinfe/semi-foundation/utils/isBothNaN';
-import InputNumberFoundation, { InputNumberAdapter } from '@douyinfe/semi-foundation/inputNumber/foundation';
+import InputNumberFoundation, { BaseInputNumberState, InputNumberAdapter } from '@douyinfe/semi-foundation/inputNumber/foundation';
 import BaseComponent from '../_base/baseComponent';
 import BaseComponent from '../_base/baseComponent';
 import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/inputNumber/constants';
 import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/inputNumber/constants';
 import { IconChevronUp, IconChevronDown } from '@douyinfe/semi-icons';
 import { IconChevronUp, IconChevronDown } from '@douyinfe/semi-icons';
 
 
 import '@douyinfe/semi-foundation/inputNumber/inputNumber.scss';
 import '@douyinfe/semi-foundation/inputNumber/inputNumber.scss';
-import { isNaN, noop } from 'lodash';
+import { isNaN, isString, noop } from 'lodash';
 import { ArrayElement } from '../_base/base';
 import { ArrayElement } from '../_base/base';
 
 
 export interface InputNumberProps extends InputProps {
 export interface InputNumberProps extends InputProps {
@@ -54,12 +54,7 @@ export interface InputNumberProps extends InputProps {
     onUpClick?: (value: string, e: React.MouseEvent<HTMLButtonElement>) => void;
     onUpClick?: (value: string, e: React.MouseEvent<HTMLButtonElement>) => void;
 }
 }
 
 
-export interface InputNumberState {
-    value?: number | string;
-    number?: number | null; // Current parsed numbers
-    focusing?: boolean;
-    hovering?: boolean;
-}
+export interface InputNumberState extends BaseInputNumberState {}
 
 
 class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
 class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
     static propTypes = {
     static propTypes = {
@@ -222,6 +217,9 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
             },
             },
             setClickUpOrDown: value => {
             setClickUpOrDown: value => {
                 this.clickUpOrDown = value;
                 this.clickUpOrDown = value;
+            },
+            updateStates: (states, callback) => {
+                this.setState(states, callback);
             }
             }
         };
         };
     }
     }
@@ -250,13 +248,15 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
     componentDidUpdate(prevProps: InputNumberProps) {
     componentDidUpdate(prevProps: InputNumberProps) {
         const { value } = this.props;
         const { value } = this.props;
         const { focusing } = this.state;
         const { focusing } = this.state;
+        let newValue;
         /**
         /**
          * To determine whether the front and back are equal
          * To determine whether the front and back are equal
          * NaN need to check whether both are NaN
          * NaN need to check whether both are NaN
          */
          */
         if (value !== prevProps.value && !isBothNaN(value, prevProps.value)) {
         if (value !== prevProps.value && !isBothNaN(value, prevProps.value)) {
             if (isNullOrUndefined(value) || value === '') {
             if (isNullOrUndefined(value) || value === '') {
-                this.setState({ value: '', number: null });
+                newValue = '';
+                this.foundation.updateStates({ value: newValue, number: null });
             } else {
             } else {
                 let valueStr = value;
                 let valueStr = value;
                 if (typeof value === 'number') {
                 if (typeof value === 'number') {
@@ -306,22 +306,30 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
                          */
                          */
                         if (this.clickUpOrDown) {
                         if (this.clickUpOrDown) {
                             obj.value = this.foundation.doFormat(valueStr, true);
                             obj.value = this.foundation.doFormat(valueStr, true);
+                            newValue = obj.value;
                         }
                         }
-                        this.setState(obj, () => this.adapter.restoreCursor());
+                        this.foundation.updateStates(obj, () => this.adapter.restoreCursor());
                     } else if (!isNaN(toNum)) {
                     } else if (!isNaN(toNum)) {
                         // Update input content when controlled input is illegal and not NaN
                         // Update input content when controlled input is illegal and not NaN
-                        this.setState({ value: this.foundation.doFormat(toNum, false) });
+                        newValue = this.foundation.doFormat(toNum, false);
+                        this.foundation.updateStates({ value: newValue });
                     } else {
                     } else {
                         // Update input content when controlled input NaN
                         // Update input content when controlled input NaN
-                        this.setState({ value: this.foundation.doFormat(valueStr, false) });
+                        newValue = this.foundation.doFormat(valueStr, false);
+                        this.foundation.updateStates({ value: newValue });
                     }
                     }
                 } else if (this.foundation.isValidNumber(parsedNum)) {
                 } else if (this.foundation.isValidNumber(parsedNum)) {
-                    this.setState({ number: parsedNum, value: this.foundation.doFormat(parsedNum) });
+                    newValue = this.foundation.doFormat(parsedNum);
+                    this.foundation.updateStates({ number: parsedNum, value: newValue });
                 } else {
                 } else {
                     // Invalid digital analog blurring effect instead of controlled failure
                     // Invalid digital analog blurring effect instead of controlled failure
-                    this.setState({ number: null, value: '' });
+                    newValue = '';
+                    this.foundation.updateStates({ number: null, value: newValue });
                 }
                 }
             }
             }
+            if (isString(newValue) && newValue !== String(this.props.value)) {
+                this.foundation.notifyChange(newValue, null);
+            }
         }
         }
 
 
         if (!this.clickUpOrDown) {
         if (!this.clickUpOrDown) {