Popconfirm 气泡确认框

贴着触发器浮出的确认气泡。

写得真好,期待下一章。

这段剧情和原作不一样吧?

已收藏。

'use client'

import { useState } from 'react'
import { Button, Inline, Popconfirm, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [comments, setComments] = useState([
    '写得真好,期待下一章。',
    '这段剧情和原作不一样吧?',
    '已收藏。',
  ])

  function remove(index: number) {
    setComments(current => current.filter((_, position) => position !== index))
  }

  return (
    <Stack gap="sm" align="stretch" className="w-96">
      {comments.map((comment, index) => (
        <Inline key={comment} align="center" justify="between" gap="sm">
          <Text size="sm">{comment}</Text>
          <Popconfirm
            title="删除这条评论?"
            description="删除后无法恢复。"
            tone="danger"
            confirmText="删除"
            onConfirm={() => remove(index)}
          >
            <Button size="sm" variant="ghost" tone="neutral">
              删除
            </Button>
          </Popconfirm>
        </Inline>
      ))}
      {!comments.length ? (
        <Text tone="muted" size="sm">
          没有评论了。
        </Text>
      ) : null}
    </Stack>
  )
}
tsx

用法

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

气泡确认框在触发器旁边问一个问题,适合删除一行、撤销一次操作这类范围小、就地可见的确认;后果重、需要打断当前任务的确认用 AlertDialog。title 是问题,description 补充后果,底部一对按钮的文字可以用 cancelText 与 confirmText 替换。children 是触发器。

'use client'

import { useState } from 'react'
import { Button, Popconfirm, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [done, setDone] = useState(false)
  return (
    <Stack gap="sm" align="start">
      <Popconfirm title="把全部通知标记为已读?" onConfirm={() => setDone(true)}>
        <Button variant="outline" tone="neutral">
          全部已读
        </Button>
      </Popconfirm>
      {done ? (
        <Text tone="muted" size="sm">
          已全部标记为已读。
        </Text>
      ) : null}
    </Stack>
  )
}
tsx

示例

危险操作

tone="danger" 把「确定」换成危险色,用于删除这类不可撤销的操作。

import { Button, Popconfirm } from '@hina-ui/react'

export default function Demo() {
  return (
    <Popconfirm
      title="移除这位成员?"
      description="移除后对方将无法访问这个项目。"
      tone="danger"
      confirmText="移除"
    >
      <Button variant="outline" tone="danger">
        移除成员
      </Button>
    </Popconfirm>
  )
}
tsx

异步确认

onConfirm 回调返回 Promise 时,气泡等它结束再收回:期间「确定」显示加载指示,「取消」、Esc 与点击外部都不可用;处理函数抛出错误时气泡保持打开。

'use client'

import { useState } from 'react'
import { Button, Popconfirm, Stack, Switch, Text } from '@hina-ui/react'

export default function Demo() {
  const [fail, setFail] = useState(false)
  const [status, setStatus] = useState('')

  async function retry() {
    setStatus('')
    await new Promise(resolve => setTimeout(resolve, 1000))
    if (fail) {
      setStatus('重试失败,请稍后再试。')
      throw new Error('retry failed')
    }
    setStatus('已重新发送。')
  }

  return (
    <Stack gap="md" align="start">
      <Switch checked={fail} onCheckedChange={setFail}>
        模拟失败
      </Switch>
      <Popconfirm
        title="重新发送这封邮件?"
        confirmText="重发"
        onConfirm={retry}
        content={
          status ? (
            <Text tone="muted" size="sm">
              {status}
            </Text>
          ) : null
        }
      >
        <Button variant="outline" tone="neutral">
          重新发送
        </Button>
      </Popconfirm>
    </Stack>
  )
}
tsx

位置

side 与 align 与 Popover 相同,默认在触发器正下方,空间不足时翻转到相反一侧。

import { Button, Inline, Popconfirm } from '@hina-ui/react'

const sides = ['top', 'right', 'bottom', 'left'] as const
const names = { top: '上方', right: '右侧', bottom: '下方', left: '左侧' }

export default function Demo() {
  return (
    <Inline gap="sm">
      {sides.map(side => (
        <Popconfirm key={side} title="确认这个位置?" side={side}>
          <Button variant="outline" tone="neutral">
            {names[side]}
          </Button>
        </Popconfirm>
      ))}
    </Inline>
  )
}
tsx

受控

open 可受控,可以从外部打开或者收回。

'use client'

import { useState } from 'react'
import { Button, Inline, Popconfirm } from '@hina-ui/react'

export default function Demo() {
  const [open, setOpen] = useState(false)
  return (
    <Inline gap="sm">
      <Button variant="outline" tone="neutral" onClick={() => setOpen(true)}>
        从外部打开
      </Button>
      <Popconfirm
        open={open}
        onOpenChange={setOpen}
        title="清空草稿?"
        description="草稿里的内容会被丢弃。"
      >
        <Button variant="outline" tone="neutral">
          清空草稿
        </Button>
      </Popconfirm>
    </Inline>
  )
}
tsx

行为

  • 打开后焦点落在「取消」按钮上,误按回车不会执行操作;收回后焦点回到触发器。
  • Esc 与点击外部只是收回气泡,不调用 onCancel;点「取消」才调用。
  • 打开期间页面停止滚动,触发器保持按下时的样式。
  • onConfirm 返回 Promise 时,气泡在其结束前处于忙碌状态,结束后收回;抛出错误则保持打开。

无障碍

  • 面板是 role="dialog",名称取自触发器,问题与说明一起关联到 aria-describedby。
  • 忙碌期间面板带有 aria-busy。
  • 按钮文字取自当前语言,可以替换。

API

Props

属性
类型
默认值
说明
title
string
—
必填。问题本身
description
string
—
补充说明
confirmText
string
当前语言
「确定」按钮的文字
cancelText
string
当前语言
「取消」按钮的文字
tone
'accent' | 'danger'
'accent'
「确定」按钮的色调
side
'top' | 'right' | 'bottom' | 'left'
'bottom'
浮出的方向
align
'start' | 'center' | 'end'
'center'
与触发器的对齐方式
sideOffset
number
8
与触发器的距离,像素
open
boolean
—
是否打开,可受控
className
string
—
追加至面板的类名

内容属性

属性
说明
children
触发器
content
说明与按钮之间的内容

回调

回调
参数
说明
onConfirm
—
点击「确定」;处理函数返回 Promise 时等待其完成
onCancel
—
点击「取消」