vue优雅的适配无障碍

为了在 Vue 项目中如何优雅地适配无障碍功能,以下是完整的实现方案:

1. 创建无障碍指令

a11y.js 指令文件

javascript 复制代码
// 无障碍常见属性常量定义
const A11Y_ATTRS = {
  ROLE: 'role',
  TABINDEX: 'tabindex',
  LABEL: 'aria-label',
  LABELLEDBY: 'aria-labelledby',
  DESCRIBEDBY: 'aria-describedby',
  LIVE: 'aria-live',
  HIDDEN: 'aria-hidden',
  DISABLED: 'aria-disabled'
}
const A11Y_ROLES = {
  BUTTON: 'button',
  LINK: 'link',
  IMAGE: 'img',
  HEADING: 'heading'
}

/**
 * 设置元素的 ARIA 属性
 * @param {HTMLElement} el - DOM 元素
 * @param {Object} binding - 指令绑定对象
 */
function setAriaAttributes (el, binding) {
  const { value } = binding
  if (value.tabIndex !== undefined) {
    el.setAttribute(A11Y_ATTRS.TABINDEX, value.tabIndex)
  }
  // 基础 ARIA 属性设置
  if (value.role) {
    el.setAttribute(A11Y_ATTRS.ROLE, value.role)
  }
  if (value.label) {
    el.setAttribute(A11Y_ATTRS.LABEL, value.label)
  }
  if (value.labelledBy) {
    el.setAttribute(A11Y_ATTRS.LABELLEDBY, value.labelledBy)
  }
  if (value.describedBy) {
    el.setAttribute(A11Y_ATTRS.DESCRIBEDBY, value.describedBy)
  }
  if (value.live) {
    el.setAttribute(A11Y_ATTRS.LIVE, value.live)
  } 
  if (value.hidden !== undefined) {
    el.setAttribute(A11Y_ATTRS.HIDDEN, value.hidden)
  }
  if (value.disabled !== undefined) {
    el.setAttribute(A11Y_ATTRS.DISABLED, value.disabled)
  }
  // 特殊角色处理
  if (value.role === A11Y_ROLES.HEADING && value.level) {
    el.setAttribute('aria-level', value.level)
  }
  // 确保按钮和链接添加 tabindex
  if (value.role === A11Y_ROLES.BUTTON || value.role === A11Y_ROLES.LINK) {
    if (!el.hasAttribute('tabindex')) {
      el.setAttribute('tabindex', '0')
    }
  }
  // 图片必须有 alt 文本
  if (value.role === A11Y_ROLES.IMAGE && !el.hasAttribute('alt')) {
    console.warn('Accessibility warning: Image elements should have alt text', el)
  }
}

/**
 * 动态更新 ARIA 属性
 * @param {HTMLElement} el - DOM 元素
 * @param {Object} value - 新值
 */
function updateAriaAttributes (el, value) {
  // 移除旧属性
  Object.values(A11Y_ATTRS).forEach(attr => {
    el.removeAttribute(attr)
  })
  // 设置新属性
  setAriaAttributes(el, { value })
}
export default {
  install (Vue, options = {}) {
    Vue.directive('a11y', {
      inserted (el, binding) {
        setAriaAttributes(el, binding)
      },
      update (el, binding) {
        if (binding.value !== binding.oldValue) {
          updateAriaAttributes(el, binding.value)
        }
      },
      componentUpdated (el, binding) {
        if (binding.value !== binding.oldValue) {
          updateAriaAttributes(el, binding.value)
        }
      }
    })
  }
}

2. 注册指令

main.js 中注册指令:

javascript 复制代码
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import a11yDirective from './directives/a11y'

Vue.directive('a11y', a11yDirective)

new Vue({
  render: h => h(App)
}).$mount('#app')

3. 使用指令

基本用法

HTML 复制代码
<template>
  <!-- 按钮 -->
  <button v-a11y="{ role: 'button', label: '提交表单' }">提交</button>
  
  <!-- 标题 -->
  <h1 v-a11y="{ role: 'heading', level: 1 }">页面标题</h1>
  
  <!-- 图片 -->
  <img 
    v-a11y="{ role: 'img', label: '公司Logo' }" 
    src="logo.png" 
    alt="公司Logo"
  >
  
  <!-- 自定义交互元素 -->
  <div 
    v-a11y="{ role: 'button', label: '关闭弹窗' }"
    @click="closeModal"
  >
    ×
  </div>
</template>

动态属性

HTML 复制代码
<template>
  <div>
    <button 
      v-a11y="{
        role: 'button',
        label: buttonLabel,
        disabled: isDisabled
      }"
      :disabled="isDisabled"
      @click="handleClick"
    >
      {{ buttonText }}
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isDisabled: false,
      buttonText: '点击我',
      buttonLabel: '这是一个可点击的按钮'
    }
  },
  methods: {
    handleClick() {
      this.isDisabled = true
      this.buttonLabel = '按钮已被点击,请等待'
    }
  }
}
</script>

4. 高级功能扩展

焦点管理指令

javascript 复制代码
// src/directives/focus-manager.js
export default {
  inserted(el, binding) {
    if (binding.value) {
      el.focus()
    }
  },
  update(el, binding) {
    if (binding.value && !binding.oldValue) {
      // 使用 setTimeout 确保在 DOM 更新后执行
      setTimeout(() => {
        el.focus()
      }, 0)
    }
  }
}

实时区域更新

HTML 复制代码
<template>
  <div 
    v-a11y="{ live: 'polite' }"
    aria-live="polite"
  >
    {{ notificationText }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      notificationText: ''
    }
  },
  methods: {
    showNotification(message) {
      this.notificationText = message
    }
  }
}
</script>

5. 最佳实践

  1. 语义化 HTML :优先使用原生语义化元素(如 <button> 而不是 <div>

  2. 标签关联

    html 复制代码
    <template>
      <div>
        <label id="username-label">用户名</label>
        <input 
          v-a11y="{ labelledBy: 'username-label' }"
          type="text"
          aria-labelledby="username-label"
        >
      </div>
    </template>
  3. 状态管理

    html 复制代码
    <template>
      <button
        v-a11y="{
          role: 'button',
          label: expanded ? '收起菜单' : '展开菜单',
          expanded: expanded
        }"
        :aria-expanded="expanded"
        @click="toggleMenu"
      >
        {{ expanded ? '▼' : '►' }}
      </button>
    </template>
  4. 测试验证

    • 使用 Chrome 开发者工具的 Lighthouse 进行无障碍测试
    • 在 Android 设备上实际测试 TalkBack 功能

6. 全局混入常用方法

javascript 复制代码
// src/mixins/a11y.js
export default {
  methods: {
    // 为动态内容提供无障碍通知
    a11yNotify(message, priority = 'polite') {
      const liveRegion = document.getElementById('a11y-live-region')
      if (liveRegion) {
        liveRegion.setAttribute('aria-live', priority)
        liveRegion.textContent = message
      } else {
        console.warn('无障碍实时区域未找到')
      }
    },
    
    // 管理焦点
    a11yFocus(elementId) {
      const el = document.getElementById(elementId)
      if (el) {
        el.focus()
      }
    }
  }
}

总结

通过封装 v-a11y 指令,我们可以:

  1. 统一管理所有无障碍属性
  2. 提供动态更新能力
  3. 内置常见模式的最佳实践
  4. 方便地扩展新功能
  5. 保持代码整洁和可维护性

这种方案既满足了 TalkBack 的基本需求,又能灵活应对各种复杂场景,是 Vue 项目中实现无障碍功能的优雅解决方案。

相关推荐
前端fighter1 小时前
线上崩溃?使用 TRAE Work 5分钟把混淆日志翻译成人话并输出复盘报告
前端·vue.js·程序员
常宇佳2 小时前
vue3 实战系列之无法识别vue组件
前端·javascript·vue.js
daols883 小时前
vue 实现基于 vxe-table 构建多维度产品对比表
前端·javascript·vue.js
mashirro5 小时前
记一次基于ruoyi-vue新增小程序服务模块,小程序登陆相关
vue.js·小程序·mybatis
常宇佳8 小时前
vue3 $refs使用方法
前端·vue.js·typescript
༄沐࿆风࿆࿆8 小时前
Spring Boot 3 + Vue 3 全栈外卖系统:RabbitMQ异步解耦、MySQL/Neo4j双引擎推建
vue.js·spring boot·java-rabbitmq
伟大的兔神17 小时前
我做了一个本地优先的 AI 图片工作台:Loomora v1.0.0 正式发布
前端·javascript·vue.js
Flynt19 小时前
我扒了Fantastic-admin 6.0的源码,聊聊"AI时代版本答案"这个称号
vue.js·ai编程·cursor
码艺-Alimjan21 小时前
Vue项目源码备份最佳实践:无视node_modules,打包体积仅2MB
前端·javascript·vue.js