组件 · 输入
ChoiceList
始终展开的列表,在选择就是对话框的主要内容时,从中选一个或几个;带搜索框,每行末尾可以有一个自己的按钮。
也叫 Selection list, Open list, Picker list
import { ChoiceList } from "@botiverse/raft-ui/wip";选择列表把每个选项都展开成一行,让人可以互相比较,用于本身就是为了做这个选择的对话框或页面:把 Agent 移到哪里,把谁加进频道。每一行有一个名称,下面最多两行说明;选一个时是单选圆圈,选几个时是复选框;当人按名字找某一项,或者列表长到一眼看不完时,列表上方还有一个搜索框。它从 @botiverse/raft-ui/wip 导出,API 在任何版本中都可能变化。
何时使用
各种选择器之间怎么选,完整说明见选择组件。
和其他字段并列的字段把选项收在一个控件后面,这样表单保持简短。当选择本身就是内容时,要打开一个字段才能看到选项,只会把人想比较的东西藏起来,所以不论多长,列表都保持展开。
结构
ChoiceList 是一个由 items 数组驱动的组件,下面这些部件都由它根据这个数组画出。
- 搜索框:一个 InputGroup,带搜索图标,有文字后还有一个 32px 的清除按钮。
search="always"时总是显示;search="auto"时超过 5 项才显示。 - 框:包住各行的一个卡片色盒子,带细线边框。搜索框出现后,框的高度固定,各行在框里滚动。
- 分组标题,
groups里每组一个:一类行(Agents、People)上方的小标题,字号和颜色与 Select 的分组标签相同。 - 行,每项一行:单选圆圈或复选框、这一项的
media(头像、图标)、它的label,以及最多两行description。整行是一个<label>。 - 行的
action:行末的一个按钮,在行旁边,但在行的<label>之外。 - 提示行:搜索除了选中的行之外什么都没匹配到时,选中的行下方显示调用方的
noMatchLabel;没有任何项时,显示调用方的empty那一行。 - 页脚:调用方的
footer,在框里、各行下方,隔着一条接缝线。它不随各行滚动,所以始终可见。
变体
状态
- 选中: 墨色圆点或墨色对勾,行本身不填充。选中的值是墨色的,和其他地方一样(状态)。
- 悬停和按下: 鼠标指针下的行用
brand-soft填充,和下拉选择列表的选项一样;按下时盖上overlay-medium(悬停与按下)。 - 禁用: 禁用的项,或者带
disabled时的整个列表,标记、名称和说明行都用统一的禁用样式,没有悬停填充。 - 没有匹配: 搜索除了选中的行之外什么都没匹配到:选中的行留着,下面是调用方给的一句短提示。
- 空: 一项都没有:框里只有调用方的
empty那一行,没有行,也没有搜索框。
行为
- 它始终是列表。 3 项也好,300 项也好,从不做成下拉。
- 整行都能选。 在一行上任何地方点击或轻点,包括它的说明行,都会选中它;带
multiple时则勾选或取消勾选它。 - 搜索框按人找东西的方式来定。 按列表的用途选
search,不要按它碰巧有多长来选:- 人记得自己要什么时,用
search="always",比如叫得出名字的人(Add members、新频道的成员)。不论有多少项,输入名字都是最快的,所以只有 2 项时也显示搜索框;用鼠标或触控板时,焦点一开始就在框里。在触屏上它不获取焦点,这样键盘不会弹出来盖住列表。 - 人要靠比较各行写的内容认出对的那一行时,用
search="auto"(默认),比如按额度和磁盘选迁移目标。这时各行本身才是重点;只有列表长到一眼看不完,即超过 5 项时,才显示搜索框,而且它从不获取焦点。项数回落到 5 个或更少时,搜索框连同里面的文字一起消失。 - 这个数量是固定的,不是 prop,所以同一类列表看起来都一样。
- 人记得自己要什么时,用
- 匹配。 搜索匹配每一项的
searchText,或者在label是字符串时匹配label,不区分大小写,匹配文字里的任意位置:给一个人的行设searchText,同时包含名字和@handle。自带搜索的产品,比如和提及共用的、带排序的人员搜索,传入searchValue,并把它找到的值按它的顺序作为matchedValues传入:列表就显示这些值,选中的行仍然排在最前,这样在每个可以选人的地方,找人的方式都一样。传入的匹配结果要对应搜索框里当前的文字;较慢的搜索还在处理新文字时,继续传入上一次的匹配结果,而不是什么都不传,这样列表不会先清空再填满。 - 搜索时,选中的项留在视野里。 搜索文字最后一次变化时已选中的行排在最前,然后是其他匹配项,按各项原来的顺序。搜索从不改变选择,清空搜索后,完整列表按原来的顺序回来。
- 选择时各行不动。 排在最前的选中行是在文字变化时取定的,而不是在选择变化时,所以一行永远不会在鼠标指针或方向键底下挪走;顺序到下一次按键时才重排。
- 列表会滚动。 有搜索框时,框最高 288px,在对话框里滚动;被截断的最后一行表示下面还有。
- 行高。 只有一行文字的行是选项高度,在桌面端用鼠标时 34px(768px 以下 36px),在触屏上 44px;每一行说明只增加它自己的高度,不增加别的。
- 分组。 有
groups时,每一项写明自己属于哪一组,各行排在所属分组的标题下,按分组的顺序;不属于任何已知分组的项排在后面,没有标题。没有内容可显示的分组,不论在搜索前还是搜索中,都不画标题。搜索时,选中的行在它们自己那一组里排在最前,所以一个人永远不会被移出 People 标题之下。 - 列表下方的出路。 要找的项可能不在列表里时,把创建它的入口放进
footer,比如 "Create an agent":各行滚动时它留在下方。要用人输入的文字给它命名("Create an agent named “Mia”"),传入searchValue和onSearchValueChange;这样搜索文字由调用方掌管,可以在页脚或noMatchLabel里引用它。 - 填满底部面板。 带
fill时,列表占满给它的高度,比如手机底部面板剩下的部分,各行在里面滚动,而不是停在 288px。 - 行操作。
action在所在行的末尾,与名称那一行居中对齐。它排在同一行的第二个网格列里,但在 DOM 里位于整组之后,所以点击它不会选中任何东西,Tab 在离开这一组后按行的顺序到达它。在会滚动的列表里(开着搜索,或fill),它离末端 12px,让开滚动条那条 16px 的槽:槽在区域里的一切之上接收指针(ScrollArea);行的文字也相应地让开它。详情在 Popover 里打开,轻点也能打开它。
内容
- 名称用
text-subhead,字重 600;每行说明用text-footnote、foreground-muted。每一行各自截断;某一行被截断时,它的完整文字就是它的title,所以用鼠标仍然能读到长描述(输入框里的长值)。 - 只有当每一行说明都在说人会跨行比较的东西时,才给一行加第二行说明("Claude Max · 5h 72% left · this week 40% left"、"3 agents · 120 GB disk free")。没有内容可写的一行就省掉(
null、false或""):它不会被画出来,从不显示成空行。一行说明从不换行:在窄屏上它在末尾被截断,所以按人最先比较的东西排序,把最说明不了问题的放在最后("3 agents · 12 GB RAM free · 120 GB disk free":先是计算机的负载,然后是内存,最后是几乎总是够用的磁盘)。截掉的部分在悬停时显示(这一行的title),也在这一行的详情里;在触屏上只有详情里有,所以顺序很重要。 - 要指出大多数人应该选的那个选项,在它的名称后面放一个默认颜色的描边 Badge("Recommended"),最多标一行,并给这一行设
searchText,因为它的label不再是字符串。不要给这一行加填充或边框:有填充的行读起来像是已选中。 - 一行里的颜色由调用方决定,用于需要颜色的值:已用完的额度时段用
warning-strong。 - raft-ui 不自带任何文案。搜索框的名称和占位文字、清除按钮的名称、无匹配那一行和空状态那一行,都由调用方用产品自己的话提供("No matches"、"No one matches")。
- 用行的名字给行操作命名("Studio Mac details"),不要只写 "Details"。
无障碍
- 单选:Base UI 的 RadioGroup,经由 raft-ui 的 RadioGroup。整组是一个 Tab 停靠点,落在选中的行上;方向键在已画出的行之间移动选择和焦点,从不移到被搜索隐藏的行。方向键到达的每一行都会被选中,所以选择会开始一件事(为那一行发出请求)的地方,用内嵌的 Combobox,它的方向键只移动高亮。
multiple:Base UI 的 CheckboxGroup,配合 raft-ui 的 Checkbox,是一个由复选框组成的group。每个复选框各是一个 Tab 停靠点,按空格键勾选:这是 WAI-ARIA 的复选框模式。多选的 listbox 会把每一行变成一个 option,而 option 里放不下它自己的按钮。- 用
aria-label给列表命名,或用aria-labelledby指向对话框里的问题。每个单选按钮或复选框只由它那一行的label命名,并由说明行描述,所以读屏软件先读名称,再读额度,而不是把整行当作名称来读。 - 行操作在组之外,也在各行的标签之外:它们从不被读进单选按钮的名称里,组的方向键也从不落到它们上面。
- 每个分组是一个由其标题命名的
group,位于列表自身的组里面,所以焦点进入时读屏软件会读出 "Agents"。方向键把所有分组当作一个列表来移动。 - 带
search="always"且使用精确指针时,焦点一开始就在搜索框里,人可以直接输入;带search="auto"或在触屏上时,焦点留在对话框放置的位置。焦点只在搜索框第一次出现时移过去一次,而且从不离开人正在输入的字段:人填写频道名称时加载出来的项,不会把焦点从名称输入框里拿走。 - 搜索框是
type="search"(WebKit 自带的取消标记被隐藏;由清除按钮代替,清除后焦点回到框里)。无匹配那一行是一个status,只要搜索框在,它就在,内容为空,没有匹配时才填入文字:读屏软件会播报文字发生变化的 status,而一个带着文字出现的 status 常常会被漏掉。匹配的选中行也算匹配,所以它下面从不出现 "No match"。 - 在触屏上,每行至少 44px 高,每个 32px 的操作保留 44px 的点击区域(触屏)。
规则
- 行自己的按钮用
action传入,不要把按钮放进label或description:在行的<label>里,按下它也会选中这一行,读屏软件还会读出单选按钮里面有一个按钮。 - 不要把 ChoiceList 和其他字段一起放在表单里;那里用何时使用表格里写的字段。
- 已选人员的可移除标签片(Add members)放在列表外面、列表上方;ChoiceList 只画列表。
groups用于人一眼就能分清的类别(Agents 和 People),不要用来把一长串列表分成许多小节:搜索找名字比看标题快。footer里只放离开列表的那条出路,一行;第二个选择列表或表单应该放在框外。
API
ChoiceList
| Prop | 类型 | 默认值 |
|---|---|---|
| string | ||
| string | ||
| The clear button's accessible name. | string | |
| The search box's text when the list first draws, such as a search carried over. | string | |
| boolean | ||
| The line drawn when there are no items at all. | ReactNode | |
| The list takes the height it is given and its rows scroll inside it, for a sheet or panel whose main content is the list. Otherwise a list with a search scrolls past 18rem. | boolean | |
| A row of the caller's under the list, inside its frame, that stays in view while the rows scroll: the way out when the item isn't there, such as "Create an agent". | ReactNode | |
| Headings that split the rows by kind, in this order, such as Agents then People. Each item names its group; a group with no rows to show, or a search that leaves it empty, draws no heading. | readonly ChoiceListGroup[] | |
| readonly ChoiceListItem[] | ||
| The values of the items that match the search text, in the order to show them, for a caller with its own search (a ranked people search shared with mentions). With it ChoiceList doesn't match `searchText` itself; it still keeps the chosen rows first and says when nothing matches. | readonly string[] | |
| boolean | ||
| The line under the chosen rows when nothing else matches. | ReactNode | |
| (value: string) => void | ||
| ((value: string) => void) | ((value: string[]) => void) | ||
| When the search box shows. `"always"` when people look for an item they know by name, such as a person: the box shows at any count, and with a mouse or trackpad focus starts in it. `"auto"` when people choose by comparing the rows: the box shows only above 5 items. | "auto" | "always" | |
| The search box's accessible name. Required: any list can come to search, since `"auto"` shows the box once the items, usually from data, pass 5. | string | |
| string | ||
| The search box's text, for a caller that uses it: a "Create “…”" row named after it, or a no-match line that quotes it. With it, the caller owns the text, and `onSearchValueChange` reports each change. | string | |
| string | readonly string[] |