You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue表单验证(Vuetify v-form)屏幕阅读器播报顺序如何控制?

Vue + Vuetify 表单无障碍播报顺序问题解决方案

问题描述

使用Vue结合Vuetify开发带无障碍特性的表单时,提交无效表单存在以下问题:

  • 需求是先向屏幕阅读器播报所有验证错误列表,再将焦点移动到第一个无效字段
  • 当前实现中屏幕阅读器会先播报聚焦字段的错误信息+aria标签,再播报全局错误列表,导致同一错误重复播报,体验冗余混乱
  • 代码逻辑上是先执行全局错误播报,再聚焦字段,但实际执行顺序不符合预期

问题代码如下:

脚本部分

const submit = async (event) => {
  event.preventDefault();
  const { valid: isValid } = await myForm.value.validate();

  if (!isValid) {
    // Get all form items with errors
    const formItems = myForm.value.items;
    
    // Collect all error messages
    const errors = formItems.reduce((acc, item) => {
      if (item.errorMessages?.length > 0) {
        acc.push(...item.errorMessages);
      }
      return acc;
    }, []);
    
    // Announce all errors
    if (errors.length > 0) {
      announceToScreenReader(`Please correct the following errors: ${errors.join('. ')}`);
    }
    
    // Find and focus the first invalid field
    const firstInvalidField = formItems.find(item => 
      item.errorMessages?.length > 0
    );
    
    if (firstInvalidField) {
      const inputElement = document.getElementById(firstInvalidField.id);
      if (inputElement) {
        inputElement.focus();
        inputElement.scrollIntoView({ behavior: 'smooth' });
      }
    }
  }
};

const announceToScreenReader = (message) => {
  const announcer = document.getElementById('sr-announcer');
  if (!announcer) {
    const newAnnouncer = document.createElement('div');
    newAnnouncer.id = 'sr-announcer';
    newAnnouncer.setAttribute('aria-live', 'polite');
    newAnnouncer.setAttribute('aria-atomic', 'true');
    newAnnouncer.classList.add('sr-only');
    document.body.appendChild(newAnnouncer);
    setTimeout(() => {
      newAnnouncer.textContent = message;
    }, 100);
  } else {
    announcer.textContent = '';
    setTimeout(() => {
      announcer.textContent = message;
    }, 100);
  }
};

模板部分

<v-form ref="myForm" @submit.prevent="submit">
  <v-text-field 
    v-model="firstName"
    :rules="[nameValidation.required, nameValidation.min]"
    :aria-label="'First Name'"
  >
    <template #message="arg">
      <div aria-live="off" id="firstNameError">{{ arg.message }}</div>
    </template>
  </v-text-field>
  
  <!-- Other form fields... -->
  
  <v-btn type="submit">Submit</v-btn>
</v-form>

解决方案

核心是确保全局错误播报完成后再触发字段聚焦,通过Promise控制执行顺序,同时优化字段错误播报逻辑避免重复。

1. 修改播报函数为异步返回Promise

让announceToScreenReader返回Promise,等待屏幕阅读器完成播报后再执行后续逻辑:

const announceToScreenReader = (message) => {
  return new Promise((resolve) => {
    const announcer = document.getElementById('sr-announcer');
    if (!announcer) {
      const newAnnouncer = document.createElement('div');
      newAnnouncer.id = 'sr-announcer';
      newAnnouncer.setAttribute('aria-live', 'polite');
      newAnnouncer.setAttribute('aria-atomic', 'true');
      newAnnouncer.classList.add('sr-only');
      document.body.appendChild(newAnnouncer);
      setTimeout(() => {
        newAnnouncer.textContent = message;
        // 给屏幕阅读器足够的播报时间后完成Promise
        setTimeout(resolve, 300);
      }, 100);
    } else {
      announcer.textContent = '';
      setTimeout(() => {
        announcer.textContent = message;
        setTimeout(resolve, 300);
      }, 100);
    }
  });
};

2. 调整提交函数的执行顺序

等待全局错误播报完成后,再执行聚焦第一个无效字段的操作:

const submit = async (event) => {
  event.preventDefault();
  const { valid: isValid } = await myForm.value.validate();

  if (!isValid) {
    const formItems = myForm.value.items;
    
    const errors = formItems.reduce((acc, item) => {
      if (item.errorMessages?.length > 0) {
        acc.push(...item.errorMessages);
      }
      return acc;
    }, []);
    
    // 初始化空Promise,无错误时直接跳过
    let announcePromise = Promise.resolve();
    if (errors.length > 0) {
      // 等待全局错误播报完成
      announcePromise = announceToScreenReader(`Please correct the following errors: ${errors.join('. ')}`);
    }
    
    // 确保播报完成后再聚焦
    await announcePromise;
    
    const firstInvalidField = formItems.find(item => 
      item.errorMessages?.length > 0
    );
    
    if (firstInvalidField) {
      const inputElement = document.getElementById(firstInvalidField.id);
      if (inputElement) {
        inputElement.focus();
        inputElement.scrollIntoView({ behavior: 'smooth' });
      }
    }
  }
};

3. 可选优化:避免字段错误重复播报

Vuetify字段默认会在聚焦时播报错误,可通过动态控制错误容器的aria-live属性避免重复:

<v-text-field 
  v-model="firstName"
  :rules="[nameValidation.required, nameValidation.min]"
  :aria-label="'First Name'"
  ref="firstNameField"
>
  <template #message="arg">
    <div 
      :aria-live="isFirstFieldFocused ? 'off' : 'polite'" 
      id="firstNameError"
    >
      {{ arg.message }}
    </div>
  </template>
</v-text-field>

添加脚本跟踪聚焦状态:

const isFirstFieldFocused = ref(false);
const firstNameField = ref(null);

onMounted(() => {
  const input = firstNameField.value?.$el.querySelector('input');
  if (input) {
    input.addEventListener('focus', () => isFirstFieldFocused.value = true);
    input.addEventListener('blur', () => isFirstFieldFocused.value = false);
  }
});

内容的提问来源于stack exchange,提问作者user20042604

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 01:03:10