CascaderSelect 级联选择
如果项目中使用的是 0.x 版本的基础组件(@icedesign/base, @ali/ice, @alife/next),请在左侧导航顶部切换组件版本。
安装方法
- 在命令行中执行以下命令
npm install @alifd/next@latest -S
开发指南
何时使用
级联选择由选择器和级联组成。把级联组件以弹层的方式隐藏,多用于表单场景。
API
CascaderSelect
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
size | 选择框大小可选值:'small', 'medium', 'large' | Enum | 'medium' |
placeholder | 选择框占位符 | String | - |
disabled | 是否禁用 | Boolean | false |
hasArrow | 是否有下拉箭头 | Boolean | true |
hasBorder | 是否有边框 | Boolean | true |
hasClear | 是否有清除按钮 | Boolean | false |
label | 自定义内联 label | ReactNode | - |
readOnly | 是否只读,只读模式下可以展开弹层但不能选 | Boolean | - |
dataSource | 数据源,结构可参考下方说明 | Array<Object> | [] |
defaultValue | (非受控)默认值 | String/Array<String> | null |
value | (受控)当前值 | String/Array<String> | - |
onChange | 选中值改变时触发的回调函数签名:Function(value: String/Array, data: Object/Array, extra: Object) => void参数:value: {String/Array} 选中的值,单选时返回单个值,多选时返回数组data: {Object/Array} 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点extra: {Object} 额外参数extra.selectedPath: {Array} 单选时选中的数据的路径extra.checked: {Boolean} 多选时当前的操作是选中还是取消选中extra.currentData: {Object} 多选时当前操作的数据extra.checkedData: {Array} 多选时所有被选中的数据extra.indeterminateData: {Array} 多选时半选的数据 | Function | - |
defaultExpandedValue | 默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置 | Array<String> | - |
expandTriggerType | 展开触发的方式可选值:'click', 'hover' | Enum | 'click' |
useVirtual | 是否开启虚拟滚动 | Boolean | false |
multiple | 是否多选 | Boolean | false |
changeOnSelect | 是否选中即发生改变, 该属性仅在单选模式下有效 | Boolean | false |
canOnlyCheckLeaf | 是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效 | Boolean | false |
checkStrictly | 父子节点是否选中不关联 | Boolean | false |
listStyle | 每列列表样式对象 | Object | - |
listClassName | 每列列表类名 | String | - |
displayRender | 选择框单选时展示结果的自定义渲染函数签名:Function(label: Array) => ReactNode参数:label: {Array} 选中路径的文本数组返回值:{ReactNode} 渲染在选择框中的内容 | Function | 单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPathlabelPath.length - 1 |
itemRender | 渲染 item 内容的方法签名:Function(item: Object) => ReactNode参数:item: {Object} 渲染节点的item返回值:{ReactNode} item node | Function | - |
showSearch | 是否显示搜索框 | Boolean | false |
filter | 自定义搜索函数签名:Function(searchValue: String, path: Array) => Boolean参数:searchValue: {String} 搜索的关键字path: {Array} 节点路径返回值:{Boolean} 是否匹配 | Function | 根据路径所有节点的文本值模糊匹配 |
resultRender | 搜索结果自定义渲染函数签名:Function(searchValue: String, path: Array) => ReactNode参数:searchValue: {String} 搜索的关键字path: {Array} 匹配到的节点路径返回值:{ReactNode} 渲染的内容 | Function | 按照节点文本 a / b / c 的模式渲染 |
resultAutoWidth | 搜索结果列表是否和选择框等宽 | Boolean | true |
notFoundContent | 无数据时显示内容 | ReactNode | 'Not Found' |
loadData | 异步加载数据函数签名:Function(data: Object) => void参数:data: {Object} 当前点击异步加载的数据 | Function | - |
header | 自定义下拉框头部 | ReactNode | - |
footer | 自定义下拉框底部 | ReactNode | - |
defaultVisible | 初始下拉框是否显示 | Boolean | false |
visible | 当前下拉框是否显示 | Boolean | - |
onVisibleChange | 下拉框显示或关闭时触发事件的回调函数签名:Function(visible: Boolean, type: String) => void参数:visible: {Boolean} 是否显示type: {String} 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 | Function | () => {} |
popupStyle | 下拉框自定义样式对象 | Object | - |
popupClassName | 下拉框样式自定义类名 | String | - |
popupContainer | 下拉框挂载的容器节点 | String/Function | - |
popupProps | 透传到 Popup 的属性对象 | Object | {} |
followTrigger | 是否跟随滚动 | Boolean | - |
数组中 Item 的自定义属性也会被透传到 onChange 函数的 data 参数中。<!— api-extra-end —>## ARIA and KeyBoard#
const dataSource = [{ value: '2974', label: '西安', children: [ { value: '2975', label: '西安市', disabled: true }, { value: '2976', label: '高陵县', checkboxDisabled: true }, { value: '2977', label: '蓝田县' }, { value: '2978', label: '户县' }, { value: '2979', label: '周至县' }, { value: '4208', label: '灞桥区' }, { value: '4209', label: '长安区' }, { value: '4210', label: '莲湖区' }, { value: '4211', label: '临潼区' }, { value: '4212', label: '未央区' }, { value: '4213', label: '新城区' }, { value: '4214', label: '阎良区' }, { value: '4215', label: '雁塔区' }, { value: '4388', label: '碑林区' }, { value: '610127', label: '其它区' } ]}];
按键 | 说明 |
---|---|
Up Arrow | 获取同级当前项前一项焦点 |
Down Arrow | 获取同级当前项后一项焦点 |
Left Arrow | 进入当前项的子元素,并获取第一个子元素为焦点 |
Right Arrow | 返回当前项的父元素并获取焦点 |
Enter | 打开目录或选择当前项 |
Esc | 关闭目录 |
SPACE | 选择当前项 |
代码示例
展示基本的单选用法。
查看源码在线预览
import { CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => {
data[1].disabled = true;
this.setState({ data });
})
.catch(e => console.log(e));
}
handleChange(value, data, extra) {
console.log(value, data, extra);
}
render() {
return <CascaderSelect style={{ width: '302px' }} dataSource={this.state.data} onChange={this.handleChange} />;
}
}
ReactDOM.render(<Demo />, mountNode);
展示可通过 expandTriggerType 来设置不同的展开触发行为,支持 click 和 hover,默认值为 click。
查看源码在线预览
import { Radio, CascaderSelect } from '@alifd/next';
const RadioGroup = Radio.Group;
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
triggerType: 'click',
data: []
};
this.handleChange = this.handleChange.bind(this);
this.handleTriggerTypeChange = this.handleTriggerTypeChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => this.setState({ data }))
.catch(e => console.log(e));
}
handleChange(value, data, extra) {
console.log(value, data, extra);
}
handleTriggerTypeChange(triggerType) {
this.setState({
triggerType
});
}
render() {
return (
<div>
<div className="trigger-check">
Expand trigger type:
<RadioGroup dataSource={['click', 'hover']} value={this.state.triggerType} onChange={this.handleTriggerTypeChange} />
</div>
<CascaderSelect style={{ width: '302px' }} expandTriggerType={this.state.triggerType} dataSource={this.state.data} onChange={this.handleChange} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.trigger-check {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.trigger-check .next-radio-group {
margin-left: 10px;
}
展示基本的多选用法。
查看源码在线预览
import { CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => {
data[1].disabled = true;
data[2].checkboxDisabled = true;
this.setState({ data });
})
.catch(e => console.log(e));
}
handleChange(value, data, extra) {
console.log(value, data, extra);
}
render() {
return <CascaderSelect style={{ width: '302px' }} multiple dataSource={this.state.data} onChange={this.handleChange} />;
}
}
ReactDOM.render(<Demo />, mountNode);
.cascader-value {
width: 500px;
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
展示受控单选以及是否选择即改变。
查看源码在线预览
import { Checkbox, CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
changeOnSelect: false,
data: []
};
this.handleCheck = this.handleCheck.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => this.setState({ data, value: '2975' }))
.catch(e => console.log(e));
}
handleCheck() {
this.setState({
changeOnSelect: !this.state.changeOnSelect,
value: null
});
}
handleChange(value, data, extra) {
console.log(value, data, extra);
this.setState({
value
});
}
render() {
return (
<div className="control-single-demo">
<label className="change-check">
<Checkbox value={!this.state.changeOnSelect} onChange={this.handleCheck} />
<span className="change-text">Enable changeOnSelect</span>
</label>
<CascaderSelect style={{ width: '302px' }} changeOnSelect={this.state.changeOnSelect} value={this.state.value} dataSource={this.state.data} onChange={this.handleChange} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.control-single-demo .change-check {
display: block;
margin-bottom: 10px;
}
.control-single-demo .change-text {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
color: #666;
font-size: 14px;
}
展示受控多选以及是否开启严格受控父子节点选中不再关联的用法。
查看源码在线预览
import { Checkbox, CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: [],
data: [],
checkStrictly: false
};
this.handleCheck = this.handleCheck.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => this.setState({ data, value: ['2975'] }))
.catch(e => console.log(e));
}
handleCheck() {
this.setState({
checkStrictly: !this.state.checkStrictly,
value: []
});
}
handleChange(value, data, extra) {
console.log(value, data, extra);
this.setState({
value
});
}
render() {
return (
<div className="control-multiple-demo">
<label className="strictly-check">
<Checkbox value={this.state.checkStrictly} onChange={this.handleCheck} />
<span className="strictly-text">Enable checkStrictly</span>
</label>
<CascaderSelect style={{ width: '302px' }} multiple checkStrictly={this.state.checkStrictly} value={this.state.value} dataSource={this.state.data} onChange={this.handleChange} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.control-multiple-demo .strictly-check {
display: block;
margin-bottom: 10px;
}
.control-multiple-demo .strictly-text {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
color: #666;
font-size: 14px;
}
通过设置 showSearch 为 true,可以开启组件的搜索功能。
查看源码在线预览
import { Checkbox, CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: [],
data: [],
multiple: false
};
this.handleCheck = this.handleCheck.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => this.setState({ data, value: ['2975'] }))
.catch(e => console.log(e));
}
handleCheck() {
this.setState({
multiple: !this.state.multiple,
value: []
});
}
handleChange(value, data, extra) {
console.log(value, data, extra);
this.setState({
value
});
}
render() {
return (
<div className="search-demo">
<label className="multiple-check">
<Checkbox value={this.state.multiple} onChange={this.handleCheck} />
<span className="multiple-text">Multiple select</span>
</label>
<CascaderSelect style={{ width: '302px' }} showSearch multiple={this.state.multiple} value={this.state.value} dataSource={this.state.data} onChange={this.handleChange} />
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.search-demo .multiple-check {
display: block;
margin-bottom: 10px;
}
.search-demo .multiple-text {
display: inline-block;
margin-left: 10px;
vertical-align: middle;
color: #666;
font-size: 14px;
}
可以通过 displayRender 来定制单选时展示的结果,可以通过 listStyle,listClassName 来定制组件宽高。
查看源码在线预览
import { CascaderSelect, Icon } from '@alifd/next';
const dataSource = [{
value: '2973',
label: '陕西',
children: [{
value: '2974',
label: '西安',
children: [
{ value: '2975', label: '西安市' },
{ value: '2976', label: '高陵县' }
]
}, {
value: '2980',
label: '铜川',
children: [
{ value: '2981', label: '铜川市' },
{ value: '2982', label: '宜君县' }
]
}]
}, {
value: '3371',
label: '新疆',
children: [{
value: '3430',
label: '巴音郭楞蒙古自治州',
children: [
{ value: '3431', label: '库尔勒市' },
{ value: '3432', label: '和静县' }
]
}]
}];
const itemRender = item => {
return (
<span>
<Icon type="account" size="xs" /> {item.label}
</span>
);
};
ReactDOM.render(<CascaderSelect style={{ width: '452px'}} listStyle={{ width: '150px', height: '160px' }} displayRender={labels => labels[labels.length - 1]} defaultValue="3431" dataSource={dataSource} itemRender={itemRender} />, mountNode);
展示动态获取数据的用法。
查看源码在线预览
import { CascaderSelect } from '@alifd/next';
const dataSource = [{
value: '2973',
label: '陕西'
}];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
dataSource
};
this.onLoadData = this.onLoadData.bind(this);
}
onLoadData(data) {
console.log(data);
return new Promise(resolve => {
setTimeout(() => {
this.setState({
dataSource: [{
value: '2973',
label: '陕西',
children: [{
value: '2974',
label: '西安',
children: [
{ value: '2975', label: '西安市', isLeaf: true },
{ value: '2976', label: '高陵县', isLeaf: true }
]
}, {
value: '2980',
label: '铜川',
children: [
{ value: '2981', label: '铜川市', isLeaf: true },
{ value: '2982', label: '宜君县', isLeaf: true }
]
}]
}]
}, resolve);
}, 500);
});
}
render() {
return <CascaderSelect style={{ width: '302px' }} dataSource={this.state.dataSource} loadData={this.onLoadData} />;
}
}
ReactDOM.render(<Demo />, mountNode);
查看源码在线预览
import { CascaderSelect } from '@alifd/next';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
.then(response => response.json())
.then(data => {
data[1].disabled = true;
this.setState({ data });
})
.catch(e => console.log(e));
}
handleChange(value, data, extra) {
console.log(value, data, extra);
}
valueRender = (item) => {
if (item.label) {
return item.label; // 正常的item
}
// value在 dataSouce里不存在时渲染。
return item.value === '432988' ? '不存在的' : item.value;
};
render() {
return <CascaderSelect valueRender={this.valueRender} defaultValue="432988" style={{ width: '302px' }} dataSource={this.state.data} onChange={this.handleChange} />;
}
}
ReactDOM.render(<Demo />, mountNode);
当聚焦在组件上时,通过aria-labelledby
对组件进行描述。关于键盘操作请参考ARIA and KeyBoard
。
查看源码在线预览
import { CascaderSelect } from '@alifd/next';
const data = [{
value: '0100',
label: '金庸',
children: [
{ value: '0101', label: '飞狐外传' },
{ value: '0102', label: '雪山飞狐' },
{ value: '0103', label: '连城诀' },
{ value: '0104', label: '天龙八部' },
{ value: '0105', label: '射雕英雄传' },
{ value: '0106', label: '白马啸西风' },
{ value: '0107', label: '鹿鼎记' },
{ value: '0108', label: '笑傲江湖' },
{ value: '0109', label: '书剑恩仇录' },
{ value: '0110', label: '神雕侠侣' },
{ value: '0111', label: '侠客行' },
{ value: '0112', label: '倚天屠龙记' },
{ value: '0113', label: '碧血剑' },
{ value: '0114', label: '鸳鸯刀' }
]
}, {
value: '0200',
label: '古龙',
children: [
{ value: '0201', label: '小李飞刀' },
{ value: '0202', label: '绝代双骄' },
{ value: '0203', label: '大旗英雄传' },
{ value: '0204', label: '英雄无泪' },
{ value: '0205', label: '边城浪子' },
{ value: '0206', label: '楚留香传奇' },
],
}, {
children: [
{ value: '0301', label: '白发魔女传' },
{ value: '0302', label: '七剑下天山' },
{ value: '0303', label: '云海玉弓缘' }
],
value: '0300',
label: '梁羽生'
}];
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
label: '',
data: []
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.setState({ data });
}
handleChange(value, data, extra) {
console.log(value, data, extra);
this.setState({
label: extra.selectedPath.map(d => d.label).join(' / '),
});
}
render() {
return (
<div>
<div id="a11y-cascader-select" >CascaderSelect: </div>
<CascaderSelect dataSource={this.state.data} onChange={this.handleChange} listStyle={{ width: '200px', height: '256px' }} aria-labelledby="a11y-cascader-select"/>
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
.next-menu-item:focus {
background-color: #F2F3F7;
}