Sprawdzanie poprawności danych wejściowych po interakcji

Funkcje internetowe: :user-valid i :user-invalid

Problem

Wyświetlanie błędów weryfikacji w momencie, gdy użytkownik skupia się na polu i zaczyna pisać, jest przedwczesne i rozprasza uwagę. Na przykład gdy użytkownik wpisuje adres e-mail (np. „uzytkownik@gm”) lub hasło o złożonych wymaganiach, pole jest technicznie nieprawidłowe, dopóki nie zostanie wypełnione. Standardowy styl :invalid powoduje natychmiastowe wyświetlenie stanu błędu, co może być frustrujące dla użytkownika.

Rozwiązanie

Pseudoklasa :user-invalid pozwala odroczyć stan błędu do momentu, gdy użytkownik „zatwierdzi” wartość (przez usunięcie zaznaczenia pola) lub spróbuje przesłać formularz. Dzięki temu opinie dotyczące weryfikacji są przekazywane dopiero po zakończeniu interakcji użytkownika z polem.

Strategia wdrażania

  1. Ograniczenie HTML: używaj standardowych atrybutów HTML5, takich jak type="email", pattern i required, aby aktywować wbudowaną logikę sprawdzania poprawności w przeglądarce.
  2. Informacje wizualne: używaj :user-invalid, aby zastosować styl błędu dopiero po interakcji.
  3. Pozytywne wzmocnienie: możesz użyć :user-valid, aby wyświetlić zielony wskaźnik „sukcesu” po spełnieniu wymagań.
  4. Łagodne przywracanie: gdy tylko użytkownik poprawi dane wejściowe na prawidłowy format, :user-invalid przestaje dopasowywać, natychmiast usuwając stan błędu.

Przewodnik po implementacji

Przypadek użycia 1. Sprawdzanie poprawności adresu e-mail

OBOWIĄZKOWO: w polach adresu e-mail używaj standardowych atrybutów HTML5. Komunikat o błędzie jest domyślnie ukryty i wyświetla się tylko wtedy, gdy przeglądarka stwierdzi, że użytkownik opuścił pole, które jest w nieprawidłowym stanie.

<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;
}

Przypadek użycia 2. Złożoność hasła

OBOWIĄZKOWE: zdefiniuj regułę złożoności, używając wzorca Regex Lookahead w atrybucie pattern. Lista reguł jest wyświetlana nad polem wprowadzania, aby pomóc użytkownikowi, i jest wyróżniona, jeśli wystąpi błąd.

<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;
}

Kreacje zastępcze i obsługa przeglądarek

Zastępcze i obsługiwane przez przeglądarkę dla :user-valid i :user-invalid

CSS dla kreacji zastępczej

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;
}

Zastępczy tag JavaScript

Użyj narzędzia wielokrotnego użytku, które śledzi stan interakcji za pomocą WeakMap. Dzięki temu unikniesz zaśmiecania modelu DOM „brudnymi” klasami lub atrybutami danych.

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);

Inne rzeczy, które warto wziąć pod uwagę

  1. Ułatwienia dostępu:
    • OBOWIĄZKOWO: użyj aria-describedby, aby połączyć listę reguł z danymi wejściowymi.
    • NIE: ukrywaj całkowicie list reguł, dopóki dane wejściowe nie będą prawidłowe. Użytkownicy muszą wiedzieć, co mają wpisać.
  2. Limity atrybutu Pattern: OBOWIĄZKOWE: atrybut pattern wykonuje pełne dopasowanie (domyślne ^...$). Upewnij się, że wyrażenie regularne hasła uwzględnia cały ciąg znaków.
  3. Rygorystyczność sprawdzania poprawności: pamiętaj, że domyślne sprawdzanie poprawności type="email" w przeglądarce jest dość liberalne (np. user@localserver może zostać zaakceptowane). Jeśli potrzebujesz bardziej rygorystycznej walidacji, możesz użyć bardziej rozbudowanej biblioteki walidacji lub walidacji niestandardowej wraz z type="email".
  4. Zarządzanie fokusem: OBOWIĄZKOWE: jeśli użytkownik prześle formularz z nieprawidłowym polem, przeglądarka automatycznie ustawi fokus na pierwszym nieprawidłowym polu. Twoje style :user-invalid zostaną zastosowane natychmiast, ponieważ próba przesłania jest traktowana jako interakcja.
  5. Spójne działanie ARIA: natywny element :user-invalid nie synchronizuje się automatycznie z atrybutami ARIA. Dodaj ten kod JavaScript, aby zachować synchronizację aria-invalid ze stanem wizualnym:
// 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);
});