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 项目中实现无障碍功能的优雅解决方案。

相关推荐
愈努力俞幸运2 小时前
vue3 demo教程(Vue Devtools)
前端·javascript·vue.js
持续前行2 小时前
在 Vue3 中使用 LogicFlow 更新节点名称
前端·javascript·vue.js
计算机学姐3 小时前
基于SpringBoot的汉服租赁系统【颜色尺码套装+个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·信息可视化·推荐算法
+VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue建筑材料管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
雪碧聊技术3 小时前
ElementPlus徽章组件:展示日期面板每天未完成的待办数量
vue.js·日期选择器·elementplus·el-badge徽章组件
沐墨染4 小时前
敏感词智能检索前端组件设计:树形组织过滤与多维数据分析
前端·javascript·vue.js·ui·数据挖掘·数据分析
xkxnq5 小时前
第二阶段:Vue 组件化开发(第 18天)
前端·javascript·vue.js
WebGISer_白茶乌龙桃5 小时前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
计算机学姐5 小时前
基于SpringBoot的汽车租赁系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·spring·汽车·推荐算法