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

fix(taggroup): 修复 tagGroup closable 关闭后, 数量指示器不变的问题,resolve #945 (#1029)

* fix(taggroup): 修复 tagGroup closable 关闭后, 数量指示器不变的问题

* docs(tag): Supplementary official website description
小丞同学 3 жил өмнө
parent
commit
05abe7a45f

+ 6 - 4
content/show/tag/index-en-US.md

@@ -258,8 +258,10 @@ import { TagGroup } from '@douyinfe/semi-ui';
 | style | Inline style | object |  |  |
 | type | Style type, one of `ghost`, `solid`, `light` | string | `light` |  |
 | visible | Toggle the visibility of the tag | boolean | true |  |
+| tagKey  | The key required by React, as the unique identifier of each tag, does not allow repetition | string | number | |
 | onClick | Callback function when clicking the tag | (e: MouseEvent) => void | - |  |
 | onClose | Callback function when the tag is closed | (tagChildren: ReactNode, e: MouseEvent) => void | - |  |
+
 ### TagGroup
 
 | Properties | Instructions | type | Default | Version |
@@ -282,10 +284,10 @@ import { TagGroup } from '@douyinfe/semi-ui';
 ### Keyboard and Focus
 
 - If the current `Tag` is interactive, then this `Tag` can be focused. Such as:
-   - When the `onClick` attribute is used, the keyboard user can activate this `Tag` with the `Enter` keys
-   - When the `closable` property is `true`, keyboard users can delete this `Tag` by pressing the `Delete` key
-   - When a `Tag` is focused, keyboard users can use the `Esc` key to defocus the currently focused `Tag`
-   
+  - When the `onClick` attribute is used, the keyboard user can activate this `Tag` with the `Enter` keys
+  - When the `closable` property is `true`, keyboard users can delete this `Tag` by pressing the `Delete` key
+  - When a `Tag` is focused, keyboard users can use the `Esc` key to defocus the currently focused `Tag`
+
 ## Design Tokens
 
 <DesignToken/>

+ 16 - 14
content/show/tag/index.md

@@ -16,11 +16,10 @@ brief: 标签是图形化标记界面上的元素的组件,达到快速识别
 import { Tag, TagGroup } from '@douyinfe/semi-ui';
 ```
 
-
 ### 基本用法
 
 基本标签用法,将内容使用 `<Tag>` 标签包裹即可。  
-可以通过添加 `closable` 属性将其变为可关闭标签,此时点击x关闭会触发 onClose 事件,在 onClose 中阻止默认事件可以使其点击后依然显示不隐藏    
+可以通过添加 `closable` 属性将其变为可关闭标签,此时点击x关闭会触发 onClose 事件,在 onClose 中阻止默认事件可以使其点击后依然显示不隐藏
 
 ```jsx live=true
 import React from 'react';
@@ -133,7 +132,7 @@ import { Tag, Button } from '@douyinfe/semi-ui';
     return (
         <div>
             <Button onClick={toggleVisible}>{visible ? 'Hide Tag': 'Show Tag'}</Button>
-            <div style={{marginTop:10}}>
+            <div style={{ marginTop:10 }}>
                 <Tag visible={visible}>Invisible tag </Tag>
             </div>
         </div>
@@ -153,17 +152,17 @@ import { TagGroup } from '@douyinfe/semi-ui';
 
 () => {
     const tagList = [
-        { color: 'white', children:'抖音'},
-        { color: 'white', children:'火山小视频'},
-        { color: 'white', children:'剪映'},
-        { color: 'white', children:'皮皮虾'},
+        { color: 'white', children:'抖音' },
+        { color: 'white', children:'火山小视频' },
+        { color: 'white', children:'剪映' },
+        { color: 'white', children:'皮皮虾' },
     ];
     const src = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/dy.png';
     const tagList2 = [
-        { color: 'white', children:'Douyin', avatarSrc:src},
-        { color: 'white', children:'Hotsoon', avatarSrc:src},
-        { color: 'white', children:'Capcut', avatarSrc:src},
-        { color: 'white', children:'Pipixia', avatarSrc:src},
+        { color: 'white', children:'Douyin', avatarSrc:src },
+        { color: 'white', children:'Hotsoon', avatarSrc:src },
+        { color: 'white', children:'Capcut', avatarSrc:src },
+        { color: 'white', children:'Pipixia', avatarSrc:src },
     ];
     const divStyle = {
         backgroundColor: 'var(--semi-color-fill-0)',
@@ -220,6 +219,7 @@ import { TagGroup } from '@douyinfe/semi-ui';
 | style | 样式 | CSSProperties |     | |
 | type  | 标签的样式类型,可选 `ghost`、 `solid`、 `light` | string  | `light`     | |
 | visible | 标签是否可见 | boolean | true    | |
+| tagKey  | React 需要的 key,作为每个标签的唯一标识,不允许重复 | string | number | |
 | onClick | 单击标签时的回调函数 | (e: MouseEvent) => void | 无   | |
 | onClose | 关闭标签时的回调函数 | (tagChildren: ReactNode, e: MouseEvent) => void | 无    | e于v1.18版本提供 |
 
@@ -245,10 +245,12 @@ import { TagGroup } from '@douyinfe/semi-ui';
 ### 键盘和焦点
 
 - 如果当前 `Tag` 可交互,那么这个 `Tag` 可被聚焦到。如:
-    - 使用了 `onClick` 属性时,键盘用户可以通过 `Enter` 键激活此 `Tag`
-    - `closable` 属性为 `true` 时,键盘用户可以通过 `Delete` 键删除此 `Tag`
-    - `Tag` 被聚焦时,键盘用户可以通过 `Esc` 键使当前聚焦 `Tag` 失焦
+  - 使用了 `onClick` 属性时,键盘用户可以通过 `Enter` 键激活此 `Tag`
+  - `closable` 属性为 `true` 时,键盘用户可以通过 `Delete` 键删除此 `Tag`
+  - `Tag` 被聚焦时,键盘用户可以通过 `Esc` 键使当前聚焦 `Tag` 失焦
+
 ## 设计变量
+
 <DesignToken/>
 
 <!-- ## 相关物料

+ 46 - 0
packages/semi-ui/tag/_story/tag.stories.js

@@ -232,3 +232,49 @@ export const AvatarTagGroup = () => <AvatarTagGroupDemo />;
 AvatarTagGroup.story = {
   name: 'avatar tagGroup',
 };
+
+const TagGroupCloseableDemo = () => {
+  const src = 'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/avatarDemo.jpeg';
+  const tagList2 = [
+      { tagKey: '1', color: 'white', children:'Douyin', avatarSrc:src,closable:true},
+      { tagKey: '2', color: 'white', children:'Hotsoon', avatarSrc:src,closable:true},
+      { tagKey: '3', color: 'white', children:'Capcut', avatarSrc:src,closable:true},
+      { tagKey: '4', color: 'black', children:'bytedance', avatarSrc:src,closable:true},
+      { tagKey: '5', color: 'white', children:'vvvvv', avatarSrc:src,closable:true},
+      { tagKey: '6', color: 'white', children:'Pipixia', avatarSrc:src,closable:true},
+  ];
+  const divStyle = {
+      backgroundColor: 'var(--semi-color-fill-0)',
+      height: 35,
+      width: 300,
+      display: 'flex',
+      alignItems: 'center',
+      padding: '0 10px',
+      marginBottom: 30,
+  };
+  const tagGroupStyle = {
+      display: 'flex',
+      alignItems: 'center',
+      width: 350,
+  };
+  return (
+      <>
+          <div style={divStyle}>
+              <TagGroup
+                  maxTagCount={3}
+                  style={tagGroupStyle}
+                  tagList={tagList2}
+                  size='large'
+                  avatarShape='circle'
+                  showPopover
+              />
+          </div>
+      </>
+  );
+};
+
+export const TagGroupCloseable = () => <TagGroupCloseableDemo />;
+
+TagGroupCloseable.story = {
+  name: 'tagGroup closable',
+}

+ 53 - 6
packages/semi-ui/tag/group.tsx

@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React, { Children, PureComponent } from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/tag/constants';
@@ -23,16 +23,19 @@ export interface TagGroupProps<T> {
     mode?: string;
 }
 
-export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
+export type TagGroupState<T> = Pick<TagGroupProps<T>, 'tagList'>
+export default class TagGroup<T> extends PureComponent<TagGroupProps<T>, TagGroupState<T> > {
     static defaultProps = {
         style: {},
         className: '',
         size: tagSize[0],
         avatarShape: 'square',
+        onClose: () => undefined,
     };
 
     static propTypes = {
         children: PropTypes.node,
+        tagKey: PropTypes.oneOf([PropTypes.string, PropTypes.number]),
         style: PropTypes.object,
         className: PropTypes.string,
         maxTagCount: PropTypes.number,
@@ -40,11 +43,36 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
         tagList: PropTypes.array,
         size: PropTypes.oneOf(tagSize),
         mode: PropTypes.string,
+        onClose: PropTypes.func,
         showPopover: PropTypes.bool,
         popoverProps: PropTypes.object,
         avatarShape: PropTypes.oneOf(avatarShapeSet),
     };
 
+    constructor(props: TagGroupProps<T>) {
+        super(props);
+        this.state = {
+            tagList: this.props.tagList
+        };
+        this.onClose = this.onClose.bind(this);
+    }
+
+    static getDerivedStateFromProps(nextProps, prevState) {
+        // 自定义 TagGroup 需要根据 props 来 rerender, normal TagGroup 根据 state 进行 rerender
+        if (nextProps.mode !== 'custom') {
+            return {
+                tagList: prevState.tagList
+            };
+        }
+        if (prevState.tagList !== nextProps.tagList) {
+            return {
+                tagList: nextProps.tagList
+            };
+        }
+        return null;
+    }
+
+    
     renderNTag(n: number, restTags: React.ReactNode) {
         const { size, showPopover, popoverProps } = this.props;
         let nTag = (
@@ -79,7 +107,9 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
     }
 
     renderMergeTags(tags: (Tag | React.ReactNode)[]) {
-        const { maxTagCount, tagList, restCount } = this.props;
+        const { maxTagCount, restCount } = this.props;
+        const { tagList } = this.state;
+
         const n = restCount ? restCount : tagList.length - maxTagCount;
         let renderTags: (Tag | React.ReactNode)[] = tags;
 
@@ -95,22 +125,39 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
     }
 
     renderAllTags() {
-        const { tagList, size, mode, avatarShape } = this.props;
-        const renderTags = tagList.map((tag, index): (Tag | React.ReactNode) => {
+        const { size, mode, avatarShape } = this.props;
+        const { tagList } = this.state;
+        const renderTags = tagList.map((tag): (Tag | React.ReactNode) => {
             if (mode === 'custom') {
                 return tag as React.ReactNode;
             }
+            
             if (!(tag as TagProps).size) {
                 (tag as TagProps).size = size;
             }
+            
             if (!(tag as TagProps).avatarShape) {
                 (tag as TagProps).avatarShape = avatarShape;
             }
-            return <Tag key={`${index}-tag`} {...(tag as TagProps)} />;
+
+            if (!(tag as TagProps).tagKey) {
+                if (typeof (tag as TagProps).children === 'string' || typeof (tag as TagProps).children === 'number') {
+                    (tag as TagProps).tagKey = (tag as TagProps).children as string | number;
+                } else {
+                    (tag as TagProps).tagKey = Math.random();
+                }
+            }
+            return <Tag {...(tag as TagProps)} key={(tag as TagProps).tagKey} onClose={this.onClose} />;
         });
         return renderTags;
     }
 
+    onClose(value, e, tagKey) {
+        const { tagList } = this.state;
+        const newTagList = tagList.filter(tag => (tag as TagProps).tagKey !== tagKey);
+        this.setState({ tagList: newTagList });
+    }
+
     render() {
         const { style, className, maxTagCount, size } = this.props;
 

+ 6 - 5
packages/semi-ui/tag/index.tsx

@@ -40,6 +40,7 @@ export default class Tag extends Component<TagProps, TagState> {
 
     static propTypes = {
         children: PropTypes.node,
+        tagKey: PropTypes.oneOf([PropTypes.string, PropTypes.number]),
         size: PropTypes.oneOf(tagSize),
         color: PropTypes.oneOf(tagColors),
         type: PropTypes.oneOf(tagType),
@@ -79,11 +80,11 @@ export default class Tag extends Component<TagProps, TagState> {
         }
     }
 
-    close(e: React.MouseEvent<HTMLElement>, value: React.ReactNode) {
+    close(e: React.MouseEvent<HTMLElement>, value: React.ReactNode, tagKey: string | number) {
         const { onClose } = this.props;
         e.stopPropagation();
         e.nativeEvent.stopImmediatePropagation();
-        onClose && onClose(value, e);
+        onClose && onClose(value, e, tagKey);
         // when user call e.preventDefault() in onClick callback, tag will not hidden
         if (e.defaultPrevented) {
             return;
@@ -96,7 +97,7 @@ export default class Tag extends Component<TagProps, TagState> {
         switch (event.key) {
             case "Backspace":
             case "Delete":
-                closable && this.close(event, this.props.children);
+                closable && this.close(event, this.props.children, this.props.tagKey);
                 handlePrevent(event);
                 break;
             case "Enter":
@@ -119,7 +120,7 @@ export default class Tag extends Component<TagProps, TagState> {
     }
 
     render() {
-        const { children, size, color, closable, visible, onClose, onClick, className, type, avatarSrc, avatarShape, tabIndex, ...attr } = this.props;
+        const { tagKey, children, size, color, closable, visible, onClose, onClick, className, type, avatarSrc, avatarShape, tabIndex, ...attr } = this.props;
         const { visible: isVisible } = this.state;
         const clickable = onClick !== Tag.defaultProps.onClick || closable;
         // only when the Tag is clickable or closable, the value of tabIndex is allowed to be passed in. 
@@ -145,7 +146,7 @@ export default class Tag extends Component<TagProps, TagState> {
         const wrapProps = clickable ? ({ ...baseProps, ...a11yProps }) : baseProps;
         const closeIcon = closable ? (
             // eslint-disable-next-line jsx-a11y/click-events-have-key-events
-            <div className={`${prefixCls}-close`} onClick={e => this.close(e, children)}>
+            <div className={`${prefixCls}-close`} onClick={e => this.close(e, children, tagKey)}>
                 <IconClose size="small" />
             </div>
         ) : null;

+ 3 - 2
packages/semi-ui/tag/interface.ts

@@ -21,13 +21,14 @@ export type TagSize = 'default' | 'small' | 'large';
 export type AvatarShape = 'circle' | 'square';
 
 export interface TagProps {
-    children?: React.ReactNode;
+    children?: React.ReactNode | string | number;
+    tagKey?: string | number;
     size?: TagSize;
     color?: TagColor;
     type?: TagType;
     closable?: boolean;
     visible?: boolean;
-    onClose?: (tagChildren: React.ReactNode, event: React.MouseEvent<HTMLElement>) => void;
+    onClose?: (tagChildren: React.ReactNode, event: React.MouseEvent<HTMLElement>, tagKey: string | number) => void;
     onClick?: React.MouseEventHandler<HTMLDivElement>;
     style?: React.CSSProperties;
     className?: string;