阅读视图

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

构建无障碍组件之Tooltip Pattern

Tooltip Pattern 详解:构建无障碍的提示信息组件

Tooltip(提示框,也称为 PopoverHintInfo BubbleHelp Text)是一种弹出式信息组件,当元素获得键盘焦点或鼠标悬停时显示相关信息。本文基于 W3C WAI-ARIA Tooltip Pattern 规范,详解如何构建无障碍的 Tooltip 组件。

注意:此设计模式仍在完善中,尚未获得任务组共识。进展和讨论记录在 aria-practices 仓库的 issue 128 中。

一、Tooltip 的定义与核心概念

1.1 什么是 Tooltip

Tooltip 是一种弹出式信息组件,具有以下特征:

  • 触发元素获得焦点鼠标悬停时显示
  • 通常有短暂的延迟后才会出现
  • Escape 键或鼠标移出时消失
  • 不接收焦点,焦点始终保持在触发元素上
  • 如果悬停内容包含可聚焦元素,应使用非模态对话框(non-modal dialog)

1.2 核心术语

术语 说明
Trigger Element 触发 Tooltip 显示的元素
Tooltip Container 包含 Tooltip 内容的容器
Delay 显示 Tooltip 前的延迟时间
Dismiss 关闭 Tooltip 的行为
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                                                     │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  Trigger Element                            │    │    │
│  │  │  (button / link / icon)                     │    │    │
│  │  │                                             │    │    │
│  │  │  ┌─────────────────────────────────────┐    │    │    │
│  │  │  │  role="tooltip"                     │    │    │    │
│  │  │  │                                     │    │    │    │
│  │  │  │  Tooltip content appears here       │    │    │    │
│  │  │  │  when trigger is focused or hovered │    │    │    │
│  │  │  │                                     │    │    │    │
│  │  │  └─────────────────────────────────────┘    │    │    │
│  │  │         ↑                                   │    │    │
│  │  │         aria-describedby                    │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  │                                                     │    │
│  │  Keyboard: Escape to dismiss                        │    │
│  │  Focus: Stays on trigger element                    │    │
│  │                                                     │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.3 典型应用场景

  • 图标按钮说明:解释图标按钮的功能
  • 表单字段提示:提供输入格式要求
  • 缩写解释:解释专业术语或缩写
  • 额外信息:提供补充说明而不占用界面空间
  • 快捷键提示:显示键盘快捷键

二、WAI-ARIA 角色与属性

2.1 基本角色

Tooltip 使用 role="tooltip" 标记。

<button
  aria-describedby="tooltip-id"
  aria-label="保存">
  💾
</button>

<div
  id="tooltip-id"
  role="tooltip"
  class="tooltip">
  保存当前文档 (Ctrl+S)
</div>

2.2 必需属性

属性 说明 示例值
role="tooltip" 标记为提示框角色 -
aria-describedby 触发元素引用 Tooltip "tooltip-id"

2.3 属性详解

aria-describedby

触发元素通过 aria-describedby 属性引用 Tooltip 元素,让辅助技术知道该元素有额外的描述信息:

<!-- 触发元素 -->
<button
  aria-describedby="save-tooltip"
  aria-label="保存">
  💾
</button>

<!-- Tooltip -->
<div
  id="save-tooltip"
  role="tooltip">
  保存当前文档 (Ctrl+S)
</div>

重要提示

  • aria-describedby 应该指向 Tooltip 的 id
  • 即使 Tooltip 当前不可见,aria-describedby 也应该存在
  • 辅助技术会在用户聚焦到触发元素时读出描述信息

三、键盘交互规范

3.1 基本键盘交互

按键 功能
Escape 关闭 Tooltip

3.2 焦点行为

  • 焦点始终保持在触发元素上,Tooltip 不接收焦点
  • 如果 Tooltip 在触发元素获得焦点时显示,则在元素失去焦点时关闭
  • 如果 Tooltip 在鼠标悬停时显示,则在鼠标移出触发元素或 Tooltip 时关闭

3.3 显示与隐藏逻辑要点

实现 Tooltip 的显示与隐藏需要考虑以下要点:

  • 延迟显示:通常设置 500ms 延迟,避免鼠标快速划过时频繁触发
  • 延迟隐藏:通常设置 100ms 延迟,给用户足够时间将鼠标移到 Tooltip 上
  • 焦点触发:元素获得焦点时立即显示,失去焦点时隐藏
  • 键盘关闭:监听 Escape 键关闭 Tooltip
  • 状态管理:使用 aria-hidden 控制可见性,配合 CSS 类名切换

四、实现方式

4.1 基础 Tooltip 结构

<!-- 触发元素 -->
<button
  class="tooltip-trigger"
  aria-describedby="save-tooltip"
  aria-label="保存">
  💾
</button>

<!-- Tooltip -->
<div
  id="save-tooltip"
  role="tooltip"
  class="tooltip"
  aria-hidden="true">
  保存当前文档 (Ctrl+S)
</div>

4.2 JavaScript 实现

class Tooltip {
  constructor(triggerElement, tooltipElement) {
    this.trigger = triggerElement;
    this.tooltip = tooltipElement;
    this.showDelay = 500; // 延迟显示时间(毫秒)
    this.hideDelay = 100; // 延迟隐藏时间(毫秒)
    this.showTimeout = null;
    this.hideTimeout = null;
    
    this.init();
  }

  init() {
    // 鼠标事件
    this.trigger.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
    this.trigger.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
    
    // 焦点事件
    this.trigger.addEventListener('focus', this.handleFocus.bind(this));
    this.trigger.addEventListener('blur', this.handleBlur.bind(this));
    
    // 键盘事件
    this.trigger.addEventListener('keydown', this.handleKeyDown.bind(this));
  }

  handleMouseEnter() {
    this.clearHideTimeout();
    this.showTimeout = setTimeout(() => {
      this.show();
    }, this.showDelay);
  }

  handleMouseLeave() {
    this.clearShowTimeout();
    this.hideTimeout = setTimeout(() => {
      this.hide();
    }, this.hideDelay);
  }

  handleFocus() {
    this.show();
  }

  handleBlur() {
    this.hide();
  }

  handleKeyDown(e) {
    if (e.key === 'Escape') {
      this.hide();
    }
  }

  show() {
    this.tooltip.classList.add('tooltip-visible');
    this.tooltip.setAttribute('aria-hidden', 'false');
  }

  hide() {
    this.tooltip.classList.remove('tooltip-visible');
    this.tooltip.setAttribute('aria-hidden', 'true');
  }

  clearShowTimeout() {
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
      this.showTimeout = null;
    }
  }

  clearHideTimeout() {
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
      this.hideTimeout = null;
    }
  }
}

4.3 表单字段 Tooltip 示例

<div class="form-field">
  <label for="email">邮箱地址</label>
  <input
    type="email"
    id="email"
    aria-describedby="email-tooltip"
    placeholder="example@domain.com">
  <div
    id="email-tooltip"
    role="tooltip"
    class="tooltip"
    aria-hidden="true">
    请输入有效的邮箱地址,格式:username@domain.com
  </div>
</div>
const emailInput = document.getElementById('email');
const emailTooltip = document.getElementById('email-tooltip');
new Tooltip(emailInput, emailTooltip);

五、最佳实践

5.1 提供清晰的描述

Tooltip 内容应该简洁明了,提供有用的信息:

<!-- 好的示例:提供有用的信息 -->
<button
  aria-describedby="format-tooltip"
  aria-label="格式化">
  📝
</button>
<div
  id="format-tooltip"
  role="tooltip">
  格式化选中的文本 (Ctrl+B)
</div>

<!-- 不好的示例:信息冗余 -->
<button
  aria-describedby="bad-tooltip"
  aria-label="保存">
  💾
</button>
<div
  id="bad-tooltip"
  role="tooltip">
  点击此按钮可以保存您的文档
</div>

5.2 避免在 Tooltip 中包含可聚焦元素

如果 Tooltip 需要包含链接、按钮等可聚焦元素,应该使用非模态对话框(non-modal dialog)而不是 Tooltip:

<!-- 错误:Tooltip 中包含可聚焦元素 -->
<div role="tooltip">
  更多信息请<a href="/help">查看帮助文档</a>
</div>

<!-- 正确:使用非模态对话框 -->
<div
  role="dialog"
  aria-modal="false"
  aria-labelledby="dialog-title">
  <h2 id="dialog-title">更多信息</h2>
  <p>更多信息请<a href="/help">查看帮助文档</a></p>
  <button>关闭</button>
</div>

5.3 设置合理的延迟时间

  • 显示延迟:通常 500ms,避免用户快速移动鼠标时频繁显示
  • 隐藏延迟:通常 100ms,给用户足够的时间将鼠标移到 Tooltip 上

5.4 确保 Tooltip 可访问

5.5 考虑移动端体验

在移动设备上,Tooltip 通常通过点击触发:

// 检测触摸设备
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;

if (isTouchDevice) {
  // 触摸设备:点击触发
  trigger.addEventListener('click', (e) => {
    e.preventDefault();
    tooltip.toggle();
  });
} else {
  // 桌面设备:悬停触发
  trigger.addEventListener('mouseenter', () => tooltip.show());
  trigger.addEventListener('mouseleave', () => tooltip.hide());
}

5.6 提供视觉反馈

  • Tooltip 应该有明显的视觉样式(背景色、边框、阴影)
  • 显示/隐藏应该有平滑的过渡动画
  • 确保 Tooltip 不遮挡重要内容

六、常见错误

6.1 忘记设置 aria-describedby

<!-- 错误 -->
<button>💾</button>
<div role="tooltip">保存文档</div>

<!-- 正确 -->
<button aria-describedby="save-tooltip">💾</button>
<div id="save-tooltip" role="tooltip">保存文档</div>

6.2 Tooltip 接收焦点

<!-- 错误:Tooltip 不应该可聚焦 -->
<div role="tooltip" tabindex="0">...</div>

<!-- 正确:Tooltip 不接收焦点 -->
<div role="tooltip">...</div>

6.3 使用 title 属性代替 Tooltip

<!-- 错误:title 属性的可访问性支持不一致 -->
<button title="保存文档">💾</button>

<!-- 正确:使用 ARIA Tooltip -->
<button aria-describedby="save-tooltip">💾</button>
<div id="save-tooltip" role="tooltip">保存文档</div>

6.4 Tooltip 内容过长

Tooltip 应该简洁,如果内容过长,考虑使用其他组件:

<!-- 不好的示例:内容过长 -->
<div role="tooltip">
  这是一个非常长的说明文字,包含了大量的详细信息...
</div>

<!-- 好的示例:简洁明了 -->
<div role="tooltip">
  保存文档 (Ctrl+S)
</div>

七、Tooltip vs 其他组件

7.1 Tooltip vs Dialog

特性 Tooltip Dialog
焦点 不接收焦点 接收焦点
内容 纯文本信息 可包含交互元素
触发方式 悬停/焦点 点击/特定操作
关闭方式 Escape/移出 点击关闭按钮/遮罩
典型用例 简短说明 确认操作、表单填写

7.2 Tooltip vs Popover

特性 Tooltip Popover
内容复杂度 简单文本 可包含丰富内容
交互性 无交互 可包含交互元素
持久性 临时显示 可持久显示
典型用例 功能说明 详细信息展示

八、总结

构建无障碍的 Tooltip 组件需要关注:

  1. 正确的角色:使用 role="tooltip"
  2. 关联属性:触发元素使用 aria-describedby 引用 Tooltip
  3. 焦点管理:Tooltip 不接收焦点,焦点始终保持在触发元素
  4. 键盘交互:支持 Escape 键关闭
  5. 显示逻辑:合理的延迟显示和隐藏
  6. 内容简洁:提供有用的信息,避免冗余
  7. 避免可聚焦元素:Tooltip 中不包含链接、按钮等可聚焦元素
  8. 位置计算:确保 Tooltip 不超出视口边界

遵循 W3C Tooltip Pattern 规范,我们能够创建既实用又无障碍的提示信息组件,为所有用户提供清晰的辅助信息。

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

构建无障碍组件之Spinbutton Pattern

Spinbutton Pattern 详解:构建无障碍数字输入控件

Spinbutton(旋转按钮,也称为 Number InputStepperNumeric SpinnerCounter)是一种输入控件,用于在预定义范围内选择离散数值。本文基于 W3C WAI-ARIA Spinbutton Pattern 规范,详解如何构建无障碍的数字输入组件。

一、Spinbutton 的定义与核心概念

1.1 什么是 Spinbutton

Spinbutton 是一种受限的数字输入控件,具有以下特征:

  • 值被限制在一组或一个范围内的离散值
  • 通常包含三个组件:
    • 文本输入框:显示当前值,通常是唯一可聚焦的组件
    • 增加按钮:用于增加数值
    • 减少按钮:用于减少数值
  • 支持直接编辑按钮调整两种方式
  • 支持小步长大步长调整

1.2 核心术语

术语 说明
Text Field 显示当前值的文本输入框
Increase Button 增加数值的按钮
Decrease Button 减少数值的按钮
Small Step 小步长调整(如按 1 增减)
Large Step 大步长调整(如按 10 增减)
Valid Value 允许范围内的有效值
┌─────────────────────────────────────────────────────────────┐
│                      Spinbutton Container                   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                                                     │    │
│  │  ┌─────────────────┐  ┌──────┐  ┌──────┐            │    │
│  │  │                 │  │  ▲   │  │  ▼   │            │    │
│  │  │   Value: 30     │  │  +   │  │  -   │            │    │
│  │  │                 │  │      │  │      │            │    │
│  │  └─────────────────┘  └──────┘  └──────┘            │    │
│  │                                                     │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  role="spinbutton"                          │    │    │
│  │  │  aria-valuenow="30"                         │    │    │
│  │  │  aria-valuemin="0"                          │    │    │
│  │  │  aria-valuemax="100"                        │    │    │
│  │  │  aria-label="Quantity"                      │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  │                                                     │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Keyboard: ↑↓ (±1) | Page Up/Down (±10) | Home/End (Min/Max)│
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.3 典型应用场景

  • 数量选择器:购物车商品数量、酒店预订人数
  • 时间选择器:小时、分钟选择
  • 日期选择器:日、月、年选择
  • 数值调节:音量控制、亮度调节
  • 评分输入:1-5 星评分

二、WAI-ARIA 角色与属性

2.1 基本角色

Spinbutton 使用 role="spinbutton" 标记。

<input
  type="text"
  role="spinbutton"
  aria-label="数量"
  aria-valuenow="1"
  aria-valuemin="0"
  aria-valuemax="10"
  value="1" />

2.2 必需属性

属性 说明 示例值
role="spinbutton" 标记为旋转按钮角色 -
aria-valuenow 当前值 "1"
aria-valuemin 最小值(如果有) "0"
aria-valuemax 最大值(如果有) "10"
aria-labelaria-labelledby 可访问标签 "数量"

2.3 可选属性

属性 说明 示例值
aria-valuetext 用户友好的值描述 "Monday"
aria-invalid 值是否无效 "true" / "false"

2.4 属性详解

aria-valuetext

aria-valuenow 的值不够友好时,使用 aria-valuetext 提供更易理解的描述:

<!-- 星期选择器:数值 1 显示为 "Monday" -->
<input
  type="text"
  role="spinbutton"
  aria-label="星期"
  aria-valuenow="1"
  aria-valuemin="1"
  aria-valuemax="7"
  aria-valuetext="Monday"
  value="Monday" />
aria-invalid

当值超出允许范围时,设置 aria-invalid="true"

<input
  type="text"
  role="spinbutton"
  aria-label="数量"
  aria-valuenow="15"
  aria-valuemin="0"
  aria-valuemax="10"
  aria-invalid="true"
  value="15" />

注意:大多数实现会阻止输入无效值,但在某些场景下可能无法完全阻止。

三、键盘交互规范

3.1 基本键盘交互

按键 功能
↑ Up Arrow 增加数值(小步长)
↓ Down Arrow 减少数值(小步长)
Home 设置值为最小值
End 设置值为最大值
Page Up(可选) 增加数值(大步长)
Page Down(可选) 减少数值(大步长)

3.2 文本编辑键盘交互

如果文本框允许直接编辑,还支持以下标准单行文本编辑键:

  • 可打印字符:在文本框中输入字符
  • 光标移动键:左右箭头、Home、End
  • 选择键:Shift + 方向键
  • 文本操作键:复制、粘贴、删除等

重要提示:确保 JavaScript 不干扰浏览器提供的文本编辑功能。

3.3 焦点行为

  • 操作过程中焦点始终保持在文本框
  • 不需要将焦点移到增减按钮上

四、实现方式

4.1 基础 Spinbutton 结构

<div class="spinbutton-container">
  <label for="quantity">数量</label>
  <div class="spinbutton-wrapper">
    <input
      type="text"
      id="quantity"
      class="spinbutton"
      role="spinbutton"
      aria-label="数量"
      aria-valuenow="1"
      aria-valuemin="0"
      aria-valuemax="10"
      value="1" />
    <div class="spinbutton-buttons">
      <button
        type="button"
        class="spinbutton-up"
        aria-label="增加"
        tabindex="-1"></button>
      <button
        type="button"
        class="spinbutton-down"
        aria-label="减少"
        tabindex="-1"></button>
    </div>
  </div>
</div>

4.2 JavaScript 实现

class Spinbutton {
  constructor(element) {
    this.input = element;
    this.min = parseFloat(this.input.getAttribute('aria-valuemin')) || 0;
    this.max = parseFloat(this.input.getAttribute('aria-valuemax')) || 100;
    this.smallStep = 1;
    this.largeStep = 10;

    this.init();
  }

  init() {
    // 键盘事件
    this.input.addEventListener('keydown', this.handleKeyDown.bind(this));

    // 直接编辑
    this.input.addEventListener('change', this.handleChange.bind(this));
    this.input.addEventListener('blur', this.handleBlur.bind(this));

    // 按钮点击
    const container = this.input.closest('.spinbutton-wrapper');
    const upButton = container.querySelector('.spinbutton-up');
    const downButton = container.querySelector('.spinbutton-down');

    if (upButton) {
      upButton.addEventListener('click', () => this.increment(this.smallStep));
    }
    if (downButton) {
      downButton.addEventListener('click', () =>
        this.decrement(this.smallStep),
      );
    }
  }

  handleKeyDown(e) {
    const currentValue =
      parseFloat(this.input.getAttribute('aria-valuenow')) || 0;

    switch (e.key) {
      case 'ArrowUp':
        e.preventDefault();
        this.increment(this.smallStep);
        break;
      case 'ArrowDown':
        e.preventDefault();
        this.decrement(this.smallStep);
        break;
      case 'PageUp':
        e.preventDefault();
        this.increment(this.largeStep);
        break;
      case 'PageDown':
        e.preventDefault();
        this.decrement(this.largeStep);
        break;
      case 'Home':
        e.preventDefault();
        this.setValue(this.min);
        break;
      case 'End':
        e.preventDefault();
        this.setValue(this.max);
        break;
    }
  }

  handleChange() {
    const value = parseFloat(this.input.value);
    if (!isNaN(value)) {
      this.setValue(value);
    }
  }

  handleBlur() {
    // 失去焦点时验证并修正值
    const value = parseFloat(this.input.value);
    if (isNaN(value)) {
      this.setValue(this.min);
    } else {
      this.setValue(value);
    }
  }

  increment(step) {
    const currentValue =
      parseFloat(this.input.getAttribute('aria-valuenow')) || 0;
    this.setValue(currentValue + step);
  }

  decrement(step) {
    const currentValue =
      parseFloat(this.input.getAttribute('aria-valuenow')) || 0;
    this.setValue(currentValue - step);
  }

  setValue(value) {
    // 限制在范围内
    value = Math.max(this.min, Math.min(this.max, value));

    // 更新 ARIA 属性
    this.input.setAttribute('aria-valuenow', value);

    // 更新显示值
    this.input.value = value;

    // 更新有效性状态
    const isValid = value >= this.min && value <= this.max;
    this.input.setAttribute('aria-invalid', !isValid);
  }
}

// 初始化
const spinbuttons = document.querySelectorAll('[role="spinbutton"]');
spinbuttons.forEach((spinbutton) => new Spinbutton(spinbutton));

4.3 带 aria-valuetext 的示例

class WeekdaySpinbutton extends Spinbutton {
  constructor(element) {
    super(element);
    this.weekdays = [
      '',
      'Monday',
      'Tuesday',
      'Wednesday',
      'Thursday',
      'Friday',
      'Saturday',
      'Sunday',
    ];
    this.smallStep = 1;
    this.largeStep = 1; // 星期没有大步长
  }

  setValue(value) {
    // 限制在范围内
    value = Math.max(this.min, Math.min(this.max, value));

    // 更新 ARIA 属性
    this.input.setAttribute('aria-valuenow', value);
    this.input.setAttribute('aria-valuetext', this.weekdays[value]);

    // 显示星期名称
    this.input.value = this.weekdays[value];
  }
}

五、最佳实践

5.1 提供清晰的标签

始终为 Spinbutton 提供描述性的标签:

<!-- 好的示例 -->
<label for="adults">成人数量</label>
<input
  type="text"
  id="adults"
  role="spinbutton"
  aria-label="成人数量"
  ... />

<!-- 不好的示例 -->
<input
  type="text"
  role="spinbutton"
  ... />

5.2 设置合理的范围

根据实际场景设置最小值和最大值:

<!-- 好的示例:酒店预订成人数量 -->
<input
  type="text"
  role="spinbutton"
  aria-label="成人数量"
  aria-valuemin="1"
  aria-valuemax="10"
  ... />

<!-- 不好的示例:没有限制 -->
<input
  type="text"
  role="spinbutton"
  ... />

5.3 使用 aria-valuetext 增强可读性

当数值不够直观时,使用 aria-valuetext

<!-- 好的示例:月份选择 -->
<input
  type="text"
  role="spinbutton"
  aria-label="月份"
  aria-valuenow="1"
  aria-valuemin="1"
  aria-valuemax="12"
  aria-valuetext="January"
  value="January" />

5.4 验证用户输入

阻止无效字符输入,或在失去焦点时修正值:

// 阻止非数字输入
spinbutton.addEventListener('keypress', (e) => {
  if (!/\d/.test(e.key)) {
    e.preventDefault();
  }
});

// 失去焦点时验证
spinbutton.addEventListener('blur', () => {
  const value = parseInt(spinbutton.value);
  if (isNaN(value) || value < min || value > max) {
    // 修正为有效值
    setValue(Math.max(min, Math.min(max, value || min)));
  }
});

5.5 考虑移动端体验

在移动设备上,考虑使用数字键盘:

<input
  type="number"
  inputmode="numeric"
  pattern="[0-9]*"
  role="spinbutton"
  ... />

5.6 提供视觉反馈

  • 无效值时显示错误状态
  • 焦点状态清晰可见
  • 按钮悬停效果
[role='spinbutton'][aria-invalid='true'] {
  border-color: #ef4444;
  background-color: #fef2f2;
}

[role='spinbutton']:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

六、常见错误

6.1 忘记设置 aria-valuenow

<!-- 错误 -->
<input
  type="text"
  role="spinbutton"
  value="5" />

<!-- 正确 -->
<input
  type="text"
  role="spinbutton"
  aria-valuenow="5"
  value="5" />

6.2 按钮可聚焦

<!-- 错误:按钮不应该可聚焦 -->
<button class="spinbutton-up"></button>

<!-- 正确:按钮设置 tabindex="-1" -->
<button
  class="spinbutton-up"
  tabindex="-1"></button>

6.3 忽略键盘交互

只实现按钮点击,不实现键盘支持(方向键、Home/End)。

6.4 不验证输入值

允许用户输入超出范围的值或无效字符。

七、Spinbutton vs 其他输入控件

7.1 Spinbutton vs Slider

特性 Spinbutton Slider
输入方式 键盘输入 + 按钮 拖拽滑块
适用场景 精确数值、离散值 连续范围、粗略选择
精度 中等
典型用例 数量、时间 音量、亮度

7.2 Spinbutton vs 普通文本输入

特性 Spinbutton 普通文本输入
值限制 有最小/最大值 无限制
步长调整 支持 不支持
辅助技术 读出当前值和范围 只读出文本
典型用例 年龄、评分 姓名、地址

八、总结

构建无障碍的 Spinbutton 组件需要关注:

  1. 正确的角色:使用 role="spinbutton"
  2. 必需的属性aria-valuenowaria-valueminaria-valuemaxaria-label
  3. 可选属性aria-valuetextaria-invalid
  4. 完整的键盘支持:方向键调整、Page Up/Down 大步长、Home/End 快捷键
  5. 直接编辑支持:允许用户直接输入值
  6. 输入验证:阻止无效字符,修正超出范围的值
  7. 清晰的标签:帮助用户理解控件用途
  8. 按钮不可聚焦:只有文本框可聚焦

遵循 W3C Spinbutton Pattern 规范,我们能够创建既实用又无障碍的数字输入控件,为所有用户提供便捷的数值选择体验。

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

❌