אינטראקציה עם תפריט הבחירה

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

הבעיה

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

הפתרון

הפסאודו-מחלקה :user-invalid פועלת בצורה חלקה עם רכיבי <select>. הוא מכבד את תהליך האינטראקציה של המשתמש: טעינה פשוטה של הדף או התמקדות/טשטוש בלי לבצע שינוי לא נחשבים כאינטראקציה, כך שהשדה נשאר ניטרלי עד שהמשתמש מנסה לבצע בחירה באופן פעיל.

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

  1. אילוץ HTML: צריך להשתמש ב-<select> עם required. האפשרות הראשונה צריכה להיות value="", ומומלץ להשבית או להסתיר אותה כדי לאלץ בחירה תקינה.
  2. משוב חזותי: משתמשים ב-:user-invalid כדי לעצב את הגבול של תיבת הבחירה.
  3. תזמון: הדפדפן מחשיב את השדה כ "הייתה אינטראקציה" אם המשתמש משנה את הערך (אפילו חזרה למצב הלא תקין שמוגדר כברירת מחדל) לפני שהוא מסיר את המיקוד מהפקד, או אחרי שליחת הטופס.

מדריך הטמעה

1. מבנה HTML

האפשרות "placeholder" היא המפתח כאן.

<form>
  <div class="field">
    <label for="country">Country</label>
    <select
      id="country"
      name="country"
      required
      aria-errormessage="country-error"
    >
      <option value="" disabled selected>Select a country...</option>
      <option value="us">United States</option>
      <option value="ca">Canada</option>
      <option value="uk">United Kingdom</option>
    </select>
    <div id="country-error" class="error-msg">
      Please select a country.
    </div>
  </div>
</form>

2. CSS

.error-msg {
  display: none;
  color: #d93025;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/*
  Only show error after the user visits the select menu.
*/
select:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

select:user-invalid + .error-msg {
  display: block;
}

select:user-valid {
  border-color: #188038;
}

תמיכה בדפדפנים וחלופות

תמיכה בדפדפנים ובמצבי חוסר זמינות של ‎ :user-valid ו-‎ :user-invalid

CSS חלופי

input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
  display: block;
}

גיבוי JavaScript

משתמשים בכלי שניתן לשימוש חוזר שעוקב אחרי מצב האינטראקציה באמצעות WeakMap. כך אפשר להימנע מ"זיהום" של ה-DOM עם מאפייני נתונים או מחלקות "מלוכלכות".

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') {
      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.checkValidity) 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 = (root = document) => {
    if (CSS.supports('selector(:user-invalid)')) return;

    root.addEventListener('blur', handleEvent, true); // Capture phase
    root.addEventListener('input', handleEvent);
    root.addEventListener('change', handleEvent);
    root.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);

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

  1. התנהגות בנייד: במכשירים ניידים, יכול להיות שהטשטוש יתבצע באופן שונה בהתאם לכלי הבחירה של מערכת ההפעלה. מומלץ לבדוק במכשירים אמיתיים.
  2. נגישות: רכיב :user-invalid מקורי לא מסתנכרן באופן אוטומטי עם מאפייני ARIA. כדי לסנכרן את aria-invalid עם המצב החזותי, מוסיפים את קוד ה-JavaScript הבא:
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
  el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};

// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
  if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});