Skip to content

警告对话框

使用方法 ​

AlertDialog 是一个只做一件事的模态对话框:在破坏性或不可逆的操作真正发生之前,再向用户确认一次。删除内容、撤销授权、丢弃未保存的修改都属于这类场景——操作一旦执行就无法回头,所以值得打断用户,把后果讲清楚,再由用户自己决定。

它实现了 WAI-ARIA 的 Alert Dialog 模式。根元素使用 role="alertdialog",标题通过 aria-labelledby 为对话框命名,后果描述通过 aria-describedby 一起播报。打开时焦点移入对话框并落在「取消」上,关闭后再回到触发它的元素。它不能通过点击遮罩关闭,Escape 则等同于取消。

vue
<script setup lang="ts">
import { AlertDialog } from '@astryx-vue/core/AlertDialog'
import { Button } from '@astryx-vue/core/Button'
import { ref } from 'vue'

const isOpen = ref(false)
</script>

<template>
  <Button label="删除项目" variant="destructive" @click="isOpen = true" />
  <AlertDialog
    :is-open="isOpen"
    title="要删除这个项目吗?"
    description="删除后无法恢复,项目下的部署与所有仪表盘都会一并移除。"
    action-label="删除项目"
    @action="isOpen = false"
    @open-change="isOpen = $event"
  />
</template>

打开状态完全由父组件掌握。按下「取消」或按 Escape 只会发出 openChange(false),组件不会自己关闭;确认按钮同样只发出 action 事件,也不会关闭对话框。这是刻意留出的位置:异步操作需要时间,你可以在操作结束后再设置 isOpen。等待期间把 isActionLoading 设为 true,确认按钮会显示加载指示,对话框保持打开:

vue
<script setup lang="ts">
import { ref } from 'vue'

const isOpen = ref(false)
const isActionLoading = ref(false)

async function revoke(): Promise<void> {
  isActionLoading.value = true
  await revokeAccess()
  isActionLoading.value = false
  isOpen.value = false
}
</script>

<template>
  <AlertDialog
    :is-action-loading="isActionLoading"
    :is-open="isOpen"
    action-label="撤销授权"
    description="该成员会立即失去所有共享文件夹和草稿的访问权限。"
    title="要撤销授权吗?"
    @action="revoke"
    @open-change="isOpen = $event"
  />
</template>

确认按钮默认使用 destructive 变体,因为对话框本身就是为确认破坏性操作而存在的。如果这次确认并不具有破坏性,例如「发布变更」,把 actionVariant 改成 primary,颜色就会和语义一致。

按钮的排列会随可用宽度变化。宽于 640px 时,操作区是一行,按「取消 → 确认」的顺序排列,标签过长时整行自动换行;640px 及以下时两个按钮都占满宽度,确认操作排在「取消」上方。无论哪种排列,初始焦点始终落在「取消」上,因为那是破坏性最小的选择;窄屏下的层级、DOM 顺序与 Tab 顺序保持一致,屏幕阅读器读到的顺序和眼睛看到的顺序不会打架。

isInline 用于文档预览:它以普通文档流渲染整个表面,不使用 <dialog>、遮罩与焦点限制。既然没有模态行为,它也不会声称自己是模态的,根元素的角色改为 role="group"。

最佳实践 ​

指引实践
推荐让确认按钮的标签说清会发生什么。「删除项目」比「确定」或「是」更能让人在按下之前判断后果。
推荐在 description 里写清后果:删除后能否恢复、会影响哪些数据、是否通知他人。用户是在这里做出决定的。
推荐保持「取消」是最不具破坏性的焦点目标。窄屏下确认操作在视觉和结构上都排在前面,但初始焦点仍然留在「取消」上。
推荐异步操作期间使用 isActionLoading 撑住对话框,等工作真正结束后再关闭它。
推荐只用来确认破坏性或不可逆的操作。普通的信息提示、表单填写改用量更轻的对话框。
避免只靠颜色表达危险。红色按钮本身不说明会发生什么,标签才是。
避免在 action 事件里立刻关闭对话框。这样一来加载状态无处安放,用户也不知道操作到底有没有成功。
避免把确认对话框套在另一个对话框里。多步流程请拆成同一个对话框里的步骤,或改用整页。

示例 ​

基本用法

Delete project?

This cannot be undone. The project, its deployments, and every dashboard built on it are removed permanently.

最常用的形态:破坏性确认。打开时焦点落在「取消」上,Escape 等同于取消,点击遮罩不会关闭。

非破坏性确认

Publish this draft?

Publishing pushes this draft to the live site and notifies 1,240 subscribers.

确认操作并不具有破坏性时把 actionVariant 设为 primary,同时用 cancelLabel 说明取消之后会发生什么。

异步操作

Revoke access?

This member loses access to every shared folder and draft immediately.

异步操作期间保持对话框打开并让确认按钮显示加载指示,操作结束后再由父组件关闭。

无障碍 ​

对话框通过原生 <dialog> 的 showModal() 打开,因此模态行为、页面其余部分的不可交互状态、以及关闭后的焦点归还都由浏览器提供。标题与描述分别通过 aria-labelledby 与 aria-describedby 关联,屏幕阅读器在进入对话框时会读出「标题 + 后果描述 + 可用操作」这一整段信息,用户不必先摸索一圈才知道发生了什么。

初始焦点固定在「取消」上,并通过 data-autofocus 标记,所以即使窄屏下按钮的排列发生变化,获得焦点的仍然是最不具破坏性的那个选择。Escape 与按下「取消」走的是同一条路径:只发出 openChange(false),是否关闭由父组件决定。点击遮罩不会关闭对话框——破坏性操作的确认不应该因为一次误触而消失。

确认按钮加载期间会带上 aria-busy,「取消」仍然可以按,用户可以取消一个还没结束的操作。

主题 ​

css
.my-alert-dialog {
  --astryx-alert-dialog-title-foreground: #171717;
  --astryx-alert-dialog-description-foreground: #525252;
  --astryx-alert-dialog-content-gap: 16px;
}

表面、遮罩、圆角与阴影全部来自对话框本身,可通过 --astryx-dialog-* 调整;上面这些令牌只影响标题、描述与操作区之间的间距。Neutral 主题会随明暗模式自动切换。