הבעיה
אימות HTML5 רגיל מספק משוב חזותי (באמצעות :invalid או :user-invalid), אבל הוא לא מסתנכרן באופן אוטומטי עם מאפייני נגישות כמו aria-invalid.
אם משתמשים בסגנון הרגיל של :invalid, יכול להיות שקוראי מסך יכריזו "רשומה לא חוקית" ברגע שמשתמש יעבור באמצעות מקש Tab לשדה חובה שכרגע ריק. זה פוגע בחוויה של משתמשים שנעזרים בטכנולוגיות מסייעות, כי ההודעה על השגיאה מושמעת לפני שהם מספיקים לבצע פעולה.
הפתרון
אנחנו רוצים שהמצב הפרוגרמטי (aria-invalid="true") יופעל בדיוק באותו רגע שבו המצב החזותי (:user-invalid) מופעל. מכיוון שהתכונה :user-invalid מסתמכת על הדגל הפנימי "user-interacted" של הדפדפן, אנחנו יכולים להשתמש ב-JavaScript כדי לבדוק אם הסלקטור הזה תואם במהלך אירועי אינטראקציה רגילים.
פרטים נוספים זמינים במאמר MDN aria-invalid.
אסטרטגיית הטמעה
- שכבה חזותית: משתמשים ב-CSS
:user-invalidכדי להציג גבולות או סמלים. - שכבת נגישות: משתמשים ב-
aria-invalidוב-aria-errormessageכדי להעביר את המצב לטכנולוגיה מסייעת (AT). - שכבת גישור חזותית ונגישה: יוצרים כלי קל משקל ב-JavaScript שמאזין לאירועים של
blurו-input, בודק אם הרכיב תואם ל-:user-invalidומעדכן את מאפייני ה-ARIA בהתאם.
מדריך הטמעה
1. מבנה HTML
מקשרים את הקלט להודעת השגיאה שלו באמצעות aria-errormessage (או aria-describedby לתמיכה רחבה יותר).
<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
אפשר לשלוט בהצגה של הודעת השגיאה באמצעות המחלקה המדומה (pseudo-class) המקורית :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
אין אירוע "UserInvalidChanged", לכן צריך להשתמש באירועים רגילים של טפסים כדי לבדוק את המצב.
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);
}
});
תמיכה בדפדפנים וחלופות
זיהוי תכונות
אפשר לבדוק אם יש תמיכה ב-CSS וב-JavaScript.
בדיקת JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS חלופי
כדי לוודא שהלוגיקה של החלופה לא נבדלת מבחינה חזותית מההתנהגות המקורית, צריך להחיל את סגנונות השגיאה גם על המחלקה המדומה וגם על מחלקת החלופה.
/* 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;
}
לוגיקת הגיבוי
אם :user-invalid חסר, צריך לעקוב באופן ידני אחרי מצב האינטראקציה באמצעות 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();
שיקולים נוספים
aria-liveלעומתaria-errormessage:aria-errormessageמקשר בין הקלט לטקסט, אבל יכול להיות שקוראי מסך לא יקריאו אותו מיד כשהוא מופיע (רק כשהקלט מקבל את המיקוד).- אם אתם צריכים הודעה מיידית כשמופיעה שגיאה (למשל, כשהמיקוד יוצא מהשדה), כדאי להוסיף
role="alert"אוaria-live="polite"למאגר של הודעת השגיאה, אבל לבדוק את זה ביסודיות כדי למנוע "הודעה כפולה" כשהמשתמש מתמקד בשדה כדי לתקן אותו.
התאמה לשפות ותרבויות שונות:
- חשוב לוודא שתוכן הטקסט של הודעת השגיאה (
#email-error) מתורגם. הלוגיקה נשארת זהה.
- חשוב לוודא שתוכן הטקסט של הודעת השגיאה (