Vấn đề
Quy trình xác thực HTML5 tiêu chuẩn cung cấp phản hồi trực quan (thông qua :invalid hoặc :user-invalid), nhưng không tự động đồng bộ hoá với các thuộc tính hỗ trợ tiếp cận như aria-invalid.
Nếu bạn sử dụng kiểu :invalid chuẩn, trình đọc màn hình có thể thông báo "Mục nhập không hợp lệ" ngay khi người dùng chuyển sang một trường bắt buộc hiện đang trống. Điều này gây gián đoạn trải nghiệm cho người dùng sử dụng công nghệ hỗ trợ, vì lỗi được thông báo trước khi có hoạt động tương tác.
Giải pháp
Chúng ta muốn trạng thái có lập trình (aria-invalid="true") được áp dụng vào đúng thời điểm trạng thái hình ảnh (:user-invalid) áp dụng. Vì :user-invalid dựa vào cờ "người dùng tương tác" nội bộ của trình duyệt, nên chúng ta có thể sử dụng JavaScript để kiểm tra xem bộ chọn này có khớp trong các sự kiện tương tác chuẩn hay không.
Hãy xem MDN aria-invalid để biết thêm chi tiết.
Chiến lược triển khai
- Lớp hình ảnh: Sử dụng CSS
:user-invalidđể hiển thị đường viền/biểu tượng. - Lớp hỗ trợ tiếp cận: Sử dụng
aria-invalidvàaria-errormessageđể thông báo trạng thái cho Công nghệ hỗ trợ (AT). - Cầu nối giữa lớp hình ảnh và lớp hỗ trợ tiếp cận: Tạo một tiện ích JavaScript gọn nhẹ để theo dõi các sự kiện
blurvàinput, kiểm tra xem phần tử có khớp với:user-invalidhay không và cập nhật các thuộc tính ARIA tương ứng.
Hướng dẫn triển khai
1. Cấu trúc HTML
Liên kết dữ liệu đầu vào với thông báo lỗi bằng aria-errormessage (hoặc aria-describedby để được hỗ trợ rộng hơn).
<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-errormessage="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>
2. CSS
Kiểm soát chế độ hiển thị của thông báo lỗi bằng lớp giả gốc :user-invalid.
.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}
3. JavaScript
Vì không có sự kiện "UserInvalidChanged", hãy kết nối với các sự kiện biểu mẫu chuẩn để kiểm tra trạng thái.
const updateAriaState = (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
// Check if the browser currently considers this input "user-invalid"
const isUserInvalid = input.matches(':user-invalid');
if (isUserInvalid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);
// Also update on input if we've already shown the error,
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
const hasAriaInvalid = input.hasAttribute('aria-invalid');
const ariaInvalid = input.getAttribute('aria-invalid');
if (hasAriaInvalid && ariaInvalid === 'true') {
updateAriaState(event);
}
});
Dự phòng và hỗ trợ trình duyệt
Phát hiện tính năng
Bạn có thể kiểm tra khả năng hỗ trợ trong CSS và JavaScript.
Kiểm tra JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS cho quảng cáo dự phòng
Để đảm bảo logic dự phòng không khác biệt về mặt hình ảnh so với hành vi gốc, bạn phải áp dụng kiểu lỗi cho cả lớp giả và lớp dự phòng.
/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
display: block;
}
Logic dự phòng
Nếu thiếu :user-invalid, hãy theo dõi trạng thái tương tác theo cách thủ công bằng WeakMap.
const UserInvalidFallback = (() => {
const dirtyState = new WeakMap();
const updateState = (input) => {
const isValid = input.checkValidity();
// Update both visual and ARIA state
input.classList.toggle('user-invalid-fallback', !isValid);
input.classList.toggle('user-valid-fallback', isValid);
if (!isValid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
const handleEvent = (event) => {
const input = event.target;
if (event.type === 'reset' && input.matches?.('form')) {
const controls = input.elements || [];
for (const control of controls) {
dirtyState.delete(control);
control.classList.remove('user-invalid-fallback');
control.classList.remove('user-valid-fallback');
control.removeAttribute('aria-invalid');
}
return;
}
if (!input.matches?.('input, textarea, select')) return;
if (event.type === 'input' || event.type === 'change') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasInteracted = true;
dirtyState.set(input, state);
if (state.hasBlurred) {
updateState(input);
}
} else if (event.type === 'blur') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasBlurred = true;
dirtyState.set(input, state);
if (state.hasInteracted) {
updateState(input);
}
}
};
const init = () => {
if (CSS.supports('selector(:user-invalid)')) return;
document.addEventListener('blur', handleEvent, true); // Capture phase required
document.addEventListener('input', handleEvent, true);
document.addEventListener('change', handleEvent, true);
document.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize globally
UserInvalidFallback.init();
Những điều cần xem xét khác
aria-liveđấu vớiaria-errormessage:aria-errormessagekết nối dữ liệu đầu vào với văn bản, nhưng trình đọc màn hình có thể không thông báo ngay khi xuất hiện (chỉ khi tập trung vào dữ liệu đầu vào).- Nếu bạn cần thông báo ngay lập tức khi lỗi xuất hiện (ví dụ: khi làm mờ), hãy cân nhắc thêm
role="alert"hoặcaria-live="polite"vào vùng chứa thông báo lỗi, nhưng hãy kiểm tra kỹ để tránh "thông báo hai lần" khi người dùng tập trung vào trường để khắc phục lỗi.
Quốc tế hoá:
- Đảm bảo nội dung văn bản của thông báo lỗi (
#email-error) được dịch. Logic vẫn giữ nguyên.
- Đảm bảo nội dung văn bản của thông báo lỗi (