输入 sta,选择 status: 后继续选择 error。没有高亮候选时,回车应用整条查询。
已应用:entry:http
'use client'
import { useState } from 'react'
import { Search } from 'lucide-react'
import {
Autocomplete,
Kbd,
Stack,
Tag,
Text,
type AutocompleteOption,
type CompletionContext,
type CompletionEdit,
} from '@hina-ui/react'
const queryValues: Record<string, string[]> = {
entry: ['http', 'rpc', 'job', 'consumer'],
status: ['ok', 'error', 'timeout'],
duration: ['>500ms', '>1s', '>5s'],
service: ['gateway', 'catalog', 'checkout', 'worker'],
}
function queryToken(context: CompletionContext) {
const { text, selectionStart, selectionEnd } = context
const start = selectionStart > 0 ? text.lastIndexOf(' ', selectionStart - 1) + 1 : 0
const nextSpace = text.indexOf(' ', selectionEnd)
const end = nextSpace < 0 ? text.length : nextSpace
const colon = text.indexOf(':', start)
const hasKey = colon >= start && colon < selectionStart
const rangeStart = hasKey ? colon + 1 : start
return {
key: hasKey ? text.slice(start, colon) : '',
prefix: text.slice(rangeStart, selectionStart).toLowerCase(),
range: [rangeStart, !hasKey && colon >= start && colon < end ? colon + 1 : end] as [
number,
number,
],
}
}
const descriptions: Record<string, string> = {
entry: '入口类型',
status: '请求状态',
duration: '请求耗时',
service: '服务名称',
}
function complete(option: AutocompleteOption, context: CompletionContext): CompletionEdit {
const token = queryToken(context)
return { range: token.range, text: option.label, keepOpen: !token.key }
}
export default function Demo() {
const [text, setText] = useState('entry:http ')
const [applied, setApplied] = useState('entry:http ')
const [options, setOptions] = useState<AutocompleteOption[]>([])
function query(context: CompletionContext) {
const token = queryToken(context)
const values = token.key ? (queryValues[token.key] ?? []) : Object.keys(queryValues)
setOptions(
values
.filter(value => value.startsWith(token.prefix))
.map(value => ({
value,
label: token.key ? value : `${value}:`,
description: token.key ? undefined : descriptions[value],
})),
)
}
return (
<Stack className="w-full max-w-lg">
<Autocomplete
value={text}
onValueChange={setText}
options={options}
getCompletion={complete}
selectOnTab
aria-label="请求查询"
placeholder="entry:http status:error duration:>500ms"
onQuery={query}
onSubmit={setApplied}
leading={<Search />}
trailing={
text !== applied ? (
<Tag size="sm" tone="warning" className="mx-2 whitespace-nowrap">
待应用
</Tag>
) : (
<Kbd className="mx-2">Enter</Kbd>
)
}
/>
<Text tone="muted" size="sm">
输入 sta,选择 status: 后继续选择 error。没有高亮候选时,回车应用整条查询。
</Text>
<Text size="sm" className="break-all">
已应用:{applied || '—'}
</Text>
</Stack>
)
}
用法
import { Autocomplete } from '@hina-ui/react'
value / onValueChange 始终是输入框里的完整字符串。选择候选可以修改这段文本,失焦不会回滚或清空。需要把值限定在选项集合内时,使用 Combobox。
options 原样作为候选列表,组件不额外筛选。未提供 getCompletion 时,选择候选会用 option.label 替换整段文本并关闭列表。键入任意内容也有效。
选择建议,也可以保留自己输入的文字。
文本:—
'use client'
import { useState } from 'react'
import { Autocomplete, FormField, Stack, Text } from '@hina-ui/react'
const candidates = ['Vue', 'React', 'Svelte', 'Solid', 'Angular']
export default function Demo() {
const [text, setText] = useState('')
const options = candidates
.filter(label => label.toLowerCase().includes(text.toLowerCase()))
.map(label => ({ value: label, label }))
return (
<Stack className="w-full max-w-sm">
<FormField label="框架" description="选择建议,也可以保留自己输入的文字。">
<Autocomplete
value={text}
onValueChange={setText}
options={options}
placeholder="输入框架名称"
/>
</FormField>
<Text tone="muted" size="sm">
文本:{text || '—'}
</Text>
</Stack>
)
}
示例
光标处连续补全
首个示例演示查询片段补全。输入 sta 后用方向键选中 status:,回车确认;列表保持打开,继续提供 ok、error、timeout。选择值后关闭列表,再按回车应用整条查询。也可以把光标移回已有片段进行替换,后面的文本会保留。
onQuery 提供当前文本与选区,输入、移动光标、改变选区和完成补全时都会通知。getCompletion(option, context) 同步返回替换范围与插入文本;范围采用原生输入框的 UTF-16 偏移,左闭右开。组件完成替换后把光标放到插入文本末尾。
function complete(option, context) {
const range = locateToken(context)
return {
range,
text: option.label,
keepOpen: option.kind === 'key',
}
}
locateToken、键值判断和候选生成由应用提供。keepOpen: true 可连续补全,完成后新的 onQuery 回调携带更新后的文本与光标。不要缓存旧选区后再用于替换,应使用 getCompletion 本次收到的 context。范围超出当前文本时抛出 RangeError。
远程候选
loading 显示加载指示和列表中的状态提示,保留输入文字。异步替换 options 会清除旧高亮,新结果不会自动选中第一项。无候选时可以通过 empty 自定义提示;加载文案使用 loadingContent。
请求、防抖、取消和过期响应处理由调用方负责。示例请求随文档部署的静态 JSON,在调用方筛选结果;改为实际搜索接口时保留相同的数据流即可。
输入时请求候选,也允许保留候选之外的文本。
文本:—
'use client'
import { useEffect, useState } from 'react'
import { Search } from 'lucide-react'
import {
Autocomplete,
FormField,
Stack,
Text,
type AutocompleteOption,
type CompletionContext,
} from '@hina-ui/react'
interface TagPage {
data: { items: { id: number; name: string; nameEn: string }[] }
}
export default function Demo() {
const [text, setText] = useState('')
const [search, setSearch] = useState('')
const [keyword, setKeyword] = useState('')
const [options, setOptions] = useState<AutocompleteOption[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setKeyword(search), 250)
return () => clearTimeout(timer)
}, [search])
function query(context: CompletionContext) {
if (search === context.text) return
setSearch(context.text)
setOptions([])
setLoading(!!context.text.trim())
setError(false)
}
useEffect(() => {
const controller = new AbortController()
if (!keyword.trim()) {
setOptions([])
setLoading(false)
return () => controller.abort()
}
setLoading(true)
setError(false)
fetch('/demo/tags.json', { signal: controller.signal })
.then(async response => {
if (!response.ok) throw new Error(String(response.status))
const result = (await response.json()) as TagPage
if (controller.signal.aborted) return
setOptions(
result.data.items
.filter(tag => tag.name.toLowerCase().includes(keyword.trim().toLowerCase()))
.slice(0, 10)
.map(tag => ({ value: tag.id, label: tag.name })),
)
})
.catch(() => {
if (!controller.signal.aborted) setError(true)
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false)
})
return () => controller.abort()
}, [keyword])
return (
<Stack className="w-full max-w-sm">
<FormField label="标签搜索" description="输入时请求候选,也允许保留候选之外的文本。">
<Autocomplete
value={text}
onValueChange={setText}
options={options}
loading={loading}
placeholder="试试「小说」或「音乐」"
onQuery={query}
leading={<Search />}
empty={
error
? '候选加载失败,修改文字后重试。'
: text.trim()
? '没有建议,仍可使用当前文本。'
: '输入文字搜索标签。'
}
/>
</FormField>
<Text tone="muted" size="sm">
文本:{text || '—'}
</Text>
</Stack>
)
}
尺寸与状态
输入面沿用 Input 的尺寸与形态。放进 FormField 后自动关联标签、说明、错误和禁用状态。未声明的属性(如 name、maxLength、aria-label)传给内部输入框。
请输入状态值。
import { Autocomplete, FormField, Stack } from '@hina-ui/react'
const options = [
{ value: 'http', label: 'entry:http' },
{ value: 'rpc', label: 'entry:rpc' },
]
export default function Demo() {
return (
<Stack className="w-full max-w-sm" gap="lg">
<FormField label="小号">
<Autocomplete options={options} size="sm" placeholder="输入查询" />
</FormField>
<FormField label="次级形态">
<Autocomplete options={options} variant="secondary" placeholder="输入查询" />
</FormField>
<FormField label="大号">
<Autocomplete options={options} size="lg" placeholder="输入查询" />
</FormField>
<FormField label="校验失败" error="请输入状态值。">
<Autocomplete options={[]} defaultValue="status:" />
</FormField>
<FormField label="只读">
<Autocomplete options={options} defaultValue="entry:http" readonly />
</FormField>
<FormField label="禁用" disabled>
<Autocomplete options={options} defaultValue="entry:http" />
</FormField>
</Stack>
)
}
在表单中
通过 FormField 的 name 关联校验规则,错误与提交期间的禁用状态会自动传给输入框。候选只提供补全建议,用户输入的其他文本也可以提交。
在 onSubmit 中调用 Form ref 的 submit():回车有高亮候选时只完成补全,没有高亮候选时才触发表单校验与提交;保存按钮也走同一套校验。
'use client'
import { useRef, useState } from 'react'
import * as v from 'valibot'
import { Autocomplete, Button, Form, FormField, Text, type FormHandle } from '@hina-ui/react'
const candidates = ['Vue', 'React', 'Svelte', 'Solid', 'Angular']
const schema = v.object({
framework: v.pipe(v.string(), v.trim(), v.nonEmpty('请输入框架名称')),
})
export default function Demo() {
const form = useRef<FormHandle>(null)
const [values, setValues] = useState({ framework: '' })
const [saved, setSaved] = useState('')
const options = candidates
.filter(label => label.toLowerCase().includes(values.framework.toLowerCase()))
.map(label => ({ value: label, label }))
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
setSaved((data as { framework: string }).framework)
}
return (
<Form ref={form} values={values} rules={schema} className="w-full max-w-sm" onSubmit={save}>
{({ submitting }) => (
<>
<FormField
name="framework"
label="主要框架"
description="选择建议,也可以填写其他框架。"
required
>
<Autocomplete
value={values.framework}
onValueChange={framework => setValues({ ...values, framework })}
options={options}
name="framework"
placeholder="选择或输入框架名称"
onSubmit={() => void form.current?.submit()}
/>
</FormField>
<Button type="submit" loading={submitting} className="self-start">
保存
</Button>
{saved && (
<Text role="status" tone="muted" size="sm">
已保存:{saved}
</Text>
)}
</>
)}
</Form>
)
}
键盘与焦点
- 聚焦、点击或输入时打开列表,保持输入框焦点,不自动高亮首项。
- 上下方向键移动高亮并跳过禁用项,列表内部滚动到当前项,不滚动外层页面。
- 回车有高亮时仅接受候选;没有高亮时关闭列表并调用
onSubmit(text),不会同时触发浏览器表单提交。 selectOnTab默认关闭。开启后,Tab 只在有高亮候选时接受它并保留输入焦点;没有高亮、Shift+Tab 仍正常移动焦点。- Esc 先关闭列表;列表已关闭时清空文本并调用
onClear。关闭和清空都不会应用查询。 - 中文等输入法组词期间不处理候选选择、提交或清除,组词完成后再更新建议。
- 失焦保留文本。
readOnly可聚焦、选择和复制文本,但不打开建议;disabled禁止交互。
输入框、列表和候选分别使用 combobox、listbox、option 语义,活动项由 aria-activedescendant 关联。为组件提供 FormField、关联的标签或 aria-label。renderOption 返回的内容用于展示,不应嵌入按钮、链接等独立交互控件。
API
Props
T extends AutocompleteOption 从 options 推断,额外业务字段在回调和渲染函数中保留类型。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | '' | 完整文本,可受控 |
open | boolean | false | 浮层状态,可受控 |
options | readonly T[] | — | 当前候选, value 在列表中唯一且稳定 |
getCompletion | (option: T, context: CompletionContext) => CompletionEdit | — | 返回同步文本编辑;默认整段替换为 label |
loading | boolean | false | 加载状态;已有候选仍可选,新请求需清除过期候选时由调用方置空 |
selectOnTab | boolean | false | Tab 接受高亮候选 |
placeholder | string | — | 占位文字 |
variant | 'primary' | 'secondary' | 'primary' | 输入面形态 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
disabled | boolean | false | 禁用 |
readOnly | boolean | false | 只读 |
invalid | boolean | false | 校验失败 |
className | string | — | 输入面样式 |
内容属性
| 属性 | 参数 | 说明 |
|---|---|---|
leading | — | 左侧图标等附加内容 |
trailing | — | 右侧快捷键提示或状态标记;RTL 下随逻辑方向排列 |
renderOption | { option: T, active: boolean } | 候选展示内容,行的交互与无障碍由组件负责 |
empty | — | 无候选提示 |
loadingContent | — | 列表内加载提示 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onValueChange | string | 文本变化 |
onOpenChange | boolean | 浮层开关变化 |
onQuery | CompletionContext | 聚焦或重新打开、文本或选区变化、补全完成 |
onSelect | AutocompleteSelection<T> | 候选被接受,包含编辑前上下文和所应用的编辑 |
onSubmit | string | 没有高亮候选时回车应用完整文本 |
onClear | — | 列表关闭时按 Esc 清空非空文本 |
Ref
ref 的 input 指向内部 HTMLInputElement,可使用 setSelectionRange() 操作选区;focus()、blur() 分别聚焦和失焦。ref 仅在挂载后可用。
类型
interface AutocompleteOption {
value: string | number
label: string
description?: string
disabled?: boolean
}
interface CompletionContext {
text: string
selectionStart: number
selectionEnd: number
}
interface CompletionEdit {
range: [number, number]
text: string
keepOpen?: boolean
}
interface AutocompleteSelection<T extends AutocompleteOption = AutocompleteOption> {
option: T
context: CompletionContext
edit: CompletionEdit
}