הודעת שגיאה נגישה

תכונות אינטרנט: ‎:user-valid ו-‎ :user-invalid

הבעיה

אימות HTML5 רגיל מספק משוב חזותי (באמצעות :invalid או :user-invalid), אבל הוא לא מסתנכרן באופן אוטומטי עם מאפייני נגישות כמו aria-invalid.

אם משתמשים בסגנון הרגיל של :invalid, יכול להיות שקוראי מסך יכריזו "רשומה לא חוקית" ברגע שמשתמש יעבור באמצעות מקש Tab לשדה חובה שכרגע ריק. זה פוגע בחוויה של משתמשים שנעזרים בטכנולוגיות מסייעות, כי ההודעה על השגיאה מושמעת לפני שהם מספיקים לבצע פעולה.

הפתרון

אנחנו רוצים שהמצב הפרוגרמטי (aria-invalid="true") יופעל בדיוק באותו רגע שבו המצב החזותי (:user-invalid) מופעל. מכיוון שהתכונה :user-invalid מסתמכת על הדגל הפנימי "user-interacted" של הדפדפן, אנחנו יכולים להשתמש ב-JavaScript כדי לבדוק אם הסלקטור הזה תואם במהלך אירועי אינטראקציה רגילים.

פרטים נוספים זמינים במאמר MDN aria-invalid.

אסטרטגיית הטמעה

  1. שכבה חזותית: משתמשים ב-CSS :user-invalid כדי להציג גבולות או סמלים.
  2. שכבת נגישות: משתמשים ב-aria-invalid וב-aria-errormessage כדי להעביר את המצב לטכנולוגיה מסייעת (AT).
  3. שכבת גישור חזותית ונגישה: יוצרים כלי קל משקל ב-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();

שיקולים נוספים

  1. ‫aria-live לעומת aria-errormessage:

    • aria-errormessage מקשר בין הקלט לטקסט, אבל יכול להיות שקוראי מסך לא יקריאו אותו מיד כשהוא מופיע (רק כשהקלט מקבל את המיקוד).
    • אם אתם צריכים הודעה מיידית כשמופיעה שגיאה (למשל, כשהמיקוד יוצא מהשדה), כדאי להוסיף role="alert" או aria-live="polite" למאגר של הודעת השגיאה, אבל לבדוק את זה ביסודיות כדי למנוע "הודעה כפולה" כשהמשתמש מתמקד בשדה כדי לתקן אותו.
  2. התאמה לשפות ותרבויות שונות:

    • חשוב לוודא שתוכן הטקסט של הודעת השגיאה (#email-error) מתורגם. הלוגיקה נשארת זהה.