代码演示
如何引入
import { Select } from '@douyinfe/semi-ui';
const Option = Select.Option;
注意
Select的直接子元素必须为 Option 或者 OptGroup,不允许为其他Element
基本使用
每个 Option 标签都必须声明 value 属性,Option 的 children 或 label 将会被渲染至下拉列表中
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
() => (
<>
<Select defaultValue="abc" style={{ width: 120 }}>
<Select.Option value="abc">抖音</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="abc" disabled style={{ width: 120 }}>
<Select.Option value="abc">抖音</Select.Option>
<Select.Option value="ulikecam">轻颜相机</Select.Option>
</Select>
<br />
<br />
<Select placeholder="请选择业务线" style={{ width: 120 }}>
<Select.Option value="abc">抖音</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 属性(当然其他属性也可以通过此方式传入)
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
() => {
const list = [
{ value: 'abc', 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
回调
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
() => (
<>
<Select multiple style={{ width: '320px' }} defaultValue={['abc', 'ulikecam']}>
<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>
<br />
<br />
<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>
<Select.Option value="xigua">西瓜视频</Select.Option>
</Select>
<br />
<br />
<Select
multiple
style={{ width: '320px' }}
defaultValue={['abc']}
max={2}
onExceed={() => Toast.warning('最多只允许选择两项')}
>
<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>
<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>
</>
);
分组
分组功能 v0.31.0 后提供
用 OptGroup 进行分组(分组功能仅支持通过 jsx 方式声明 children 使用,不支持 optionList 方式传入)
注意
1. OptGroup必须为Select的直接子元素,不允许有Fragment或DIV等其他元素阻隔
2. 若Select的children需要动态更新,OptGroup上的key也需要进行更新,否则Select无法识别
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>
);
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
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
仅影响背景颜色等样式表现
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
控制右侧下拉箭头是否展示
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="abc">抖音</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="abc">抖音</Select.Option>
<Select.Option value="ulikecam">轻颜相机</Select.Option>
<Select.Option value="jianying">剪映</Select.Option>
<Select.Option value="xigua">西瓜视频</Select.Option>
</Select>
</>
);
内嵌标签
通过设置insetLabel
,你可以给 Select 设置 label,可以传入 string 或者 ReactNode
当传入类型为 ReactNode 时,注意要自行处理 label 与文本之间的间隔
import React, { useState } from 'react';
import { Select } from '@douyinfe/semi-ui';
() => {
const list = [
{ value: 'abc', label: '抖音' },
{ value: 'ulikecam', label: '轻颜相机' },
{ value: 'jianying', label: '剪映' },
{ value: 'toutiao', label: '今日头条' },
];
const colorList = ['red', 'light-blue', 'yellow', 'purple', 'pink', 'green'].map(color => {
return {
value: `rgba(var(--semi-${color}-4), 1)`,
label: (
<span
style={{
color: `rgba(var(--semi-${color}-4), 1)`,
}}
>
{`--semi-${color}-4`}
</span>
),
};
});
const [colorVal, setColotVal] = useState('--semi-light-blue-3');
return (
<>
<Select style={{ width: 300 }} optionList={list} insetLabel="业务线" defaultValue="abc"></Select>
<br />
<br />
<Select
style={{ width: 300 }}
optionList={colorList}
value={colorVal}
insetLabel={
<div
style={{
marginLeft: 12,
display: 'flex',
}}
>
<div
style={{
display: 'block',
width: 5,
height: 5,
border: 'solid 7px transparent',
borderRadius: '50%',
borderColor: 'rgba(var(--semi-light-blue-3), 1)',
}}
></div>
<span style={{ marginLeft: 4 }}>色值</span>
</div>
}
></Select>
</>
);
};
在顶部/底部渲染附加项
我们在弹出层顶部、底部分别预留了插槽,当你需要在弹出层中添加自定义 node 时
可以通过innerBottomSlot
或者outerBottomSlot
传入,自定义 node 将会被渲染在弹出层底部;可以通过innerTopSlot
或者outerTopSlot
传入,自定义 node 将会被渲染在弹出层顶部。
innerTopSlot
和 innerBottomSlot
将会被渲染在 optionList 内部,当滚动到 optionList 顶部/底部时展现outerTopSlot
和 outerBottomSlot
将会被渲染为与 optionList 平级,无论 optionList 是否滚动,都会始终展现
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="abc">抖音</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="abc">抖音</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 将内容插入顶部插槽
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 handleTabClick = 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={() => handleTabClick(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 决定。
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="abc">抖音</Select.Option>
<Select.Option value="ulikecam">轻颜相机</Select.Option>
<Select.Option value="jianying">剪映</Select.Option>
<Select.Option value="xigua">西瓜视频</Select.Option>
</Select>
</>
);
};
动态修改 Options
如果需要动态更新 Options,应该使用受控的 value
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
组件
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
class Link extends React.Component {
get provinces() {
return ['四川', '广东'];
}
get maps() {
return {
四川: ['成都', '都江堰'],
广东: ['广州', '深圳', '东莞'],
};
}
constructor() {
super();
this.state = {
provinces: this.provinces,
maps: this.maps,
citys: this.maps[this.provinces[0]],
city: this.maps[this.provinces[0]][0],
};
this.provinceChange = this.provinceChange.bind(this);
this.cityChange = this.cityChange.bind(this);
}
provinceChange(newProvince) {
const { maps } = this.state;
this.setState({ citys: maps[newProvince], city: maps[newProvince][0] });
}
cityChange(city) {
this.setState({ city });
}
render() {
const { provinces, citys, city } = this.state;
return (
<React.Fragment>
<Select
style={{ width: '150px', margin: '10px' }}
onChange={this.provinceChange}
defaultValue={provinces[0]}
>
{provinces.map(pro => (
<Select.Option value={pro} key={pro}>
{pro}
</Select.Option>
))}
</Select>
<Select style={{ width: '150px', margin: '10px' }} value={city} onChange={this.cityChange}>
{citys.map(c => (
<Select.Option value={c} key={c}>
{c}
</Select.Option>
))}
</Select>
</React.Fragment>
);
}
}
开启搜索
将 filter
置为 true,开启搜索能力。默认搜索策略将为 input 输入值与 option 的 label 值进行 include 对比
默认情况下,多选选中后会自动清空搜索关键字。若你希望保留,可以通过 autoClearSearchValue 设为 false 关闭默认行为(v2.3 后提供)
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
() => (
<>
<Select filter style={{ width: 180 }} placeholder="带搜索功能的单选">
<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>
<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区域清空按钮,仅清空已选项。点击搜索框中的清空按钮,仅清空搜索文本
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 属性
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
函数,检索时输入小写字母也能搜到相应内容。
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 abc">
<Select.Option value="abc">ABC</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
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 after 1.6.0-beta
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
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
() => (
<Select
placeholder="自定义弹出层样式的"
style={{ width: 180 }}
dropdownStyle={{ width: 250 }}
dropdownClassName="test"
>
<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>
);
获取选项的其他属性
默认情况下onChange
只能拿到 value,如果需要拿选中节点的其他属性,可以使用onChangeWithObject
属性
此时onChange
函数的入参将会是 object,包含 option 的各种属性,例如 onChange({ value, label, ...rest })
注意
当 onChangeWithObject 置为 true 时,`defaultValue`/`Value`也应为 object,且须带有`value`、`label` key
import React from 'react';
import { Select, TextArea } from '@douyinfe/semi-ui';
() => {
const list = [
{ value: 'abc', 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
属性使用,自动选中第一项,当输入完内容直接回车时,可立即创建
注意
当开启allowCreate后,不会再响应对Children或者optionList的更新
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 的高度,必传
注意事项
Semi Select virtualize 功能是基于 react-window 的封装,虚拟化列表默认会被包裹在 `will-change: transform` 的 div 内部。 在某些浏览器(例如 Chrome),某些特定的屏幕尺寸下,屏幕物理像素尺寸与浏览器处理的像素无法对齐时,会自动开启抗锯齿。从而导致虚拟列表中的文本字体可能会在特定场景下存在模糊的情况。 will-change 对于复杂元素的渲染会有性能改善,所以我们默认不会对 react-window的样式进行覆盖。如果你希望关闭这个效果,可以通过自行覆盖 CSS,将 will-change 设置为 unset 解决
.semi-select-option-list > div {
will-change: unset !important; // 由于 react-window自带样式是内联的,所以这里用 important 覆盖
}
import React from 'react';
import { Select } from '@douyinfe/semi-ui';
class VirtualizeDemo extends React.Component {
constructor(props) {
super(props);
let newOptions = Array.from({ length: 3000 }, (v, i) => ({ label: `option-${i}`, value: i }));
this.state = {
optionList: newOptions,
};
}
render() {
let { groups, optionList } = this.state;
let virtualize = {
height: 270,
width: '100%',
itemSize: 36, // px
};
return (
<>
<Select
placeholder="拥有3k个Option的Select"
style={{ width: 260 }}
filter
onSearch={this.handleSearch}
virtualize={virtualize}
optionList={optionList}
></Select>
</>
);
}
}
自定义触发器
如果 Select 默认的触发器样式满足不了你的需求,可以用triggerRender
自定义选择框的展示
如果想保留搜索筛选能力,又不希望自己渲染 Input 相关的结构,可以同时通过 searchPosition='dropdown',将默认的搜索框置于下拉列表中
triggerRender 入参如下
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
}
import React, { useState } from 'react';
import { Select } from '@douyinfe/semi-ui';
import { IconAppCenter, IconChevronDown } from '@douyinfe/semi-icons';
() => {
const [valList, setValList] = useState(['abc', 'ulikecam']);
const [val, setVal] = useState('abc');
const list = [
{ value: 'abc', 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={{
minWidth: '112',
height: 32,
display: 'flex',
alignItems: 'center',
paddingLeft: 8,
borderRadius: 3,
}}
>
<div
style={{
margin: 4,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
flexGrow: 1,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
}}
>
{value.map(item => item.label).join(' , ')}
<IconChevronDown style={{ margin: '0 8px', flexShrink: 0 }} />
</div>
</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>无边框无背景色的触发器</h4>
<Select
value={val}
onChange={value => setVal(value)}
triggerRender={triggerRender2}
optionList={list}
filter
searchPosition='dropdown'
style={{ width: 240, marginTop: 20, outline: 0 }}
></Select>
</div>
);
};
自定义候选项渲染
- 简单的自定义:通过 Option 的 label 属性或者 children 传入 ReactNode,你可以控制候选项的渲染,此时内容会自动带上内边距、背景色等样式
- 完全自定义:通过传入
renderOptionItem
,你可以完全接管列表中候选项的渲染,并且从回调入参中,获取到相关的状态值。实现更高自由度的结构渲染
注意事项:- props 传入的 style 需在 wrapper dom 上进行消费,否则在虚拟化场景下会无法正常使用
- 选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从 props 中获取到相对的 boolean 值
- onMouseEnter 需在 wrapper dom 上绑定,否则上下键盘操作时显示会有问题
- 如果你的自定义 item 为 Select.Option,需要将 renderProps.onClick 透传给 Option 的 onSelect prop
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,
});
const searchWords = [inputValue];
// Notice:
// 1.props传入的style需在wrapper dom上进行消费,否则在虚拟化场景下会无法正常使用
// 2.选中(selected)、聚焦(focused)、禁用(disabled)等状态的样式需自行加上,你可以从props中获取到相对的boolean值
// 3.onMouseEnter需在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: 'abc', 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}
/>
</>
);
};
.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;
}
}
}