uni-app 无障碍访问

章节介绍

无障碍访问(Accessibility)是指确保应用能够被所有用户使用,包括那些有视觉、听觉、运动或认知障碍的用户。uni-app 作为一个跨平台开发框架,支持多种无障碍特性,使开发者能够创建对所有用户友好的应用。本章节将详细介绍 uni-app 中的无障碍访问实现方法,包括无障碍属性的使用、屏幕阅读器支持、键盘导航等功能。

核心知识点

1. 无障碍访问基础概念

  • 无障碍访问(Accessibility):确保应用能够被所有用户使用,包括残障用户
  • 屏幕阅读器(Screen Reader):将屏幕内容转换为语音或盲文的辅助技术
  • 键盘导航(Keyboard Navigation):允许用户使用键盘操作应用
  • ARIA(Accessible Rich Internet Applications):一组用于增强 Web 内容可访问性的属性
  • WCAG(Web Content Accessibility Guidelines):Web 内容无障碍指南,提供了无障碍设计的标准

2. uni-app 中的无障碍支持

uni-app 提供了多种无障碍特性:

  • 无障碍属性:通过 aria-* 属性增强组件的可访问性
  • 屏幕阅读器支持:适配不同平台的屏幕阅读器
  • 键盘导航:支持键盘操作和焦点管理
  • 语义化标签:使用语义化的组件和标签

3. 无障碍属性的使用

  • aria-label:为组件提供替代文本
  • aria-labelledby:引用其他元素的文本作为标签
  • aria-describedby:提供组件的详细描述
  • aria-hidden:控制元素是否对屏幕阅读器可见
  • aria-disabled:指示组件是否禁用
  • aria-checked:指示复选框或单选按钮的状态
  • aria-selected:指示选项是否被选中
  • aria-expanded:指示元素是否展开

4. 屏幕阅读器适配

  • iOS VoiceOver:苹果设备的屏幕阅读器
  • Android TalkBack:安卓设备的屏幕阅读器
  • Windows Narrator:Windows 系统的屏幕阅读器
  • Web 屏幕阅读器:如 NVDA、JAWS 等

5. 键盘导航实现

  • 焦点管理:控制元素的焦点顺序和状态
  • 键盘事件:处理键盘按键事件
  • 跳过导航:提供跳过重复内容的链接
  • 键盘陷阱:避免用户陷入无法退出的键盘导航循环

实用案例分析

案例一:基本无障碍属性使用

场景:为按钮、输入框等组件添加无障碍属性,提高屏幕阅读器的识别能力。

实现步骤

  1. 为按钮添加 aria-label 属性
  2. 为输入框添加 aria-labelledby 属性
  3. 为复杂组件添加 aria-describedby 属性
  4. 测试屏幕阅读器的识别效果

代码示例

<template>
  <view class="container">
    <view class="form-item">
      <text id="username-label">用户名</text>
      <input 
        type="text" 
        placeholder="请输入用户名" 
        aria-labelledby="username-label"
      />
    </view>
    <view class="form-item">
      <text id="password-label">密码</text>
      <input 
        type="password" 
        placeholder="请输入密码" 
        aria-labelledby="password-label"
      />
    </view>
    <view class="button-group">
      <button 
        class="login-button" 
        aria-label="登录按钮,点击后将提交表单"
      >
        登录
      </button>
      <button 
        class="cancel-button" 
        aria-label="取消按钮,点击后将重置表单"
      >
        取消
      </button>
    </view>
    <view class="hint" id="login-hint">
      <text>请输入正确的用户名和密码</text>
    </view>
  </view>
</template>

<style>
.container {
  padding: 20rpx;
}

.form-item {
  margin-bottom: 30rpx;
}

.form-item text {
  display: block;
  margin-bottom: 10rpx;
  font-weight: bold;
}

.form-item input {
  width: 100%;
  padding: 20rpx;
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
}

.button-group {
  display: flex;
  justify-content: space-between;
  margin-bottom: 30rpx;
}

.button-group button {
  flex: 1;
  padding: 20rpx;
  border-radius: 8rpx;
  margin: 0 10rpx;
}

.login-button {
  background-color: #007AFF;
  color: white;
}

.cancel-button {
  background-color: #f5f5f5;
  color: #333;
}

.hint {
  font-size: 24rpx;
  color: #666;
}
</style>

案例二:自定义组件的无障碍实现

场景:为自定义的开关组件添加无障碍属性,使其能够被屏幕阅读器正确识别。

实现步骤

  1. 创建自定义开关组件
  2. 添加 aria-checked 属性指示状态
  3. 添加 aria-label 属性提供描述
  4. 实现键盘操作支持

代码示例

<!-- components/AccessibleSwitch.vue -->
<template>
  <view 
    class="switch-container" 
    :class="{ 'active': value }"
    @click="toggle"
    @keydown.enter="toggle"
    @keydown.space="toggle"
    tabindex="0"
    role="switch"
    :aria-checked="value"
    :aria-label="label"
  >
    <view class="switch-track">
      <view class="switch-thumb"></view>
    </view>
    <text v-if="showLabel" class="switch-label">{{ labelText }}</text>
  </view>
</template>

<script>
export default {
  name: 'AccessibleSwitch',
  props: {
    value: {
      type: Boolean,
      default: false
    },
    label: {
      type: String,
      default: '开关'
    },
    labelText: {
      type: String,
      default: ''
    },
    showLabel: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    toggle() {
      this.$emit('input', !this.value)
      this.$emit('change', !this.value)
    }
  }
}
</script>

<style>
.switch-container {
  display: flex;
  align-items: center;
  cursor: pointer;
  outline: none;
}

.switch-track {
  position: relative;
  width: 120rpx;
  height: 60rpx;
  background-color: #ddd;
  border-radius: 30rpx;
  transition: background-color 0.3s;
}

.switch-container.active .switch-track {
  background-color: #007AFF;
}

.switch-thumb {
  position: absolute;
  top: 4rpx;
  left: 4rpx;
  width: 52rpx;
  height: 52rpx;
  background-color: white;
  border-radius: 50%;
  transition: transform 0.3s;
}

.switch-container.active .switch-thumb {
  transform: translateX(60rpx);
}

.switch-label {
  margin-left: 20rpx;
  font-size: 28rpx;
}

/* 焦点样式 */
.switch-container:focus {
  box-shadow: 0 0 0 2rpx #007AFF;
}
</style>

在页面中使用:

<template>
  <view class="container">
    <accessible-switch 
      v-model="notifications"
      label="通知开关"
      label-text="接收应用通知"
      :show-label="true"
    />
    <accessible-switch 
      v-model="location"
      label="位置开关"
      label-text="允许访问位置信息"
      :show-label="true"
    />
    <accessible-switch 
      v-model="bluetooth"
      label="蓝牙开关"
      label-text="启用蓝牙功能"
      :show-label="true"
    />
  </view>
</template>

<script>
import AccessibleSwitch from '@/components/AccessibleSwitch.vue'

export default {
  components: {
    AccessibleSwitch
  },
  data() {
    return {
      notifications: true,
      location: false,
      bluetooth: false
    }
  }
}
</script>

<style>
.container {
  padding: 20rpx;
}

.container > view {
  margin-bottom: 30rpx;
}
</style>

案例三:键盘导航实现

场景:为应用添加键盘导航支持,允许用户使用键盘操作菜单和表单。

实现步骤

  1. 添加 tabindex 属性控制焦点顺序
  2. 实现键盘事件处理
  3. 添加跳过导航链接
  4. 测试键盘导航效果

代码示例

<template>
  <view class="container">
    <!-- 跳过导航链接 -->
    <a href="#main-content" class="skip-link">跳过导航</a>
    
    <!-- 导航菜单 -->
    <view class="nav">
      <view 
        class="nav-item" 
        :class="{ 'active': currentPage === 'home' }"
        @click="navigate('home')"
        @keydown.enter="navigate('home')"
        @keydown.space="navigate('home')"
        tabindex="0"
        role="button"
        :aria-label="`导航到首页,当前${currentPage === 'home' ? ' active' : ''}`"
      >
        首页
      </view>
      <view 
        class="nav-item" 
        :class="{ 'active': currentPage === 'products' }"
        @click="navigate('products')"
        @keydown.enter="navigate('products')"
        @keydown.space="navigate('products')"
        tabindex="0"
        role="button"
        :aria-label="`导航到产品页,当前${currentPage === 'products' ? ' active' : ''}`"
      >
        产品
      </view>
      <view 
        class="nav-item" 
        :class="{ 'active': currentPage === 'about' }"
        @click="navigate('about')"
        @keydown.enter="navigate('about')"
        @keydown.space="navigate('about')"
        tabindex="0"
        role="button"
        :aria-label="`导航到关于页,当前${currentPage === 'about' ? ' active' : ''}`"
      >
        关于
      </view>
    </view>
    
    <!-- 主要内容 -->
    <view id="main-content" class="main-content">
      <text class="title">{{ pageTitle }}</text>
      <view class="form">
        <view class="form-item">
          <text id="name-label">姓名</text>
          <input 
            type="text" 
            placeholder="请输入姓名" 
            aria-labelledby="name-label"
            tabindex="0"
          />
        </view>
        <view class="form-item">
          <text id="email-label">邮箱</text>
          <input 
            type="email" 
            placeholder="请输入邮箱" 
            aria-labelledby="email-label"
            tabindex="0"
          />
        </view>
        <view class="button-group">
          <button 
            class="submit-button" 
            @click="submit"
            @keydown.enter="submit"
            @keydown.space="submit"
            tabindex="0"
            role="button"
            aria-label="提交表单"
          >
            提交
          </button>
          <button 
            class="reset-button" 
            @click="reset"
            @keydown.enter="reset"
            @keydown.space="reset"
            tabindex="0"
            role="button"
            aria-label="重置表单"
          >
            重置
          </button>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      currentPage: 'home',
      pageTitle: '首页'
    }
  },
  methods: {
    navigate(page) {
      this.currentPage = page
      switch(page) {
        case 'home':
          this.pageTitle = '首页'
          break
        case 'products':
          this.pageTitle = '产品页'
          break
        case 'about':
          this.pageTitle = '关于页'
          break
      }
    },
    submit() {
      uni.showToast({
        title: '表单提交成功',
        icon: 'success'
      })
    },
    reset() {
      // 重置表单逻辑
      uni.showToast({
        title: '表单已重置',
        icon: 'none'
      })
    }
  }
}
</script>

<style>
.container {
  padding: 20rpx;
}

/* 跳过导航链接 */
.skip-link {
  position: absolute;
  top: -40rpx;
  left: 0;
  background-color: #007AFF;
  color: white;
  padding: 10rpx;
  text-decoration: none;
  z-index: 1000;
}

.skip-link:focus {
  top: 0;
}

/* 导航菜单 */
.nav {
  display: flex;
  background-color: #f5f5f5;
  border-radius: 8rpx;
  padding: 0 10rpx;
  margin-bottom: 30rpx;
}

.nav-item {
  flex: 1;
  padding: 20rpx;
  text-align: center;
  cursor: pointer;
}

.nav-item.active {
  background-color: #007AFF;
  color: white;
  border-radius: 8rpx;
}

.nav-item:focus {
  outline: 2rpx solid #007AFF;
  border-radius: 8rpx;
}

/* 主要内容 */
.main-content {
  margin-top: 20rpx;
}

.title {
  font-size: 36rpx;
  font-weight: bold;
  margin-bottom: 30rpx;
}

/* 表单 */
.form {
  margin-top: 20rpx;
}

.form-item {
  margin-bottom: 30rpx;
}

.form-item text {
  display: block;
  margin-bottom: 10rpx;
  font-weight: bold;
}

.form-item input {
  width: 100%;
  padding: 20rpx;
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
}

.form-item input:focus {
  outline: 2rpx solid #007AFF;
}

/* 按钮组 */
.button-group {
  display: flex;
  justify-content: space-between;
  margin-top: 40rpx;
}

.button-group button {
  flex: 1;
  padding: 20rpx;
  border-radius: 8rpx;
  margin: 0 10rpx;
}

.submit-button {
  background-color: #007AFF;
  color: white;
}

.reset-button {
  background-color: #f5f5f5;
  color: #333;
}

.button-group button:focus {
  outline: 2rpx solid #007AFF;
}
</style>

无障碍访问最佳实践

1. 语义化标记

  • 使用语义化标签:使用合适的 HTML 标签和 uni-app 组件
  • 添加适当的角色:使用 role 属性指定元素的角色
  • 保持结构清晰:使用清晰的层次结构组织内容

2. 颜色和对比度

  • 足够的对比度:确保文本和背景的对比度符合 WCAG 标准
  • 避免仅使用颜色:不要仅依赖颜色来传达信息
  • 提供颜色调整选项:允许用户调整应用的颜色方案

3. 表单设计

  • 明确的标签:为每个表单控件提供明确的标签
  • 错误提示:提供清晰的错误提示和修复建议
  • 键盘可访问:确保所有表单控件都可以通过键盘访问

4. 多媒体内容

  • 替代文本:为图片提供 alt 属性
  • 字幕和 transcripts:为视频和音频提供字幕和文字记录
  • 音频描述:为视频提供音频描述,解释视觉内容

5. 动画和交互

  • 可暂停的动画:允许用户暂停或关闭动画
  • 足够的时间:为用户提供足够的时间阅读和交互
  • 减少闪烁:避免可能引起癫痫发作的闪烁效果
  • 提供反馈:为用户操作提供明确的反馈

6. 测试和验证

  • 使用屏幕阅读器测试:在不同平台上测试屏幕阅读器的使用
  • 键盘导航测试:测试完全使用键盘操作应用的能力
  • 无障碍验证工具:使用工具如 axe、WAVE 等验证无障碍性
  • 用户测试:邀请残障用户测试应用

7. 性能考虑

  • 减少不必要的动画:避免可能影响屏幕阅读器性能的动画
  • 优化焦点管理:确保焦点移动流畅,无延迟
  • 减少网络请求:确保音频描述和字幕等内容加载迅速

总结回顾

本章节介绍了 uni-app 中的无障碍访问实现方法,包括:

  1. 无障碍访问基础概念:了解无障碍访问的重要性和基本概念
  2. uni-app 中的无障碍支持:了解 uni-app 提供的无障碍特性
  3. 无障碍属性的使用:掌握 aria-* 属性的使用方法
  4. 屏幕阅读器适配:了解如何适配不同平台的屏幕阅读器
  5. 键盘导航实现:掌握键盘导航的实现方法
  6. 无障碍访问最佳实践:学习无障碍设计的最佳实践

通过本章节的学习,您应该能够:

  • 为 uni-app 应用添加基本的无障碍支持
  • 使用无障碍属性增强组件的可访问性
  • 实现键盘导航支持
  • 适配不同平台的屏幕阅读器
  • 遵循无障碍设计的最佳实践

无障碍访问是现代应用开发的重要组成部分,通过合理的无障碍实现,可以确保应用能够被所有用户使用,包括残障用户。在实际开发中,应根据应用的具体需求,结合 WCAG 指南,不断优化和完善无障碍功能,创建更加包容和友好的应用。

« 上一篇 uni-app 国际化 下一篇 » uni-app 应用安全