项目信息
单击文字修改。回车保存,Esc 取消;多行文本用 Ctrl / ⌘ + Enter 保存。
import { Card, Editable, FormField, Stack, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Card className="w-full max-w-lg">
<Stack gap="lg">
<Text weight="medium">项目信息</Text>
<FormField label="项目名称">
<Editable defaultValue="Hina UI 组件库" maxlength={60} />
</FormField>
<FormField label="简介">
<Editable
defaultValue="为内容站与管理后台提供一致、克制的交互体验。"
multiline
rows={3}
/>
</FormField>
<Text size="sm" tone="muted">
单击文字修改。回车保存,Esc 取消;多行文本用 Ctrl / ⌘ + Enter 保存。
</Text>
</Stack>
</Card>
)
}
用法
import { Editable } from '@hina-ui/react'
用于名称、标题、备注等平时以文本展示、需要时就地修改的字段。value / onValueChange 是已确认的值,键入内容只修改内部草稿;保存后才更新,取消恢复原值。需要始终显示输入框、逐字同步表单值时使用 Input 或 Textarea。
默认单击进入编辑,自动聚焦并选中文字。回车或焦点离开整个组件时保存,Esc 取消。焦点在输入框与保存、取消按钮之间移动不会提交,取消按钮也不会先触发失焦保存。
已保存:未命名文档
'use client'
import { useState } from 'react'
import { Editable, Stack, Text } from '@hina-ui/react'
export default function Demo() {
const [title, setTitle] = useState('未命名文档')
return (
<Stack className="w-full max-w-sm">
<Editable value={title} onValueChange={setTitle} aria-label="文档名称" />
<Text size="sm" tone="muted">
已保存:{title || '—'}
</Text>
</Stack>
)
}
示例
激活与确认方式
activationMode 支持单击、双击与手动触发。单击与双击模式都支持聚焦后用 Enter、空格进入编辑;仅用 Tab 聚焦不会直接修改内容。手动模式通过编辑按钮、editing / onEditingChange 或 ref 的 edit() 进入。
submitMode 决定快捷提交方式,显式保存按钮始终可用:
| 值 | 行为 |
|---|---|
both | 回车或焦点离开组件时保存,默认值 |
enter | 回车保存;焦点移出后保留草稿与编辑态 |
blur | 焦点离开组件时保存;回车不提交 |
manual | 仅显式保存;回车、焦点移出都保留草稿 |
首个示例中的 multiline 使用多行输入:Enter 换行,Ctrl / ⌘ + Enter 按 submitMode 的回车规则保存。中文等输入法组词期间不处理保存和取消。
多行编辑复用 Textarea,超出 rows 后由 ScrollArea 接管滚动,输入时保持光标可见。
键盘聚焦后仍可按 Enter 或空格进入。
草稿一直保留到按下保存或取消。
import { Editable, FormField, Stack } from '@hina-ui/react'
export default function Demo() {
return (
<Stack className="w-full max-w-sm" gap="lg">
<FormField label="双击进入" description="键盘聚焦后仍可按 Enter 或空格进入。">
<Editable defaultValue="双击修改文档标题" activationMode="dblclick" />
</FormField>
<FormField label="手动进入" description="草稿一直保留到按下保存或取消。">
<Editable defaultValue="按编辑按钮修改" activationMode="manual" submitMode="manual" />
</FormField>
</Stack>
)
}
异步保存与失败重试
通过 onSave={save} 提供保存函数。它接收新值、旧值,可以返回 Promise;成功后才调用 onValueChange 与 onSubmit。值没有变化时直接结束编辑,不重复保存。
保存期间保留草稿和输入框,显示加载态,禁止重复保存与取消。抛出 Error 会显示其 message,其他异常使用通用失败提示,并调用 onError;用户可以修改后重试或取消。修改草稿会清除旧错误。不要在异步的 onSubmit 回调里执行需要组件等待的请求,onSubmit 是保存成功后的通知。
外部更新 value、关闭 editing、设为禁用或只读、卸载组件,都会使旧的保存结果失效,防止旧值回写。网络请求本身是否取消由业务方负责。
保存完成前保留编辑态,失败后可继续修改或重试。
'use client'
import { useState } from 'react'
import { Editable, FormField, Stack, Switch, Text } from '@hina-ui/react'
export default function Demo() {
const [name, setName] = useState('创作者工作台')
const [fail, setFail] = useState(false)
const [saved, setSaved] = useState(false)
async function save(value: string) {
setSaved(false)
if (!value.trim()) throw new Error('工作区名称不能为空')
await new Promise(resolve => setTimeout(resolve, 800))
if (fail) throw new Error('暂时无法保存,请关闭模拟失败后重试')
setSaved(true)
}
return (
<Stack className="w-full max-w-sm">
<FormField label="工作区名称" description="保存完成前保留编辑态,失败后可继续修改或重试。">
<Editable value={name} onValueChange={setName} onSave={save} submitMode="enter" />
</FormField>
<FormField label="模拟保存失败" orientation="horizontal">
<Switch checked={fail} onCheckedChange={setFail} />
</FormField>
{saved && (
<Text role="status" size="sm" tone="muted">
工作区名称已保存。
</Text>
)}
</Stack>
)
}
自定义展示与操作区
renderPreview 自定义文本的展示形态,不替换组件的激活和焦点逻辑。单击或双击模式下预览由按钮承载,不要在其中嵌套按钮、链接等交互元素。renderActions 接收编辑状态和操作方法,可换成文字按钮。
controls={false} 可隐藏默认操作区;与手动模式组合时,需要通过 renderActions 或 ref 方法提供进入、保存和取消的入口。
'use client'
import { Button, Editable, FormField, Stack, Tag } from '@hina-ui/react'
export default function Demo() {
return (
<Stack className="w-full max-w-sm">
<FormField label="发布版本">
<Editable
defaultValue="1.8.0"
activationMode="manual"
submitMode="manual"
renderPreview={({ value }) => <Tag tone="info">v{value}</Tag>}
renderActions={({ editing, saving, edit, submit, cancel }) =>
editing ? (
<>
<Button size="sm" loading={saving} onClick={submit}>
保存版本
</Button>
<Button size="sm" variant="ghost" tone="neutral" disabled={saving} onClick={cancel}>
取消
</Button>
</>
) : (
<Button size="sm" variant="outline" tone="neutral" onClick={edit}>
修改版本
</Button>
)
}
/>
</FormField>
</Stack>
)
}
尺寸与状态
三档字号、输入高度和内边距与 Input 对齐。单行预览和输入使用相同宽度与文字起点,操作区显示在下方,编辑时不会挤窄文本。只读内容可以选择与复制,禁用状态禁止进入编辑。
内容可以选择与复制。
import { Editable, FormField, Stack } from '@hina-ui/react'
export default function Demo() {
return (
<Stack className="w-full max-w-sm">
<Editable size="sm" value="小号文本" aria-label="小号" />
<Editable size="md" value="默认文本" aria-label="默认" />
<Editable size="lg" value="大号文本" aria-label="大号" />
<FormField label="只读" description="内容可以选择与复制。">
<Editable value="只读项目名称" readonly />
</FormField>
<FormField label="禁用">
<Editable value="暂时无法修改" disabled />
</FormField>
<FormField label="尚未填写">
<Editable placeholder="添加备注…" />
</FormField>
</Stack>
)
}
在表单中
FormField 自动关联标签、描述、错误和禁用状态。字段完成编辑后,Form 对已确认的值进行校验。示例用 editing / onEditingChange 在编辑期间禁用整份表单的提交按钮,避免提交尚未确认的草稿。
required、maxLength 是 Editable 自身的输入限制;复杂规则使用 Form 的规则,或在 onSave 内校验并抛出带提示的异常。name 让原生 FormData 收集已确认值。
'use client'
import { useState } from 'react'
import * as v from 'valibot'
import { Button, Editable, Form, FormField, Text } from '@hina-ui/react'
const schema = v.object({
displayName: v.pipe(v.string(), v.trim(), v.minLength(2, '显示名称至少需要 2 个字')),
})
export default function Demo() {
const [values, setValues] = useState({ displayName: '' })
const [editing, setEditing] = useState(false)
const [saved, setSaved] = useState('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
setSaved((data as { displayName: string }).displayName)
}
return (
<Form values={values} rules={schema} className="w-full max-w-sm" onSubmit={save}>
{({ submitting }) => (
<>
<FormField
name="displayName"
label="显示名称"
description="先确认字段修改,再保存整份资料。"
required
>
<Editable
value={values.displayName}
onValueChange={displayName => setValues({ ...values, displayName })}
editing={editing}
onEditingChange={setEditing}
name="displayName"
placeholder="填写显示名称"
/>
</FormField>
<Button type="submit" loading={submitting} disabled={editing} className="self-start">
保存资料
</Button>
{saved && (
<Text role="status" size="sm" tone="muted">
已保存:{saved}
</Text>
)}
</>
)}
</Form>
)
}
键盘与焦点
- 预览态:Tab 聚焦;Enter、空格进入编辑。手动模式使用编辑按钮。
- 单行编辑:Enter 按
submitMode保存;不会意外提交外层表单。 - 多行编辑:Enter 换行,Ctrl / ⌘ + Enter 按
submitMode保存。 - Esc 取消当前草稿,不同时关闭所在的 Dialog;组词和保存期间不取消。
- 保存、取消后,仍在组件内的焦点回到预览;如果用户已移到外部,不抢回焦点。
- 初始
editing或defaultEditing为true时可以在 SSR 中输出输入态,挂载时不会自动抢焦点。之后进入编辑时才自动聚焦。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | '' | 已确认文本,可受控 |
editing | boolean | false | 编辑状态,可受控 |
activationMode | 'click' | 'dblclick' | 'manual' | 'click' | 进入编辑的方式 |
submitMode | 'enter' | 'blur' | 'both' | 'manual' | 'both' | 快捷提交方式 |
selectOnFocus | boolean | true | 进入编辑后全选文字 |
multiline | boolean | false | 使用多行文本框 |
rows | number | 3 | 多行输入的初始行数 |
controls | boolean | true | 显示默认操作区 |
onSave | EditableSave | — | 更新值之前等待的保存回调 |
placeholder | string | 本地化文案 | 空值提示,同时用于输入框 |
name | string | — | 原生表单字段名,收集已确认值 |
required | boolean | false | 不允许确认空值 |
maxLength | number | — | 输入长度上限 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
disabled | boolean | false | 禁用 |
readOnly | boolean | false | 只读,允许复制文本 |
invalid | boolean | false | 标记校验失败 |
className | string | — | 根节点样式 |
其余属性(如 id、aria-label、autoComplete)传给当前预览或输入节点。className 作用于外层;style 作用于当前预览或输入节点。
内容属性
| 属性 | 参数 | 说明 |
|---|---|---|
renderPreview | { value: string, empty: boolean } | 展示已确认的值 |
renderActions | EditableControls | 替换默认操作区 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onValueChange | string | 保存成功后的新值 |
onEditingChange | boolean | 编辑状态变化 |
onEdit | — | 进入编辑 |
onSubmit | value: string, previousValue: string | 修改保存成功 |
onCancel | draft: string | 取消并丢弃的草稿 |
onError | unknown | 保存回调抛出的异常 |
Ref 与类型
ref 暴露 edit()、submit(): Promise<boolean>、cancel()、focus(),以及当前的 input、draft、saving、error。input 仅在客户端编辑态存在。submit() 返回是否成功完成;保存期间 cancel() 不生效。
type EditableSave = (value: string, previousValue: string) => void | Promise<void>
interface EditableControls {
editing: boolean
draft: string
dirty: boolean
saving: boolean
disabled: boolean
error: string
edit: () => void
submit: () => Promise<boolean>
cancel: () => void
}