跳到主要内容

令牌输入

TokenInput 在可编辑文本里混排令牌:提及、文件、技能引用整体移动、删除、复制,并可序列化为文本。

TokenInput(令牌输入)是一个文本框,但文本里可以嵌入令牌,例如消息里提到的人、提示词里附带的文件。每个令牌作为一个整体移动、删除与复制。只需要纯文本时用多行文本;只需要在键入触发字符时给建议、不需要令牌时用触发菜单配普通输入。

组合

TokenInput
├── TokenInputEditor
└── TriggerMenu

TokenInput 持有值,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 重做。

相关组件

  • 触发菜单:键入触发字符时的候选列表,也可配普通输入。
  • 多行文本:纯多行文本,共享提交快捷键约定。
  • 组合框:只需要选一个值、不需要混排文本时用它。

输入关键词搜索…

↑↓ 移动↵ 选择Esc 关闭