Editable 原地编辑

在原位置编辑文本,确认或取消修改。

项目信息

单击文字修改。回车保存,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>
  )
}
tsx

用法

import { Editable } from '@hina-ui/react'
ts

用于名称、标题、备注等平时以文本展示、需要时就地修改的字段。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>
  )
}
tsx

示例

激活与确认方式

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>
  )
}
tsx

异步保存与失败重试

通过 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>
  )
}
tsx

自定义展示与操作区

renderPreview 自定义文本的展示形态,不替换组件的激活和焦点逻辑。单击或双击模式下预览由按钮承载,不要在其中嵌套按钮、链接等交互元素。renderActions 接收编辑状态和操作方法,可换成文字按钮。

controls={false} 可隐藏默认操作区;与手动模式组合时,需要通过 renderActions 或 ref 方法提供进入、保存和取消的入口。

v1.8.0
'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>
  )
}
tsx

尺寸与状态

三档字号、输入高度和内边距与 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>
  )
}
tsx

在表单中

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>
  )
}
tsx

键盘与焦点

  • 预览态: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
}
ts