Browse Source

Feat: [Select] Added showRestTagsPopover and restTagsPopoverProps to support displaying redundant tags through popover (#1212)

* feat: [Select] Added showRestTagsPopover and restTagsPopoverProps to support displaying redundant tags through popover

* feat: [Select] Added showRestTagsPopover and restTagsPopoverProps to support displaying redundant tags through popover

Co-authored-by: pointhalo <[email protected]>
YyumeiZhang 3 years ago
parent
commit
ac9385e4ce

+ 11 - 6
content/input/select/index-en-US.md

@@ -73,6 +73,8 @@ 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
 
+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
 
 ```jsx live=true
@@ -89,7 +91,7 @@ import { Select } from '@douyinfe/semi-ui';
         </Select>
         <br />
         <br />
-        <Select multiple style={{ width: '320px' }} defaultValue={['abc', 'hotsoon', 'pipixia']} maxTagCount={2}>
+        <Select multiple maxTagCount={2} showRestTagsPopover={true} restTagsPopoverProps={{ position: 'top' }} style={{ width: '320px' }} defaultValue={['abc', 'hotsoon', 'pipixia']} >
             <Select.Option value="abc">Semi</Select.Option>
             <Select.Option value="hotsoon">Hotsoon</Select.Option>
             <Select.Option value="pipixia">Pipixia</Select.Option>
@@ -113,6 +115,7 @@ import { Select } from '@douyinfe/semi-ui';
     </>
 );
 ```
+
 ### With Group
 
 Grouping Option with `OptGroup`(Only supports the declaration of children through jsx, and does not support pass in through optionList)
@@ -1252,12 +1255,12 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 
 ### Select Props
 
-| Properties | Instructions | Type | Default |
-| --- | --- | --- | --- |
+| Properties | Instructions | Type | Default | version |
+| --- | --- | --- | --- | --- |
 | allowCreate | Whether to allow the user to create new entries. Needs to be used with `filter` | boolean | false |
 | arrowIcon | Customize the right drop-down arrow Icon, when the showClear switch is turned on and there is currently a selected value, hover will give priority to the clear icon <br/>**supported after v1.15.0** | ReactNode |  |
 | autoAdjustOverflow | Whether the pop-up layer automatically adjusts the direction when it is obscured (only vertical direction is supported for the time being, and the inserted parent is body) | boolean | true |
-| autoClearSearchValue     |  After selecting the option, whether to automatically clear the search keywords, it will take effect when mutilple and filter are both enabled.<br/>**supported after v2.3.0** | boolean                      | true                                |
+| autoClearSearchValue     |  After selecting the option, whether to automatically clear the search keywords, it will take effect when mutilple and filter are both enabled.<br/>**supported after v2.3.0** | boolean   | true  |
 | autoFocus | Whether automatically focus when component mount | boolean | false |
 | className | The CSS class name of the wrapper element | string |  |
 | clickToHide | When expanded, click on the selection box to automatically put away the drop-down list | boolean | false |
@@ -1278,7 +1281,7 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | loading | Does the drop-down list show the loading animation | boolean | false |
 | max | Maximum number of choices, effective only in multi-selection mode | number |  |
 | maxTagCount | In multi-selection mode, when the option is beyond maxTag Count, the subsequent option is rendered in the form of + N | number |  |
-| maxHeight | Maximum height of `optionList` in the pop-up layer | string | number | 270 |
+| maxHeight | Maximum height of `optionList` in the pop-up layer | string \| number | 270 |
 | multiple | Whether allow multiple selection | boolean | false |
 | outerBottomSlot | Rendered at the bottom of the pop-up layer, custom slot level with optionList | ReactNode |  |
 | outerTopSlot | Rendered at the top of the pop-up layer, custom slot level with optionList <br/>**supported after v1.6.0** |
@@ -1290,15 +1293,17 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | remote | Whether to turn on remote search, when remote is true, the input content will not be locally filtered and matched | boolean | false |
 | renderCreateItem | When allowCreate is true, you can customize the rendering of the creation label | function(inputValue: string) | InputValue => 'Create' + InputValue |
 | renderSelectedItem | Customize the rendering of selected tabs in the selection box | function(option) |  |
+| restTagsPopoverProps | The configuration properties of the [Popover](/en-US/show/popover#API%20Reference)     | PopoverProps     | {}        | 2.22.0 |
 | showArrow | Whether to show arrow icon | boolean | true |
 | showClear | Whether to show the clear button | boolean | false |
+| showRestTagsPopover | When the number of tags exceeds maxTagCount and hover reaches +N, whether to display the remaining content through Popover | boolean | false | 2.22.0 |
 | size | Size, optional value `default` / `small` / `large` | string | 'default' |
 | spacing | Spacing between popup layer and trigger | number | 4 |
 | stopPropagation | Whether to prevent click events on the popup layer from bubbling | boolean | true | |
 | style | Inline Style | object |  |
 | suffix | An input helper rendered after | ReactNode |  |
 | triggerRender | Custom DOM of trigger | function |  |
-| virtualize | List virtualization, used to optimize performance in the case of a large number of nodes, composed of height, width, and itemSize <br/>** supported after v0.37.0 ** | object |  |
+| virtualize | List virtualization, used to optimize performance in the case of a large number of nodes, composed of height, width, and itemSize <br/>** supported after v0.37.0 ** | object |  | |
 | validateStatus | Verification result, optional `warning`, `error`, `default` (only affect the style background color) | string | 'default' |
 | value | The currently selected value is passed as a controlled component, used in conjunction with `onchange` | string\|number\|array |  |
 | zIndex | Popup layer z-index | number | 1030 |

+ 8 - 3
content/input/select/index.md

@@ -82,6 +82,8 @@ import { Select } from '@douyinfe/semi-ui';
 
 配置 `maxTagCount` 可以限制已选项展示的数量,超出部分将以+N 的方式展示
 
+使用 `showRestTagsPopover` (>= v2.22.0) 可以设置在超出 `maxTagCount` 后,hover +N 是否显示 Popover,默认为 `false`。并且,还可以在 `restTagsPopoverProps` 属性中配置 Popover。
+
 配置 `max` 属性可限制最大可选的数量,超出最大限制数量后无法选中,同时会触发`onExceed`回调
 
 ```jsx live=true
@@ -97,7 +99,7 @@ import { Select } from '@douyinfe/semi-ui';
             <Select.Option value='xigua'>西瓜视频</Select.Option>
         </Select>
         <br/><br/>
-        <Select multiple style={{ width: '320px' }} defaultValue={['abc', 'ulikecam', 'jianying']} maxTagCount={2}>
+        <Select multiple maxTagCount={2} showRestTagsPopover={true} restTagsPopoverProps={{ position: 'top' }} style={{ width: '320px' }} defaultValue={['abc', 'ulikecam', 'jianying']} >
             <Select.Option value='abc'>抖音</Select.Option>
             <Select.Option value='ulikecam'>轻颜相机</Select.Option>
             <Select.Option value='jianying'>剪映</Select.Option>
@@ -432,6 +434,7 @@ import { Select } from '@douyinfe/semi-ui';
 ```
 
 通过 outerTopSlot 将内容插入顶部插槽
+
 ```jsx live=true
 import React from 'react';
 import { Select } from '@douyinfe/semi-ui';
@@ -1306,8 +1309,8 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 
 ### Select Props
 
-| 属性                     | 说明                                                                                                                                                                                                      | 类型                                  | 默认值                            |
-| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------------------------------- |
+| 属性    | 说明     | 类型      | 默认值     |  版本     |
+| -------| -------- | ---------| --------- |--------- |
 | allowCreate              | 是否允许用户创建新条目,需配合 filter 使用                                                                                                                                                                | boolean                               | false                             |
 | arrowIcon            | 自定义右侧下拉箭头Icon,当showClear开关打开且当前有选中值时,hover会优先显示clear icon <br/>**v1.15.0 后提供**                                                                                                                                                                 | ReactNode     |                             |
 | autoAdjustOverflow       | 浮层被遮挡时是否自动调整方向(暂时仅支持竖直方向,且插入的父级为 body)                                                                                                            | boolean                               | true                              |
@@ -1356,6 +1359,7 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | renderCreateItem         | allowCreate 为 true 时,可自定义创建标签的渲染                                                                                                                                 | function(inputValue:string)           | inputValue => '创建' + inputValue |
 | renderSelectedItem       | 通过 renderSelectedItem 自定义选择框中已选项标签的渲染                                                                                                                          | function(option)                      |                                   |
 | renderOptionItem         | 通过 renderOptionItem 完全自定义下拉列表中候选项的渲染                                                                                                                          | function(props) 入参详见Demo                      |                                   |
+| restTagsPopoverProps    | Popover 的配置属性,可以控制 position、zIndex、trigger 等,具体参考[Popover](/zh-CN/show/popover#API%20%E5%8F%82%E8%80%83) | PopoverProps | {} | 2.22.0 |
 | remote                   | 是否开启远程搜索,当 remote 为 true 时,input 内容改变后不会进行本地筛选匹配                                                                                                     | boolean                               | false                             |
 | size                     | 大小,可选值 `default`/`small`/`large`                                                                                                                                                                    | string                                | 'default'       |
 | style                    | 样式                                                                                                                                                                                                      | object                                |                 |
@@ -1363,6 +1367,7 @@ import { Select, Checkbox } from '@douyinfe/semi-ui';
 | suffix                   | 选择框的后缀标签                                                                                                                                                                  | ReactNode                             |                                   |
 | showClear                | 是否展示清除按钮                                                                                                                                                                 | boolean                               | false                             |
 | showArrow                | 是否展示下拉箭头                                                                                                                                                                | boolean                               | true                              |
+| showRestTagsPopover | 当超过 maxTagCount,hover 到 +N 时,是否通过 Popover 显示剩余内容 | boolean | false | 2.22.0 | 
 | spacing                  | 浮层与选择器的距离                                                                                                                                                             | number                                | 4                                 |
 | triggerRender            | 自定义触发器渲染                                                                                                                                                          | function                              |                                   |
 | value                    | 当前选中的的值,传入该值时将作为受控组件,配合 `onChange` 使用                                                                                                                                             | string\|number\|array                 |                                   |

+ 9 - 1
packages/semi-foundation/select/foundation.ts

@@ -1084,5 +1084,13 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
     updateScrollTop() {
         this._adapter.updateScrollTop();
     }
-    
+
+    updateIsFullTags() {
+        const { isFullTags } = this.getStates();
+        if (!isFullTags) {
+            this._adapter.setState({ 
+                isFullTags: true,
+            });
+        }
+    }
 }

+ 28 - 0
packages/semi-ui/select/_story/select.stories.jsx

@@ -632,6 +632,32 @@ export const SelectMultiple = () => (
     <br />
     <br />
     maxTagCount = 3
+    <br />
+    <Select
+      multiple={true}
+      maxTagCount={3}
+      style={{
+        width: '350px',
+      }}
+      defaultValue={[1, 2, 3]}
+      placeholder="fefe"
+      insetLabel="标签"
+      onSelect={(...res) => console.log(res)}
+      onDeselect={(...res) => console.log(res)}
+    >
+      <Option value={1}>opt1</Option>
+      <Option value={2}>opt2</Option>
+      <Option value={3}>opt3</Option>
+      <Option value="4">opt4</Option>
+      <Option value={5}>opt5</Option>
+      <Option value={6}>opt6</Option>
+      <Option value={7}>opt7</Option>
+      <Option value={8}>opt8</Option>
+    </Select>
+    <br />
+    <br />
+    maxTagCount = 3, showRestTagsPopover
+    <br />
     <Select
       multiple={true}
       maxTagCount={3}
@@ -643,6 +669,7 @@ export const SelectMultiple = () => (
       insetLabel="标签"
       onSelect={(...res) => console.log(res)}
       onDeselect={(...res) => console.log(res)}
+      showRestTagsPopover={true}
     >
       <Option value={1}>opt1</Option>
       <Option value={2}>opt2</Option>
@@ -656,6 +683,7 @@ export const SelectMultiple = () => (
     <br />
     <br />
     maxTagCount = 3, max=5
+    <br />
     <Select
       multiple={true}
       maxTagCount={3}

+ 93 - 37
packages/semi-ui/select/index.tsx

@@ -1,6 +1,6 @@
 /* eslint-disable max-len */
 /* eslint-disable max-lines-per-function */
-import React, { Fragment, MouseEvent, ReactInstance } from 'react';
+import React, { Fragment, MouseEvent, ReactInstance, ReactNode } from 'react';
 import ReactDOM from 'react-dom';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
@@ -12,7 +12,7 @@ import { isEqual, isString, noop, get, isNumber } from 'lodash';
 import Tag from '../tag/index';
 import TagGroup from '../tag/group';
 import LocaleConsumer from '../locale/localeConsumer';
-import Popover from '../popover/index';
+import Popover, { PopoverProps } from '../popover/index';
 import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
 import { FixedSizeList as List } from 'react-window';
 import { getOptionsFromGroup } from './utils';
@@ -152,7 +152,9 @@ export type SelectProps = {
     onBlur?: (e: React.FocusEvent) => void;
     onListScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
     children?: React.ReactNode;
-    preventScroll?: boolean
+    preventScroll?: boolean;
+    showRestTagsPopover?: boolean;
+    restTagsPopoverProps: PopoverProps
 } & Pick<
 TooltipProps,
 | 'spacing'
@@ -177,7 +179,8 @@ export interface SelectState {
     keyboardEventSet: any; // {}
     optionGroups: Array<any>;
     isHovering: boolean;
-    isFocusInContainer: boolean
+    isFocusInContainer: boolean;
+    isFullTags: boolean
 }
 
 // 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
@@ -311,7 +314,9 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         remote: false,
         autoAdjustOverflow: true,
         autoClearSearchValue: true,
-        arrowIcon: <IconChevronDown aria-label='' />
+        arrowIcon: <IconChevronDown aria-label='' />,
+        showRestTagsPopover: false,
+        restTagsPopoverProps: {},
         // 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
@@ -345,6 +350,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             optionGroups: [],
             isHovering: false,
             isFocusInContainer: false,
+            isFullTags: false,
         };
         /* Generate random string */
         this.selectOptionListID = '';
@@ -928,11 +934,38 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         );
     }
 
+    getTagItem = (item: any, i: number, renderSelectedItem: RenderSelectedItemFn) => {
+        const { size, disabled: selectDisabled } = this.props;
+        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 });
+        };
+        const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
+        const basic = {
+            disabled,
+            closable: !disabled,
+            onClose,
+        };
+        if (isRenderInTag) {
+            return (
+                <Tag {...basic} color="white" size={size || 'large'} key={value} tabIndex={-1}>
+                    {content}
+                </Tag>
+            );
+        } else {
+            return <Fragment key={value}>{content}</Fragment>;
+        }
+    }
+
     renderMultipleSelection(selections: Map<OptionProps['label'], any>, filterable: boolean) {
         let { renderSelectedItem } = this.props;
-        const { placeholder, maxTagCount, size } = this.props;
-        const { inputValue } = this.state;
-        const selectDisabled = this.props.disabled;
+        const { showRestTagsPopover, restTagsPopoverProps, placeholder, maxTagCount } = this.props;
+        const { inputValue, isFullTags } = this.state;
         const renderTags = [];
 
         const selectedItems = [...selections];
@@ -944,34 +977,62 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             });
         }
 
-        const mapItems = maxTagCount ? selectedItems.slice(0, maxTagCount) : selectedItems; // no need to render rest tag when maxTagCount is setting
+        let mapItems = [];
+        let tags = [];
+        let tagContent: ReactNode;
 
-        const tags = mapItems.map((item, i) => {
-            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 });
-            };
-            const { content, isRenderInTag } = (renderSelectedItem as RenderMultipleSelectedItemFn)(item[1], { index: i, disabled, onClose });
-            const basic = {
-                disabled,
-                closable: !disabled,
-                onClose,
-            };
-            if (isRenderInTag) {
-                return (
-                    <Tag {...basic} color="white" size={size || 'large'} key={value} tabIndex={-1}>
-                        {content}
-                    </Tag>
+        if (!isNumber(maxTagCount)) {
+            // maxTagCount is not set, all tags are displayed
+            mapItems = selectedItems;
+            tags = mapItems.map((item, i) => {
+                return this.getTagItem(item, i, renderSelectedItem);
+            });
+            tagContent = tags;
+        } 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 {
-                return <Fragment key={value}>{content}</Fragment>;
+                // 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);
+                });
+
+                tagContent = (
+                    <TagGroup<"custom"> 
+                        tagList={tags} 
+                        maxTagCount={n} 
+                        restCount={selectedItems.length - maxTagCount} 
+                        size="large" 
+                        mode="custom"
+                    />
+                );
             }
-        });
+        }
 
         const contentWrapperCls = cls({
             [`${prefixcls}-content-wrapper`]: true,
@@ -986,11 +1047,6 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             // [prefixcls + '-selection-text-inactive']: !inputValue && !tags.length,
         });
         const placeholderText = placeholder && !inputValue ? <span className={spanCls}>{placeholder}</span> : null;
-        const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
-
-        const NotOneLine = !maxTagCount; // Multiple lines (that is, do not set maxTagCount), do not use TagGroup, directly traverse with Tag, otherwise Input cannot follow the correct position
-
-        const tagContent = NotOneLine ? tags : <TagGroup<"custom"> tagList={tags} maxTagCount={n} restCount={maxTagCount ? selectedItems.length - maxTagCount : undefined} size="large" mode="custom"/>;
 
         return (
             <>

+ 3 - 1
packages/semi-ui/tag/group.tsx

@@ -17,6 +17,7 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
         size: tagSize[0],
         avatarShape: 'square',
         onTagClose: () => undefined,
+        onPlusNMouseEnter: () => undefined,
     };
 
     static propTypes = {
@@ -35,7 +36,7 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
     };
 
     renderNTag(n: number, restTags: React.ReactNode) {
-        const { size, showPopover, popoverProps } = this.props;
+        const { size, showPopover, popoverProps, onPlusNMouseEnter } = this.props;
         let nTag = (
             <Tag
                 closable={false}
@@ -43,6 +44,7 @@ export default class TagGroup<T> extends PureComponent<TagGroupProps<T>> {
                 color="grey"
                 style={{ backgroundColor: 'transparent' }}
                 key="_+n"
+                onMouseEnter={onPlusNMouseEnter}
             >
                 +{n}
             </Tag>

+ 1 - 0
packages/semi-ui/tag/index.tsx

@@ -33,6 +33,7 @@ export default class Tag extends Component<TagProps, TagState> {
         type: tagType[0] as TagType,
         onClose: () => undefined,
         onClick: () => undefined,
+        onMouseEnter: () => undefined,
         style: {},
         className: '',
         shape: 'square',

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

@@ -41,6 +41,7 @@ export interface TagProps {
     onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
     'aria-label'?: React.AriaAttributes['aria-label'];
     tabIndex?: number; // use internal, when tag in taInput, we want to use left arrow and right arrow to control the tag focus, so the tabIndex need to be -1. 
+    onMouseEnter?: () => void
 }
 
 export interface TagGroupProps<T> {
@@ -54,5 +55,6 @@ export interface TagGroupProps<T> {
     popoverProps?: PopoverProps;
     avatarShape?: AvatarShape;
     mode?: string;
-    onTagClose: (tagChildren: React.ReactNode, event: React.MouseEvent<HTMLElement>, tagKey: string | number) => void
+    onTagClose?: (tagChildren: React.ReactNode, event: React.MouseEvent<HTMLElement>, tagKey: string | number) => void;
+    onPlusNMouseEnter?: () => void
 }