TokenInput(令牌输入)是一个文本框,但文本里可以嵌入令牌,例如消息里提到的人、提示词里附带的文件。每个令牌作为一个整体移动、删除与复制。只需要纯文本时用多行文本;只需要在键入触发字符时给建议、不需要令牌时用触发菜单配普通输入。
组合
TokenInput
├── TokenInputEditor
└── TriggerMenuTokenInput 持有值,TokenInputEditor 是人真正编辑的文本区。放在它里面的触发菜单在键入 @、/ 等触发字符时给出候选。编辑器自己管理内容、选区与撤销,所以让它渲染内容,用 renderToken 定制令牌外观:通常是 TokenInputToken 包一个 TokenInputTokenRemove。
用法
import { TokenInput, TokenInputEditor, type TokenInputValue } from "@ui-sdk/react/token-input";
const draft: TokenInputValue = ["解释一下 ", { type: "file", value: "src/routes.ts", label: "routes.ts" }];
<TokenInput defaultValue={draft}>
<TokenInputEditor aria-label="提示词" />
</TokenInput>;值是文本与令牌组成的数组。每个令牌有 type(种类)、value(应用用的 id)、label(读者看到的文字),可选 id、status("pending" / "unresolved")与 data(渲染用的附加数据,不参与序列化)。
配合触发菜单
由代码提供令牌时可以只用 TokenInput;需要候选列表就在它里面放一个触发菜单:
import { TokenInput, TokenInputEditor, TokenInputToken } from "@ui-sdk/react/token-input";
import { TriggerMenu, TriggerMenuContent, TriggerMenuItem, TriggerMenuList, type TriggerMenuDataItem } from "@ui-sdk/react/trigger-menu";
const people = [{ value: "hollis", label: "何雨" }, { value: "ines", label: "伊娜" }];
<TokenInput renderToken={(token) => <TokenInputToken>{token.label}</TokenInputToken>}>
<TokenInputEditor placeholder="键入 @ 提及某人…" />
<TriggerMenu triggers={[{ char: "@", type: "mention", items: people }]}>
<TriggerMenuContent>
<TriggerMenuList>
{(state) =>
(state.items as TriggerMenuDataItem[]).map((item) => (
<TriggerMenuItem key={item.value} value={item.value} label={item.label}>{item.label}</TriggerMenuItem>
))
}
</TriggerMenuList>
</TriggerMenuContent>
</TriggerMenu>
</TokenInput>;键入 @ 后用方向键与 Enter 选中一个人,它就变成一枚令牌。菜单在令牌输入内部时会自动找到编辑器;放在外部时把控制器的 editor 传给它。
令牌外观与删除方式
renderToken 收到令牌,返回它的内容,通常是 TokenInputToken 加一个 TokenInputTokenRemove。可以按 type 选图标,或读 data 显示运行状态。不传 renderToken 时每个令牌显示自己的 label。
<TokenInput tokenDeletion="select" renderToken={(token) => (
<TokenInputToken>
{token.label}
<TokenInputTokenRemove>×</TokenInputTokenRemove>
</TokenInputToken>
)}>
<TokenInputEditor aria-label="消息" />
</TokenInput>tokenDeletion 决定光标贴着令牌时 Backspace 的行为:"whole" 一次按键删掉整个令牌;"select" 先选中它,再按一次才删除;"revert" 把它还原成可编辑文本,触发菜单会再次在它上面打开。
TokenInputTokenRemove 按下即删除该令牌并把焦点留在编辑器;它渲染 span 且对辅助技术隐藏,因为键盘用户用 Backspace 删除。
文本互转与复制
codec 负责令牌与文本之间的转换,用于剪贴板、表单与草稿。默认格式是 [@label](type:value),复制到其他应用再粘回来能还原令牌。
import { fromText, toText, type TokenCodec } from "@ui-sdk/react/token-input";
const chat: TokenCodec = {
format: (token) => `<@${token.value}>`,
parse: (text) =>
text.split(/(<@[\w.-]+>)/).map((part) => {
const id = /^<@([\w.-]+)>$/.exec(part)?.[1];
return id ? { type: "mention", value: id, label: `@${id}` } : part;
}),
};
<TokenInput codec={chat} copy="label" defaultValue={fromText("通知 <@hollis> 和 <@ines> 上线", chat)}>
<TokenInputEditor aria-label="消息" />
</TokenInput>;toText、fromText、toLabel 可自行转换值。copy="codec" 保留可跨应用还原的格式,copy="label" 复制出人所读到的文字,粘回令牌输入时仍能还原令牌。codec 还可以提供 escape,决定纯文本如何写出才能被 parse 读回为文本。默认解析器只接受字母、数字、_、- 组成的令牌类型,其他类型需要自定义 codec。
受控、控制器与提交
value / defaultValue 加 onValueChange 即可受控。回调的 details.reason 区分 input-change、paste、drop、cut、undo / redo、remove-press、item-press、reset、restore、imperative,details.removed 列出被删掉的令牌,details.cancel() 可拦截。参见状态与事件。
createTokenInputController() 创建的控制器能在外部读写:getValue / setValue / getText / getLabel / insert / updateToken / removeToken / undo / redo / canUndo / canRedo / clear / focus。insert 支持 { at: "caret" | "end" } 与 resolve:传入 promise 时令牌先处于 pending,resolve 后更新、reject 则移除;updateToken(id, patch) 改状态或 data,不产生撤销步骤、不移动光标。
submitShortcut="enter" 时 Enter 提交、Shift + Enter 换行;"mod+enter" 时相反,更适合中日文输入。候选菜单打开或输入法正在组字时,按键优先交给它们。传 onSubmit 可自行处理提交,此时表单不提交;不传则提交所在表单。给根传 name 才会提交 codec 序列化后的文本,required 阻止空内容提交,form 关联其他表单 id,重置回到 defaultValue。参见控制器与表单。
样式与状态
- 编辑器
token-input-editor:data-empty(内容为空)、data-disabled、data-readonly。占位符通过aria-placeholder提供,用::before画出来。 - 令牌
token-input-token:data-token-type(如file、mention)、data-pending、data-unresolved、data-selected(在选区内或被选中待删)、data-disabled、data-readonly。 - 删除按钮
token-input-token-remove:data-disabled、data-readonly。 - 不写
renderToken时,承载令牌的token-input-host带有同样的令牌状态属性,因此不用自定义渲染也能画出外观。
[data-slot="token-input-editor"][data-empty]::before {
content: attr(aria-placeholder);
color: var(--color-muted-foreground);
}
[data-slot="token-input-token"] {
border-radius: 6px;
background: var(--color-brand-wash);
padding-inline: 0.375rem;
}
[data-slot="token-input-token"][data-selected] {
outline: 2px solid var(--color-brand);
}
[data-slot="token-input-token"][data-pending] {
opacity: 0.6;
}
[data-slot="token-input-token"][data-unresolved] {
background: var(--color-warning-muted);
}详见样式与动画。
API
TokenInput 常用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
value / defaultValue |
TokenInputValue |
文本与令牌的数组,受控与非受控。 |
onValueChange |
(value, details) => void |
值变化时触发,含 removed,可取消。 |
renderToken |
(token) => ReactNode |
渲染每个令牌的内容。 |
tokenDeletion |
"whole" | "select" | "revert" |
退格键如何处理令牌。 |
copy |
"codec" | "label" |
复制写出的纯文本格式。 |
codec |
TokenCodec |
令牌与文本的互转规则。 |
submitShortcut |
"enter" | "mod+enter" |
提交快捷键。 |
getTokenDescription |
(token) => string |
令牌除标签外要朗读的信息,如种类与状态。 |
history |
UndoStackOptions |
撤销步数与「停顿多久算新一步」。 |
name / form |
string |
提交字段名与关联表单 id。 |
disabled / readOnly / required |
boolean |
禁用、只读、必填。 |
controller |
object |
createTokenInputController() 创建的控制器。 |
TokenInputEditor 另有 placeholder(string)。
键盘与无障碍
编辑器是多行文本框:textbox 角色加 aria-multiline;内部有触发菜单时再加上 aria-autocomplete 与 aria-haspopup。用字段的可见标签或 aria-label 命名。每个令牌读作它的标签加种类(例如「routes.ts 文件」),插入与删除都会被播报,浏览器自带的拼写检查、自动纠错与输入法照常可用。
| 按键 | 行为 |
|---|---|
Backspace / Delete |
按 tokenDeletion 删除光标前或后的令牌。 |
ArrowLeft / ArrowRight |
一次按键跨过整个令牌,光标不会停在令牌内部。 |
Enter |
提交;触发菜单打开时选中高亮候选。 |
Shift + Enter |
换行。 |
Command / Control + Enter |
submitShortcut="mod+enter" 时提交,此时 Enter 换行。 |
Command / Control + Z |
撤销上一步,连续键入合并为一步,粘贴与令牌改动各算一步。 |
Shift + Command + Z、Control + Y |
重做。 |