Răsfoiți Sursa

fix: [Select] modify the tags of trigger display when maxTagCount is exists (#1353)

* fix: select +n tag be block when width is no enough

* fix: modify the tags of trigger display when maxTagCount is exsit

* fix: remove useless updateOverflowItemCount in single selection

* feat: add ellipsisTrigger and improve demo

* feat: improve docsite

Co-authored-by: pointhalo <[email protected]>
YannLynn 2 ani în urmă
părinte
comite
1b29bcd154

+ 24 - 0
content/input/select/index-en-US.md

@@ -68,11 +68,16 @@ import { Select } from '@douyinfe/semi-ui';
 ```
 
 ### Multi-choice
+Since v2.28, the selector will have its own maxHeight 300, and the content can be viewed by scrolling vertically after it exceeds.
 
 Configuration `multiple` properties that can support multi-selection
 
 Configuration `maxTagCount`. You can limit the number of options displayed, and the excess will be displayed in the form of + N
 
+Configure `ellipsisTrigger` (>= v2.28.0) to do adaptive processing on the overflow part of the tag. When the width is insufficient, the last tag content will be truncated. After enabling this function, there will be a certain performance loss, and it is not recommended to use it in large form scenarios
+
+Configure `expandRestTagsOnClick` (>= v2.28.0) to display all remaining tags by clicking when `maxTagCount` is set
+
 Use `showRestTagsPopover` (>= v2.22.0) to set whether hover +N displays Popover after exceeding `maxTagCount`, the default is `false`. Also, popovers can be configured in the `restTagsPopoverProps` property
 
 Configuration `max` Properties can limit the maximum number of options and cannot be selected beyond the maximum limit, while triggering`On Exceed`callback
@@ -119,6 +124,23 @@ import { Select } from '@douyinfe/semi-ui';
             <Select.Option value="pipixia">Pipixia</Select.Option>
             <Select.Option value="xigua">BuzzVideo</Select.Option>
         </Select>
+        <br />
+        <br />
+        <Select
+            multiple
+            maxTagCount={2}
+            showRestTagsPopover={true}
+            restTagsPopoverProps={{ position: 'top' }}
+            style={{ width: '220px' }}
+            defaultValue={['xigua', 'hotsoon', 'pipixia', 'abc']}
+            ellipsisTrigger
+            expandRestTagsOnClick
+        >
+            <Select.Option value="abc">Semi</Select.Option>
+            <Select.Option value="hotsoon">Hotsoon</Select.Option>
+            <Select.Option value="pipixia">Pipixia</Select.Option>
+            <Select.Option value="xigua">BuzzVideo</Select.Option>
+        </Select>
     </>
 );
 ```
@@ -1311,6 +1333,8 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | dropdownMargin | Popup layer calculates the size of the safe area when the current direction overflows, used in scenes covered by fixed elements, more detail refer to [issue#549](https://github.com/DouyinFE/semi-design/issues/549), same as Tooltip margin | object\|number |  | 2.25.0 |
 | dropdownStyle | The inline style of the pop-up layer | object |  |
 | emptyContent | Content displayed when there is no result. When set to null, the drop-down list will not be displayed | string | ReactNode |  |
+| ellipsisTrigger | When maxTagCount exists and is multi-select, whether to perform adaptive processing on the overflow part of the tag(When the width is insufficient, the last tag content is truncated). After enabling this function, there will be a certain performance loss, and it is not recommended to use it in large form scenarios  | boolean   | false       | 2.28.0 | 
+| expandRestTagsOnClick | When maxTagCount exists and is multi-selected, select whether to expand redundant Tags when the panel is open       | boolean                          | false       | 2.28.0 | 
 | filter | Whether searchable or not, the default is false. When `true` is passed, it means turn on search ability, default filtering policy is whether the label matches search input<br/>When the input type is function, the function arguments are searchInput, option. It should return true when the option meets the filtering conditions, otherwise it returns false. | false | boolean\|function |  |
 | getPopupContainer | Specifies the parent DOM, and the popup layer will be rendered to the DOM, you need to set 'position: relative`| function(): HTMLElement | () => document.body |
 | inputProps | When filter is true, the additional configuration parameters of the input, please refer to the Input component for specific configurable properties (note: please do not pass in `value`, `ref`, `onChange`, `onFocus`, otherwise it will override Select related callbacks and affect component behavior)  | object | | 2.2.0|

+ 25 - 1
content/input/select/index.md

@@ -79,11 +79,16 @@ import { Select } from '@douyinfe/semi-ui';
 ```
 
 ### 多选
+自 v2.28后,select 的选择器会自带 maxHeight 300,内容超出后可以通过垂直滚动查看。
 
 配置`multiple`属性,可以支持多选
 
 配置 `maxTagCount` 可以限制已选项展示的数量,超出部分将以+N 的方式展示
 
+配置 `ellipsisTrigger` (>= v2.28.0) 对溢出部分的 tag 做自适应处理,当宽度不足时,最后一个tag内容作截断处理。开启该功能后会有一定性能损耗,不推荐在大表单场景下使用
+
+配置 `expandRestTagsOnClick` (>= v2.28.0) 可以在设置 `maxTagCount` 情况下通过点击展示全剩余的tag
+
 使用 `showRestTagsPopover` (>= v2.22.0) 可以设置在超出 `maxTagCount` 后,hover +N 是否显示 Popover,默认为 `false`。并且,还可以在 `restTagsPopoverProps` 属性中配置 Popover。
 
 配置 `max` 属性可限制最大可选的数量,超出最大限制数量后无法选中,同时会触发`onExceed`回调
@@ -115,7 +120,7 @@ import { Select } from '@douyinfe/semi-ui';
             <Select.Option value="jianying">剪映</Select.Option>
             <Select.Option value="xigua">西瓜视频</Select.Option>
         </Select>
-
+        
         <br />
         <br />
         <Select
@@ -130,6 +135,23 @@ import { Select } from '@douyinfe/semi-ui';
             <Select.Option value="jianying">剪映</Select.Option>
             <Select.Option value="xigua">西瓜视频</Select.Option>
         </Select>
+        <br />
+        <br />
+        <Select
+            multiple
+            maxTagCount={2}
+            showRestTagsPopover={true}
+            restTagsPopoverProps={{ position: 'top' }}
+            style={{ width: '220px' }}
+            defaultValue={['xigua', 'ulikecam', 'jianying', 'abc']}
+            ellipsisTrigger
+            expandRestTagsOnClick
+        >
+            <Select.Option value="abc">抖音</Select.Option>
+            <Select.Option value="ulikecam">轻颜相机</Select.Option>
+            <Select.Option value="jianying">剪映</Select.Option>
+            <Select.Option value="xigua">西瓜视频</Select.Option>
+        </Select>
     </>
 );
 ```
@@ -1374,6 +1396,8 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | dropdownStyle | 弹出层的样式 | object |  |
 | dropdownMargin | 弹出层计算溢出时的增加的冗余值,详见[issue#549](https://github.com/DouyinFE/semi-design/issues/549),作用同 Tooltip margin | object\|number |  | 2.25.0 |
 | emptyContent | 无结果时展示的内容。设为 null 时,下拉列表将不展示 | string\|ReactNode |  |
+| ellipsisTrigger | 当 maxTagCount 存在且为多选时,是否对溢出部分的 tag 做自适应处理(当宽度不足时,最后一个tag内容作截断处理)。开启该功能后会有一定性能损耗,不推荐在大表单场景下使用       | boolean   | false       | 2.28.0 | 
+| expandRestTagsOnClick | 当maxTagCount存在且为多选时,select 在面板打开状态下是否展开多余的 Tag        | boolean   | false       | 2.28.0 | 
 | filter | 是否可搜索,默认为 false。传入 true 时,代表开启搜索并采用默认过滤策略(label 是否与 sugInput 匹配),传入值为函数时,会接收 sugInput, option 两个参数,当 option 符合筛选条件应返回 true,否则返回 false | boolean \|function(sugInput, option) | false |
 | getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` | function():HTMLElement | () => document.body |
 | inputProps | filter 为 true 时, input 输入框的额外配置参数,具体可配置属性请参考 Input 组件(注意:请不要传入 value、ref、onChange、onFocus,否则会覆盖 Select 相关回调,影响组件行为) | object |  | 2.2.0 |

+ 20 - 0
packages/semi-foundation/select/foundation.ts

@@ -46,6 +46,7 @@ export interface SelectAdapter<P = Record<string, any>, S = Record<string, any>>
     notifyMouseEnter(event: any): void;
     updateHovering(isHover: boolean): void;
     updateScrollTop(index?: number): void;
+    updateOverflowItemCount(count: number): void;
     getContainer(): any;
     getFocusableElements(node: any): any[];
     getActiveElement(): any;
@@ -220,6 +221,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
             selections = this._updateSingle(propValue, originalOptions);
         } else {
             selections = this._updateMultiple(propValue as (PropValue)[], originalOptions);
+            this.updateOverflowItemCount(selections.size);
         }
         // Update the text in the selection box
         this._adapter.updateSelection(selections);
@@ -464,6 +466,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
         } else {
             // Uncontrolled components, update ui
             this._adapter.updateSelection(selections);
+            this.updateOverflowItemCount(selections.size);
             // In multi-select mode, the drop-down pop-up layer is repositioned every time the value is changed, because the height selection of the selection box may have changed
             this._adapter.rePositionDropdown();
             let { options } = this.getStates();
@@ -531,6 +534,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
         } else {
             this._notifyDeselect(item.value, item);
             this._adapter.updateSelection(selections);
+            this.updateOverflowItemCount(selections.size);
             this.updateOptionsActiveStatus(selections);
             // Repostion drop-down layer, because the selection may have changed the number of rows, resulting in a height change
             this._adapter.rePositionDropdown();
@@ -1102,6 +1106,22 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
         this._adapter.updateScrollTop();
     }
 
+    updateOverflowItemCount(selectionLength: number, overFlowCount?: number) {
+        const { maxTagCount, ellipsisTrigger } = this.getProps();
+        if (!ellipsisTrigger) {
+            return ;
+        }
+        if (overFlowCount) {
+            this._adapter.updateOverflowItemCount(overFlowCount);
+        } else if (typeof maxTagCount === 'number') {
+            if (selectionLength - maxTagCount > 0) {
+                this._adapter.updateOverflowItemCount(selectionLength - maxTagCount);
+            } else {
+                this._adapter.updateOverflowItemCount(0);
+            }
+        }
+    }
+
     updateIsFullTags() {
         const { isFullTags } = this.getStates();
         if (!isFullTags) {

+ 27 - 0
packages/semi-foundation/select/select.scss

@@ -8,6 +8,7 @@ $module: #{$prefix}-select;
 $single: #{$module}-single;
 $filterable: #{$module}-filterable;
 $multiple: #{$module}-multiple;
+$overflowList: #{$prefix}-overflow-list;
 
 .#{$module} {
     box-sizing: border-box;
@@ -24,6 +25,10 @@ $multiple: #{$module}-multiple;
     cursor: pointer;
     transition: background-color $transition_duration-select-bg $transition_function-select-bg $transition_delay-select-bg, border $transition_duration-select-border $transition_function-select-border $transition_delay-select-border;
     transform: scale($transform_scale-select);
+
+    max-height: 300px;
+    overflow-y: auto;
+
     &:hover {
         background-color: $color-select-bg-hover;
         border: $width-select-border-hover solid $color-select-border-hover;
@@ -198,6 +203,28 @@ $multiple: #{$module}-multiple;
         display: flex;
         align-items: center;
         height: 100%;
+
+        &-collapse {
+            display: inline-flex;
+            flex-shrink: 0;
+            width: 100%;
+
+            .#{$overflowList}-overflow {
+                max-width: 100%; // when one tag exceed the max width
+                min-width: 50px;
+            }
+
+            &>&-tag {
+                background-color: transparent;
+            }
+
+            &>&-N {
+                background-color: transparent;
+                padding: 4px;
+                color: var(--semi-color-text-0);
+                font-size: 12px;
+            }
+        }
     }
 
     &-multiple {

+ 91 - 2
packages/semi-ui/overflowList/_story/overflowList.stories.jsx

@@ -439,5 +439,94 @@ OverflowListWithSlide.story = {
   name: 'overflowList with slide',
 };
 
-// TODO large data will cause memory heap
-// stories.add('large amount of data', () => <LargeData />);
+export const FixDisplayFlexDemo = () =>{
+    const [width, setWidth] = useState(100);
+    const renderOverflow = items => {
+          return items.length ? <Tag style={{ flex: '0 0 auto' }}>+{items.length}</Tag> : null;
+      };
+    const renderItem = (item, ind) => {
+        return (
+            <Tag color="blue" key={item.key} style={{ marginRight: 8, flex: '0 0 auto' }}>
+                {item.icon}
+                {item.key}
+            </Tag>
+        );
+    };
+
+    const items = [
+        { icon: <IconAlarm style={{ marginRight: 4 }} />, key: 'alarm' },
+        { icon: <IconBookmark style={{ marginRight: 4 }} />, key: 'bookmark' },
+        { icon: <IconCamera style={{ marginRight: 4 }} />, key: 'camera' },
+        { icon: <IconDuration style={{ marginRight: 4 }} />, key: 'duration' },
+        { icon: <IconEdit style={{ marginRight: 4 }} />, key: 'edit' },
+        { icon: <IconFolder style={{ marginRight: 4 }} />, key: 'folder' },
+    ];
+
+    return (
+        <div>
+            <Slider step={1} value={width} onChange={value => setWidth(value)} />
+            <br />
+            <br />
+            <div style={{ width: `${width}%`, display: 'flex' }}>
+                <OverflowList
+                    style={{ width: '100%' }}
+                    items={items}
+                    minVisibleItems={3}
+                    overflowRenderer={renderOverflow}
+                    visibleItemRenderer={renderItem}
+                />
+            </div>
+        </div>
+    );
+}
+
+FixDisplayFlexDemo.story = {
+  name: 'overflowList with display flex',
+};
+
+export const FixFirstLongTagDemo = () =>{
+    const [width, setWidth] = useState(20);
+    const renderOverflow = items => {
+          return items.length ? <Tag style={{ flex: '0 0 auto' }}>+{items.length}</Tag> : null;
+      };
+    const renderItem = (item, ind) => {
+        return (
+            <Tag color="blue" key={item.key} style={{ marginRight: 8, flex: '0 0 auto' }}>
+                {item.icon}
+                {item.key}
+            </Tag>
+        );
+    };
+
+    const items = [
+        { icon: <IconAlarm style={{ marginRight: 4, width: 400 }} />, key: 'alarm' },
+        { icon: <IconBookmark style={{ marginRight: 4 }} />, key: 'bookmark' },
+        { icon: <IconCamera style={{ marginRight: 4 }} />, key: 'camera' },
+        { icon: <IconDuration style={{ marginRight: 4 }} />, key: 'duration' },
+        { icon: <IconEdit style={{ marginRight: 4 }} />, key: 'edit' },
+        { icon: <IconFolder style={{ marginRight: 4 }} />, key: 'folder' },
+    ];
+
+    return (
+        <div>
+            <div>修复第一个item就溢出, 不触发 onOverflow 问题</div>
+            <Slider step={1} value={width} onChange={value => setWidth(value)} />
+            <br />
+            <br />
+            <div style={{ width: `${width}%` }}>
+                <OverflowList
+                    items={items}
+                    onOverflow={(items)=>{
+                      console.log('触发了onOverflow', items);
+                    }}
+                    overflowRenderer={renderOverflow}
+                    visibleItemRenderer={renderItem}
+                />
+            </div>
+        </div>
+    );
+}
+
+FixFirstLongTagDemo.story = {
+  name: 'overflowList with first long tag',
+};

+ 22 - 30
packages/semi-ui/overflowList/index.tsx

@@ -3,7 +3,7 @@ import React, { CSSProperties, ReactNode, MutableRefObject, RefCallback, Key, Re
 import cls from 'classnames';
 import BaseComponent from '../_base/baseComponent';
 import PropTypes from 'prop-types';
-import { isEqual, omit, isNull, isUndefined, isFunction, get } from 'lodash';
+import { isEqual, isFunction, get } from 'lodash';
 import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/overflowList/constants';
 import ResizeObserver, { ResizeEntry } from '../resizeObserver';
 import IntersectionObserver from './intersectionObserver';
@@ -11,6 +11,7 @@ import IntersectionObserver from './intersectionObserver';
 import OverflowListFoundation, { OverflowListAdapter } from '@douyinfe/semi-foundation/overflowList/foundation';
 
 import '@douyinfe/semi-foundation/overflowList/overflowList.scss';
+import { cloneDeep } from '../_utils';
 
 const prefixCls = cssClasses.PREFIX;
 const Boundary = strings.BOUNDARY_MAP;
@@ -92,7 +93,7 @@ class OverflowList extends BaseComponent<OverflowListProps, OverflowListState> {
             visibleState: new Map(),
             itemSizeMap: new Map(),
             overflowStatus: "calculating",
-            pivot: 0,
+            pivot: -1,
             overflowWidth: 0,
             maxCount: 0,
         };
@@ -114,15 +115,24 @@ class OverflowList extends BaseComponent<OverflowListProps, OverflowListState> {
             // reset visible state if the above props change.
             newState.direction = OverflowDirection.GROW;
             newState.lastOverflowCount = 0;
+            newState.maxCount = 0;
             if (props.renderMode === RenderMode.SCROLL) {
                 newState.visible = props.items;
                 newState.overflow = [];
             } else {
-                newState.visible = [];
-                newState.overflow = [];
+                let maxCount = props.items.length;
+                if (Math.floor(prevState.containerWidth / numbers.MINIMUM_HTML_ELEMENT_WIDTH) !== 0) {
+                    maxCount = Math.min(maxCount, Math.floor(prevState.containerWidth / numbers.MINIMUM_HTML_ELEMENT_WIDTH));
+                }
+
+                const isCollapseFromStart = props.collapseFrom === Boundary.START;
+                const visible = isCollapseFromStart ? cloneDeep(props.items).reverse().slice(0, maxCount) : props.items.slice(0, maxCount);
+                const overflow = isCollapseFromStart ? cloneDeep(props.items).reverse().slice(maxCount) : props.items.slice(maxCount);
+                newState.visible = visible;
+                newState.overflow = overflow;
+                newState.maxCount = maxCount;
             }
-            newState.pivot = 0;
-            newState.maxCount = 0;
+            newState.pivot = -1;
             newState.overflowStatus = "calculating";
         }
         return newState;
@@ -170,25 +180,7 @@ class OverflowList extends BaseComponent<OverflowListProps, OverflowListState> {
         if (this.isScrollMode() || overflowStatus !== "calculating") {
             return;
         }
-        if (visible.length === 0 && overflow.length === 0 && this.props.items.length !== 0) {
-            // 推测container最多能渲染的数量
-            // Figure out the maximum number of items in this container
-            const maxCount = Math.min(this.props.items.length, Math.floor(containerWidth / numbers.MINIMUM_HTML_ELEMENT_WIDTH));
-            // 如果collapseFrom是start, 第一次用来计算容量时,倒转列表顺序渲染
-            // If collapseFrom === start, render item from end to start. Figuring out how many items in the end could fit in container.
-            const isCollapseFromStart = this.props.collapseFrom === Boundary.START;
-            const visible = isCollapseFromStart ? this.foundation.getReversedItems().slice(0, maxCount) : this.props.items.slice(0, maxCount);
-            const overflow = isCollapseFromStart ? this.foundation.getReversedItems().slice(maxCount) : this.props.items.slice(maxCount);
-            this.setState({
-                overflowStatus: 'calculating',
-                visible,
-                overflow,
-                maxCount: maxCount,
-            });
-            this.itemSizeMap.clear();
-        } else {
-            this.foundation.handleCollapseOverflow();
-        }
+        this.foundation.handleCollapseOverflow();
     }
 
     resize = (entries: Array<ResizeEntry> = []): void => {
@@ -217,12 +209,12 @@ class OverflowList extends BaseComponent<OverflowListProps, OverflowListState> {
         return this.props.overflowRenderer(overflow);
     };
 
-    getItemKey = (item, defalutKey?: Key) => {
+    getItemKey = (item, defaultKey?: Key) => {
         const { itemKey } = this.props;
         if (isFunction(itemKey)) {
             return itemKey(item);
         }
-        return get(item, itemKey || 'key', defalutKey);
+        return get(item, itemKey || 'key', defaultKey);
     }
 
     renderItemList = () => {
@@ -327,9 +319,9 @@ class OverflowList extends BaseComponent<OverflowListProps, OverflowListState> {
             });
         }
         const { maxCount } = this.state;
-        // 已经按照最大值maxCount渲染完毕,触发真正的渲染。(-1 是overflow部分会占1)
-        // Already rendered maxCount items, trigger the real rendering. (-1 for the overflow part)
-        if (this.itemSizeMap.size === maxCount - 1) {
+        // 已经按照最大值maxCount渲染完毕,触发真正的渲染
+        // Already rendered maxCount items, trigger the real rendering
+        if (this.itemSizeMap.size === maxCount) {
             this.setState({
                 overflowStatus: 'calculating'
             });

+ 203 - 1
packages/semi-ui/select/_story/select.stories.jsx

@@ -2995,10 +2995,212 @@ RenderSelectedItemCallCount.story = {
   name: 'RenderSelectedItemCallCount',
 };
 
+const RenderSelectedItemWithMaxTagCount = () => {
+  const list = [
+      { "name": "夏可漫", "email": "[email protected]", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/avatarDemo.jpeg" },
+      { "name": "申悦", "email": "[email protected]", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg" },
+      { "name": "曲晨一", "email": "[email protected]", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/8bd8224511db085ed74fea37205aede5.jpg" },
+      { "name": "文嘉茂", "email": "[email protected]", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/6fbafc2d-e3e6-4cff-a1e2-17709c680624.png" },
+  ];
+
+  const renderMultipleWithCustomTag = (optionNode, { onClose }) => {
+      const content = (
+          <Tag
+              avatarSrc={optionNode.avatar}
+              avatarShape='circle'
+              closable={true}
+              onClose={onClose}
+              size='large'
+              style={{ maxWidth: '100%'}}
+          >
+              {optionNode.email}
+          </Tag>
+      );
+      return {
+          isRenderInTag: false,
+          content
+      };
+  };
+
+  const renderMultipleWithCustomTag2 = (optionNode, { onClose }) => {
+      const content = (
+          <Tag
+              avatarSrc={optionNode.avatar}
+              avatarShape='square'
+              closable={true}
+              onClose={onClose}
+              size='large'
+              style={{ maxWidth: '100%'}}
+          >
+              {optionNode.name}
+          </Tag>
+      );
+      return {
+          isRenderInTag: false,
+          content
+      };
+  };
+
+  const renderCustomOption = (item, index) => {
+      const optionStyle = {
+          display: 'flex',
+          paddingLeft: 24,
+          paddingTop: 10,
+          paddingBottom: 10
+      };
+      return (
+          <Select.Option value={item.name} style={optionStyle} showTick={true}  {...item} key={item.email}>
+              <Avatar size="small" src={item.avatar} />
+              <div style={{ marginLeft: 8 }}>
+                  <div style={{ fontSize: 14 }}>{item.email}</div>
+                  <div style={{ color: 'var(--color-text-2)', fontSize: 12, lineHeight: '16px', fontWeight: 'normal' }}>{item.email}</div>
+              </div>
+          </Select.Option>
+      );
+  };
+
+  return (
+      <>
+        renderSelectedItem + maxTagCount=10 + defaultValue.length=2
+        <br />
+        <Select
+            placeholder='请选择'
+            maxTagCount={10}
+            style={{ width: 350, marginTop: 20 }}
+            onChange={v => console.log(v)}
+            defaultValue={['夏可漫', '申悦']}
+            multiple
+            renderSelectedItem={renderMultipleWithCustomTag}
+            ellipsisTrigger
+            showRestTagsPopover
+            expandRestTagsOnClick
+        >
+            {list.map((item, index) => renderCustomOption(item, index))}
+        </Select>
+        <br />
+        <br />
+        renderSelectedItem + maxTagCount=1 + defaultValue.length=2 + filter
+        <br />
+        <Select
+            placeholder='请选择'
+            maxTagCount={1}
+            filter
+            style={{ width: 350, marginTop: 20 }}
+            onChange={v => console.log(v)}
+            defaultValue={['夏可漫', '申悦']}
+            multiple
+            renderSelectedItem={renderMultipleWithCustomTag2}
+            ellipsisTrigger
+            showRestTagsPopover
+            expandRestTagsOnClick
+        >
+            {list.map((item, index) => renderCustomOption(item, index))}
+        </Select>
+      </>
+  );
+};
+
+
+export const NPlusTruncationStrategy = () => {
+    const shortVal = ['semi11', 'semi1']
+    const val = ['semi11', 'semi1', 'semi3', 'semi4', 'semi10']
+    const allSelect = ['semi11', 'semi1', 'semi2', 'semi3', 'semi4', 'semi5', 'semi6', 'semi7', 'semi8', 'semi9', 'semi10']
+
+    const options = [
+        { label: 'semi1semi1', value: 'semi1' },
+        { label: 'semi2semi2semi2', value: 'semi2' },
+        { label: 'semi3semi3semi3semi3', value: 'semi3' },
+        { label: 'semi4semi4semi4semi4semi4', value: 'semi4' },
+        { label: 'semi5semi5semi5semi5semi5semi5', value: 'semi5' },
+        { label: 'semi6semi6semi6semi6semi6semi6semi6', value: 'semi6' },
+        { label: 'semi7semi7semi7semi7semi7semi7semi7', value: 'semi7' },
+        { label: 'semi8semi8semi8semi8semi8semi8semi8', value: 'semi8' },
+        { label: 'semi9semi9semi9semi9semi9semi9semi9', value: 'semi9' },
+        { label: 'semi10semi10semi10semi10semi10semi10', value: 'semi10' },
+        { label: '我是中文超长选项我真的真的真的真的真的真的超级长', value: 'semi11' },
+    ];
+    // expandRestTagsOnClick
+
+    return (
+        <>
+            <h4>未设置宽度 和 maxTagCount </h4>
+            defaultValue.length = 5
+            <br /><br />
+            <Select multiple optionList={options} defaultValue={val} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br /><br />
+            
+            <h4>未设置宽度</h4>
+            maxTagCount = 2 + defaultValue.length = 5
+            <br /><br />
+            <Select maxTagCount={2} multiple optionList={options} defaultValue={val} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select> 
+            <br /><br />
+            maxTagCount = 2 + defaultValue.length = 5 + expandRestTagsOnClick=false
+            <br /><br />
+            <Select maxTagCount={2} multiple optionList={options} defaultValue={val} expandRestTagsOnClick={false} ellipsisTrigger showRestTagsPopover></Select>
+            <br /><br />
+            maxTagCount = 6 + defaultValue.length = 5
+            <br /><br />
+            <Select maxTagCount={6} multiple optionList={options} defaultValue={val} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 6 + defaultValue.length = 5 + filter
+            <br /><br />
+            <Select maxTagCount={6} multiple optionList={options} defaultValue={val} filter ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br /><br />
+
+            <h4>定宽</h4>
+            maxTagCount = 2 + defaultValue.length = 2
+            <br /><br />
+            <Select style={{ width: '350px' }} maxTagCount={2} multiple optionList={options} defaultValue={shortVal} showClear ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 5 + defaultValue.length = 5
+            <br /><br />
+            <Select style={{ width: '550px' }} maxTagCount={5} multiple  optionList={options} defaultValue={val} showClear ellipsisTrigger showRestTagsPopove expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 10 + defaultValue.length = 11
+            <br /><br />
+            <Select style={{ width: '550px' }} maxTagCount={10} multiple  optionList={options} defaultValue={allSelect} showClear ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 10 + defaultValue.length = 11 + filter
+            <br /><br />
+            <Select style={{ width: '550px' }} maxTagCount={10} multiple  optionList={options} defaultValue={allSelect} filter showClear ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 10 + defaultValue.length = 11 + expandRestTagsOnClick=false
+            <br /><br />
+            <Select style={{ width: '550px' }} maxTagCount={10} multiple  optionList={options} defaultValue={allSelect} expandRestTagsOnClick={false} showClear ellipsisTrigger showRestTagsPopover></Select>
+            <br /><br /><br />
+
+            <h4>能保证正常渲染的最小宽度至少是120px</h4>
+            <Select style={{ width: '120px' }} maxTagCount={10} multiple  optionList={options} defaultValue={val} showClear ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br /><br />
+
+            <h4>前缀/后缀/insetLabel</h4>
+            maxTagCount = 2 + defaultValue.length = 2 + prefix
+            <br /><br />
+            <Select style={{ width: '500px' }} maxTagCount={2} prefix={<IconSearch />} multiple  optionList={options} defaultValue={shortVal} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 6 + defaultValue.length = 5 + suffix
+            <br /><br />
+            <Select style={{ width: '500px' }} maxTagCount={6} suffix={<IconSearch />} multiple  optionList={options} defaultValue={val} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br />
+            maxTagCount = 6 + defaultValue.length = 11 + insetLabel
+            <br /><br />
+            <Select style={{ width: '500px' }} maxTagCount={6} insetLabel={<IconSearch />} multiple  optionList={options} defaultValue={allSelect} ellipsisTrigger showRestTagsPopover expandRestTagsOnClick></Select>
+            <br /><br /><br />
+
+            <h4>renderSelectedItem</h4>
+            <RenderSelectedItemWithMaxTagCount />
+            <br />
+        </>
+    )
+}
+
+NPlusTruncationStrategy.story = {
+  name: 'NPlusTruncationStrategy',
+};
 
 export const emptyContent = () => {
   const list = null;
   return (
     <Select placeholder='请选择业务线' emptyContent={null} style={{ width: 180 }} optionList={list} defaultOpen={true}/>
   )
-}
+}

+ 209 - 75
packages/semi-ui/select/index.tsx

@@ -8,9 +8,12 @@ import ConfigContext, { ContextValue } from '../configProvider/context';
 import SelectFoundation, { SelectAdapter } from '@douyinfe/semi-foundation/select/foundation';
 import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/select/constants';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
-import { isEqual, isString, noop, get, isNumber } from 'lodash';
+import { isEqual, isString, noop, get, isNumber, isFunction } from 'lodash';
 import Tag from '../tag/index';
 import TagGroup from '../tag/group';
+import OverflowList from '../overflowList/index';
+import Space from '../space/index';
+import Text from '../typography/text';
 import LocaleConsumer from '../locale/localeConsumer';
 import Popover, { PopoverProps } from '../popover/index';
 import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
@@ -111,6 +114,7 @@ export type SelectProps = {
     size?: SelectSize;
     disabled?: boolean;
     emptyContent?: React.ReactNode;
+    expandRestTagsOnClick?: boolean;
     onDropdownVisibleChange?: (visible: boolean) => void;
     zIndex?: number;
     position?: Position;
@@ -118,6 +122,7 @@ export type SelectProps = {
     dropdownClassName?: string;
     dropdownStyle?: React.CSSProperties;
     dropdownMargin?: PopoverProps['margin'];
+    ellipsisTrigger?: boolean;
     outerTopSlot?: React.ReactNode;
     innerTopSlot?: React.ReactNode;
     outerBottomSlot?: React.ReactNode;
@@ -183,7 +188,9 @@ export interface SelectState {
     optionGroups: Array<any>;
     isHovering: boolean;
     isFocusInContainer: boolean;
-    isFullTags: boolean
+    isFullTags: boolean;
+    // The number of really-hidden items when maxTagCount is set
+    overflowItemCount: number
 }
 
 // Notes: Use the label of the option as the identifier, that is, the option in Select, the value is allowed to be the same, but the label must be unique
@@ -206,6 +213,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         children: PropTypes.node,
         clearIcon: PropTypes.node,
         defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
+        ellipsisTrigger: PropTypes.bool,
         value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
         placeholder: PropTypes.node,
         onChange: PropTypes.func,
@@ -222,6 +230,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         size: PropTypes.oneOf<SelectProps['size']>(strings.SIZE_SET),
         disabled: PropTypes.bool,
         emptyContent: PropTypes.node,
+        expandRestTagsOnClick: PropTypes.bool,
         onDropdownVisibleChange: PropTypes.func,
         zIndex: PropTypes.number,
         position: PropTypes.oneOf(strings.POSITION_SET),
@@ -282,8 +291,8 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         onListScroll: PropTypes.func,
         arrowIcon: PropTypes.node,
         preventScroll: PropTypes.bool,
-    // open: PropTypes.bool,
-    // tagClosable: PropTypes.bool,
+        // open: PropTypes.bool,
+        // tagClosable: PropTypes.bool,
     };
 
     static defaultProps: Partial<SelectProps> = {
@@ -322,6 +331,8 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         arrowIcon: <IconChevronDown aria-label='' />,
         showRestTagsPopover: false,
         restTagsPopoverProps: {},
+        expandRestTagsOnClick: false,
+        ellipsisTrigger: false,
         // Radio selection is different from the default renderSelectedItem for multiple selection, so it is not declared here
         // renderSelectedItem: (optionNode) => optionNode.label,
         // The default creator rendering is related to i18, so it is not declared here
@@ -357,6 +368,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             isHovering: false,
             isFocusInContainer: false,
             isFullTags: false,
+            overflowItemCount: 0
         };
         /* Generate random string */
         this.selectOptionListID = '';
@@ -405,7 +417,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 this.setState({ focusIndex });
             },
             // eslint-disable-next-line @typescript-eslint/no-empty-function
-            scrollToFocusOption: () => {},
+            scrollToFocusOption: () => { },
         };
 
         const filterAdapter = {
@@ -436,7 +448,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                     // let isInPanel = optionsDom && optionsDom.contains(e.target);
                     // let isInTrigger = triggerDom && triggerDom.contains(e.target);
                     if (optionsDom && !optionsDom.contains(e.target as Node) &&
-                      triggerDom && !triggerDom.contains(e.target as Node)) {
+                        triggerDom && !triggerDom.contains(e.target as Node)) {
                         cb(e);
                     }
                 };
@@ -551,6 +563,9 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             updateFocusState: (isFocus: boolean) => {
                 this.setState({ isFocus });
             },
+            updateOverflowItemCount: (overflowItemCount: number) => {
+                this.setState({ overflowItemCount });
+            },
             focusTrigger: () => {
                 try {
                     const { preventScroll } = this.props;
@@ -663,11 +678,11 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             <Input
                 ref={this.inputRef as any}
                 size={size}
-                aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}`: ''}
+                aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}` : ''}
                 onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
                     // if multiple and filter, when use tab key to let select get focus
                     // need to manual update state isFocus to let the focus style take effect
-                    if (multiple && Boolean(filter)){
+                    if (multiple && Boolean(filter)) {
                         this.setState({ isFocus: true });
                     }
                     // prevent event bubbling which will fire trigger onFocus event
@@ -883,15 +898,15 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         const isEmpty = !options.length || !options.some(item => item._show);
         return (
             // eslint-disable-next-line jsx-a11y/no-static-element-interactions
-            <div 
-                id={`${prefixcls}-${this.selectOptionListID}`} 
+            <div
+                id={`${prefixcls}-${this.selectOptionListID}`}
                 className={cls({
                     // When emptyContent is null and the option is empty, there is no need for the drop-down option for the user,
                     // so there is no need to set padding through this className
                     [`${prefixcls}-option-list-wrapper`]: !(isEmpty && emptyContent === null),
-                }, dropdownClassName)} 
+                }, dropdownClassName)}
                 style={style}
-                ref={this.setOptionContainerEl} 
+                ref={this.setOptionContainerEl}
                 onKeyDown={e => this.foundation.handleContainerKeyDown(e)}
             >
                 {outerTopSlot ? <div className={`${prefixcls}-option-list-outer-top-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{outerTopSlot}</div> : null }
@@ -961,6 +976,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             }
             this.foundation.removeTag({ label, value });
         };
+
         const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
         const basic = {
             disabled,
@@ -978,13 +994,18 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         }
     }
 
-    renderMultipleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
+    renderTag(item: [React.ReactNode, any], i: number, isCollapseItem?: boolean) {
+        const { size, disabled: selectDisabled } = this.props;
         let { renderSelectedItem } = this.props;
-        const { showRestTagsPopover, restTagsPopoverProps, placeholder, maxTagCount } = this.props;
-        const { inputValue, isFullTags } = this.state;
-        const renderTags = [];
-
-        const selectedItems = [...selections];
+        const label = item[0];
+        const { value } = item[1];
+        const disabled = item[1].disabled || selectDisabled;
+        const onClose = (tagContent: React.ReactNode, e: MouseEvent) => {
+            if (e && typeof e.preventDefault === 'function') {
+                e.preventDefault(); // make sure that tag will not hidden immediately in controlled mode
+            }
+            this.foundation.removeTag({ label, value });
+        };
 
         if (typeof renderSelectedItem === 'undefined') {
             renderSelectedItem = (optionNode: OptionProps) => ({
@@ -992,82 +1013,195 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 content: optionNode.label,
             });
         }
+        const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
+        const basic = {
+            disabled,
+            closable: !disabled,
+            onClose,
+        };
+        const realContent = isCollapseItem && !isFunction(this.props.renderSelectedItem)
+            ? (
+                <Text size='small' ellipsis={{ rows: 1, showTooltip: { type: 'popover', opts: { style: { width: 'auto', fontSize: 12 } } } }} >
+                    {content}
+                </Text>
+            )
+            : content;
+        if (isRenderInTag) {
+            return (
+                <Tag {...basic} color="white" size={size || 'large'} key={value} style={{ maxWidth: '100%' }}>
+                    {realContent}
+                </Tag>
+            );
+        } else {
+            return <Fragment key={value}>{realContent}</Fragment>;
+        }
+    }
+
+    renderNTag(n: number, restTags: [React.ReactNode, any][]) {
+        const { size, showRestTagsPopover, restTagsPopoverProps } = this.props;
+        let nTag = (
+            <Tag
+                closable={false}
+                size={size || 'large'}
+                color='grey'
+                className={`${prefixcls}-content-wrapper-collapse-tag`}
+                key={`_+${n}`}
+                style={{ marginRight: 0, flexShrink: 0 }}
+            >
+                +{n}
+            </Tag>
+        );
+
+        if (showRestTagsPopover) {
+            nTag = (
+                <Popover
+                    showArrow
+                    content={
+                        <Space spacing={2} wrap style={{ maxWidth: '400px' }}>
+                            {restTags.map((tag, index) => (this.renderTag(tag, index)))}
+                        </Space>
+                    }
+                    trigger="hover"
+                    position="top"
+                    autoAdjustOverflow
+                    {...restTagsPopoverProps}
+                    key={`_+${n}_Popover`}
+                >
+                    {nTag}
+                </Popover>
+            );
+        }
+        return nTag;
+    }
+
+    renderOverflow(items: [React.ReactNode, any][], index: number) {
+        const isCollapse = true;
+        return items.length && items[0]
+            ? this.renderTag(items[0], index, isCollapse)
+            : null;
+    }
 
-        let mapItems = [];
-        let tags = [];
+    handleOverflow(items: [React.ReactNode, any][]) {
+        const { overflowItemCount, selections } = this.state;
+        const { maxTagCount } = this.props;
+        const maxVisibleCount = selections.size - maxTagCount;
+        const newOverFlowItemCount = maxVisibleCount > 0 ? maxVisibleCount + items.length - 1 : items.length - 1;
+        if (items.length > 1 && overflowItemCount !== newOverFlowItemCount) {
+            this.foundation.updateOverflowItemCount(selections.size, newOverFlowItemCount);
+        }
+    }
+
+
+    renderCollapsedTags(selections: [React.ReactNode, any][], length: number | undefined): React.ReactElement {
+        const { overflowItemCount } = this.state;
+        const normalTags = typeof length === 'number' ? selections.slice(0, length) : selections;
+        return (
+            <div className={`${prefixcls}-content-wrapper-collapse`}>
+                <OverflowList
+                    items={normalTags}
+                    overflowRenderer={overflowItems => this.renderOverflow(overflowItems as [React.ReactNode, any][], length - 1)}
+                    onOverflow={overflowItems => this.handleOverflow(overflowItems as [React.ReactNode, any][])}
+                    visibleItemRenderer={(item, index) => this.renderTag(item as [React.ReactNode, any], index)}
+                />
+                {overflowItemCount > 0 && this.renderNTag(overflowItemCount, selections.slice(selections.length - overflowItemCount))}
+            </div>
+        );
+    }
+
+    renderOneLineTags(selectedItems: [React.ReactNode, any][], n: number | undefined): React.ReactElement {
+        let { renderSelectedItem } = this.props;
+        const { showRestTagsPopover, restTagsPopoverProps, maxTagCount } = this.props;
+        const { isFullTags } = this.state;
         let tagContent: ReactNode;
 
-        if (!isNumber(maxTagCount)) {
-            // maxTagCount is not set, all tags are displayed
-            mapItems = selectedItems;
-            tags = mapItems.map((item, i) => {
+        if (typeof renderSelectedItem === 'undefined') {
+            renderSelectedItem = (optionNode: OptionProps) => ({
+                isRenderInTag: true,
+                content: optionNode.label,
+            });
+        }
+        if (showRestTagsPopover) {
+            // showRestTagsPopover = true,
+            const mapItems = isFullTags ? selectedItems : selectedItems.slice(0, maxTagCount);
+            const tags = mapItems.map((item, i) => {
                 return this.getTagItem(item, i, renderSelectedItem);
             });
-            tagContent = tags;
+
+            tagContent = (
+                <TagGroup<"custom">
+                    tagList={tags}
+                    maxTagCount={n}
+                    restCount={isFullTags ? undefined : (selectedItems.length - maxTagCount)}
+                    size="large"
+                    mode="custom"
+                    showPopover={showRestTagsPopover}
+                    popoverProps={restTagsPopoverProps}
+                    onPlusNMouseEnter={() => {
+                        this.foundation.updateIsFullTags();
+                    }}
+                />
+            );
         } else {
-            // maxTagCount is set
-            if (showRestTagsPopover) {
-                // showRestTagsPopover = true,
-                mapItems = isFullTags ? selectedItems : selectedItems.slice(0, maxTagCount);
-                tags = mapItems.map((item, i) => {
-                    return this.getTagItem(item, i, renderSelectedItem);
-                });
-                const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
-
-                tagContent = (
-                    <TagGroup<"custom"> 
-                        tagList={tags} 
-                        maxTagCount={n} 
-                        restCount={isFullTags ? undefined : (selectedItems.length - maxTagCount)}
-                        size="large" 
-                        mode="custom"
-                        showPopover={showRestTagsPopover}
-                        popoverProps={restTagsPopoverProps}
-                        onPlusNMouseEnter={() => { 
-                            this.foundation.updateIsFullTags();
-                        }}
-                    />
-                );
-            } else {
-                // If maxTagCount is set, showRestTagsPopover is false/undefined, 
-                // then there is no popover when hovering, no extra Tags are displayed, 
-                // only the tags and restCount displayed in the trigger need to be passed in
-                mapItems = selectedItems.slice(0, maxTagCount);
-                const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
-                tags = mapItems.map((item, i) => {
-                    return this.getTagItem(item, i, renderSelectedItem);
-                });
+            // If maxTagCount is set, showRestTagsPopover is false/undefined, 
+            // then there is no popover when hovering, no extra Tags are displayed, 
+            // only the tags and restCount displayed in the trigger need to be passed in
+            const mapItems = selectedItems.slice(0, maxTagCount);
+            const tags = mapItems.map((item, i) => {
+                return this.getTagItem(item, i, renderSelectedItem);
+            });
+            tagContent = (
+                <TagGroup<"custom">
+                    tagList={tags}
+                    maxTagCount={n}
+                    restCount={selectedItems.length - maxTagCount}
+                    size="large"
+                    mode="custom"
+                />
+            );
+        }
+        return tagContent;
+    }
 
-                tagContent = (
-                    <TagGroup<"custom"> 
-                        tagList={tags} 
-                        maxTagCount={n} 
-                        restCount={selectedItems.length - maxTagCount} 
-                        size="large" 
-                        mode="custom"
-                    />
-                );
-            }
+
+    renderMultipleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
+        let { renderSelectedItem } = this.props;
+        const { placeholder, maxTagCount, expandRestTagsOnClick, ellipsisTrigger } = this.props;
+        const { inputValue, isOpen } = this.state;
+
+        const selectedItems = [...selections];
+
+        if (typeof renderSelectedItem === 'undefined') {
+            renderSelectedItem = (optionNode: OptionProps) => ({
+                isRenderInTag: true,
+                content: optionNode.label,
+            });
         }
 
         const contentWrapperCls = cls({
             [`${prefixcls}-content-wrapper`]: true,
-            [`${prefixcls}-content-wrapper-one-line`]: maxTagCount,
-            [`${prefixcls}-content-wrapper-empty`]: !tags.length,
+            [`${prefixcls}-content-wrapper-one-line`]: maxTagCount && !isOpen,
+            [`${prefixcls}-content-wrapper-empty`]: !selectedItems.length,
         });
 
         const spanCls = cls({
             [`${prefixcls}-selection-text`]: true,
-            [`${prefixcls}-selection-placeholder`]: !tags.length,
-            [`${prefixcls}-selection-text-hide`]: tags && tags.length,
-            // [prefixcls + '-selection-text-inactive']: !inputValue && !tags.length,
+            [`${prefixcls}-selection-placeholder`]: !selectedItems.length,
+            [`${prefixcls}-selection-text-hide`]: selectedItems && selectedItems.length,
         });
         const placeholderText = placeholder && !inputValue ? <span className={spanCls}>{placeholder}</span> : null;
+        const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
+        const NotOneLine = !maxTagCount;
+
+        const oneLineTags = ellipsisTrigger ? this.renderCollapsedTags(selectedItems, n) : this.renderOneLineTags(selectedItems, n);
+
+        const tagContent = NotOneLine || (expandRestTagsOnClick && isOpen)
+            ? selectedItems.map((item, i) => this.renderTag(item, i))
+            : oneLineTags;
 
         return (
             <>
                 <div className={contentWrapperCls}>
-                    {tags && tags.length ? tagContent : placeholderText}
+                    {selectedItems && selectedItems.length ? tagContent : placeholderText}
                     {!filterable ? null : this.renderInput()}
                 </div>
             </>
@@ -1184,7 +1318,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             });
 
         const showClear = this.props.showClear &&
-      (selections.size || inputValue) && !disabled && (isHovering || isOpen);
+            (selections.size || inputValue) && !disabled && (isHovering || isOpen);
 
         const arrowContent = showArrow ? (
             <div className={`${prefixcls}-arrow`} x-semi-prop="arrowIcon">
@@ -1255,7 +1389,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 style={style}
                 id={this.selectID}
                 tabIndex={tabIndex}
-                aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}`: ''}
+                aria-activedescendant={focusIndex !== -1 ? `${this.selectID}-option-${focusIndex}` : ''}
                 onMouseEnter={this.onMouseEnter}
                 onMouseLeave={this.onMouseLeave}
                 onFocus={e => this.foundation.handleTriggerFocus(e)}