使用场景
与 TreeSelect 组件的区别:
- TreeSelect: 核心价值在于目标节点,层级结构是为了方便用户快速筛选出目标选项,最终的节点才是用户想要的内容,常见于文件/文件夹选择、组织架构、权限分配等场景。
- Cascader: 核心价值在于路径,用户选择的不是一个孤立的点,而是一条从根到叶的完整路径,常用于地理位置,商品分类等场景。
代码演示
如何引入
import { Cascader } from '@fibrous/semi-ui-solid';
基本用法
最简单的用法,默认只可以选叶子节点。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" />;
}多选
设置 multiple,可以进行多选。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader defaultValue={['zhejiang', 'ningbo', 'jiangbei']} style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" multiple />;
}可搜索的
通过设置 filterTreeNode 属性可支持搜索功能。
默认对 label 值进行搜索(使用字符串的 includes 方法进行匹配,不区分大小写),可通过 treeNodeFilterProp 指定其他属性值进行搜索。label 只接受文本,节点内容用 renderLabel 定制时,搜索仍匹配 label 文本;需要匹配其他文本时,可在 treeData 中使用其他字段存储,并通过 treeNodeFilterProp 指定该字段进行搜索。
默认搜索结果只会展示叶子结点的路径,想要显示更多的结果,可以设置 filterLeafOnly 为 false。
filterLeafOnly=false:
renderLabel 自定义选项内容,仍对 label 文本进行搜索
代码
import { IconMapPinStroked } from '@fibrous/semi-icons-solid';
import { Cascader, type CascaderData, Typography } from '@fibrous/semi-ui-solid';
// The label inside an option must not be focusable, so the node only adds a decorative icon to the text
const renderLabel = (item: CascaderData) => (
<span style={{ display: 'inline-flex', 'align-items': 'center', 'column-gap': '4px' }}>
<IconMapPinStroked />
{item.label}
</span>
);
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<div>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="默认对label值进行搜索" filterTreeNode />
<br />
<br />
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="对value值进行搜索" filterTreeNode treeNodeFilterProp="value" />
<br />
<br />
<Typography.Title heading={6}>filterLeafOnly=false:</Typography.Title>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="filterLeafOnly=false" filterTreeNode filterLeafOnly={false} />
<br />
<br />
<Typography.Title heading={6}>renderLabel 自定义选项内容,仍对 label 文本进行搜索</Typography.Title>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="renderLabel" filterTreeNode renderLabel={renderLabel} />
</div>
);
}可搜索的多选
支持多选和搜索同时使用,在这种场景下,可以通过按下 BackSpace 键来删除对应的已选项目。
代码
import { createSignal } from 'solid-js';
import { Cascader, type CascaderValue } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
const [value, setValue] = createSignal<CascaderValue>(['zhejiang', 'ningbo', 'haishu']);
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" value={value()} multiple filterTreeNode onChange={(newValue) => setValue(newValue)} />;
}可以使用 filterSorter 对筛选后的数据进行排序。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: 'Product',
value: 'Product',
children: [
{
label: 'Semi-Material',
value: 'Semi-Material',
},
{
label: 'Semi-DSM',
value: 'Semi-DSM',
},
{
label: 'Semi',
value: 'Semi',
},
{
label: 'Semi-C2D',
value: 'Semi-C2D',
},
{
label: 'Semi-D2C',
value: 'Semi-D2C',
},
],
},
];
export default function CascaderDemo() {
return (
<div>
<Cascader
style={{ width: '300px' }}
treeData={treeData}
placeholder="输入 s 查看排序效果"
filterTreeNode
filterSorter={(first, second, inputValue) => {
const firstData = first[first.length - 1];
const lastData = second[second.length - 1];
if (firstData.label === inputValue) {
return -1;
} else if (lastData.label === inputValue) {
return 1;
} else {
return String(firstData.label) < String(lastData.label) ? -1 : 1;
}
}}
/>
</div>
);
}如果想要自定义渲染搜索后的选项,可以使用 filterRender 实现整行的自定义渲染,函数参数如下:
interface CascaderFilterRenderProps {
class: string; // 选项的 class,需要绑定到返回的根元素上
inputValue: string; // 搜索栏搜索内容
disabled: boolean; // 是否禁用
data: CascaderData[]; // 搜索结果数据,即这条路径上的各级节点
selected: boolean; // 单选时的选中状态
checkStatus: {
// 多选时的选中状态
checked: boolean;
halfChecked: boolean;
};
onClick: (e: MouseEvent) => void; // 单选点击选中回调
onCheck: (e: CheckboxEvent) => void; // 多选点击选中回调,Checkbox 的 onChange 签名
}
返回单个元素时,该元素获得选项的 id、role="option" 和选中状态;返回文本或多个节点时,组件包一层 li。
使用示例如下
鼠标 hover 到选项可查看被省略文本完整内容
代码
import { Cascader, Checkbox, type CascaderFilterRenderProps, Typography } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: 'Semi',
value: 'Semi',
children: [
{
label: 'Semi-Material Semi-Material Semi-Material Semi-Material',
value: 'Semi-Material',
},
{
label: 'Semi-DSM Semi-DSM Semi-DSM Semi-DSM',
value: 'Semi-DSM',
},
{
label: 'Semi Design Semi Design Semi Design Semi Design',
value: 'Semi',
},
{
label: 'Semi-C2D Semi-C2D Semi-C2D Semi-C2D Semi-C2D',
value: 'Semi-C2D',
},
{
label: 'Semi-D2C Semi-D2C Semi-D2C Semi-D2C Semi-D2C ',
value: 'Semi-D2C',
},
],
},
];
const renderSearchOptionSingle = (props: CascaderFilterRenderProps) => (
<li class={props.class} style={{ 'justify-content': 'flex-start' }} role="option" aria-selected={props.selected} onClick={(e) => props.onClick(e)}>
<Typography.Text ellipsis={{ showTooltip: { opts: { style: { 'word-break': 'break-all' } } } }} style={{ width: '270px', color: props.selected ? 'var(--semi-color-primary)' : undefined }}>
{props.data.map((item) => item.label).join(' / ')}
</Typography.Text>
</li>
);
const renderSearchOptionMultiple = (props: CascaderFilterRenderProps) => {
const label = () => props.data.map((item) => item.label).join(' / ');
return (
<li class={props.class} style={{ 'justify-content': 'flex-start' }} role="option" aria-label={label()} aria-selected={props.checkStatus.checked}>
<Checkbox tabIndex={-1} onChange={props.onCheck} indeterminate={props.checkStatus.halfChecked} checked={props.checkStatus.checked}>
<Typography.Text ellipsis={{ showTooltip: { opts: { style: { 'word-break': 'break-all' } } } }} style={{ width: '250px' }}>
{label()}
</Typography.Text>
</Checkbox>
</li>
);
};
export default function CascaderDemo() {
return (
<div>
<p>鼠标 hover 到选项可查看被省略文本完整内容</p>
<br />
<Cascader style={{ width: '320px' }} treeData={treeData} placeholder="单选,输入 s 自定义搜索选项渲染结果" filterTreeNode filterRender={renderSearchOptionSingle} />
<br />
<Cascader multiple style={{ width: '320px', 'margin-top': '20px' }} treeData={treeData} placeholder="多选,输入 s 自定义搜索选项渲染结果" filterTreeNode filterRender={renderSearchOptionMultiple} />
</div>
);
}如果搜索结果中存在大量 Option,可以通过设置 virtualizeInSearch 开启搜索结果面板的虚拟化来优化性能,virtualizeInSearch 是一个包含下列值的对象:
- height: Option 列表高度值(number,单位 px),缺省 180,与下拉框高度一致
- width: Option 列表宽度值
- itemSize: 每行 Option 的高度
代码
import { Cascader, Checkbox, type CascaderFilterRenderProps, Typography } from '@fibrous/semi-ui-solid';
const treeData = ['通用', '场景'].map((label, m) => ({
label: label,
value: m,
children: Array.from({ length: 100 }, (_item, n) => ({
value: `${m}-${n}`,
label: `${m}-${n} 第二级`,
children: Array.from({ length: 20 }, (_item, o) => ({
value: `${m}-${n}-${o}`,
label: `${m}-${n}-${o} 第三级详细内容`,
})),
})),
}));
const virtualize = {
// Height = default panel 180px minus 2 * 8px padding
height: 172,
width: 320,
itemSize: 36,
};
const renderFilterItem = (props: CascaderFilterRenderProps) => (
<div class={props.class} style={{ 'justify-content': 'start', padding: '8px 16px 8px 12px', 'box-sizing': 'border-box' }}>
<Checkbox onChange={props.onCheck} indeterminate={props.checkStatus.halfChecked} checked={props.checkStatus.checked} style={{ 'margin-right': '8px' }} />
<Typography.Text ellipsis={{ showTooltip: { opts: { style: { 'word-break': 'break-all' } } } }} style={{ 'max-width': '260px' }}>
{props.data.map((item) => item.label).join(' | ')}
</Typography.Text>
</div>
);
export default function CascaderDemo() {
return <Cascader multiple filterTreeNode style={{ width: '320px' }} treeData={treeData} placeholder="输入 通用 or 场景 进行搜索" virtualizeInSearch={virtualize} filterRender={renderFilterItem} />;
}限制标签展示数量
在多选的场景中,利用 maxTagCount 可以限制展示的标签数量,超出部分将以 +N 的方式展示。
使用 showRestTagsPopover 可以设置在超出 maxTagCount 后,hover +N 是否显示 Popover,默认为 false。并且,还可以在 restTagsPopoverProps 属性中配置 Popover。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<Cascader
style={{ width: '300px' }}
treeData={treeData}
placeholder="请选择所在地区"
multiple
showRestTagsPopover
restTagsPopoverProps={{ position: 'top' }}
maxTagCount={1}
defaultValue={[
['zhejiang', 'ningbo', 'haishu'],
['zhejiang', 'hangzhou', 'xihu'],
]}
/>
);
}限制选中数量
在多选的场景中,利用 max 可以限制多选选中的数量。超出 max 后将触发 onExceed 回调。
代码
import { Cascader, Toast } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<Cascader
style={{ width: '300px' }}
treeData={treeData}
placeholder="请选择所在地区"
multiple
max={1}
onExceed={(v) => {
Toast.warning('exceed max');
console.log(v);
}}
defaultValue={[['zhejiang', 'ningbo', 'haishu']]}
/>
);
}选择即改变
在单选的情况下,还可以通过设置 changeOnSelect,允许选中父级选项。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<div>
<Cascader style={{ width: '300px' }} treeData={treeData} changeOnSelect placeholder="选择即改变" />
<br />
<br />
<Cascader style={{ width: '300px' }} treeData={treeData} changeOnSelect placeholder="可搜索的选择即改变" filterTreeNode />
</div>
);
}自定义显示
可以通过 displayProp 设置回填选项显示的属性值,默认为 label。
单选
多选
代码
import { Cascader, Typography } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<>
<Typography.Title heading={6}>单选</Typography.Title>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="回填时显示数据的value值" displayProp="value" defaultValue={['zhejiang', 'ningbo', 'jiangbei']} />
<br />
<br />
<Typography.Title heading={6}>多选</Typography.Title>
<Cascader multiple style={{ width: '300px' }} treeData={treeData} defaultValue={['zhejiang', 'ningbo', 'jiangbei']} placeholder="回填时显示数据的value值" displayProp="value" />
</>
);
}可以通过设置 displayRender 设定回填格式。
单选 (multiple=false) 时, displayRender((labelPath: Array<string | number>) => JSX.Element), 其中 labelPath 是由 label 构成的 path 数组。
多选 (multiple=true) 时, displayRender((item: Entity, index: number) => JSX.Element), 其中 item 为节点的相关数据,类型见下方的 Entity。
interface Entity {
children?: Entity[]; // 子节点列表
data: CascaderData; // treedata
ind: number; // index
key: string; // key
level: number; // 节点层级
parent?: Entity; // 父节点数据
parentKey?: string; // 父节点 key
path: string[]; // key 路径
pos: string; // 节点在 treeData 中的层级位置,如 '0-0-1'
valuePath: string[]; // value 路径
}
单选
多选
代码
import type { JSX } from 'solid-js';
import { Cascader, Tag, Typography } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<>
<Typography.Title heading={6}>单选</Typography.Title>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="自定义回填时显示数据的格式" displayRender={(list) => '已选择:' + list.join(' -> ')} defaultValue={['zhejiang', 'ningbo', 'jiangbei']} />
<br />
<br />
<Typography.Title heading={6}>多选</Typography.Title>
<Cascader
multiple
style={{ width: '300px' }}
treeData={treeData}
defaultValue={['zhejiang', 'ningbo', 'jiangbei']}
placeholder="自定义回填时显示数据的格式"
displayRender={(item) => (
<Tag style={{ 'margin-right': '4px' }} color="white">
{item.data.label as JSX.Element}
</Tag>
)}
/>
</>
);
}自定义分隔符
可以使用 separator 设置分隔符, 包括:搜索时显示在下拉框的内容以及单选时回显到 Trigger 的内容的分隔符。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader style={{ width: '300px' }} treeData={treeData} defaultValue={['zhejiang', 'ningbo', 'jiangbei']} filterTreeNode separator=" > " />;
}禁用
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<div>
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" disabled />
<br />
<br />
<Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" defaultValue={['zhejiang', 'hangzhou', 'xihu']} filterTreeNode disabled />
</div>
);
}严格禁用
可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。
以下面的 demo 为例,节点"宁波"开启了严格禁用,因此,当我们改变其父节点"浙江省"的选中状态时,也不会影响到节点"宁波"的选中状态。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
disabled: true,
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader style={{ width: '300px' }} treeData={treeData} multiple placeholder="请选择所在地区" disableStrictly />;
}展示子菜单的时机
可以使用 showNext 设置展开 Dropdown 子菜单的触发时机,可选: click(默认)、hover。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" showNext="hover" />;
}点击选中
在多选模式下,默认情况下点击非叶子节点不会触发选中。你可以通过 clickToSelect 开启点击任意节点即选中的功能。
这个 API 在配合 showNext="hover" 使用时特别有用:鼠标悬浮展开子菜单,点击则选中当前节点。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader style={{ width: '300px' }} treeData={treeData} multiple placeholder="请选择所在地区" showNext="hover" clickToSelect />;
}在顶部/底部渲染附加项
我们在级联选择器的顶部、底部分别预留了插槽,你可以通过 topSlot 或 bottomSlot 来设置。
代码
import { Cascader, Typography } from '@fibrous/semi-ui-solid';
const slotStyle = {
height: '36px',
display: 'flex',
padding: '0 32px',
'align-items': 'center',
cursor: 'pointer',
'border-top': '1px solid var(--semi-color-border)',
};
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
return (
<Cascader
style={{ width: '300px' }}
treeData={treeData}
placeholder="请选择所在地区"
bottomSlot={
<div style={slotStyle}>
<Typography.Text>找不到相关选项?</Typography.Text>
<Typography.Text link>去新建</Typography.Text>
</div>
}
/>
);
}受控
传入 value 时即为受控组件,可以配合 onChange 使用。就地修改受控数组不会被识别,更新选中项时需要传入新的数组。
代码
import { createSignal } from 'solid-js';
import { Cascader, type CascaderSimpleValue } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
const [value, setValue] = createSignal<CascaderSimpleValue[]>([]);
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" value={value()} onChange={(newValue) => setValue(newValue)} />;
}自动合并 value
在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 autoMergeValue 来设置,默认为 true。当 autoMergeValue 和 leafOnly 同时开启时,后者优先级更高。
代码
import { createSignal } from 'solid-js';
import { Cascader, type CascaderValue } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
const [value, setValue] = createSignal<CascaderValue>([]);
const onChange = (newValue: CascaderValue) => {
console.log(newValue);
setValue(newValue);
};
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="autoMergeValue 为 false" value={value()} multiple autoMergeValue={false} onChange={onChange} />;
}仅叶子节点
多选时,可以通过开启 leafOnly 来设置 value 只包含叶子节点,即显示的 Tag 和 onChange 的参数 value 只包含叶子节点。
代码
import { createSignal } from 'solid-js';
import { Cascader, type CascaderValue } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
export default function CascaderDemo() {
const [value, setValue] = createSignal<CascaderValue>([]);
const onChange = (newValue: CascaderValue) => {
console.log(newValue);
setValue(newValue);
};
return <Cascader style={{ width: '300px' }} treeData={treeData} placeholder="开启 leafOnly" value={value()} multiple leafOnly onChange={onChange} />;
}节点选中关系
多选时,可以使用 checkRelation 来设置节点之间选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated' 时,意味着节点之间的选中互不影响。
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: '亚洲',
value: 'Asia',
children: [
{
label: '中国',
value: 'China',
children: [
{
label: '北京',
value: 'Beijing',
},
{
label: '上海',
value: 'Shanghai',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
},
];
export default function CascaderDemo() {
return <Cascader multiple defaultValue={[['Asia'], ['Asia', 'China', 'Beijing']]} checkRelation="unRelated" style={{ width: '300px' }} treeData={treeData} />;
}动态更新数据
treeData 整体替换即可更新数据;内容相同的新数组不会重置展开和选中状态。
代码
import { createSignal } from 'solid-js';
import { Button, Cascader, type CascaderData } from '@fibrous/semi-ui-solid';
export default function CascaderDemo() {
const [treeData, setTreeData] = createSignal<CascaderData[]>([]);
const add = () => {
const itemLength = Math.floor(Math.random() * 3) + 1;
const newTreeData = Array.from({ length: itemLength }, (_v, i) => {
const length = Math.floor(Math.random() * 3);
const children = Array.from({ length }, (_cv, ci) => ({
key: `${i}-${ci}`,
label: `Item-${i}-${ci}`,
value: `${i}-${ci}`,
}));
return {
key: `${i}`,
label: `Item-${i}`,
value: `${i}`,
children,
};
});
setTreeData(newTreeData);
};
return (
<>
<Cascader style={{ width: '300px' }} treeData={treeData()} placeholder="请选择" />
<br />
<br />
<Button onClick={add}>动态改变数据</Button>
</>
);
}异步加载数据
可以使用 loadData 实现异步加载数据
不能与搜索同时使用
代码
import { createSignal } from 'solid-js';
import { Cascader, type CascaderData } from '@fibrous/semi-ui-solid';
const initialData: CascaderData[] = [
{
label: 'Node1',
value: '0-0',
},
{
label: 'Node2',
value: '0-1',
},
{
label: 'Node3',
value: '0-2',
isLeaf: true,
},
];
const updateTreeData = (list: CascaderData[], value: CascaderData['value'], children: CascaderData[]): CascaderData[] => {
return list.map((node) => {
if (node.value === value) {
return { ...node, children };
}
if (node.children) {
return { ...node, children: updateTreeData(node.children, value, children) };
}
return node;
});
};
export default function CascaderDemo() {
const [data, setData] = createSignal<CascaderData[]>(initialData);
const onLoadData = (selectedOpt: CascaderData[]) => {
const targetOpt = selectedOpt[selectedOpt.length - 1];
const { label, value } = targetOpt;
return new Promise<void>((resolve) => {
if (targetOpt.children) {
resolve();
return;
}
setTimeout(() => {
setData((origin) =>
updateTreeData(origin, value, [
{
label: `${label} - 1`,
value: `${label}-1`,
isLeaf: selectedOpt.length > 1,
},
{
label: `${label} - 2`,
value: `${label}-2`,
isLeaf: selectedOpt.length > 1,
},
])
);
resolve();
}, 1000);
});
};
return <Cascader style={{ width: '300px' }} treeData={data()} loadData={onLoadData} placeholder="Please select" />;
}远程搜索
设置 remote 后,搜索输入不再走本地匹配,而是仅触发 onSearch 回调,由你根据输入异步拉取 treeData。这与 Select 的 remote 行为一致。
代码
import { createSignal } from 'solid-js';
import { debounce } from 'es-toolkit';
import { Cascader, type CascaderData, Spin } from '@fibrous/semi-ui-solid';
const fetchByKeyword = (keyword: string) =>
new Promise<CascaderData[]>((resolve) => {
const delay = 200 + Math.floor(Math.random() * 800);
setTimeout(() => {
if (!keyword) {
resolve([]);
return;
}
resolve([
{ label: `${keyword} - 选项 A`, value: `${keyword}-a` },
{ label: `${keyword} - 选项 B`, value: `${keyword}-b` },
{ label: `${keyword} - 选项 C`, value: `${keyword}-c` },
]);
}, delay);
});
export default function CascaderDemo() {
const [treeData, setTreeData] = createSignal<CascaderData[]>([]);
const [loading, setLoading] = createSignal(false);
const latest = { token: 0 };
const handleSearch = debounce((input: string) => {
if (!input) {
setTreeData([]);
setLoading(false);
return;
}
latest.token += 1;
const token = latest.token;
setLoading(true);
void fetchByKeyword(input).then((next) => {
// Drop stale results that arrive after a newer request
if (token !== latest.token) {
return;
}
setTreeData(next);
setLoading(false);
});
}, 300);
return (
<Spin spinning={loading()}>
<Cascader style={{ width: '300px' }} placeholder="输入关键词远程搜索" filterTreeNode remote treeData={treeData()} onSearch={handleSearch} onChange={(v) => console.log('selected:', v)} />
</Spin>
);
}超长列表
当你的数据结构层级特别深时,Cascader下拉菜单可能会超出屏幕,此时我们建议为下拉菜单设置 overflow-x: auto 以及一个合适的 width 宽度( 建议以N+0.5列的宽度为准,最右侧显示半列,以给用户一种右侧尚有待展开项,可以水平方向滚动的视觉暗示)
代码
import { Cascader } from '@fibrous/semi-ui-solid';
const treeData = [
{
label: 'A',
value: 'A',
children: [
{
label: 'B',
value: 'B',
children: [
{
label: 'C',
value: 'C',
children: [
{
label: 'D',
value: 'D',
children: [
{
label: 'E',
value: 'E',
children: [
{
label: 'F',
value: 'F',
},
],
},
],
},
],
},
],
},
],
},
];
export default function CascaderDemo() {
return <Cascader dropdownClassName="components-cascader-demo" style={{ width: '300px' }} treeData={treeData} placeholder="请选择所在地区" />;
}.components-cascader-demo {
.semi-cascader-option-lists {
max-width: 510px;
overflow-x: auto;
}
}
自定义 Trigger
如果默认的触发器样式满足不了你的需求,可以用 triggerRender 自定义选择框的展示。
triggerRender 的入参类型为 CascaderTriggerRenderProps,如下:
interface CascaderTriggerRenderProps {
/* Cascader 的 props,不含 ref */
componentProps: CascaderProps;
/* 是否禁用 Cascader */
disabled: boolean;
/**
* 已选中的 node 在 treeData 中的层级位置,如下例子,
* 当选中浙江省-杭州市-萧山区时,此处 value 为 '0-0-1';多选时为 Set<string>
*/
value: string | undefined | Set<string>;
/* 展开时为搜索词,收起时为已选路径的展示文本 */
inputValue: JSX.Element;
/**
* 用于更新 input 框值的函数,当你在 triggerRender 自定义的
* Input 组件值更新时,你应该调用该函数,用于向 Cascader 内部
* 同步状态, 使用时需要设置 filterTreeNode 参数非 false
*/
onSearch: (inputValue: string) => void;
/* 用于清空值的函数;绑定在触发器内的控件上时传入点击事件,避免这次点击同时打开下拉 */
onClear: (event?: MouseEvent) => void;
/* Placeholder */
placeholder?: string;
/* 用于删除单个 item,入参为该节点在 treeData 中的层级位置(与 value 的格式相同) */
onRemove: (pos: string) => void;
}
读取入参时保持 props.value 这样的访问形式,不要解构,否则会丢失响应。
代码
import { type JSX, Show } from 'solid-js';
import { IconChevronDown, IconClose } from '@fibrous/semi-icons-solid';
import { Button, ButtonGroup, Cascader, type CascaderData, type CascaderTriggerRenderProps, Tag, TagInput } from '@fibrous/semi-ui-solid';
const treeData: CascaderData[] = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{
label: '西湖区',
value: 'xihu',
},
{
label: '萧山区',
value: 'xiaoshan',
},
{
label: '临安区',
value: 'linan',
},
],
},
{
label: '宁波市',
value: 'ningbo',
children: [
{
label: '海曙区',
value: 'haishu',
},
{
label: '江北区',
value: 'jiangbei',
},
],
},
],
},
];
const getLabelFromValue = (value: string) => {
const [first, ...rest] = value.split('-').map((item) => Number(item));
let node: CascaderData | undefined = treeData[first];
rest.forEach((item) => {
node = node?.children?.[item];
});
return node?.label as JSX.Element | undefined;
};
// The clear button is a sibling of the trigger button: buttons cannot nest
const triggerRenderSingle = (props: CascaderTriggerRenderProps) => (
<ButtonGroup theme="light">
<Button icon={<IconChevronDown />} iconPosition="right">
{props.value ? getLabelFromValue(props.value) : props.placeholder}
</Button>
<Show when={props.value}>
<Button
icon={<IconClose />}
aria-label="清除"
onClick={(e) => {
e.stopPropagation();
props.onClear(e);
}}
/>
</Show>
</ButtonGroup>
);
const triggerRenderMultiple = (props: CascaderTriggerRenderProps<true>) => (
<TagInput
value={[...props.value]}
onInputChange={(value) => props.onSearch(value)}
renderTagItem={(value) => (
<Tag tagKey={value} closable onClose={() => props.onRemove(value)} style={{ 'margin-left': '2px' }}>
{getLabelFromValue(value)}
</Tag>
)}
/>
);
export default function CascaderDemo() {
return (
<>
<Cascader treeData={treeData} placeholder="Custom Trigger" triggerRender={triggerRenderSingle} />
<br />
<Cascader triggerRender={triggerRenderMultiple} multiple filterTreeNode treeData={treeData} style={{ width: '300px' }} placeholder="Custom Trigger" />
</>
);
}API 参考
Cascader
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| aria-label | 组合框的可访问名称,次于 aria-labelledby,优先于 insetLabel 的内容。 | string | - |
| arrowIcon | 自定义右侧下拉箭头,传入 null 时不显示箭头。 | JSX.Element | - |
| autoAdjustOverflow | 下拉被边缘遮挡时是否自动调整展开方向。 | boolean | - |
| autoMergeValue | 多选时,选中父节点后 value 不再包含它的子孙节点。leafOnly 也开启时,leafOnly 优先。 | boolean | true |
| borderless | 是否使用无边框模式。 | boolean | false |
| bottomSlot | 下拉底部的插槽内容。 | JSX.Element | - |
| changeOnSelect | 单选时是否允许选择非叶子节点。 | boolean | false |
| checkRelation | 多选时节点之间的选中关系:related 为父子联动,unRelated 为互不影响。 | 'related' | 'unRelated' | "related" |
| clearIcon | 自定义清除按钮的图标,showClear 为 true 时有效。 | JSX.Element | - |
| clickToSelect | 多选时点击任意节点即选中,常与 showNext="hover" 配合:悬停展开子菜单,点击选中当前节点。 | boolean | false |
| defaultOpen | 是否在挂载时默认展开下拉。 | boolean | false |
| defaultValue | 默认选中的条目,只在挂载时读取:单选为节点或路径,多选为路径数组。 | TypeModeValue<Multiple> | - |
| disabled | 是否禁用。 | boolean | - |
| disableStrictly | 是否开启严格禁用。开启后,禁用的节点不会因父级或子级的选中关系改变选中状态。 | boolean | false |
| displayProp | 回填到触发器与标签中的节点属性。 | string | "label" |
| displayRender | 自定义回填内容。单选时参数为路径上各级节点 label 的数组;多选时参数为节点的 Entity 与索引,返回每个标签的内容。 | TypeByMultiple<Multiple, (selected: Array<string | number>) => JSX.Element, (selected: Entity, idx: number) => JSX.Element> | - |
| dropdownClassName | 下拉菜单的 class。 | string | - |
| dropdownStyle | 下拉菜单的样式,可以是键为 kebab-case 的对象,也可以是字符串。 | JSX.CSSProperties | string | - |
| emptyContent | 没有可选数据或搜索无结果时展示的内容,默认为「暂无数据」;传入 null 时不展示。 | JSX.Element | - |
| enableLeafClick | 多选时,点击叶子节点的选项是否触发勾选。 | boolean | false |
| expandIcon | 自定义子菜单的展开图标。 | JSX.Element | - |
| filterLeafOnly | 搜索结果是否只展示叶子节点的路径。 | boolean | true |
| filterRender | 自定义渲染每一条搜索结果,参数为 CascaderFilterRenderProps。返回单个元素时,该元素获得选项的 id、role="option" 和选中状态;返回文本或多个节点时,组件包一层 li。 | Type(props: FilterRenderProps) => JSX.Element | - |
| filterSorter | 对搜索结果排序。参数为两条结果路径(从根节点到匹配节点的节点数组)与输入内容,返回比较值。 | Type(first: CascaderData[], second: CascaderData[], inputValue: string) => number | - |
| filterTreeNode | 是否开启搜索。为 true 时按 treeNodeFilterProp 的值匹配输入,不区分大小写;也可以传入匹配函数,参数为输入内容、节点的属性字符串与节点数据。 | Type((inputValue: string, treeNodeString: string, data?: CascaderData) => boolean) | boolean | false |
| id | 选择框根元素(触发器)的 id。 | string | - |
| insetLabel | 嵌入触发器内的标签,作为组合框的名称来源;有 prefix 时不渲染。 | JSX.Element | - |
| insetLabelId | insetLabel 内容的 id,用于关联组合框的名称;未设置时由组件生成。 | string | - |
| keyMaps | 自定义节点中 value、label、children、disabled、isLeaf 的字段名。 | BasicCascaderProps['keyMaps'] | - |
| leafOnly | 多选时 value 与标签只包含叶子节点。 | boolean | false |
| loadData | 异步加载数据,参数为从根到当前节点的路径,需要返回 Promise。不能与搜索同时使用。 | Type(selectOptions: CascaderData[]) => Promise<void> | - |
| max | 多选时选中数量的上限,超出后触发 onExceed。 | number | - |
| maxTagCount | 多选时标签的最大展示数量,超出部分以 +N 展示。 | number | - |
| motion | 下拉框弹出时是否有动画。 | boolean | true |
| multiple | 是否多选。它改变 value、defaultValue、onChange、onSelect、displayRender 与 triggerRender 的类型,没有全局默认值。 | Multiple | false |
| onBlur | 失焦或关闭下拉时的回调,不是 DOM 的 blur 事件;通过 ref 的 close() 关闭时不传参数。 | Type(e?: MouseEvent | KeyboardEvent) => void | - |
| onChange | 选中项变化时的回调。参数为选中路径的 value 数组,多选时为路径数组;onChangeWithObject 为 true 时参数为节点数据。 | Type(value: ByMultiple<Multiple, Array<SimpleValueType>, Array<Array<SimpleValueType>>>) => void | - |
| onChangeWithObject | 为 true 时 onChange 的参数为节点数据(含 value 键),而不是 value;受控的 value 与 defaultValue 也要传节点数据。 | boolean | - |
| onClear | 点击清除按钮时的回调。 | Type() => void | - |
| onDropdownVisibleChange | 下拉展开或收起时的回调,参数为是否可见。 | Type(visible: boolean) => void | - |
| onExceed | 多选时超出 max 后的回调,参数为已选中的 Entity 数组。 | Type(checkedItem: Entity[]) => void | - |
| onFocus | 组合框获得焦点,或下拉被鼠标、键盘打开时的回调,不是 DOM 的 focus 事件。 | Type(e: MouseEvent | KeyboardEvent) => void | - |
| onListScroll | 列视图中某一列滚动时的回调,参数为事件与 { panelIndex, activeNode };搜索结果列表的滚动不触发。 | Type(e: Event, panel: ScrollPanelProps) => void | - |
| onLoad | 节点加载完毕时的回调,参数为新加载的 key 集合与该节点的数据。 | Type(newLoadedKeys: Set<string>, data: CascaderData) => void | - |
| onSearch | 搜索文本变化时的回调,参数为输入的内容。 | Type(value: string) => void | - |
| onSelect | 节点被选中时的回调,参数为选中项的 value;多选时只选中一个节点为单个值,否则为数组。 | Type(value: ByMultiple<Multiple, string | number, string | number | Array<string | number>>) => void | - |
| placeholder | 选择框的占位文字。 | string | - |
| position | 下拉的展开方向,默认 bottomLeft(RTL 下为 bottomRight)。 | Position | - |
| prefix | 前缀标签,渲染在触发器内左侧。 | JSX.Element | - |
| preventScroll | 组件内的 focus 是否阻止浏览器滚动文档以显示获得焦点的元素。 | boolean | - |
| ref | 接收触发器元素,该元素挂有下方 Methods 中的 open、close、focus、blur 与 search 方法。 | TypeRef<CascaderElement> | - |
| remote | 是否开启远程搜索。开启后输入不再做本地过滤,只触发 onSearch,由使用方异步更新 treeData。 | boolean | false |
| renderLabel | 自定义列视图中选项的内容,参数为节点数据;搜索结果、回显与标签仍显示节点的 label 文本。 | Type(item: CascaderData) => JSX.Element | - |
| restTagsPopoverProps | 剩余标签 Popover 的属性,如 position、trigger,类型同 PopoverProps。 | PopoverProps | - |
| searchPlaceholder | 搜索框的占位文字,未设置时使用 placeholder。 | string | - |
| searchPosition | 搜索框的位置:trigger 在触发器内,custom 由使用方自定义,此时用 ref 的 search 方法传入搜索词。 | 'trigger' | 'custom' | "trigger" |
| separator | 路径的分隔符,用于搜索结果,以及单选时回填到触发器的内容。 | string | " / " |
| showClear | 是否展示清除按钮。 | boolean | false |
| showNext | 展开子菜单的时机:click 或 hover。 | 'click' | 'hover' | "click" |
| showRestTagsPopover | 超出 maxTagCount 后,悬停 +N 时是否用 Popover 展示剩余标签。 | boolean | false |
| size | 选择框的尺寸,可选 small、default、large。在 InputGroup 中时以 InputGroup 的尺寸为准。 | CascaderType | - |
| stopPropagation | 是否阻止下拉框上的点击事件冒泡。 | boolean | string | true |
| suffix | 后缀标签,渲染在触发器内右侧。 | JSX.Element | - |
| topSlot | 下拉顶部的插槽内容。 | JSX.Element | - |
| treeData | 节点数据,结构见 CascaderData。 | TypeArray<CascaderData> | [] |
| treeNodeFilterProp | 搜索时用于匹配输入的节点属性。 | string | "label" |
| triggerRender | 自定义触发器的渲染,参数为 TriggerRenderProps,返回的元素替换默认的触发器内容。 | Type(props: TriggerRenderProps<Multiple>) => JSX.Element | - |
| validateStatus | 触发器的校验状态,可选 default、error、warning,只影响样式。 | ValidateStatus | - |
| value | 受控的选中条目,类型同 defaultValue。传入后即为受控组件,配合 onChange 更新。 | TypeModeValue<Multiple> | - |
| virtualizeInSearch | 搜索结果的虚拟列表配置,由 height、width、itemSize 组成,用于结果很多的场景;height 缺省为 180。 | Virtualize | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| blur | 使组合框失焦。 | Type() => void |
| close | 关闭下拉列表。 | Type() => void |
| focus | 聚焦组合框(搜索框或触发器),不展开下拉。 | Type() => void |
| open | 展开下拉列表。 | Type() => void |
| search | 按传入的文本搜索,需同时设置 filterTreeNode;searchPosition 为 custom 时由使用方自定义展示搜索框。 | Type(value: string) => void |
Cascader 把 class、style、ref 与未声明的原生属性落在根元素(触发器)上,aria-label、aria-labelledby、aria-describedby、aria-errormessage、aria-invalid、aria-required 落在当前的 combobox 上(见 ARIA)。onFocus、onBlur 在打开、关闭下拉时触发,不是 DOM 焦点事件。
CascaderData
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 子节点 | CascaderData[] | - |
| disabled | 不可选状态 | boolean | - |
| isLeaf | 叶子节点 | boolean | - |
| label | 展示的文本(必填),自定义内容用 renderLabel | string|number | - |
| loading | 正在加载 | boolean | - |
| value | 属性值(必填) | string|number | - |
Methods
Cascader 的 ref 指向触发器元素,该元素上挂有 open()、close()、focus()、blur() 和 search(value) 方法,类型为 CascaderElement。search 按输入了该值来搜索,需同时设置 filterTreeNode 开启搜索,searchPosition 为 custom 时自定义展示搜索框。
import { Cascader, type CascaderElement } from '@fibrous/semi-ui-solid';
let cascader: CascaderElement | undefined;
const open = () => cascader?.open();
<Cascader ref={(node) => (cascader = node)} treeData={treeData} />;
Accessibility
ARIA
- Cascader 支持传入
aria-label、aria-describedby、aria-errormessage、aria-invalid、aria-labelledby、aria-required来表示该 Cascader 的相关信息;
- 这些
aria-*属性落在当前的 combobox 上:不可搜索时是根元素(触发器),可搜索时是搜索框,多选且搜索框在触发器内时是标签输入框;
- combobox 的名称按优先级取:
aria-labelledby、aria-label、实际渲染出来的 insetLabel(combobox 的aria-labelledby指向它,未传insetLabelId时组件生成 id;有 prefix 时 insetLabel 不渲染,也不作名称),都没有时为语言包中的「级联选择」。下拉中的 tree 或搜索结果 listbox 使用同一个名称来源; - 按 ARIA 1.2 的 combobox 写法,任一时刻只有一个元素是 combobox,
aria-controls指向下拉中的列表,下拉与 combobox 同名。不可搜索时触发器是 select-only combobox,已选路径是它的值;可搜索时搜索框本身是 combobox,带aria-autocomplete="list",触发器不再有角色和 Tab 停靠点。单选的搜索框只在搜索时出现,未搜索时触发器仍是 combobox;搜索框在输入前为空,已选路径以aria-describedby关联为它的描述;搜索中选中结果后焦点回到触发器; - 各列合起来是一棵
role="tree":选项为treeitem,带aria-level(所在列)、aria-setsize、aria-posinset、aria-disabled,可展开的选项带aria-expanded;单选时用aria-selected,多选时用aria-checked(半选为mixed);
- 搜索结果是
role="listbox",选项为option,用aria-selected表示选中,多选时表示勾选。filterRender返回单个元素时,该元素会被加上id、role="option"和上述状态,否则组件包一层带这些属性的li;
- 选项前的勾选图标和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。
键盘交互
Cascader 按 WAI-ARIA Combobox 处理键盘:焦点始终留在触发器或搜索框上,aria-activedescendant 指向当前选项,当前选项显示焦点环。下拉刚打开时没有当前选项。
- 下拉关闭时:
ArrowDown、ArrowUp、Enter打开下拉;不可搜索时,Space也打开下拉,Home、End打开下拉并移到第一列的第一个、最后一个选项。可搜索时,Space、Home、End用于编辑文字 Enter:有当前选项时等同点击它,多选时勾选或取消勾选;禁用选项不响应ArrowDown、ArrowUp:在当前选项所在的列(或搜索结果)中移到下一个、上一个选项,第一次按ArrowDown移到第一列的第一个选项;Home、End:移到这一列的第一个、最后一个选项ArrowRight:展开当前选项,显示它的下一列;已展开时移到下一列的第一个选项(RTL 下与ArrowLeft互换)ArrowLeft:收起展开的当前选项;否则移到上一列的父选项Space:有当前选项时与Enter相同(焦点在搜索框里时用于输入)- 输入字符:下拉打开时,从当前选项往后,移到已显示的各列中第一个以输入内容开头的选项,各列保持不变;快速连续输入的字符合成一个搜索词。焦点在搜索框里时字符用于输入;下拉关闭时不响应
Esc:关闭下拉
焦点在搜索框里且已输入文字时,只有 ArrowDown、ArrowUp、Enter 用于移动和选择,其余按键用于编辑文字。方向键只移动当前选项,不像悬停那样自动展开下一列,也不触发 loadData;ArrowRight 展开时才加载。
showClear 开启、有已选值或搜索文本且未禁用时,触发器内渲染清除按钮。它是原生 button,名称为语言包中的「清空已选值」,在 hover、下拉打开或焦点位于 Cascader 内时显示。从触发器或搜索框按 Tab 移到清除按钮,Enter 或 Space 清空,效果与鼠标点击相同,清空后焦点回到触发器或搜索框。多选时也可在搜索框中用 Backspace 删除标签。
文案规范
- 选择器选项
- 如果没有默认选项,就使用 "Select" 做占位文案
- 选项要按首字母顺序或者其他有逻辑的排列顺序,使用户更好地找到选项
- 使用语句书写规范(首字母大写,其余小写),避免在句尾使用逗号和分号
- 清晰表达出选项所表示的选择目的
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration-cascader_option-bg | var(--semi-transition_duration-none) | |
| $transition_function-cascader_option-bg | var(--semi-transition_function-easeIn) | |
| $transition_delay-cascader_option-bg | var(--semi-transition_delay-none) | |
| $radius-cascader | var(--semi-border-radius-small) | 级联选择触发器圆角 |
| $radius-cascader_option_empty | var(--semi-border-radius-medium) | 级联选择菜单空状态圆角 |
| $color-cascader_default-border-default | transparent | 级联选择触发器描边颜色 - 默认 |
| $color-cascader_default-border-hover | transparent | 级联选择触发器描边颜色 - 悬浮 |
| $color-cascader_default-border-focus | var(--semi-color-focus-border) | 级联选择触发器描边颜色 - 选中 |
| $color-cascader_default-bg-default | var(--semi-color-fill-0) | 级联选择触发器背景颜色 - 默认 |
| $color-cascader_default-bg-hover | var(--semi-color-fill-1) | 级联选择触发器背景颜色 - 悬浮 |
| $color-cascader_default-bg-active | var(--semi-color-fill-2) | 级联选择触发器背景颜色 - 按下 |
| $color-cascader_option_list-border-default | var(--semi-color-fill-0) | 级联选择各级菜单分割线颜色 |
| $color-cascader_option_main-text-default | var(--semi-color-text-0) | 级联选择菜单项文字颜色 |
| $color-cascader_option-bg-hover | var(--semi-color-fill-0) | 级联选择菜单项背景颜色 - 悬浮 |
| $color-cascader_option-bg-active | var(--semi-color-fill-1) | 级联选择菜单项背景颜色 - 按下 |
| $color-cascader_option-bg-selected | var(--semi-color-primary-light-default) | 级联选择菜单项背景颜色 - 命中 |
| $color-cascader_option_disabled-text-default | var(--semi-color-disabled-text) | 级联选择菜单项文字颜色 - 禁用 |
| $color-cascader_option_disabled-bg-hover | transparent | 级联选择菜禁用单项背景颜色 - 悬浮 |
| $color-cascader_option_disabled-bg-active | transparent | 级联选择菜禁用单项背景颜色 - 按下 |
| $color-cascader_tag_disabled-bg-default | transparent | 级联选择菜禁用Tag背景颜色 |
| $color-cascader_warning-bg-default | var(--semi-color-warning-light-default) | 级联选择触发器警告背景颜色 |
| $color-cascader_warning-border-default | var(--semi-color-warning-light-default) | 级联选择触发器警告描边颜色 |
| $color-cascader_warning-bg-hover | var(--semi-color-warning-light-hover) | 级联选择触发器警告背景颜色 - 悬浮 |
| $color-cascader_warning-border-hover | var(--semi-color-warning-light-hover) | 级联选择触发器警告描边颜色 - 悬浮 |
| $color-cascader_warning-bg-focus | var(--semi-color-warning-light-default) | 级联选择触发器警告背景颜色 - 选中 |
| $color-cascader_warning-border-focus | var(--semi-color-warning) | 级联选择触发器警告描边颜色 - 选中 |
| $color-cascader_warning-bg-active | var(--semi-color-warning-light-active) | 级联选择触发器警告背景颜色 - 按下 |
| $color-cascader_warning-border-active | var(--semi-color-warning-light-active) | 级联选择触发器警告描边颜色 - 按下 |
| $color-cascader_danger-bg-default | var(--semi-color-danger-light-default) | 级联选择触发器危险背景颜色 |
| $color-cascader_danger-border-default | var(--semi-color-danger-light-default) | 级联选择触发器危险描边颜色 |
| $color-cascader_danger-bg-hover | var(--semi-color-danger-light-hover) | 级联选择触发器危险背景颜色 - 悬浮 |
| $color-cascader_danger-border-hover | var(--semi-color-danger-light-hover) | 级联选择触发器危险描边颜色 - 悬浮 |
| $color-cascader_danger-bg-focus | var(--semi-color-danger-light-default) | 级联选择触发器危险背景颜色 - 选中 |
| $color-cascader_danger-border-focus | var(--semi-color-danger) | 级联选择触发器危险描边颜色 - 选中 |
| $color-cascader_danger-bg-active | var(--semi-color-danger-light-active) | 级联选择触发器危险背景颜色 - 按下 |
| $color-cascader_danger-border-active | var(--semi-color-danger-light-active) | 级联选择触发器危险描边颜色 - 按下 |
| $color-cascader_prefix_suffix_text-default | var(--semi-color-text-2) | 级联选择前后缀文字颜色 |
| $spacing-cascader_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 默认 |
| $spacing-cascader_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 默认 |
| $spacing-cascader_small_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 小尺寸 |
| $spacing-cascader_small_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 小尺寸 |
| $spacing-cascader_large_selection-paddingLeft | 12px | 级联选择触发器左侧内边距 - 大尺寸 |
| $spacing-cascader_large_selection-paddingRight | 12px | 级联选择触发器右侧内边距 - 大尺寸 |
| $spacing-cascader_selection_multiple-paddingLeft | $spacing-extra-tight | 级联选择触发器多选时左侧内边距 - 默认 |
| $spacing-cascader_selection_multiple-paddingRight | $spacing-extra-tight | 级联选择触发器多选时右侧内边距 - 默认 |
| $spacing-cascader_small_selection_multiple-paddingLeft | $spacing-extra-tight | 级联选择触发器多选时左侧内边距 - 小尺寸 |
| $spacing-cascader_small_selection_multiple-paddingRight | $spacing-extra-tight | 级联选择触发器多选时左侧内边距 - 小尺寸 |
| $spacing-cascader_large_selection_multiple-paddingLeft | $spacing-extra-tight | 级联选择触发器多选时左侧内边距 - 大尺寸 |
| $spacing-cascader_large_selection_multiple-paddingRight | $spacing-extra-tight | 级联选择触发器多选时左侧内边距 - 大尺寸 |
| $spacing-cascader_selection_tag-marginLeft | $spacing-none | 级联选择触发器多选时标签的水平左外边距 |
| $spacing-cascader_selection_tag-marginRight | $spacing-super-tight | 级联选择触发器多选时标签的水平右外边距 |
| $spacing-cascader_selection_tag-marginY | 1px | 级联选择触发器多选时标签的垂直外边距 |
| $spacing-cascader_selection_tagInput-marginLeft | -$spacing-extra-tight | 级联选择触发器多选搜索时 TagInput 的左外边距 |
| $spacing-cascader_selection_input-marginLeft | $spacing-extra-tight | 级联选择触发器多选搜索时输入框的左外边距 |
| $spacing-cascader_selection_n-marginRight | $spacing-extra-tight | 超出 maxTagCount 后,+n 的右外边距 |
| $spacing-cascader_option-icon-marginLeft | 8px | 级联选择菜单项图标左侧外边距 |
| $color-cascader_selection_n-text-default | var(--semi-color-text-0) | 超出 maxTagCount 后,+n 的文字默认颜色 |
| $color-cascader_selection_n-text-disabled | var(--semi-color-disabled-text) | 超出 maxTagCount 后,+n 的文字disabled颜色 |
| $color-cascader_selection_text_inactive | var(--semi-color-text-2) | 级联选择单选inpu输入框和text并存时,text颜色 |
| $color-cascader_selection-text-default | var(--semi-color-text-0) | 级联选择选中项文字颜色 |
| $color-cascader_placeholder-text-default | var(--semi-color-text-2) | 级联选择未选中项文字颜色 |
| $color-cascader-icon-default | var(--semi-color-text-2) | 级联选择触发器图标颜色 - 默认 |
| $color-cascader-icon-hover | var(--semi-color-primary-hover) | 级联选择触发器图标颜色 - 悬浮 |
| $color-cascader-icon-active | var(--semi-color-primary-active) | 级联选择触发器图标颜色 - 按下 |
| $color-cascader_option-icon-default | $color-cascader-icon-default | 级联选择菜单项图标颜色 |
| $color-cascader_input_disabled-bg-default | var(--semi-color-disabled-fill) | 级联选择禁用态背景颜色 |
| $color-cascader_input_disabled-bg-hover | var(--semi-color-disabled-fill) | 级联选择禁用态背景颜色 - 悬浮 |
| $color-cascader_input_disabled-text-default | var(--semi-color-disabled-text) | 级联选择禁用态文字颜色 |
| $color-cascader_input_disabled-icon-default | var(--semi-color-disabled-text) | 级联选择禁用态图标颜色 |
| $color-cascader_search-border-default | var(--semi-color-fill-0) | 级联选择搜索描边颜色 |
| $color-cascader_option_empty-text-default | var(--semi-color-disabled-text) | 级联选择空状态文字颜色 |
| $color-cascader_input-bg-default | transparent | 级联选择输入框背景颜色 |
| $color-cascader_input-border-default | none | 级联选择输入框描边颜色 |
| $color-cascader_select_highlight | var(--semi-color-primary) | 级联选择器搜索命中后文字高亮的颜色 |
| $font-cascader_select-fontWeight | $font-weight-bold | 级联选择菜单项字重 - 选中 |
| $font-cascader-fontWeight | $font-weight-regular | 级联选择菜单项字重 - 未选中 |
| $font-cascader_selection_n-fontSize | $font-size-small | 超出 maxTagCount 后,+n 的文字尺寸 |
| $font-cascader_prefix_suffix_fontWeight | $font-weight-bold | 级联选择 prefix/suffix 文字字重 |
| $width-cascader-border | 1px | 级联选择触发器描边宽度 |
| $width-cascader_option_list-border | 1px | 级联选择各级菜单分割线宽度 |
| $width-cascader | 80px | 级联选择触发器最小宽度 |
| $height-cascader_default | $height-control-default | 级联选择触发器高度 - 默认 |
| $height-cascader_small | $height-control-small | 级联选择触发器高度 - 小尺寸 |
| $height-cascader_large | $height-control-large | 级联选择触发器高度 - 大尺寸 |
| $width-cascader_hover-border | $width-cascader-border | 级联选择触发器描边宽度 - 悬浮 |
| $width-cascader_focus-border | $border-thickness-control-focus | 级联选择触发器描边宽度 - 选中态 |
| $width-cascader_search-border | 1px | 级联选择触搜索描边宽度 |
| $width-cascader-icon | 32px | 级联选择触发器图标尺寸 |
| $width-cascader-option-icon | 16px | 级联选择菜单项图标尺寸 |
| $width-cascader_option | 150px | 级联选择各级菜单宽度 |
| $height-cascader_option_list | 180px | 级联选择菜单高度 |
| $height-cascader_selection_tagInput_wrapper_small | 22px | 级联选择多选搜索时搜索框最小高度 - 小尺寸 |
| $height-cascader_selection_tagInput_wrapper_default | 30px | 级联选择多选搜索时搜索框最小高度 - 默认尺寸 |
| $height-cascader_selection_tagInput_wrapper_large | 38px | 级联选择多选搜索时搜索框最小高度 - 大尺寸 |
| $height-cascader_selection_wrapper_small | 22px | 级联选择单选搜索时搜索框高度 - 小尺寸 |
| $height-cascader_selection_wrapper | 30px | 级联选择单选搜索时搜索框高度 - 默认尺寸 |
| $height-cascader_selection_wrapper_large | 38px | 级联选择单选搜索时搜索框高度 - 大尺寸 |
| $spacing-cascader_text-marginX | $spacing-base-tight | 级联选择 prefix/suffix 文字水平内间距 |
| $spacing-cascader_icon-marginX | $spacing-tight | 级联选择 prefix/suffix 图标水平内间距 |
| $spacing-cascader_label_checkbox-marginRight | $spacing-tight | 级联选择 checkbox 的右间距 |
| $spacing-cascader_selection_n-paddingX | $spacing-tight | 级联选择 +N 的水平间距 |
| $spacing-cascader_flatten_list-paddingRight | 64px | 级联选择搜索结果菜单右侧内边距 |
| $spacing-cascader_empty_icon-marginRight | 8px | 级联选择搜索结果菜单 |
| $spacing-cacader_option_list-paddingY | 4px | 级联选择下拉菜单列表垂直内边距 |
| $spacing-cacader_option_list-paddingX | 0px | 级联选择下拉菜单列表水平内边距 |
| $spacing-cascader_search-paddingX | 12px | 级联选择搜索水平内边距 |
| $spacing-cascader_search-paddingY | 8px | 级联选择搜索垂直内边距 |
| $spacing-cascader_option_empty-paddingY | 8px | 级联选择空结果垂直内边距 |
| $spacing-cascader_option_empty-paddingX | 12px | 级联选择空结果水平内边距 |
| $spacing-cascader_option-paddingTop | 8px | 级联选择菜单项顶部内边距 |
| $spacing-cascader_option-paddingBottom | 8px | 级联选择菜单项底部内边距 |
| $spacing-cascader_option-paddingLeft | 12px | 级联选择菜单项左侧内边距 |
| $spacing-cascader_option-paddingRight | 16px | 级联选择菜单项右侧内边距 |