阅读视图

发现新文章,点击刷新页面。

构建无障碍组件之Dialog Pattern

Dialog (Modal) Pattern 详解:构建无障碍模态对话框

模态对话框是 Web 应用中常见的交互组件,用于在不离开当前页面的情况下展示重要信息或获取用户输入。本文基于 W3C WAI-ARIA Dialog Pattern 规范,详解如何构建无障碍的模态对话框。

一、Dialog 的定义与核心功能

Dialog(对话框)是覆盖在主窗口或其他对话框之上的窗口。模态对话框会阻断用户与底层内容的交互,直到对话框关闭。底层内容通常会被视觉遮挡或变暗,以明确当前焦点在对话框内。

与 Alert Dialog 不同,普通 Dialog 适用于各种需要用户交互的场景,如表单填写、信息展示、设置配置等。它不强调紧急性,用户可以自主决定是否与之交互。

二、Dialog 的关键特性

模态对话框具有以下核心特性:

焦点限制:对话框包含独立的 Tab 序列,Tab 和 Shift+Tab 仅在对话框内循环,不会移出对话框外部。

背景禁用:对话框背后的内容处于 inert 状态,用户无法与之交互。尝试与背景交互通常会导致对话框关闭。

层级管理:对话框可以嵌套,新的对话框覆盖在旧对话框之上,形成层级结构。

三、WAI-ARIA 角色与属性

3.1 基本角色

role="dialog" 是对话框的基础角色,用于标识模态或非模态对话框元素。

aria-modal="true" 明确告知辅助技术这是一个模态对话框,背景内容当前不可用。

3.2 标签与描述

aria-labelledby 引用对话框标题元素,为对话框提供可访问名称。

aria-describedby 引用包含对话框主要内容的元素,帮助屏幕阅读器用户理解对话框目的。

<dialog
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc">
  <h2 id="dialog-title">用户设置</h2>
  <p id="dialog-desc">请配置您的个人偏好设置。</p>
  <!-- 对话框内容 -->
</dialog>

四、键盘交互规范

4.1 焦点管理

打开对话框时:焦点应移动到对话框内的某个元素。通常移动到第一个可聚焦元素,但根据内容不同可能有不同策略:

  • 内容包含复杂结构(列表、表格)时,可将焦点设置在内容的静态元素上,便于用户理解
  • 内容较长时,将焦点设置在标题或顶部段落,避免内容滚动出视野
  • 简单确认对话框,焦点可设置在主要操作按钮

关闭对话框时:焦点应返回到触发对话框的元素,除非该元素已不存在。

4.2 键盘操作

按键 功能
Tab 移动到对话框内下一个可聚焦元素,到达末尾时循环到第一个
Shift + Tab 移动到对话框内上一个可聚焦元素,到达开头时循环到最后一个
Escape 关闭对话框
// 焦点管理示例
function openDialog(dialog, triggerElement) {
  dialog.triggerElement = triggerElement;
  dialog.showModal();

  // 将焦点设置到第一个可聚焦元素或标题
  const focusable = dialog.querySelector(
    'button, [href], input, select, textarea',
  );
  if (focusable) {
    focusable.focus();
  }
}

function closeDialog(dialog) {
  dialog.close();
  // 恢复焦点到触发元素
  if (dialog.triggerElement) {
    dialog.triggerElement.focus();
  }
}

五、实现方式

5.1 原生 dialog 元素

HTML5 <dialog> 元素是推荐实现方式,内置模态行为和无障碍支持:

  • 自动焦点管理showModal() 自动将焦点移动到对话框内第一个可聚焦元素
  • 内置 ESC 关闭:用户按 ESC 键自动关闭对话框
  • 自动模态背景:自动创建背景遮罩,阻止与底层内容交互
  • 焦点循环:Tab 键在对话框内自动循环,不会移出对话框
  • 内置 ARIA 属性:浏览器自动处理 aria-modal 等属性
  • Top Layer 支持:模态对话框显示在浏览器顶层,不受 z-index 限制
<dialog
  id="settings-dialog"
  aria-labelledby="dialog-title">
  <div class="dialog-header">
    <h2 id="dialog-title">设置</h2>
    <button
      onclick="this.closest('dialog').close()"
      aria-label="关闭"></button>
  </div>
  <div class="dialog-content">
    <label>
      用户名
      <input type="text" />
    </label>
  </div>
  <div class="dialog-footer">
    <button onclick="this.closest('dialog').close()">取消</button>
    <button onclick="saveSettings()">保存</button>
  </div>
</dialog>

<button onclick="document.getElementById('settings-dialog').showModal()">
  打开设置
</button>

5.2 div + ARIA 实现

需要手动处理焦点管理和背景交互。这种方式适用于需要自定义动画、复杂布局或旧浏览器兼容的场景:

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  class="modal-overlay">
  <div class="modal-content">
    <h2 id="dialog-title">确认操作</h2>
    <p>确定要执行此操作吗?</p>
    <button>取消</button>
    <button>确认</button>
  </div>
</div>

六、最佳实践

初始焦点策略和键盘交互的详细规范请参考 4.1 焦点管理。在实际应用中,建议遵循以下策略:

  • 信息展示:焦点设置在标题或内容开头,便于屏幕阅读器顺序阅读
  • 表单输入:焦点设置在第一个输入框
  • 确认操作:焦点设置在主操作按钮或取消按钮(视风险而定)

6.1 关闭方式

提供多种关闭方式提升用户体验:

  • ESC 键关闭(原生 dialog 自动支持)
  • 关闭按钮
  • 点击背景遮罩关闭(可选)
  • 明确的取消/确认按钮

6.2 嵌套对话框

支持多层对话框嵌套,每层新对话框覆盖在上层:

<dialog id="layer1">
  <button onclick="document.getElementById('layer2').showModal()">
    打开第二层
  </button>
</dialog>

<dialog id="layer2">
  <p>第二层对话框</p>
</dialog>

6.3 避免滥用

对话框会中断用户流程,应谨慎使用:

  • 优先使用非模态方式展示非关键信息
  • 避免对话框内再嵌套复杂导航
  • 保持对话框内容简洁,避免过多滚动

七、Dialog 与 Alert Dialog 的区别

特性 Dialog Alert Dialog
用途 一般交互、表单、配置 紧急确认、警告、错误
紧急性 非紧急 紧急,需立即响应
关闭方式 多种方式 通常只有确认/取消
角色 role="dialog" role="alertdialog"
系统提示音 可能有

八、总结

构建无障碍的模态对话框需要关注三个核心:正确的 ARIA 属性声明、合理的焦点管理、完整的键盘交互支持。原生 <dialog> 元素简化了实现,但开发者仍需理解无障碍原理,确保所有用户都能顺利使用。

遵循 W3C Dialog Pattern 规范,我们能够创建既美观又包容的对话框组件,为不同能力的用户提供一致的体验。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

构建无障碍组件之Alert Dialog Pattern

Alert Dialog Pattern 详解:构建无障碍中断式对话框

Alert Dialog 是 Web 无障碍交互的重要组件。本文详解其 WAI-ARIA 实现要点,涵盖角色声明、键盘交互、最佳实践,助你打造中断式对话框,让关键信息触达每位用户。

一、Alert Dialog 的定义与核心功能

Alert Dialog(警告对话框)是一种模态对话框,它会中断用户的工作流程以传达重要信息并获取响应。与普通的 Alert 通知不同,Alert Dialog 需要用户明确与之交互后才能继续其他操作。这种设计适用于需要用户立即关注和做出决定的场景。

在实际应用中,Alert Dialog 广泛应用于各种需要用户确认或紧急通知的场景。例如,删除操作前的确认提示、表单提交失败的错误确认、离开页面时的未保存更改提醒等。这些场景都需要用户明确响应才能继续操作,因此 Alert Dialog 成为最佳选择。

二、Alert Dialog 的特性与注意事项

Alert Dialog 组件具有几个重要的特性,这些特性决定了它的适用场景和实现方式。首先,Alert Dialog 会获取键盘焦点,确保用户的注意力集中在对话框上。其次,Alert Dialog 通常会阻止用户与页面的其他部分交互,直到用户关闭对话框。这种模态特性确保了用户必须处理重要信息才能继续操作。

Alert Dialog 组件的设计还需要考虑几个关键因素。首先,Alert Dialog 应该始终包含一个明确的关闭方式,如确认按钮或取消按钮。其次,对话框应该有一个清晰的标题,通过 aria-labelledbyaria-label 关联。另外,对话框的内容应该通过 aria-describedby 关联,以便屏幕阅读器能够正确读取完整信息。这些属性的正确使用对于无障碍体验至关重要。

三、WAI-ARIA 角色、状态和属性

正确使用 WAI-ARIA 属性是构建无障碍 Alert Dialog 组件的技术基础。Alert Dialog 组件的 ARIA 要求包含多个属性的配合使用。

role="alertdialog" 是 Alert Dialog 组件的必需属性,它向辅助技术表明这个元素是一个警告对话框。这个属性使浏览器和辅助技术能够将 Alert Dialog 与其他类型的对话框区分开来,从而提供特殊的处理方式,如播放系统提示音。

aria-labelledbyaria-label 用于标识对话框的标题。如果对话框有可见的标题标签,应该使用 aria-labelledby 引用该标题元素;如果没有可见标题,则使用 aria-label 提供标签。

aria-describedby 用于引用包含警告消息的元素。这确保屏幕阅读器能够朗读完整的对话框内容,包括详细的说明和操作提示。

<!-- Alert Dialog 基本结构 -->
<dialog
  id="confirm-dialog"
  role="alertdialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc">
  <form method="dialog">
    <h2 id="dialog-title">确认删除</h2>
    <p id="dialog-desc">您确定要删除这个文件吗?此操作无法撤销。</p>
    <div class="actions">
      <button value="confirm">确认删除</button>
      <button value="cancel">取消</button>
    </div>
  </form>
</dialog>

值得注意的是,Alert Dialog 与普通 Dialog 的主要区别在于 Alert Dialog 用于紧急或重要信息,并且通常包含确认/取消按钮。用户无法忽略 Alert Dialog,必须做出响应才能继续操作。

四、键盘交互规范

Alert Dialog 的键盘交互遵循模态对话框的交互模式。用户可以通过多种方式与 Alert Dialog 进行交互。

  • EnterSpace 用于激活默认按钮,通常是对话框中的主要操作按钮。
  • Tab 键用于在对话框内的焦点元素之间切换,焦点会循环停留 在对话框内部。
  • Escape 键通常用于关闭对话框,相当于点击取消按钮。
// ESC 键关闭对话框示例
document.addEventListener('keydown', function (e) {
  if (e.key === 'Escape' && dialog.open) {
    dialog.close();
  }
});

焦点管理是 Alert Dialog 的关键部分。当对话框打开时,焦点应该立即移动到对话框内部或默认按钮上。当对话框关闭时,焦点应该返回到打开对话框的元素。这种焦点管理确保了键盘用户能够保持其工作上下文。

五、完整示例

以下是一个完整的 Alert Dialog 实现示例,展示了正确的 HTML 结构、ARIA 属性和焦点管理。

<dialog
  id="confirm-dialog"
  role="alertdialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc">
  <form method="dialog">
    <h2 id="dialog-title">确认删除</h2>
    <p id="dialog-desc">您确定要删除这个文件吗?此操作无法撤销。</p>
    <div class="dialog-actions">
      <button
        class="btn btn-ghost"
        value="cancel">
        取消
      </button>
      <button
        class="btn btn-error"
        value="confirm">
        删除
      </button>
    </div>
  </form>
</dialog>

<button
  id="delete-btn"
  class="btn btn-error">
  删除文件
</button>

<script>
  const dialog = document.getElementById('confirm-dialog');
  const deleteBtn = document.getElementById('delete-btn');
  let previousActiveElement;

  deleteBtn.addEventListener('click', function () {
    previousActiveElement = document.activeElement;
    dialog.showModal();
  });

  dialog.addEventListener('close', function () {
    if (dialog.returnValue === 'confirm') {
      console.log('文件已删除');
    }
    previousActiveElement.focus();
  });
</script>

六、最佳实践

6.1 实现方式对比

Alert Dialog 可以通过两种方式实现:使用 div 配合 ARIA 属性,或使用原生 <dialog> 元素。

传统方式(div + ARIA)
<div
  role="alertdialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc">
  <h2 id="dialog-title">确认删除</h2>
  <p id="dialog-desc">您确定要删除这个文件吗?</p>
  <button>确认</button>
  <button>取消</button>
</div>

这种方式需要开发者手动处理焦点管理、ESC 键关闭、背景锁定等逻辑。

推荐方式(原生 dialog)
<dialog
  role="alertdialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc">
  <form method="dialog">
    <h2 id="dialog-title">确认删除</h2>
    <p id="dialog-desc">您确定要删除这个文件吗?</p>
    <button value="confirm">确认</button>
    <button value="cancel">取消</button>
  </form>
</dialog>

HTML 原生 <dialog> 元素简化了实现,它提供了:

  • 自动焦点管理
  • 内置 ESC 键支持
  • 自动模态背景
  • 内置 ARIA 属性

<dialog> 元素的默认 roledialog,表示普通对话框。对于 Alert Dialog,需要显式设置 role="alertdialog" 来告诉辅助技术这是一个需要紧急处理的对话框,从而获得系统提示音等特殊处理。

6.2 焦点管理

正确的焦点管理对于键盘用户和无障碍体验至关重要。打开对话框时,焦点应该移动到对话框内部或默认按钮。关闭对话框时,焦点应该返回到触发对话框的元素。

// 焦点管理最佳实践
function openDialog(dialog) {
  const previousFocus = document.activeElement;
  dialog.showModal();

  // 移动焦点到对话框内
  const focusableElements = dialog.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
  );
  if (focusableElements.length > 0) {
    focusableElements[0].focus();
  }

  // 保存关闭时的焦点元素
  dialog.dataset.previousFocus = previousFocus;
}

function closeDialog(dialog) {
  dialog.close();
  const previousFocus = document.querySelector(
    `[data-focus-id="${dialog.dataset.focusId}"]`,
  );
  if (previousFocus) {
    previousFocus.focus();
  }
  dialog.remove();
}

6.3 避免过度使用

Alert Dialog 会中断用户的工作流程,因此应该谨慎使用。只有在真正需要用户立即响应的情况下才使用 Alert Dialog。对于非紧急信息,应该考虑使用普通的 Alert 或 Toast 通知。

<!-- 不推荐:过度使用 Alert Dialog -->
<dialog
  open
  role="alertdialog">
  <h2>提示</h2>
  <p>您的设置已保存。</p>
  <button onclick="this.closest('dialog').close()">确定</button>
</dialog>

<!-- 推荐:使用普通 Alert -->
<div role="alert">您的设置已保存。</div>

6.4 屏幕阅读器兼容性

确保 <dialog> 对屏幕阅读器用户友好。<dialog> 元素内置了无障碍支持,但仍然建议对 Alert Dialog 设置 role="alertdialog" 来区分紧急对话框。

<!-- 屏幕阅读器友好的 dialog -->
<dialog
  id="session-dialog"
  role="alertdialog">
  <form method="dialog">
    <h2>重要提醒</h2>
    <p>您的会话将在 5 分钟后过期。请尽快保存您的工作。</p>
    <div class="actions">
      <button value="continue">继续使用</button>
      <button value="exit">退出</button>
    </div>
  </form>
</dialog>

七、Alert 与 Alert Dialog 的区别

理解 AlertAlert Dialog 的区别对于正确选择通知组件至关重要。虽然两者都是用于传达重要信息,但它们服务于不同的目的和使用场景。

Alert 是一种被动通知组件,它不需要用户进行任何交互操作。Alert 会在不被中断用户工作流程的前提下自动通知用户重要信息。用户可以继续当前的工作,Alert 只是在视觉和听觉上提供通知。这种设计适用于不紧急、不需要用户立即响应的信息,例如操作成功确认、后台处理完成通知等。

Alert Dialog 则是一种需要用户主动响应的对话框组件。当用户需要做出决定或者提供确认时,应该使用 Alert Dialog。Alert Dialog 会中断用户的工作流程,获取键盘焦点,要求用户必须与之交互才能继续其他操作。这种设计适用于紧急警告、确认删除操作、放弃更改确认等需要用户明确响应的场景。

选择建议:如果信息需要用户立即响应并做出决定,使用 Alert Dialog;如果只是被动通知信息,使用 Alert。

八、总结

构建无障碍的对话框组件需要关注元素选择、焦点管理、键盘交互三个层面的细节。从元素选择角度,推荐优先使用原生 <dialog> 元素,它内置了无障碍支持和焦点管理。从焦点管理角度,需要确保打开和关闭时焦点的正确移动。从用户体验角度,应该避免过度使用对话框,只在真正需要用户响应时使用。

WAI-ARIA Alert Dialog Pattern 为我们提供了清晰的指导方针,遵循这些规范能够帮助我们创建更加包容和易用的 Web 应用。每一个正确实现的对话框,都是提升用户体验和确保重要信息有效传达的重要一步。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

❌