Masalah
Validasi HTML5 standar memberikan masukan visual (melalui :invalid atau :user-invalid), tetapi tidak otomatis disinkronkan dengan atribut aksesibilitas seperti aria-invalid.
Jika Anda menggunakan gaya :invalid standar, pembaca layar mungkin akan mengucapkan "Entri tidak valid" saat pengguna berpindah ke kolom wajib diisi yang saat ini kosong. Hal ini menciptakan pengalaman yang mengganggu bagi pengguna yang menggunakan teknologi pendukung, karena error diumumkan sebelum interaksi terjadi.
Solusi
Kita ingin status terprogram (aria-invalid="true") diterapkan pada saat yang sama persis dengan penerapan status visual (:user-invalid). Karena :user-invalid mengandalkan flag "user-interacted" internal browser, kita dapat menggunakan JavaScript untuk memeriksa apakah pemilih ini cocok selama peristiwa interaksi standar.
Lihat MDN aria-invalid untuk mengetahui detail selengkapnya.
Strategi Penerapan
- Lapisan Visual: Gunakan
:user-invalidCSS untuk menampilkan batas/ikon. - Lapisan Aksesibilitas: Gunakan
aria-invaliddanaria-errormessageuntuk mengomunikasikan status ke Teknologi Pendukung (AT). - Lapisan Aksesibilitas & Visual Bridge: Buat utilitas JavaScript ringan yang memproses peristiwa
blurdaninput, memeriksa apakah elemen cocok dengan:user-invalid, dan memperbarui atribut ARIA sebagaimana mestinya.
Panduan Penerapan
1. Struktur HTML
Tautkan input Anda ke pesan errornya menggunakan aria-errormessage (atau aria-describedby untuk dukungan yang lebih luas).
<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
Kontrol visibilitas pesan error menggunakan pseudo-class native :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
Karena tidak ada peristiwa "UserInvalidChanged", kaitkan ke peristiwa formulir standar untuk memeriksa status.
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);
}
});
Fallback & Dukungan Browser
Deteksi Fitur
Anda dapat memeriksa dukungan di CSS dan JavaScript.
Pemeriksaan JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS untuk Penggantian
Untuk memastikan logika penggantian Anda secara visual tidak dapat dibedakan dari perilaku native, Anda harus menerapkan gaya error ke class pseudo dan class penggantian.
/* 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;
}
Logika Penggantian
Jika :user-invalid tidak ada, lacak status interaksi secara manual menggunakan 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();
Pertimbangan Lainnya
aria-livevs.aria-errormessage:aria-errormessagemenghubungkan input ke teks, tetapi pembaca layar mungkin tidak langsung membacakannya saat muncul (hanya saat memfokuskan input).- Jika Anda memerlukan pengumuman segera saat error muncul (misalnya, saat blur), pertimbangkan untuk menambahkan
role="alert"atauaria-live="polite"ke penampung pesan error, tetapi uji secara menyeluruh untuk menghindari "pengumuman ganda" saat pengguna memfokuskan kolom untuk memperbaikinya.
Internasionalisasi:
- Pastikan konten teks pesan error (
#email-error) diterjemahkan. Logikanya tetap sama.
- Pastikan konten teks pesan error (