אימות הקלט אחרי האינטראקציה

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

הבעיה

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

הפתרון

הפסאודו-מחלקה :user-invalid מאפשרת לדחות את מצב השגיאה עד שהמשתמש "מתחייב" לערך (על ידי טשטוש השדה) או מנסה לשלוח את הטופס. כך אפשר לוודא שהמשוב על האימות יופיע רק אחרי שהמשתמש יסיים את האינטראקציה עם השדה.

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

  1. אילוץ HTML: צריך להשתמש במאפייני HTML5 סטנדרטיים כמו type="email",‏ pattern ו-required כדי להפעיל את לוגיקת האימות המובנית של הדפדפן.
  2. משוב חזותי: צריך להשתמש ב-:user-invalid כדי להחיל סגנון שגיאה רק אחרי אינטראקציה.
  3. חיזוק חיובי: אפשר להשתמש ב-:user-valid כדי להציג אינדיקטור ירוק של "הצלחה" אחרי שהמשתמש עומד בדרישות.
  4. חזרה למצב תקין: ברגע שהמשתמש מתקן את הקלט לפורמט תקין, :user-invalid מפסיק להתאים, ומצב השגיאה מוסר מיד.

מדריך הטמעה

תרחיש לדוגמה 1: אימות כתובת אימייל

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

<form>
  <div class="field">
    <label for="email">Email Address</label>
    <!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
    <span id="email-hint" class="hint">Format: you@example.com</span>
    <!-- DO: Use standard HTML validation attributes like type="email" and required -->
    <input
      type="email"
      id="email"
      name="email"
      required
      autocomplete="email"
      aria-describedby="email-hint"
      aria-errormessage="email-error"
    >
    <div id="email-error" class="error-msg">
      <span aria-hidden="true">❌</span> Please enter a valid email address.
    </div>
  </div>
</form>
.hint {
  display: block;
  color: #5f6368;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

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

/*
  DO: Only show error styles after user interaction.
  Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
  display: block;
}

/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
  border-color: #188038;
}

תרחיש לדוגמה 2: מורכבות הסיסמה

חובה: צריך להגדיר את כלל המורכבות באמצעות דפוס Regex Lookahead במאפיין pattern. רשימת הכללים מוצגת מעל לשדה הקלט כדי להנחות את המשתמש, והיא מודגשת אם יש שגיאה.

<form>
  <div class="field">
    <label for="password">New Password</label>
    <!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
    <ul id="password-rules" class="rules-list">
      <li>At least 8 characters</li>
      <li>One uppercase letter</li>
      <li>One number</li>
      <li>One special character</li>
    </ul>
    <!-- DO: Use pattern and minlength for complex password validation
         DO: Match all constraints with lookaheads via pattern attribute
     -->
    <input
      type="password"
      id="password"
      autocomplete="new-password"
      required
      pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
      minlength="8"
      aria-describedby="password-rules"
    >
  </div>
</form>
/* DO: State the default styling as neutral */
.rules-list { 
  color: #5f6368; 
  margin-bottom: 0.5rem;
}

/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
  color: #d93025;
  font-weight: 600;
}

/* DO: Add success indications for :user-valid state */
input:user-valid {
  border-color: #188038;
}

/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
  display: none;
}

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

תמיכה בדפדפנים ובמצבי חוסר זמינות של ‎ :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. נגישות:
    • חובה: צריך להשתמש ב-aria-describedby כדי לקשר את רשימת הכללים לקלט.
    • מה לא לעשות: להסתיר את רשימת הכללים עד שהקלט תקין. המשתמשים צריכים לדעת מה להקליד.
  2. מגבלות על מאפייני התבנית: חובה: המאפיין pattern מבצע התאמה מלאה (^...$ משתמע). צריך לוודא שהביטוי הרגולרי של הסיסמה מתייחס למחרוזת כולה.
  3. רמת הקפדנות של האימות: חשוב לזכור שאימות ברירת המחדל של type="email" בדפדפן הוא די מתירני (למשל, user@localserver עשוי לעבור). אם אתם צריכים אימות מחמיר יותר, יכול להיות שתצטרכו להשתמש בספריית אימות חזקה יותר או בפונקציית אימות מותאמת אישית לצד type="email".
  4. ניהול המיקוד: חובה: אם משתמש שולח את הטופס עם שדה לא תקין, הדפדפן יתמקד באופן אוטומטי בשדה הלא תקין הראשון. הסגנונות של :user-invalid יחולו באופן מיידי כי ניסיון שליחה נחשב לאינטראקציה.
  5. חוויית ARIA עקבית: :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);
});