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
相关产品推荐
相关产品推荐

