UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

1,442 lines (1,288 loc) 75.3 kB
--- localeCode: zh-CN order: 46 category: 输入类 title: Select 选择器 icon: doc-select width: 60% brief: 用户可以通过 Select 选择器从一个选项集合中去选中一个或多个选项,并呈现最终选择结果 --- ## 代码演示 ### 如何引入 ```jsx import import { Select } from '@douyinfe/semi-ui'; const Option = Select.Option; ``` <Notice title='注意'> Select的直接子元素必须为 Option 或者 OptGroup,不允许为其他Element </Notice> ### 基本使用 每个 Option 标签都必须声明 value 属性,Option 的 children 或 label 将会被渲染至下拉列表中 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select defaultValue="douyin" style={{ width: 120 }}> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying" disabled> 剪映 </Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <br /> <br /> <Select defaultValue="douyin" disabled style={{ width: 120 }}> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> <br /> <br /> <Select placeholder="请选择业务线" style={{ width: 120 }}> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying" disabled> 剪映 </Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> </> ); ``` ### 以数组形式传入 Option 可以直接通过`optionList`传入一个对象数组,每个对象必须包含 value/label 属性(当然其他属性也可以通过此方式传入) ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const list = [ { value: 'douyin', label: '抖音', otherKey: 0 }, { value: 'ulikecam', label: '轻颜相机', disabled: true, otherKey: 1 }, { value: 'jianying', label: '剪映', otherKey: 2 }, { value: 'toutiao', label: '今日头条', otherKey: 3 }, ]; return <Select placeholder="请选择业务线" style={{ width: 180 }} optionList={list}></Select>; }; ``` ### 多选 自 v2.28后,select 的选择器会自带 maxHeight 270,内容超出后可以通过垂直滚动查看。 配置`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`回调 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select multiple style={{ width: '320px' }} defaultValue={['douyin', 'ulikecam']}> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <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: '320px' }} defaultValue={['douyin', 'ulikecam', 'jianying']} > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <br /> <br /> <Select multiple style={{ width: '320px' }} defaultValue={['douyin']} max={2} onExceed={() => Toast.warning('最多只允许选择两项')} > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <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', 'douyin']} ellipsisTrigger expandRestTagsOnClick > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> </> ); ``` ### 分组 用 OptGroup 进行分组(分组功能仅支持通过 jsx 方式声明 children 使用,不支持 optionList 方式传入) <Notice title='注意'> 1. OptGroup 必须为 Select 的直接子元素,不允许有 Fragment 或 DIV 等其他元素阻隔 2. 若 Select 的 children 需要动态更新,OptGroup 上的 key 也需要进行更新,否则 Select 无法识别 </Notice> ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <Select placeholder="" style={{ width: 180 }} filter> <Select.OptGroup label="Asia"> <Select.Option value="a-1">China</Select.Option> <Select.Option value="a-2">Korea</Select.Option> </Select.OptGroup> <Select.OptGroup label="Europe"> <Select.Option value="b-1">Germany</Select.Option> <Select.Option value="b-2">France</Select.Option> </Select.OptGroup> <Select.OptGroup label="South America"> <Select.Option value="c-1">Peru</Select.Option> </Select.OptGroup> </Select> ); ``` ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const data = [ { label: 'Asia', children: [ { value: 'a-1', label: 'China' }, { value: 'a-2', label: 'Korea' }, ], }, { label: 'Europe', children: [ { value: 'b-1', label: 'Germany' }, { value: 'b-2', label: 'France' }, ], }, { label: 'South America', children: [{ value: 'c-1', label: 'Peru' }], }, ]; return ( <Select placeholder="" style={{ width: 180 }} filter> {data.map((group, index) => ( <Select.OptGroup label={group.label} key={`${index}-${group.label}`}> {group.children.map((option, index2) => ( <Select.Option value={option.value} key={`${index2}-${group.label}`}> {option.label} </Select.Option> ))} </Select.OptGroup> ))} </Select> ); }; ``` ### 不同尺寸 通过 Size 控制选择器的大小尺寸: `small` / `default` / `large` ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select placeholder="请选择业务线" style={{ width: '180px' }} size="small"> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> <br /> <br /> <Select placeholder="请选择业务线" style={{ width: '180px' }}> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> <br /> <br /> <Select placeholder="请选择业务线" style={{ width: '180px' }} size="large"> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> </> ); ``` ### 不同校验状态样式 validateStatus: default / warning / error 仅影响背景颜色等样式表现 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select style={{ width: '180px' }}> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> <br /> <br /> <Select style={{ width: '180px' }} validateStatus="warning"> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> <br /> <br /> <Select style={{ width: '180px' }} validateStatus="error"> <Select.Option value="ulikecam">轻颜相机</Select.Option> </Select> </> ); ``` ### 配置前缀、后缀、清除按钮 - 可以通过`prefix`传入选择框前缀,通过`suffix`传入选择框后缀,可以为文本或者 ReactNode 当 prefix、suffix 传入的内容为文本或者 Icon 时,会自动带上左右间隔,若为自定义 ReactNode,则左右间隔为 0 - 通过`showClear`控制清除按钮是否展示 - 通过`showArrow`控制右侧下拉箭头是否展示 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; import { IconVigoLogo, IconGift } from '@douyinfe/semi-icons'; () => ( <> <Select style={{ width: '320px' }} defaultValue={'ulikecam'} prefix={<IconVigoLogo />} showClear={true}> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <br /> <br /> <Select style={{ width: '320px' }} defaultValue={'ulikecam'} prefix={<IconVigoLogo />} suffix={<IconGift />} showArrow={false} > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> </> ); ``` ### 在顶部/底部渲染附加项 我们在弹出层顶部、底部分别预留了插槽,当你需要在弹出层中添加自定义 node 时 可以通过`innerBottomSlot`或者`outerBottomSlot`传入,自定义 node 将会被渲染在弹出层底部;可以通过`innerTopSlot`或者`outerTopSlot`传入,自定义 node 将会被渲染在弹出层顶部。 - `innerTopSlot` 和 `innerBottomSlot`将会被渲染在 optionList 内部,当滚动到 optionList 顶部/底部时展现 - `outerTopSlot` 和 `outerBottomSlot`将会被渲染为与 optionList 平级,无论 optionList 是否滚动,都会始终展现 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { let innerSlotStyle = { backgroundColor: 'var(--color-white)', height: '36px', display: 'flex', alignItems: 'center', cursor: 'pointer', paddingLeft: 32, borderTop: '1px solid var(--semi-color-border)', borderRadius: '0 0 6px 6px', color: 'var(--semi-color-link)', }; let innerSlotNode = <div style={innerSlotStyle}>点击加载更多</div>; let outSlotStyle = { backgroundColor: 'var(--semi-color-fill-0)', height: '36px', display: 'flex', paddingLeft: 32, color: 'var(--semi-color-link)', alignItems: 'center', cursor: 'pointer', borderTop: '1px solid var(--semi-color-border)', borderRadius: '0 0 6px 6px', }; let outSlotNode = ( <div style={outSlotStyle}> <span style={{ color: 'var(--semi-color-link)' }}>未找到应用?</span> </div> ); return ( <div> <p>outerBottomSlot:</p> <Select style={{ width: 300 }} dropdownStyle={{ width: 180 }} maxHeight={150} outerBottomSlot={outSlotNode} placeholder="自定义外侧底部slot,始终显示" defaultOpen autoAdjustOverflow={false} position="bottom" > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="duoshan">多闪</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <p style={{ marginTop: 200 }}>innerBottomSlot:</p> <Select style={{ width: 300 }} dropdownStyle={{ width: 180 }} maxHeight={150} innerBottomSlot={innerSlotNode} placeholder="自定义内侧底部slot,滚动至底部显示" > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="duoshan">多闪</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> </div> ); }; ``` 通过 outerTopSlot 将内容插入顶部插槽 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const list = { component: [ { value: 'select', label: '选择器' }, { value: 'tabs', label: '标签' }, { value: 'avatar', label: '头像' }, { value: 'button', label: '按钮' }, ], design: [ { value: 'color', label: '颜色' }, { value: 'dark', label: '暗色模式' }, { value: 'icon', label: '图标' }, { value: 'font', label: '字体' }, ], feedback: [ { value: 'faq', label: '常见问题' }, { value: 'join', label: '加入用户群' }, { value: 'hornbill', label: '犀鸟反馈问题' }, ], }; const [key, setKey] = useState('component'); const [value, setValue] = useState({ value: 'faq', label: '常见问题' }); const handleTdouyinlick = itemKey => { setKey(itemKey); }; const tabStyle = { cursor: 'pointer', marginRight: 12, paddingBottom: 4, }; const tabActiveStyle = { ...tabStyle, borderBottom: '1px solid var(--semi-color-primary)', fontWeight: 700, }; const tabWrapper = { display: 'flex', paddingTop: 8, paddingLeft: 32, borderBottom: '0.5px solid var(--semi-color-border)', }; const tabOptions = [ { itemKey: 'component', label: '组件' }, { itemKey: 'design', label: '设计' }, { itemKey: 'feedback', label: '反馈' }, ]; const outerTopSlotNode = ( <div style={tabWrapper}> {tabOptions.map((item, index) => { style = item.itemKey === key ? tabActiveStyle : tabStyle; return ( <div style={style} key={item.itemKey} onClick={() => handleTdouyinlick(item.itemKey)}> {item.label} </div> ); })} </div> ); return ( <Select defaultOpen autoAdjustOverflow={false} value={value} onChangeWithObject onChange={obj => setValue(obj)} style={{ width: 200 }} outerTopSlot={outerTopSlotNode} optionList={list[key]} /> ); }; ``` ### 受控组件 传入 value 时 Select 为受控组件,所选中的值完全由 value 决定。 ```jsx live=true hideInDSM import React, { useState } from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { let [value, setValue] = useState('xigua'); return ( <> <Select value={value} style={{ width: '300px' }} onChange={setValue} placeholder="受控的Select"> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> </> ); }; ``` ### 动态修改 Options 如果需要动态更新 Options,应该使用受控的 value ```jsx live=true hideInDSM import React, { useState } from 'react'; import { Select, Button } from '@douyinfe/semi-ui'; () => { let [options, setOptions] = useState([1, 2, 3, 4]); function add() { let length = Math.ceil(Math.random() * 10); let newOptions = Array.from({ length }, (v, i) => i + 1); setOptions(newOptions); } return ( <> <Select style={{ width: '180px' }} placeholder="请选择" value={4}> {options.map(option => ( <Select.Option value={option} key={option}> {option} </Select.Option> ))} </Select> <br /> <br /> <Button onClick={add}>changeOptions Dynamic</Button> </> ); }; ``` ### 联动 使用受控 value,实现不同 Select 之间的联动。如果是带有层级关系的复杂联动建议直接使用`Cascader`组件 ```jsx live=true hideInDSM import React, { useState } from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const provinces = ['四川', '广东']; const maps = { 四川: ['成都', '都江堰'], 广东: ['广州', '深圳', '东莞'], }; const [province, setProvince] = useState(provinces[0]); const citys = maps[province]; const [city, setCity] = useState(citys[0]); const provinceChange = newProvince => { setProvince(newProvince); setCity(maps[newProvince][0]); }; const cityChange = newCity => setCity(newCity); return ( <> <Select style={{ width: '150px', margin: '10px' }} onChange={provinceChange} value={province}> {provinces.map(pro => ( <Select.Option value={pro} key={pro}> {pro} </Select.Option> ))} </Select> <Select style={{ width: '150px', margin: '10px' }} value={city} onChange={cityChange}> {citys.map(c => ( <Select.Option value={c} key={c}> {c} </Select.Option> ))} </Select> </> ); }; ``` ### 开启搜索 将 `filter` 置为 true,开启搜索能力。默认搜索策略将为 input 输入值与 option 的 label 值进行 include 对比 默认情况下,多选选中后会自动清空搜索关键字。若你希望保留,可以通过 autoClearSearchValue 设为 false 关闭默认行为(v2.3 后提供) ```jsx live=true hideInDSM import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select filter style={{ width: 180 }} placeholder="带搜索功能的单选"> <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <br /> <br /> <Select filter multiple style={{ width: 300 }} placeholder="带搜索功能的多选" autoClearSearchValue={false}> <Select.Option value="semi-0">Semi-0</Select.Option> <Select.Option value="semi-1">Semi-1</Select.Option> <Select.Option value="semi-2">Semi-2</Select.Option> <Select.Option value="semi-3">Semi-3</Select.Option> <Select.Option value="semi-4">Semi-4</Select.Option> </Select> </> ); ``` ### 搜索框位置 默认搜索框展示于 Select 的 Trigger 触发器上。通过 `searchPosition` 可以指定不同的位置,可选 `dropdown`、`trigger`。 在 v2.61.0后提供 若希望定制位于 dropdown 中的 Input 搜索框的 placeholder,可以通过 `searchPlaceholder` 控制 若 `searchPosition` 值为 `trigger`,当showClear=true 时,点击Trigger区域的清空按钮,将同时清空已选项以及搜索框中的文本 若 `searchPosition` 值为 `dropdown`,当showClear=true 时,点击Trigger区域清空按钮,仅清空已选项。点击搜索框中的清空按钮,仅清空搜索文本 ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <> <Select filter searchPosition='dropdown' style={{ width: 200 }} defaultValue={'ulikecam'} placeholder='我的搜索框在下拉菜单中' searchPlaceholder="带搜索功能的单选" > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> <br /> <br /> <Select filter searchPosition='dropdown' multiple style={{ width: 300 }} defaultValue={['semi-1']} placeholder='我的搜索框在下拉菜单中' searchPlaceholder="带搜索功能的多选" autoClearSearchValue={false} > <Select.Option value="semi-0">Semi-0</Select.Option> <Select.Option value="semi-1">Semi-1</Select.Option> <Select.Option value="semi-2">Semi-2</Select.Option> <Select.Option value="semi-3">Semi-3</Select.Option> <Select.Option value="semi-4">Semi-4</Select.Option> </Select> </> ); ``` ### 远程搜索 带有远程搜索,防抖请求,加载状态的多选示例 通过`filter`开启搜索能力 将`remote`设置为 true 关闭对当前数据的筛选过滤 通过动态更新`optionList`更新下拉菜单中的备选项 使用受控的 value 属性 ```jsx live=true import React from 'react'; import { debounce } from 'lodash-es'; import { Select } from '@douyinfe/semi-ui'; () => { const [loading, setLoading] = useState(false); const optionList = [ { value: 'douyin', label: '抖音', type: 1 }, { value: 'xingtu', label: '醒图', type: 2 }, { value: 'jianying', label: '剪映', type: 3 }, { value: 'toutiao', label: '今日头条', type: 4 }, ]; const [list, setList] = useState(optionList); const [value, setValue] = useState(''); const handleMultipleChange = newValue => { setValue(newValue); }; const handleSearch = inputValue => { setLoading(true); let result = []; if (inputValue) { let length = Math.ceil(Math.random() * 100); result = Array.from({ length }, (v, i) => { return { value: inputValue + i, label: `相近业务 ${inputValue}${i}`, type: i + 1 }; }); setTimeout(() => { setLoading(false); setList(result); }, 1000); } else { setLoading(false); } }; return ( <Select style={{ width: 300 }} filter remote onChangeWithObject multiple value={value} onSearch={debounce(handleSearch, 1000)} optionList={list} loading={loading} onChange={handleMultipleChange} emptyContent={null} ></Select> ); }; ``` ### 自定义搜索逻辑 可以将 `filter` 置为自定义函数,定制你想要的搜索策略 如下例子,选项 label 值都是大写,默认的检索策略是字符串 include 对比,会区分大小写。 通过传入自定义 `filter` 函数,检索时输入小写字母也能搜到相应内容。 ```jsx live=true hideInDSM import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { function searchLabel(sugInput, option) { let label = option.label.toUpperCase(); let sug = sugInput.toUpperCase(); return label.includes(sug); } return ( <Select filter={searchLabel} style={{ width: '180px' }} placeholder="try douyin"> <Select.Option value="douyin">douyin</Select.Option> <Select.Option value="ulikecam">HOTSOON</Select.Option> <Select.Option value="jianying">PIPIXIA</Select.Option> <Select.Option value="xigua">XIGUA</Select.Option> </Select> ); }; ``` ### 自定义已选项标签渲染 默认情况下,选中选项后会将 option.label 或 option.children 的内容回填到选择框中 但你可以通过 `renderSelectedItem` 自定义选择框中已选项标签的渲染结构 - 单选时 `renderSelectedItem(optionNode:object) => content:ReactNode` - 多选时 `renderSelectedItem(optionNode:object, { index:number, onClose:function }) => { isRenderInTag:bool, content:ReactNode }` - isRenderInTag 为 true 时,会自动将 content 包裹在 Tag 中渲染(带有背景色以及关闭按钮) - isRenderInTag 为 false 时,将直接渲染返回的 content ```jsx live=true import React from 'react'; import { Select, Avatar, Tag } from '@douyinfe/semi-ui'; () => { const list = [ { name: '夏可漫', email: 'xiakeman@example.com', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/dy.png', }, { name: '申悦', email: 'shenyue@example.com', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bag.jpeg', }, { name: '曲晨一', email: 'quchenyi@example.com', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/Viamaker.png', }, { name: '文嘉茂', email: 'wenjiamao@example.com', avatar: 'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/6fbafc2d-e3e6-4cff-a1e2-17709c680624.png', }, ]; const renderSelectedItem = optionNode => ( <div style={{ display: 'flex', alignItems: 'center' }}> <Avatar src={optionNode.avatar} size="small"> {optionNode.abbr} </Avatar> <span style={{ marginLeft: 8 }}>{optionNode.email}</span> </div> ); // avatarSrc & avatarShape are supported const renderMultipleWithCustomTag = (optionNode, { onClose }) => { const content = ( <Tag avatarSrc={optionNode.avatar} avatarShape="circle" closable={true} onClose={onClose} size="large"> {optionNode.name} </Tag> ); return { isRenderInTag: false, content, }; }; const renderMultipleWithCustomTag2 = (optionNode, { onClose }) => { const content = ( <Tag avatarSrc={optionNode.avatar} avatarShape="square" closable={true} onClose={onClose} size="large"> {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.name}</div> <div style={{ color: 'var(--color-text-2)', fontSize: 12, lineHeight: '16px', fontWeight: 'normal' }} > {item.email} </div> </div> </Select.Option> ); }; return ( <> <Select placeholder="请选择" style={{ width: 280, height: 40 }} onChange={v => console.log(v)} defaultValue={'申悦'} renderSelectedItem={renderSelectedItem} > {list.map((item, index) => renderCustomOption(item, index))} </Select> <Select placeholder="请选择" maxTagCount={2} style={{ width: 280, marginTop: 20 }} onChange={v => console.log(v)} defaultValue={['申悦', '曲晨一']} multiple renderSelectedItem={renderMultipleWithCustomTag} > {list.map((item, index) => renderCustomOption(item, index))} </Select> <Select placeholder="请选择" maxTagCount={2} style={{ width: 280, marginTop: 20 }} onChange={v => console.log(v)} defaultValue={['申悦', '曲晨一']} multiple renderSelectedItem={renderMultipleWithCustomTag2} > {list.map((item, index) => renderCustomOption(item, index))} </Select> </> ); }; ``` ### 自定义弹出层样式 你可以通过 dropdownClassName、dropdownStyle 控制弹出层的样式 例如当自定义弹出层的宽度时,可以通过 dropdownStyle 传入 width ```jsx live=true hideInDSM import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => ( <Select placeholder="自定义弹出层样式的" style={{ width: 180 }} dropdownStyle={{ width: 250 }} dropdownClassName="test" > <Select.Option value="douyin">抖音</Select.Option> <Select.Option value="ulikecam">轻颜相机</Select.Option> <Select.Option value="jianying">剪映</Select.Option> <Select.Option value="xigua">西瓜视频</Select.Option> </Select> ); ``` ### 获取选项的其他属性 默认情况下`onChange`只能拿到 value,如果需要拿选中节点的其他属性,可以使用`onChangeWithObject`属性 此时`onChange`函数的入参将会是 object,包含 option 的各种属性,例如 `onChange({ value, label, ...rest })` <Notice title='注意'> 当 onChangeWithObject 置为 true 时,`defaultValue` / `Value` 也应为 object,且须带有 `value`、`label` key </Notice> ```jsx live=true hideInDSM import React from 'react'; import { Select, TextArea } from '@douyinfe/semi-ui'; () => { const list = [ { value: 'douyin', label: '抖音', type: 1 }, { value: 'ulikecam', label: '轻颜相机', type: 2 }, { value: 'jianying', label: '剪映', type: 3 }, { value: 'toutiao', label: '今日头条', type: 4 }, ]; const [cbValue, setCbValue] = useState(); const [multipleCbValue, setMultipleCbValue] = useState(); const onChange = value => { setCbValue(value); console.log(value); }; const onMultipleChange = value => { setMultipleCbValue(value); console.log(value); }; return ( <div> <div> <Select style={{ width: 150 }} onChangeWithObject optionList={list} placeholder="单选" defaultValue={list[0]} onChange={onChange} ></Select> <h4>onChange回调:</h4> <TextArea style={{ width: 320, marginBottom: 48 }} autosize value={JSON.stringify(cbValue)} rows={2} /> </div> <div> <Select style={{ width: 320 }} onChangeWithObject multiple optionList={list} onChange={onMultipleChange} placeholder="多选" ></Select> <h4>onChange回调:</h4> <TextArea style={{ width: 320 }} autosize value={JSON.stringify(multipleCbValue)} /> </div> </div> ); }; ``` ### 创建条目 设置`allowCreate`,可以创建并选中选项中不存在的条目 允许通过 `renderCreateItem` 自定义创建标签时的内容显示(通过返回 ReactNode,注意你需要自定义样式),该函数默认值为 (input, isFocus, style) => '创建' + input 可以配合`defaultActiveFirstOption`属性使用,自动选中第一项,当输入完内容直接回车时,可立即创建 <Notice title='注意'> 当开启allowCreate后,不会再响应对Children或者optionList的更新 </Notice> ```jsx live=true import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const optionList = [ { value: 'douyin', label: '抖音' }, { value: 'ulikecam', label: '轻颜相机' }, { value: 'jianying', label: '剪映' }, { value: 'toutiao', label: '今日头条' }, ]; return ( <> <Select style={{ width: 400 }} optionList={optionList} allowCreate={true} multiple={true} filter={true} onChange={v => console.log(v)} defaultActiveFirstOption ></Select> <br /> <br /> <Select style={{ width: 400 }} optionList={optionList} allowCreate={true} multiple={true} filter={true} placeholder="With renderCreateItem" renderCreateItem={(input, isFocus, style) => (<div style={{ padding: 10, ...style }}>Create Item:{input}</div>)} onChange={v => console.log(v)} defaultActiveFirstOption ></Select> </> ); }; ``` ### 虚拟化 传入`virtualize`时开启列表虚拟化,用于大量 Option 节点的情况优化性能 virtualize 是一个包含下列值的对象: - height: Option 列表高度值,默认 270 (v2.20.8 前为 300) - width: Option 列表宽度值,默认 100% - itemSize: 每行 Option 的高度,必传 <Notice title='注意事项'> Semi Select virtualize 功能是基于 react-window 的封装,虚拟化列表默认会被包裹在 `will-change: transform` 的 div 内部。 在某些浏览器(例如 Chrome),某些特定的屏幕尺寸下,屏幕物理像素尺寸与浏览器处理的像素无法对齐时,会自动开启抗锯齿。从而导致虚拟列表中的文本字体可能会在特定场景下存在模糊的情况。 will-change 对于复杂元素的渲染会有性能改善,所以我们默认不会对 react-window 的样式进行覆盖。如果你希望关闭这个效果,可以通过自行覆盖 CSS,将 will-change 设置为 unset 解决 </Notice> <Notice title='关于高度设置'> 当 virtualize.height 大于默认值 270px 时,为了避免出现双滚动条问题,需要将 maxHeight 属性设置为与 virtualize.height 相同的值。 例如:当设置 virtualize.height 为 400px 时,应同时设置 maxHeight={400}。 </Notice> ```css .semi-select-option-list > div { will-change: unset !important; // 由于 react-window自带样式是内联的,所以这里用 important 覆盖 } ``` ```jsx live=true hideInDSM import React from 'react'; import { Select } from '@douyinfe/semi-ui'; () => { const newOptions = Array.from({ length: 3000 }, (v, i) => ({ label: `option-${i}`, value: i })); let virtualize = { height: 270, width: '100%', itemSize: 36, // px }; return ( <Select placeholder="拥有3k个Option的Select" style={{ width: 260 }} filter virtualize={virtualize} optionList={newOptions} ></Select> ); }; ``` ### 自定义触发器 如果 Select 默认的触发器样式满足不了你的需求,可以用`triggerRender`自定义选择框的展示 如果想保留搜索筛选能力,又不希望自己渲染 Input 相关的结构,可以同时通过 searchPosition='dropdown',将默认的搜索框置于下拉列表中 triggerRender 入参如下 ```typescript interface TriggerRenderProps { value: array<object> // 当前所有已选中的options inputValue: string; // 当前input框的输入值 onSearch: (inputValue: string) => void; // 用于更新 input框值的函数,当你在triggerRender自定义的Input组件值更新时你应该调用该函数,用于向Select内部同步状态。注意 filter 需同时设为true, v2.32 提供 onRemove: (option: object) => void; // 用于移除单个已选项,option至少需带有 label、value 两项,v2.32提供 onClear: () => void; // 用于清空值的函数 disabled: boolean; // 是否禁用Select placeholder: string; // Select的placeholder componentProps: object // 所有用户传给Select的props } ``` ```jsx live=true import React, { useState } from 'react'; import { Select, Tag } from '@douyinfe/semi-ui'; import { IconAppCenter, IconChevronDown } from '@douyinfe/semi-icons'; () => { const [valList, setValList] = useState(['douyin', 'ulikecam']); const list = [ { value: 'douyin', label: '抖音' }, { value: 'ulikecam', label: '轻颜相机' }, { value: 'jianying', label: '剪映' }, { value: 'toutiao', label: '今日头条' }, ]; const triggerRender = ({ value }) => { return ( <div style={{ minWidth: '112', backgroundColor: 'var(--semi-color-primary-light-default)', height: 32, display: 'flex', alignItems: 'center', paddingLeft: 12, borderRadius: 3, color: 'var(--semi-color-primary)', }} > <div style={{ fontWeight: 600, flexShrink: 0, fontSize: 14, }} > 业务线 </div> <div style={{ margin: 4, whiteSpace: 'nowrap', textOverflow: 'ellipsis', flexGrow: 1, overflow: 'hidden', }} > {value.map(item => item.label).join(' , ')} </div> <IconAppCenter style={{ marginRight: 8, flexShrink: 0 }} /> </div> ); }; const triggerRender2 = ({ value, ...rest }) => { return ( <div style={{ margin: 4, whiteSpace: 'nowrap', textOverflow: 'ellipsis', flexGrow: 1, overflow: 'hidden', display: 'flex', alignItems: 'center', }} > <Tag size='large' color='cyan' shape='circle' suffixIcon={<IconChevronDown />}> {value.map(item => item.label).join(' / ')} </Tag> </div> ); }; return ( <div> <h4>不同背景色的触发器</h4> <Select value={valList} triggerRender={triggerRender} optionList={list} onChange={value => setValList(value)} multiple filter searchPosition='dropdown' style={{ width: 240 }} ></Select> <br /> <br /> <h4>使用 circle Tag 作为触发器</h4> <Select value={valList} onChange={value => setValList(value)} triggerRender={triggerRender2} optionList={list} filter multiple searchPosition='dropdown' style={{ width: 240, marginTop: 20, outline: 0 }} ></Select> </div> ); }; ``` 下例是更复杂的例子:复用了 TagInput 拖拽排序能力,通过 triggerRender 为 Select 增加排序 ```jsx live=true import React, { useState } from 'react'; import { Select, TagInput } from '@douyinfe/semi-ui'; () => { const [valList, setValList] = useState(['douyin', 'ulikecam']); const [inputVal, setInputVal] = useState(''); const list = [ { value: 'douyin', label: '抖音' }, { value: 'ulikecam', label: '轻颜相机' }, { value: 'jianying', label: '剪映' }, { value: 'toutiao', label: '今日头条' }, ]; const handleSort = (currentLabels) => { const newValue = currentLabels.map(item => list.find((i) => i.label === item).value) setValList(newValue); }; const triggerRender = ({ value, onSearch, onClear }) => { return ( <div onKeyDown={e=>e.stopPropagation()}> <TagInput draggable allowDuplicates={false} value={value.map(item => item.label)} inputValue={inputVal} onInputChange={(word) => { onSearch(word); setInputVal(word); }} onChange={handleSort} onClear={() => onClear()} showClear /> </div> ); }; return ( <> <h4>可对已选项拖拽重新排序的 Select </h4> <Select value={valList} triggerRender={triggerRender} optionList={list} onChange={value => setValList(value)} multiple filter style={{ width: 240 }} ></Select> </> ); }; ``` ### 自定义候选项渲染 - 简单的自定义:通过 Option 的 label 属性或者 children 传入 ReactNode,你可以控制候选项的渲染,此时内容会自动带上内边距、背景色等样式 - 完全自定义:通过传入`renderOptionItem`,你可以完全接管列表中候选项的渲染,并且从回调入参中,获取到相关的状态值。实现更高自由度的结构渲染 注意事项: 1. props 传入的 style 需在 wrapper dom 上进行消费,否则在虚拟化场景下会无法正常使用 2. props 传入的 className、onMouseEnter 需在 wrapper dom 上进行消费,否则上下键盘操作时显示会有问题 3. 选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从 props 中获取到相对的 boolean 值 4. 如果你的自定义 item 为 Select.Option,需要将 renderProps.onClick 透传给 Option 的 onSelect prop ```jsx live=true import React from 'react'; import { Select, Checkbox, Highlight } from '@douyinfe/semi-ui'; () => { const [inputValue, setInputValue] = useState(''); const renderOptionItem = renderProps => { const { disabled, selected, label, value, focused, className, style, onMouseEnter, onClick, empty, emptyContent, ...rest } = renderProps; const optionCls = classNames({ ['custom-option-render']: true, ['custom-option-render-focused']: focused, ['custom-option-render-disabled']: disabled, ['custom-option-render-selected']: selected, className }); const searchWords = [inputValue]; // Notice: // 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用 // 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值 // 3.onMouseEnter、className需在wrapper dom上绑定,否则上下键盘操作时显示会有问题 return ( <div style={style} className={optionCls} onClick={() => onClick()} onMouseEnter={e => onMouseEnter()}> <Checkbox checked={selected} /> <div className="option-right"> <Highlight sourceString={label} searchWords={searchWords} /> </div> </div> ); }; const optionList = [ { value: 'douyin', label: '抖音', otherKey: 0 }, { value: 'ulikecam', label: '轻颜相机', disabled: true, otherKey: 1 }, { value: 'jianying', label: '剪映', otherKey: 2 }, { value: 'toutiao', label: '今日头条', otherKey: 3 }, ]; return ( <> <Select filter placeholder="单选" onSearch={(v) => setInputValue(v)} dropdownClassName="components-select-demo-renderOptionItem" optionList={optionList} style={{ width: 180 }} renderOptionItem={renderOptionItem} /> <br /> <br /> <Select filter placeholder="多选" multiple onSearch={(v) => setInputValue(v)} dropdownClassName="components-select-demo-renderOptionItem" optionList={optionList} style={{ width: 320 }} renderOptionItem={renderOptionItem} /> </> ); }; ``` ```scss .components-select-demo-renderOptionItem { .custom-option-render { display: flex; font-size: 14px; line-height: 20px; word-break: break-all; padding-left: 12px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; color: var(--semi-color-text-0); position: relative; display: flex; align-items: center; cursor: pointer; box-sizing: border-box; .option-right { margin-left: 8px; display: inline-flex; align-items: center; } &:active { background-color: var(--semi-color-fill-1); } &-focused { background-color: var(--semi-color-fill-0); } &-selected { //font-weight: 700; } &-disabled { color: var(--semi-color-disabled-text); cursor: not-allowed; } &:first-of-type { margin-top: 4px; } &:last-of-type { margin-bottom: 4px; } } } ``` ## API 参考 ### Select Props | 属性 | 说明 | 类型 | 默认值 | 版本 | | --- |---------------------------------------------------------------------------------------------------------------------------------------| --- | --- | --- | | allowCreate | 是否允许用户创建新条目,需配合 filter 使用。该项为true时不再响应 optionList的变更 | boolean | false | | arrowIcon | 自定义右侧下拉箭头 Icon,当 showClear 开关打开且当前有选中值时,hover 会优先显示 clear icon | ReactNode | | | | autoAdjustOverflow | 浮层被遮挡时是否自动调整方向(暂时仅支持竖直方向,且插入的父级为 body) | boolean | true | | autoClearSearchValue | 选中选项后,是否自动清空搜索关键字,当 mutilple、filter 都开启时生效 | boolean | true | 2.3.0 | | autoFocus | 初始渲染时是否自动 focus | boolean | false | | borderless | 无边框模式 >=2.33.0 | boolean | | | className | 类名 | string | | | clearIcon | 可用于自定义清除按钮, showClear为true时有效 | ReactNode | | 2.25.0 | clickToHide | 已展开时,点击选择框是否自动收起下拉列表 | boolean | false | | defaultValue | 初始选中的值 | string\|number\|array | | | defaultOpen | 是否默认展开下拉列表 | boolean | false | | disabled | 是否禁用 | boolean | false | | defaultActiveFirstOption | 是否默认高亮第一个选项(按回车可直接选中) <br/>**v2.17.0 之后默认值从 false 变为 true** | boolean | true | | dropdownClassName | 弹出层的 className | string | | | dropdownMatchSelectWidth | 下拉菜单最小宽度是否等于 Select | boolean | true | | 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` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置。 | function():HTMLElement | () => document.body | | inputProps | filter 为 true 时, input 输入框的额外配置参数,具体可配置属性请参考 Input 组件(注意:请不要传入 value、ref、onChange、onFocus,否则会覆盖 Select 相关回调,影响组件行为) | object | | 2.2.0 | | innerTopSlot | 渲染在弹出层顶部,在 optionList 内部的自定义 slot | ReactNode | | | innerBottomSlot | 渲染在弹出层底部,在 optionList 内部的自定义 slot | ReactNode | | | loading | 下拉列表是否展示加载动画 | boolean | false | | maxTagCount | 多选模式下,已选项超出 maxTagCount 时,后续选项会被渲染成+N 的形式 | number | | | max | 最多可选几项,仅在多选模式下生效 | number | | | maxHeight | 下拉菜单中 `optionList` 的最大高度。**注意:当使用虚拟化列表且 virtualize.height 大于默认值 270px 时,需要将 maxHeight 设置为与 virtualize.height 相同的值,以避免出现双滚动条问题** | string\|number | 270 | | multiple | 是否多选 | boolean | false | | outerTopSlot | 渲染在弹出层顶部,与 optionList 平级的自定义 slot | ReactNode | | | outerBottomSlot | 渲染在弹出层底部,与 optionList 平级的自定义 slot | ReactNode | | | optionList | 可以通过该属性传入 Option,请确保数组内每个元素都具备 label、value 属性 | array(\[{value, label}\]) | | | placeholder | 选择框默认文字 | ReactNode | | | position | 菜单展开的位置,可选项同 Tooltip position | string | 'bottomLeft' | | rePosKey | 可以更新该项值手动触发弹出层的重新定位 | string\|number | | | | prefix | 选择框的前缀标签 | ReactNode | | | preventScroll | 指示浏览器是否应滚动文档以显示新聚焦的元素,作用于组件内的 focus 方法 | boolean | | | | renderCreateItem | allowCreate 为 true 时,可自定义创建标签的渲染。与虚拟化结合使用时,必须将第三个参数style传入自定义DOM中消费(v2.44.1后提供) | function(inputVa