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 小时前
TinyRobot SuggestionPills紧凑的建议按钮组组件
前端·vue.js
如果超人不会飞2 小时前
TinyRobot Container构建优雅的AI对话容器
前端·vue.js
如果超人不会飞2 小时前
TinyRobot SuggestionPopover智能建议弹出框组件
前端·vue.js
zhedream3 小时前
从模糊到清晰:一次组件重构里的开发哲学
vue.js
如果超人不会飞3 小时前
TinyRobot AI 对话组件库全组件使用指南
前端·vue.js
如果超人不会飞4 小时前
Vue.js
vue.js
往事随风灬5 小时前
我被 Volta 的“智能”坑了一下午:pnpm 为何无视项目 Node 版本?
前端·vue.js
如果超人不会飞5 小时前
TinyVue Layout 组件完全指南:别再手写 float 和 flex 了,栅格早该这样用
vue.js