Browse Source

Merge branch 'main' into release

pointhalo 1 year ago
parent
commit
8f03b68264

+ 2 - 0
content/show/image/index-en-US.md

@@ -469,6 +469,8 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | className        | custom style class name              | string            | - | |
 | crossOrigin      | Passthrough to the crossorigin of the native img tag | 'anonymous' \| 'use-credentials' |-| |
 | fallback         | Custom loading failed display content | ReactNode  | - | |
+| imgCls           | Custom style class name, transparently passed to img node | string            | - | |
+| imgStyle         | Custom styles, transparently passed to img node | CSSProperties     | - | |
 | height           | Image display height                 | number            | - | |
 | onClick          | Click callback on image              | (event: Event) => void | - | |
 | onError          | Load error callback                  | (event: Event) => void | - | |

+ 2 - 0
content/show/image/index.md

@@ -471,6 +471,8 @@ import { Image, ImagePreview } from '@douyinfe/semi-ui';
 | crossOrigin       | 透传给原生 img 标签的 crossorigin         | 'anonymous'|'use-credentials'| - | |
 | fallback          | 加载失败容错地址或者自定义加载失败时的显示内容 | ReactNode  | - | |
 | height            | 图片显示高度                             | number            | - | |
+| imgCls            | 自定义样式类名,透传给 img 节点              | string            | - | |
+| imgStyle          | 自定义样式,透传给 img 节点                | CSSProperties     | - | |
 | onClick           | 点击图片的回调                            | (event: any) => void | - | |
 | onError           | 加载错误回调                              | (event: Event) => void | - | |
 | onLoad            | 加载成功回调                              | (event: Event) => void | - | |

+ 11 - 0
cypress/e2e/input.spec.js

@@ -65,4 +65,15 @@ describe('input', () => {
         cy.get('body').click();
         cy.get('.semi-input-wrapper').eq(2).children('input').should('not.be.focused');
     });
+
+    it('input autofocus should focus to text end', () => {
+        cy.visit('http://localhost:6006/iframe.html?args=&id=input--fix-input-auto-focus&viewMode=story');
+        cy.wait(300);
+        cy.window().then(window => {
+            const inputStr = window.document.body.querySelector('.semi-input').value.length;
+            const count = inputStr.length;
+            cy.get('div[data-cy=start]').should('contain.text', inputStr);
+            cy.get('div[data-cy=end]').should('contain.text', inputStr);
+        });
+    });
 });

+ 6 - 0
cypress/e2e/textarea.spec.js

@@ -110,4 +110,10 @@ describe('textarea', () => {
             expect(scrollHeight).eq(clientHeight);
         });
     });
+
+    it('textarea autofocus should focus to text end', () => {
+        cy.visit('http://localhost:6006/iframe.html?args=&id=input--fix-text-area-auto-focus&viewMode=story');
+        cy.get('div[data-cy=start]').should('contain.text', 0);
+        cy.get('div[data-cy=end]').should('contain.text', 0);
+    });
 });

+ 27 - 1
cypress/e2e/treeSelect.spec.js

@@ -151,7 +151,7 @@ describe('treeSelect', () => {
         cy.get('.semi-tree-option').should('have.length', 4);
     });
 
-    it.only('filterTreeNode + loadData', () => {
+    it('filterTreeNode + loadData', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=treeselect--load-data');
         cy.get('.semi-tree-select-selection').eq(0).trigger('click');
         cy.wait(1000);
@@ -160,6 +160,32 @@ describe('treeSelect', () => {
         cy.get('.semi-tree-select-selection').eq(0).trigger('click');
         cy.wait(1000);
         cy.get('.semi-tree-option.semi-tree-option-level-1.semi-tree-option-selected.semi-tree-option-collapsed').should('exist');
+    });
+
+    it('multiple, checkRelation = unRelated, triggerRender', () => {
+        cy.visit("http://127.0.0.1:6006/iframe.html?id=treeselect--trigger-render-add-method");
+        cy.get('.semi-tagInput').eq(1).trigger('click');
+        cy.get('.semi-tree-option').eq(2).trigger('click');
+        cy.get('.semi-tree-option').eq(3).trigger('click');
+        cy.get('.semi-tagInput-wrapper').eq(1).get('.semi-tag-content').should('have.length', 2);
+        cy.get('.semi-tagInput-wrapper').eq(1).get('.semi-tag-content').eq(0).contains('北京');
+        cy.get('.semi-tagInput-wrapper').eq(1).get('.semi-tag-content').eq(1).contains('上海');
+    });
+
+    it('multiple, checkRelation = related, triggerRender', () => {
+        cy.visit("http://127.0.0.1:6006/iframe.html?id=treeselect--trigger-render-add-method");
+        cy.get('.semi-tagInput').eq(0).trigger('click');
+        cy.get('.semi-tree-option').eq(2).trigger('click');
+        cy.get('.semi-tree-option').eq(3).trigger('click');
+        cy.get('.semi-tagInput-wrapper').eq(1).get('.semi-tag-content').should('have.length', 1);
+        cy.get('.semi-tagInput-wrapper').eq(1).get('.semi-tag-content').eq(0).contains('亚洲');
+    });
+
+    it('single, triggerRender', () => {
+        cy.visit("http://127.0.0.1:6006/iframe.html?id=treeselect--trigger-render-add-method");
+        cy.get('.semi-button').eq(0).trigger('click');
+        cy.get('.semi-tree-option').eq(0).trigger('click');
+        cy.get('.semi-button-content-left').eq(0).contains('亚洲');
     })
 });
 

+ 0 - 1
packages/semi-foundation/banner/banner.scss

@@ -20,7 +20,6 @@ $module: #{$prefix}-banner;
         .#{$module}-content {
             display: flex;
             flex: 1;
-            align-items: top;
         }
     }
 

+ 0 - 14
packages/semi-foundation/input/foundation.ts

@@ -40,10 +40,6 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
         super({ ...InputFoundation.inputDefaultAdapter, ...adapter });
     }
 
-    init() {
-        this._setInitValue();
-    }
-
     destroy() {
         if (this._timer) {
             clearTimeout(this._timer);
@@ -53,16 +49,6 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
 
     setDisable() {}
 
-    _setInitValue() {
-        const { defaultValue, value } = this.getProps();
-        let v = defaultValue;
-        if (this._isControlledComponent()) {
-            v = value;
-        }
-        this._adapter.setValue(v);
-    // this.checkAllowClear(v);
-    }
-
     setValue(value: any) {
         this._adapter.setValue(value);
     }

+ 0 - 16
packages/semi-foundation/input/textareaFoundation.ts

@@ -48,24 +48,8 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter>
         });
     }
 
-    init() {
-        this.setInitValue();
-    }
-
     destroy() { }
 
-    setInitValue() {
-        const {
-            defaultValue,
-            value
-        } = this.getProps();
-        let v = defaultValue;
-        if (this._isControlledComponent()) {
-            v = value;
-        }
-        this._adapter.setValue(v);
-    }
-
     handleValueChange(v: string) {
         this._adapter.setValue(v);
     }

+ 9 - 0
packages/semi-foundation/tooltip/foundation.ts

@@ -189,6 +189,14 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
         }
     }
 
+    updateStateIfCursorOnTrigger = (trigger: HTMLElement)=>{
+        if (trigger?.matches?.(":hover")) {
+            const eventNames = this._adapter.getEventName();
+            const triggerEventSet = this.getState("triggerEventSet");
+            triggerEventSet[eventNames.mouseEnter]?.();
+        }
+    }
+
     _generateEvent(types: ArrayElement<typeof strings.TRIGGER_SET>) {
         const eventNames = this._adapter.getEventName();
         const triggerEventSet = {
@@ -647,6 +655,7 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
         return style;
     }
 
+
     /**
      * 耦合的东西比较多,稍微罗列一下:
      *

+ 17 - 0
packages/semi-ui/image/__test__/image.test.js

@@ -0,0 +1,17 @@
+import { Image } from '../../index';
+import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
+
+
+describe('Image', () => {
+    it('custom imgCls & imgStyle', () => {
+        let spyOnClick = sinon.spy(() => { });
+        const imageComponent = (<Image 
+            imgCls="custom-img-cls"
+            imgStyle={{ maxWidth: 300}}
+            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
+        />);
+        const image = mount(imageComponent, { attachTo: document.getElementById('container') });
+        expect(image.find(`.${BASE_CLASS_PREFIX}-image-img`).at(0).hasClass('custom-img-cls')).toBe(true);
+        expect(image.find(`.${BASE_CLASS_PREFIX}-image-img`).at(0).getDOMNode().style.maxWidth).toBe('300px');
+    });
+})

+ 8 - 0
packages/semi-ui/image/_story/image.stories.jsx

@@ -105,6 +105,14 @@ export const basicImage = () => {
     </>
 )}
 
+export const ImgClsAndStyle = () => {
+    return <Image 
+        imgCls="custom-img-cls"
+        imgStyle={{ maxWidth: 300}}
+        src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
+    />
+}
+
 export const LoadErrorImage = () => (
     <>
         <p>加载失败默认样式</p>

+ 6 - 1
packages/semi-ui/image/image.tsx

@@ -174,7 +174,10 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
 
     render() {
         const { src, loadStatus, previewVisible } = this.state;
-        const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, fallback, placeholder, imageID, setDownloadName, ...restProps } = this.props;
+        const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, 
+            fallback, placeholder, imageID, setDownloadName, imgCls, imgStyle,
+            ...restProps 
+        } = this.props;
         const outerStyle = Object.assign({ width, height }, style);
         const outerCls = cls(prefixCls, className);
         const canPreview = loadStatus === "success" && preview && !this.isInGroup();
@@ -197,9 +200,11 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
                     src={this.isInGroup() && this.isLazyLoad() ? undefined : src}
                     data-src={src}
                     alt={alt}
+                    style={imgStyle}
                     className={cls(`${prefixCls}-img`, {
                         [`${prefixCls}-img-preview`]: showPreviewCursor,
                         [`${prefixCls}-img-error`]: loadStatus === "error",
+                        [imgCls]: Boolean(imgCls),
                     })}
                     width={width}
                     height={height}

+ 3 - 1
packages/semi-ui/image/interface.tsx

@@ -25,7 +25,9 @@ export interface ImageProps extends BaseProps{
     crossOrigin?: "anonymous"| "use-credentials";
     children?: ReactNode;
     imageID?: number;
-    setDownloadName?: (src: string) => string
+    setDownloadName?: (src: string) => string;
+    imgStyle?: React.CSSProperties;
+    imgCls?: string
 }
 
 export interface PreviewProps extends BaseProps {

+ 38 - 1
packages/semi-ui/input/_story/input.stories.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useRef } from 'react';
+import React, { useState, useCallback, useRef, useEffect } from 'react';
 import GraphemeSplitter from 'grapheme-splitter';
 import { isFunction, isString } from 'lodash';
 
@@ -1031,3 +1031,40 @@ export const TextAutoSizeResize = () => {
     </div>
   )
 };
+
+export const FixInputAutoFocus = () => {
+  const longStr = 'semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design';
+  const inputRef = useRef();
+  const [selection, setSelection] = useState();
+  useEffect(() => {
+    const start = inputRef.current.selectionStart;
+    const end = inputRef.current.selectionEnd;
+    setSelection({ start, end, length: longStr.length });
+  }, []);
+  return (
+    <div>
+      <Input ref={inputRef} style={{ width: 200 }} autoFocus defaultValue={longStr} />
+      <div data-cy="start">start: {selection?.start}</div>
+      <div data-cy="end">end: {selection?.end}</div>
+    </div>
+  )
+};
+
+export const FixTextAreaAutoFocus = () => {
+  const inputRef = useRef();
+  const [selection, setSelection] = useState();
+  useEffect(() => {
+    const start = inputRef.current.selectionStart;
+    const end = inputRef.current.selectionEnd;
+    setSelection({ start, end });
+  }, []);
+  const longStr = 'semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design semi design';
+  return (
+    <div>
+      <TextArea ref={inputRef} style={{ width: 200 }} autoFocus defaultValue={longStr} />
+      <div data-cy="start">start: {selection?.start}</div>
+      <div data-cy="end">end: {selection?.end}</div>
+    </div>
+  )
+};
+

+ 3 - 3
packages/semi-ui/input/index.tsx

@@ -157,9 +157,10 @@ class Input extends BaseComponent<InputProps, InputState> {
 
     constructor(props: InputProps) {
         super(props);
+        const initValue = 'value' in props ? props.value : props.defaultValue;
         this.state = {
-            value: '',
-            cachedValue: null, // Cache current props.value value
+            value: initValue,
+            cachedValue: props.value, // Cache current props.value value
             disabled: false,
             props: {},
             isFocus: false,
@@ -223,7 +224,6 @@ class Input extends BaseComponent<InputProps, InputState> {
     componentDidMount(): void {
         // autofocus is changed from the original support of input to the support of manually calling the focus method,
         // so that preventScroll can still take effect under the setting of autofocus
-        this.foundation.init();
         const { disabled, autoFocus, preventScroll } = this.props;
         if (!disabled && (autoFocus || this.props['autofocus'])) {
             this.inputRef.current.focus({ preventScroll });

+ 3 - 1
packages/semi-ui/input/textarea.tsx

@@ -113,12 +113,14 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
 
     constructor(props: TextAreaProps) {
         super(props);
+        const initValue = 'value' in props ? props.value : props.defaultValue;
         this.state = {
-            value: '',
+            value: initValue,
             isFocus: false,
             isHover: false,
             height: 0,
             minLength: props.minLength,
+            cachedValue: props.value,
         };
         this.focusing = false;
         this.foundation = new TextAreaFoundation(this.adapter);

+ 19 - 3
packages/semi-ui/tooltip/index.tsx

@@ -1,5 +1,5 @@
-import React, { isValidElement, cloneElement, CSSProperties } from 'react';
-import ReactDOM from 'react-dom';
+import React, { isValidElement, cloneElement, CSSProperties, ReactInstance } from 'react';
+import ReactDOM, { findDOMNode } from 'react-dom';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { throttle, noop, get, omit, each, isEmpty, isFunction, isEqual } from 'lodash';
@@ -20,7 +20,13 @@ import '@douyinfe/semi-foundation/tooltip/tooltip.scss';
 
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import { isHTMLElement } from '../_base/reactUtils';
-import { getActiveElement, getDefaultPropsFromGlobalConfig, getFocusableElements, stopPropagation } from '../_utils';
+import {
+    getActiveElement,
+    getDefaultPropsFromGlobalConfig,
+    getFocusableElements,
+    runAfterTicks,
+    stopPropagation,
+} from '../_utils';
 import Portal from '../_portal/index';
 import ConfigContext, { ContextValue } from '../configProvider/context';
 import TriangleArrow from './TriangleArrow';
@@ -459,6 +465,16 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         this.mounted = true;
         this.getPopupContainer = this.props.getPopupContainer || this.context.getPopupContainer || defaultGetContainer;
         this.foundation.init();
+        runAfterTicks(()=>{
+            let triggerEle = this.triggerEl.current;
+            if (triggerEle) {
+                if (!(triggerEle instanceof HTMLElement)) {
+                    triggerEle = findDOMNode(triggerEle as ReactInstance);
+                }
+            }
+            this.foundation.updateStateIfCursorOnTrigger(triggerEle as HTMLElement);
+        }, 1);
+
     }
 
     componentWillUnmount() {

+ 23 - 1
packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx

@@ -2284,11 +2284,19 @@ export const triggerRenderAddMethod = () => {
 
   return (
     <>
+      <TreeSelect
+          triggerRender={renderTrigger1}
+          treeData={treeData}
+          placeholder='Single, Custom Trigger'
+          onChange={onValueChange}
+          style={{ width: 300 }}
+      />
+      <br />
       <TreeSelect
           triggerRender={renderTrigger1}
           multiple
           treeData={treeData}
-          placeholder='Custom Trigger'
+          placeholder='Multiple, custom Trigger'
           onChange={onValueChange}
           style={{ width: 300 }}
       />
@@ -2305,6 +2313,7 @@ export const triggerRenderAddMethod = () => {
       />
       <br />
       <TreeSelect
+          defaultExpandAll
           triggerRender={renderTrigger3}
           filterTreeNode
           searchPosition="trigger"
@@ -2314,6 +2323,19 @@ export const triggerRenderAddMethod = () => {
           onChange={onValueChange}
           style={{ width: 300 }}
       />
+      <br />
+       <TreeSelect
+          defaultExpandAll
+          checkRelation={'unRelated'} 
+          triggerRender={renderTrigger3}
+          filterTreeNode
+          searchPosition="trigger"
+          multiple
+          treeData={treeData}
+          placeholder='multiple, checkRelation = unRelated'
+          onChange={onValueChange}
+          style={{ width: 300 }}
+      />
     </>
   );
 }

+ 20 - 24
packages/semi-ui/treeSelect/index.tsx

@@ -446,21 +446,6 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                     props.multiple,
                     valueEntities
                 );
-            } else if ((!isExpandControlled && needUpdateTreeData) && props.value) {
-                // 当 treeData 已经设置具体的值,并且设置了 props.loadData ,则认为 treeData 的更新是因为 loadData 导致的
-                // 如果是因为 loadData 导致 treeData改变, 此时在这里重新计算 key 会导致为未选中的展开项目被收起
-                // 所以此时不需要重新计算 expandedKeys,因为在点击展开按钮时候已经把被展开的项添加到 expandedKeys 中
-                // When treeData has a specific value and props.loadData is set, it is considered that the update of treeData is caused by loadData
-                // If the treeData is changed because of loadData, recalculating the key here will cause the unselected expanded items to be collapsed
-                // So there is no need to recalculate expandedKeys at this time, because the expanded item has been added to expandedKeys when the expand button is clicked
-                if (!(prevState.treeData && prevState.treeData?.length > 0 && props.loadData)) {
-                    newState.expandedKeys = calcExpandedKeysForValues(
-                        props.value,
-                        keyEntities,
-                        props.multiple,
-                        valueEntities
-                    );
-                }
             }
 
             if (!newState.expandedKeys) {
@@ -1082,9 +1067,10 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             searchPosition,
             triggerRender,
             borderless,
+            checkRelation,
             ...rest
         } = this.props;
-        const { inputValue, selectedKeys, checkedKeys, keyEntities, isFocus } = this.state;
+        const { inputValue, selectedKeys, checkedKeys, keyEntities, isFocus, realCheckedKeys } = this.state;
         const filterable = Boolean(filterTreeNode);
         const useCustomTrigger = typeof triggerRender === 'function';
         const mouseEvent = showClear ?
@@ -1119,9 +1105,19 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 },
                 className
             );
-        const triggerRenderKeys = multiple ? normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true) : selectedKeys;
-        const inner = useCustomTrigger ? (
-            <Trigger
+        let inner: React.ReactNode | React.ReactNode[];
+        if (useCustomTrigger) {
+            let triggerRenderKeys = [];
+            if (multiple) {
+                if (checkRelation === 'related') {
+                    triggerRenderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true);
+                } else if (checkRelation === 'unRelated') {
+                    triggerRenderKeys = [...realCheckedKeys];
+                }
+            } else {
+                triggerRenderKeys = selectedKeys;
+            }
+            inner = <Trigger
                 inputValue={inputValue}
                 value={triggerRenderKeys.map((key: string) => get(keyEntities, [key, 'data']))}
                 disabled={disabled}
@@ -1132,9 +1128,9 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 componentProps={{ ...this.props }}
                 onSearch={this.search}
                 onRemove={this.removeTag}
-            />
-        ) : (
-            [
+            />;
+        } else {
+            inner = [
                 <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
                 <Fragment key={'selection'}>
                     <div className={`${prefixcls}-selection`}>{this.renderSelectContent()}</div>
@@ -1148,8 +1144,8 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                     }
                 </Fragment>,
                 <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
-            ]
-        );
+            ];
+        }
         const tabIndex = disabled ? null : 0;
         /**
          * Reasons for disabling the a11y eslint rule: