使用场景
与 TreeSelect 组件的区别:
- TreeSelect: 核心价值在于目标节点,层级结构是为了方便用户快速筛选出目标选项,最终的节点才是用户想要的内容,常见于文件/文件夹选择、组织架构、权限分配等场景。
- Cascader: 核心价值在于路径,用户选择的不是一个孤立的点,而是一条从根到叶的完整路径,常用于地理位置,商品分类等场景。
代码演示
如何引入
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
</script>
基本用法
最简单的用法,默认只可以选叶子节点。
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
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' },
],
},
],
},
];
</script>
<Cascader placeholder="请选择所在地区" style="width: 300px" {treeData} />多选
设置 multiple,可以进行多选。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
// Cascader has no defaultValue: bind a state seeded with the initial selection
let value = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
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' },
],
},
],
},
];
</script>
<Cascader bind:value multiple placeholder="请选择所在地区" style="width: 300px" {treeData} />可搜索的
通过设置 filterTreeNode 属性可支持搜索功能。
默认对 label 值进行搜索(使用字符串的 includes 方法进行匹配,不区分大小写),可通过 treeNodeFilterProp 指定其他属性值进行搜索。label 只接受文本,节点内容用 label snippet 定制时,搜索仍匹配 label 文本;需要匹配其他文本时,可在 treeData 中使用其他字段存储,并通过 treeNodeFilterProp 指定该字段进行搜索。
默认搜索结果只会展示叶子结点的路径,想要显示更多的结果,可以设置 filterLeafOnly 为 false。
filterLeafOnly=false:
Label 为自定义节点,指定其他属性进行搜索
代码
<script lang="ts">
import { IconMapPinStroked } from '@fibrae/semi-icons-svelte';
import { Cascader, TypographyTitle } from '@fibrae/semi-ui-svelte';
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' },
],
},
],
},
];
// The label snippet adds a decorative icon in the columns; labelText holds the text that search matches
const labelNodeTreeData = [
{
label: '浙江省',
labelText: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
labelText: '杭州市',
value: 'hangzhou',
children: [
{ label: '西湖区', labelText: '西湖区', value: 'xihu' },
{ label: '萧山区', labelText: '萧山区', value: 'xiaoshan' },
{ label: '临安区', labelText: '临安区', value: 'linan' },
],
},
{
label: '宁波市',
labelText: '宁波市',
value: 'ningbo',
children: [
{ label: '海曙区', labelText: '海曙区', value: 'haishu' },
{ label: '江北区', labelText: '江北区', value: 'jiangbei' },
],
},
],
},
];
</script>
<div>
<Cascader filterTreeNode placeholder="默认对label值进行搜索" style="width: 300px" {treeData} />
<br />
<br />
<Cascader filterTreeNode placeholder="对value值进行搜索" style="width: 300px" {treeData} treeNodeFilterProp="value" />
<br />
<br />
<TypographyTitle heading={6}>filterLeafOnly=false:</TypographyTitle>
<Cascader filterLeafOnly={false} filterTreeNode placeholder="filterLeafOnly=false" style="width: 300px" {treeData} />
<br />
<br />
<TypographyTitle heading={6}>Label 为自定义节点,指定其他属性进行搜索</TypographyTitle>
<Cascader displayProp="labelText" filterTreeNode placeholder="Search for labelText" style="width: 300px" treeData={labelNodeTreeData} treeNodeFilterProp="labelText">
{#snippet label({ item })}
<span style="display: inline-flex; align-items: center; column-gap: 4px">
<IconMapPinStroked />
{item.label}
</span>
{/snippet}
</Cascader>
</div>可搜索的多选
支持多选和搜索同时使用,在这种场景下,可以通过按下 BackSpace 键来删除对应的已选项目。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>(['zhejiang', 'ningbo', 'haishu']);
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' },
],
},
],
},
];
</script>
<Cascader bind:value filterTreeNode multiple placeholder="请选择所在地区" style="width: 300px" {treeData} />可以使用 filterSorter 对筛选后的数据进行排序。
代码
<script lang="ts">
import { Cascader, type CascaderProps } from '@fibrae/semi-ui-svelte';
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' },
],
},
];
// The sorter receives the two result paths, so compare their last nodes
const filterSorter: CascaderProps['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 firstData.label < lastData.label ? -1 : 1;
}
};
</script>
<div>
<Cascader {filterSorter} filterTreeNode placeholder="输入 s 查看排序效果" style="width: 300px" {treeData} />
</div>如果想要自定义渲染搜索后的选项,可以使用 filterItem snippet 实现整行的自定义渲染,snippet 参数的类型为 CascaderFilterItemSnippetProps:
interface CascaderFilterItemSnippetProps {
props: CascaderFilterItemDomProps; // 选项的 id、role、aria-selected、class 和点击处理,需要展开到 snippet 的根元素上
inputValue: string; // 搜索栏搜索内容
disabled: boolean; // 是否禁用
data: CascaderData[]; // 搜索结果数据,即这条路径上的各级节点
selected: boolean; // 单选时的选中状态
checkStatus: {
// 多选时的选中状态
checked: boolean;
halfChecked: boolean;
};
onCheck: (event: CheckboxEvent) => void; // 多选点击选中,绑定到 Checkbox 的 onChange
}
把 props 展开到根元素上(如 <li {...props}>),单选时点击这一行即选中;开启 virtualizeInSearch 时 props.style 是行的定位样式,自己的 style 要与它拼接,不能覆盖。
使用示例如下
鼠标 hover 到选项可查看被省略文本完整内容
代码
<script lang="ts">
import { Cascader, Checkbox, TypographyText, type CascaderData, type TypographyEllipsis } from '@fibrae/semi-ui-svelte';
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 ellipsis: TypographyEllipsis = { showTooltip: { opts: { style: 'word-break: break-all' } } };
const pathLabel = (data: CascaderData[]) => data.map((item) => item.label).join(' / ');
</script>
<div>
<p>鼠标 hover 到选项可查看被省略文本完整内容</p>
<br />
<Cascader filterTreeNode placeholder="单选,输入 s 自定义搜索选项渲染结果" style="width: 320px" {treeData}>
{#snippet filterItem({ props, data, selected })}
<li {...props} style="justify-content: flex-start">
<TypographyText {ellipsis} style={`width: 270px; ${selected ? 'color: var(--semi-color-primary)' : ''}`}>
{pathLabel(data)}
</TypographyText>
</li>
{/snippet}
</Cascader>
<br />
<Cascader filterTreeNode multiple placeholder="多选,输入 s 自定义搜索选项渲染结果" style="width: 320px; margin-top: 20px" {treeData}>
{#snippet filterItem({ props, data, checkStatus, onCheck })}
<li {...props} aria-label={pathLabel(data)} style="justify-content: flex-start">
<Checkbox checked={checkStatus.checked} indeterminate={checkStatus.halfChecked} onChange={onCheck} tabindex={-1}>
<TypographyText {ellipsis} style="width: 250px">
{pathLabel(data)}
</TypographyText>
</Checkbox>
</li>
{/snippet}
</Cascader>
</div>如果搜索结果中存在大量 Option,可以通过设置 virtualizeInSearch 开启搜索结果面板的虚拟化来优化性能,virtualizeInSearch 是一个包含下列值的对象:
- height: Option 列表高度值(number,单位 px),缺省 180,与下拉框高度一致
- width: Option 列表宽度值
- itemSize: 每行 Option 的高度
代码
<script lang="ts">
import { Cascader, Checkbox, TypographyText, type TypographyEllipsis } from '@fibrae/semi-ui-svelte';
const treeData = ['通用', '场景'].map((label, m) => ({
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 ellipsis: TypographyEllipsis = { showTooltip: { opts: { style: 'word-break: break-all' } } };
</script>
<Cascader filterTreeNode multiple placeholder="输入 通用 or 场景 进行搜索" style="width: 320px" {treeData} virtualizeInSearch={virtualize}>
{#snippet filterItem({ props, data, onCheck, checkStatus })}
<div {...props} style={`${props.style ?? ''}; justify-content: start; padding: 8px 16px 8px 12px; box-sizing: border-box`}>
<Checkbox checked={checkStatus.checked} indeterminate={checkStatus.halfChecked} onChange={onCheck} style="margin-right: 8px" />
<TypographyText {ellipsis} style="max-width: 260px">
{data.map((item) => item.label).join(' | ')}
</TypographyText>
</div>
{/snippet}
</Cascader>限制标签展示数量
在多选的场景中,利用 maxTagCount 可以限制展示的标签数量,超出部分将以 +N 的方式展示。
使用 showRestTagsPopover 可以设置在超出 maxTagCount 后,hover +N 是否显示 Popover,默认为 false。并且,还可以在 restTagsPopoverProps 属性中配置 Popover。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([
['zhejiang', 'ningbo', 'haishu'],
['zhejiang', 'hangzhou', 'xihu'],
]);
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' },
],
},
],
},
];
const restTagsPopoverProps = { position: 'top' } as const;
</script>
<Cascader bind:value maxTagCount={1} multiple placeholder="请选择所在地区" {restTagsPopoverProps} showRestTagsPopover style="width: 300px" {treeData} />限制选中数量
在多选的场景中,利用 max 可以限制多选选中的数量。超出 max 后将触发 onExceed 回调。
代码
<script lang="ts">
import { Cascader, Toast, type CascaderEntity, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([['zhejiang', 'ningbo', 'haishu']]);
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' },
],
},
],
},
];
const onExceed = (checked: CascaderEntity[]) => {
Toast.warning('exceed max');
console.log(checked);
};
</script>
<Cascader bind:value max={1} multiple {onExceed} placeholder="请选择所在地区" style="width: 300px" {treeData} />选择即改变
在单选的情况下,还可以通过设置 changeOnSelect,允许选中父级选项。
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
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' },
],
},
],
},
];
</script>
<div>
<Cascader changeOnSelect placeholder="选择即改变" style="width: 300px" {treeData} />
<br />
<br />
<Cascader changeOnSelect filterTreeNode placeholder="可搜索的选择即改变" style="width: 300px" {treeData} />
</div>自定义显示
可以通过 displayProp 设置回填选项显示的属性值,默认为 label。
单选
多选
代码
<script lang="ts">
import { Cascader, TypographyTitle, type CascaderValue } from '@fibrae/semi-ui-svelte';
let single = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
let multiple = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
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' },
],
},
],
},
];
</script>
<TypographyTitle heading={6}>单选</TypographyTitle>
<Cascader bind:value={single} displayProp="value" placeholder="回填时显示数据的value值" style="width: 300px" {treeData} />
<br />
<br />
<TypographyTitle heading={6}>多选</TypographyTitle>
<Cascader bind:value={multiple} displayProp="value" multiple placeholder="回填时显示数据的value值" style="width: 300px" {treeData} />displayRender 拆成两个 snippet。
单选 (multiple 为 false) 时,用 display snippet 设定回填内容,参数为 { labels, path },其中 labels 是路径上各级节点的 displayProp 值,path 是路径上的节点数据(值不在 treeData 中时为原始值,与开启 onChangeWithObject 时 onChange 的参数相同);snippet 内容为空时显示 placeholder。
多选 (multiple 为 true) 时,用 tag snippet 整体替换每个标签,参数为 { entity, index, disabled, onClose },其中 entity 为节点的相关数据,onClose 取消该节点的选中,需要关闭按钮时把它绑定到自己的 Tag 上。可搜索的多选同样生效。entity 的类型为 CascaderEntity,如下:
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 路径
}
数据项的 label 只接受文本。要在列视图的选项里渲染自定义内容,使用 label snippet,参数为 { item },item 为该节点的数据;搜索结果、回填和标签仍显示节点的文本。
单选
多选
代码
<script lang="ts">
import { Cascader, Tag, TypographyTitle, type CascaderValue } from '@fibrae/semi-ui-svelte';
let single = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
let multiple = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
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' },
],
},
],
},
];
</script>
<TypographyTitle heading={6}>单选</TypographyTitle>
<Cascader bind:value={single} placeholder="自定义回填时显示数据的格式" style="width: 300px" {treeData}>
{#snippet display({ labels })}
已选择:{labels.join(' -> ')}
{/snippet}
</Cascader>
<br />
<br />
<TypographyTitle heading={6}>多选</TypographyTitle>
<Cascader bind:value={multiple} multiple placeholder="自定义回填时显示数据的格式" style="width: 300px" {treeData}>
{#snippet tag({ entity })}
<Tag color="white" style="margin-right: 4px">{entity.data.label}</Tag>
{/snippet}
</Cascader>自定义分隔符
可以使用 separator 设置分隔符, 包括:搜索时显示在下拉框的内容以及单选时回显到 Trigger 的内容的分隔符。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>(['zhejiang', 'ningbo', 'jiangbei']);
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' },
],
},
],
},
];
</script>
<Cascader bind:value filterTreeNode separator=" > " style="width: 300px" {treeData} />禁用
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>(['zhejiang', 'hangzhou', 'xihu']);
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{ label: '西湖区', value: 'xihu' },
{ label: '萧山区', value: 'xiaoshan' },
{ label: '临安区', value: 'linan' },
],
},
],
},
];
</script>
<div>
<Cascader disabled placeholder="请选择所在地区" style="width: 300px" {treeData} />
<br />
<br />
<Cascader bind:value disabled filterTreeNode placeholder="请选择所在地区" style="width: 300px" {treeData} />
</div>严格禁用
可以使用 disableStrictly 来开启严格禁用。开启严格禁用后,当节点是 disabled 的时候,则不能通过子级或者父级的关系改变选中状态。
以下面的 demo 为例,节点"宁波"开启了严格禁用,因此,当我们改变其父节点"浙江省"的选中状态时,也不会影响到节点"宁波"的选中状态。
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
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' },
],
},
],
},
];
</script>
<Cascader disableStrictly multiple placeholder="请选择所在地区" style="width: 300px" {treeData} />展示子菜单的时机
可以使用 showNext 设置展开 Dropdown 子菜单的触发时机,可选: click(默认)、hover。
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{ label: '西湖区', value: 'xihu' },
{ label: '萧山区', value: 'xiaoshan' },
{ label: '临安区', value: 'linan' },
],
},
],
},
];
</script>
<Cascader placeholder="请选择所在地区" showNext="hover" style="width: 300px" {treeData} />点击选中
在多选模式下,默认情况下点击非叶子节点不会触发选中。你可以通过 clickToSelect 开启点击任意节点即选中的功能。
这个 API 在配合 showNext="hover" 使用时特别有用:鼠标悬浮展开子菜单,点击则选中当前节点。
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
const treeData = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{ label: '西湖区', value: 'xihu' },
{ label: '萧山区', value: 'xiaoshan' },
],
},
{
label: '宁波市',
value: 'ningbo',
children: [{ label: '海曙区', value: 'haishu' }],
},
],
},
];
</script>
<Cascader clickToSelect multiple placeholder="请选择所在地区" showNext="hover" style="width: 300px" {treeData} />在顶部/底部渲染附加项
我们在级联选择器的顶部、底部分别预留了插槽,你可以通过 topSlot 或 bottomSlot snippet 来设置。
代码
<script lang="ts">
import { Cascader, TypographyText } from '@fibrae/semi-ui-svelte';
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' },
],
},
],
},
];
</script>
<Cascader placeholder="请选择所在地区" style="width: 300px" {treeData}>
{#snippet bottomSlot()}
<div style="height: 36px; display: flex; padding: 0 32px; align-items: center; cursor: pointer; border-top: 1px solid var(--semi-color-border)">
<TypographyText>找不到相关选项?</TypographyText>
<TypographyText link>去新建</TypographyText>
</div>
{/snippet}
</Cascader>受控
用 bind:value 绑定 value 即为受控组件,onChange 同样会触发。Svelte 版没有 defaultValue:需要初始选中项时,绑定一个带初始值的 $state,不绑定则为非受控;bind:value={get, set} 为完全受控。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([]);
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' },
],
},
],
},
];
</script>
<Cascader bind:value placeholder="请选择所在地区" style="width: 300px" {treeData} />自动合并 value
在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 autoMergeValue 来设置,默认为 true。当 autoMergeValue 和 leafOnly 同时开启时,后者优先级更高。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([]);
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' },
],
},
],
},
];
</script>
<Cascader autoMergeValue={false} bind:value multiple placeholder="autoMergeValue 为 false" style="width: 300px" {treeData} />仅叶子节点
多选时,可以通过开启 leafOnly 来设置 value 只包含叶子节点,即显示的 Tag 和 bind:value、onChange 的值只包含叶子节点。
代码
<script lang="ts">
import { Cascader, type CascaderChangeValue, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([]);
const onChange = (v: CascaderChangeValue) => {
console.log(v);
};
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' },
],
},
],
},
];
</script>
<Cascader bind:value leafOnly multiple {onChange} placeholder="开启 leafOnly" style="width: 300px" {treeData} />节点选中关系
多选时,可以使用 checkRelation 来设置节点之间选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated' 时,意味着节点之间的选中互不影响。
代码
<script lang="ts">
import { Cascader, type CascaderValue } from '@fibrae/semi-ui-svelte';
let value = $state<CascaderValue>([['Asia'], ['Asia', 'China', 'Beijing']]);
const treeData = [
{
label: '亚洲',
value: 'Asia',
children: [
{
label: '中国',
value: 'China',
children: [
{ label: '北京', value: 'Beijing' },
{ label: '上海', value: 'Shanghai' },
],
},
],
},
{
label: '北美洲',
value: 'North America',
},
];
</script>
<Cascader bind:value checkRelation="unRelated" multiple style="width: 300px" {treeData} />动态更新数据
treeData 可以整体替换,也可以就地修改 $state 数据(如 push、给 children 赋值),都会被识别并重新计算;内容相同的新数组不会重置展开和选中状态。
代码
<script lang="ts">
import { Button, Cascader, type CascaderData } from '@fibrae/semi-ui-svelte';
let treeData = $state<CascaderData[]>([]);
const add = () => {
const itemLength = Math.floor(Math.random() * 3) + 1;
treeData = Array.from({ length: itemLength }, (_, i) => {
const length = Math.floor(Math.random() * 3);
const children = Array.from({ length }, (_, ci) => ({
key: `${i}-${ci}`,
label: `Item-${i}-${ci}`,
value: `${i}-${ci}`,
}));
return {
key: `${i}`,
label: `Item-${i}`,
value: `${i}`,
children,
};
});
};
</script>
<Cascader placeholder="请选择" style="width: 300px" {treeData} />
<br />
<br />
<Button onclick={add}>动态改变数据</Button>异步加载数据
可以使用 loadData 实现异步加载数据
不能与搜索同时使用
代码
<script lang="ts">
import { Cascader, type CascaderData } from '@fibrae/semi-ui-svelte';
let treeData = $state<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[] =>
list.map((node) => {
if (node.value === value) {
return { ...node, children };
}
if (node.children) {
return { ...node, children: updateTreeData(node.children, value, children) };
}
return node;
});
const loadData = (selectedOpt: CascaderData[]) => {
const targetOpt = selectedOpt[selectedOpt.length - 1];
const { label, value } = targetOpt;
return new Promise<void>((resolve) => {
if (targetOpt.children) {
resolve();
return;
}
setTimeout(() => {
treeData = updateTreeData(treeData, value, [
{
label: `${label} - 1`,
value: `${label}-1`,
isLeaf: selectedOpt.length > 1,
},
{
label: `${label} - 2`,
value: `${label}-2`,
isLeaf: selectedOpt.length > 1,
},
]);
resolve();
}, 1000);
});
};
</script>
<Cascader {loadData} placeholder="Please select" style="width: 300px" {treeData} />远程搜索
设置 remote 后,搜索输入不再走本地匹配,而是仅触发 onSearch 回调,由你根据输入异步拉取 treeData。这与 Select 的 remote 行为一致。
代码
<script lang="ts">
import { debounce } from 'es-toolkit';
import { Cascader, Spin, type CascaderData } from '@fibrae/semi-ui-svelte';
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);
});
let treeData = $state<CascaderData[]>([]);
let loading = $state(false);
let latestToken = 0;
const onSearch = debounce((input: string) => {
if (!input) {
treeData = [];
loading = false;
return;
}
latestToken += 1;
const token = latestToken;
loading = true;
void fetchByKeyword(input).then((next) => {
// Drop stale results that arrive after a newer request
if (token !== latestToken) {
return;
}
treeData = next;
loading = false;
});
}, 300);
</script>
<Spin spinning={loading}>
<Cascader filterTreeNode onChange={(v) => console.log('selected:', v)} {onSearch} placeholder="输入关键词远程搜索" remote style="width: 300px" {treeData} />
</Spin>超长列表
当你的数据结构层级特别深时,Cascader下拉菜单可能会超出屏幕,此时我们建议为下拉菜单设置 overflow-x: auto 以及一个合适的 width 宽度( 建议以N+0.5列的宽度为准,最右侧显示半列,以给用户一种右侧尚有待展开项,可以水平方向滚动的视觉暗示)
代码
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
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' }],
},
],
},
],
},
],
},
],
},
];
</script>
<Cascader dropdownClassName="components-cascader-demo" placeholder="请选择所在地区" style="width: 300px" {treeData} />.components-cascader-demo {
.semi-cascader-option-lists {
max-width: 510px;
overflow-x: auto;
}
}
自定义 Trigger
如果默认的触发器样式满足不了你的需求,可以用 trigger snippet 自定义选择框的展示。
trigger 的 snippet 参数类型为 CascaderTriggerSnippetProps,如下:
interface CascaderTriggerSnippetProps {
/**
* 已选中的 node 在 treeData 中的层级位置,如下例子,
* 当选中浙江省-杭州市-萧山区时,此处 value 为 '0-0-1';多选时为 Set<string>
*/
value: string | Set<string> | undefined;
/* 展开时为搜索词;可搜索的单选收起时为已选路径的展示文本 */
inputValue: string;
/* Placeholder */
placeholder?: string;
/* 是否禁用 Cascader */
disabled: boolean;
/**
* 用于更新 input 框值的函数,当你在 trigger 自定义的
* Input 组件值更新时,你应该调用该函数,用于向 Cascader 内部
* 同步状态, 使用时需要设置 filterTreeNode 参数非 false
*/
onSearch: (inputValue: string) => void;
/* 用于清空值的函数;绑定在触发器内的控件上时传入点击事件,避免这次点击同时打开下拉 */
onClear: (event?: MouseEvent) => void;
/* 用于删除单个 item,入参为该节点在 treeData 中的层级位置(与 value 的格式相同) */
onRemove: (pos: string) => void;
}
代码
<script lang="ts">
import { IconChevronDown, IconClose } from '@fibrae/semi-icons-svelte';
import { Button, ButtonGroup, Cascader, Tag, TagInput, type CascaderData } from '@fibrae/semi-ui-svelte';
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;
};
</script>
{#snippet chevron()}
<IconChevronDown />
{/snippet}
{#snippet close()}
<IconClose />
{/snippet}
<Cascader placeholder="Custom Trigger" {treeData}>
{#snippet trigger({ value, placeholder, onClear })}
<!-- The clear button is a sibling of the trigger button: buttons cannot nest -->
<ButtonGroup theme="light">
<Button icon={chevron} iconPosition="right">
{value ? getLabelFromValue(value as string) : placeholder}
</Button>
{#if value}
<Button
aria-label="清除"
icon={close}
onclick={(event) => {
event.stopPropagation();
onClear(event);
}}
/>
{/if}
</ButtonGroup>
{/snippet}
</Cascader>
<br />
<Cascader filterTreeNode multiple placeholder="Custom Trigger" style="width: 300px" {treeData}>
{#snippet trigger({ value, onSearch, onRemove })}
<TagInput onInputChange={onSearch} value={Array.from(value as Set<string>)}>
{#snippet tag({ value: pos })}
<Tag
closable
onClose={(event) => {
event.preventDefault();
onRemove(pos);
}}
style="margin-left: 2px"
tagKey={pos}
>
{getLabelFromValue(pos)}
</Tag>
{/snippet}
</TagInput>
{/snippet}
</Cascader>API 参考
Cascader
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| arrowIcon | 自定义右侧下拉箭头;传 null 隐藏箭头。有清除按钮可见时,悬停优先显示清除按钮。 | Snippet | null | - |
| autoAdjustOverflow | 下拉被边缘遮挡时是否自动调整展开方向。 | boolean | - |
| autoMergeValue | 多选时,某个父节点被选中后,值不再包含它的子孙节点。挂载后修改不会生效;与 leafOnly 同时开启时,后者优先。 | boolean | true |
| borderless | 是否使用无边框模式。 | boolean | false |
| bottomSlot | 下拉底部的附加内容。 | Snippet | - |
| changeOnSelect | 单选时是否允许选择非叶子节点。 | boolean | false |
| checkRelation | 多选时节点之间选中状态的关系:related 为父子联动,unRelated 为互不影响。 | 'related' | 'unRelated' | "related" |
| clearIcon | 自定义清除按钮的图标,showClear 为 true 时有效;内容为空时使用默认图标。 | Snippet | - |
| clickToSelect | 多选时,点击任意节点即触发选中,常与 showNext 为 hover 配合:悬停展开子菜单,点击选中当前节点。 | boolean | false |
| defaultOpen | 是否在挂载时展开下拉。 | boolean | false |
| disabled | 是否禁用。禁用后不可 Tab 到达。 | boolean | - |
| disableStrictly | 是否严格禁用。开启后,禁用的节点不会因父级或子级的选中状态变化而改变。 | boolean | false |
| display | 单选时已选路径的回显,参数为 { labels, path }:labels 是各级节点的 displayProp,path 是节点数据,值不在 treeData 中时为原始值;内容为空时显示 placeholder。 | TypeSnippet<[CascaderDisplaySnippetProps]> | - |
| displayProp | 回填选项和标签显示的节点属性,默认 label。 | string | "label" |
| dropdownClassName | 下拉的 class。 | string | - |
| dropdownStyle | 下拉的样式,为 CSS 字符串。 | string | - |
| emptyContent | 没有可选数据或搜索无结果时展示的内容;未提供时显示语言包中的默认文案,传 null 隐藏空状态。 | Snippet | null | - |
| enableLeafClick | 多选时,点击叶子节点所在的选项是否触发勾选。 | boolean | false |
| expandIcon | 自定义展开图标;内容为空时使用默认图标。 | Snippet | - |
| filterItem | 替换搜索结果中的一行,参数为 { props, inputValue, disabled, data, checkStatus, selected, onCheck };props 要展开到行的根元素上。 | TypeSnippet<[CascaderFilterItemSnippetProps]> | - |
| filterLeafOnly | 搜索结果是否只展示叶子节点的路径。 | boolean | true |
| filterSorter | 对筛选后的结果排序,参数为两条结果路径(从根节点到匹配节点的节点数组)与输入内容。 | Type(first: CascaderData[], second: CascaderData[], inputValue: string) => number | - |
| filterTreeNode | 是否开启搜索:true 时按 treeNodeFilterProp 对应的属性值匹配输入,不区分大小写;也可以传入自定义的匹配函数。 | Typeboolean | ((inputValue: string, treeNodeString: string, data?: CascaderData) => boolean) | false |
| insetLabel | 嵌在选择框内的标签;同时提供 prefix 时不渲染。 | Snippet | - |
| insetLabelId | insetLabel 内容的 id,用于关联 combobox 的名称;不传时在使用 insetLabel snippet 时由组件生成。 | string | - |
| keyMaps | 自定义节点中 value、label、children、disabled、isLeaf 的字段名。 | CascaderKeyMapProps | - |
| label | 列视图中选项的内容,参数为 { item };搜索结果、回显和标签仍显示节点的文本。 | TypeSnippet<[CascaderLabelSnippetProps]> | - |
| leafOnly | 多选时,值与展示的标签只包含叶子节点。挂载后修改不会生效。 | boolean | false |
| loadData | 异步加载数据,参数为从根到当前节点的路径,需要返回 Promise;reject 后节点不再显示加载态,可以再次展开重试。不能与搜索同时使用。 | Type(selectOptions: CascaderData[]) => Promise<void> | - |
| max | 多选时选中数量的上限,超出时触发 onExceed。 | number | - |
| maxTagCount | 多选时标签的最大展示数量,超出的部分以 +N 展示。 | number | - |
| motion | 是否开启下拉的弹出动画。 | boolean | true |
| multiple | 是否多选。它没有全局默认值。 | boolean | false |
| onBlur | 关闭下拉时的回调:点击外部时参数为 MouseEvent,Esc 为 KeyboardEvent,通过方法 close 关闭时为 undefined。不是 DOM 失焦事件。 | Type(event?: MouseEvent | KeyboardEvent | FocusEvent) => void | - |
| onChange | 选中值变化时的回调。单选为所选路径,多选为各选中路径的数组;开启 onChangeWithObject 时为节点数据。 | Type(value: CascaderChangeValue) => void | - |
| onChangeWithObject | 是否让 onChange 携带每个节点的数据而不是它的 value。开启后绑定的 value 也要传节点数据,并含有 value。这是布尔属性,不是回调。 | boolean | false |
| onClear | 点击清除按钮清空选中值时的回调。 | Type() => void | - |
| onDropdownVisibleChange | 下拉展开或收起时的回调,参数是是否展开。 | Type(visible: boolean) => void | - |
| onExceed | 多选时选中数量超出 max 时的回调,参数是已选节点的 Entity 数组。 | Type(checkedItem: CascaderEntity[]) => void | - |
| onFocus | 点击或按键打开下拉时的回调,参数是那次 MouseEvent 或 KeyboardEvent。不是 DOM 焦点事件,Tab 进出不会触发。 | Type(event: MouseEvent | KeyboardEvent | FocusEvent) => void | - |
| onKeyDown | 在 Cascader 处理按键之前的回调,每次按键只调用一次;在其中调用 preventDefault 不会阻止 Cascader 的处理。除清除按钮与自定义触发器内控件上的按键外,事件在触发器的捕获阶段发出,此时调用 stopPropagation 会让搜索框收不到该键,并跳过触发器对它的处理(打开下拉、Esc 关闭),选项仍会处理该键。 | Type(event: KeyboardEvent) => void | - |
| onListScroll | 滚动列视图中的某一列时的回调,参数是滚动事件与该列的信息。搜索结果的滚动不触发。 | Type(event: Event, panel: CascaderScrollPanelProps) => void | - |
| onLoad | 节点加载完毕时的回调,参数是已加载的 key 集合与该节点的数据。 | Type(newLoadedKeys: Set<string>, data: CascaderData) => void | - |
| onSearch | 搜索输入变化时的回调,参数是输入的文本。 | Type(value: string) => void | - |
| onSelect | 选中某个节点时的回调,参数是该节点的 value;多选时只勾选了一个节点时是它的 value,勾选多个时是 value 数组。 | Type(value: string | number | Array<string | number>) => void | - |
| placeholder | 选择框的占位文字。 | string | - |
| position | 下拉的展开方向,默认 bottomLeft,RTL 下为 bottomRight。 | Position | - |
| prefix | 前缀内容。 | Snippet | - |
| preventScroll | 组件内的 focus 是否阻止浏览器滚动文档以显示获得焦点的元素。 | boolean | - |
| remote | 是否开启远程搜索。开启后输入不再做本地过滤,只触发 onSearch,由使用方异步更新 treeData。 | boolean | false |
| restTagsPopoverProps | 剩余标签 Popover 的配置,可设置 position、trigger 等,参见 Popover。 | TypeOmit<PopoverProps, 'children' | 'child' | 'content'> | - |
| searchPlaceholder | 搜索框的占位文字,不传时使用 placeholder。 | string | - |
| searchPosition | 搜索框的位置:trigger 在触发器内,custom 由使用方自定义。 | 'trigger' | 'custom' | "trigger" |
| separator | 分隔符,用于搜索结果和单选回填到触发器的路径。 | string | " / " |
| showClear | 是否展示清除按钮。 | boolean | false |
| showNext | 展开子菜单的时机:click 为点击,hover 为悬停。 | 'click' | 'hover' | "click" |
| showRestTagsPopover | 超出 maxTagCount 后,悬停 +N 时是否用 Popover 展示剩余标签。 | boolean | false |
| size | 选择框的尺寸。 | CascaderSize | - |
| stopPropagation | 是否阻止下拉上的点击事件冒泡。 | boolean | string | true |
| suffix | 后缀内容。 | Snippet | - |
| tag | 多选时整体替换每个标签,参数为 { entity, index, disabled, onClose },onClose 取消该节点的选中。 | TypeSnippet<[CascaderTagSnippetProps]> | - |
| topSlot | 下拉顶部的附加内容。 | Snippet | - |
| treeData | 节点数据,label 只接受文本,节点内容用 label snippet。原地修改 $state 数据也会被识别,内容相同的新数组不会重置展开和选中状态。 | CascaderData[] | [] |
| treeNodeFilterProp | 搜索时用于匹配输入的节点属性,默认 label。 | string | "label" |
| trigger | 替换整个触发器内容,参数为 { value, inputValue, placeholder, disabled, onSearch, onClear, onRemove }。 | TypeSnippet<[CascaderTriggerSnippetProps]> | - |
| validateStatus | 触发器的校验状态,只影响样式。 | ValidateStatus | - |
| value | 当前选中的条目,用 bind:value 绑定。不传时为非受控,需要初始选中项时绑定一个带初始值的 $state;bind:value={get, set} 为完全受控。单选时是节点或节点路径,多选时可以是路径数组。 | CascaderValue | - |
| virtualizeInSearch | 搜索结果的虚拟列表配置,由 height、width、itemSize 组成,用于结果很多的场景;height 缺省为 180,与下拉框高度一致。 | CascaderVirtualize | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| blur | 触发器或搜索框失焦。 | Type() => void |
| close | 关闭下拉。 | Type() => void |
| focus | 聚焦触发器或搜索框。 | Type() => void |
| open | 打开下拉。 | Type() => void |
| search | 按输入了该关键词来搜索,需同时设置 filterTreeNode 开启搜索。 | Type(keyword: string) => void |
Cascader 把 class、style 与未声明的原生属性落在根元素(触发器)上,aria-label、aria-labelledby、aria-describedby、aria-errormessage、aria-invalid、aria-required 落在当前的 combobox 上(见 ARIA)。onChange、onSelect、onFocus、onBlur、onKeyDown 是组件的回调,会遮住根元素上同名的原生事件;onFocus、onBlur 在打开、关闭下拉时触发,不是 DOM 焦点事件。style 和 dropdownStyle 是 CSS 字符串。
CascaderData
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 子节点 | CascaderData[] | - |
| disabled | 不可选状态 | boolean | - |
| isLeaf | 叶子节点 | boolean | - |
| label | 展示的文本(必填),自定义内容使用 label snippet | string|number | - |
| loading | 正在加载 | boolean | - |
| value | 属性值(必填) | string|number | - |
Methods
Cascader 通过 export function 暴露 open()、close()、focus()、blur() 和 search(value),用 bind:this 取得实例后调用。search 按输入了该值来搜索,需同时设置 filterTreeNode 开启搜索,searchPosition 为 custom 时自定义展示搜索框。
<script lang="ts">
import { Cascader } from '@fibrae/semi-ui-svelte';
let cascader = $state<Cascader>();
</script>
<Cascader bind:this={cascader} {treeData} />
<button onclick={() => cascader?.open()}>打开</button>
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表示选中,多选时表示勾选。filterItem的props带有选项的id、role="option"和aria-selected,需要展开到 snippet 的根元素上;
- 选项前的勾选图标和多选复选框只是状态的图示,对读屏软件隐藏,也不接收焦点。
键盘交互
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 | 级联选择菜单项右侧内边距 |