Opinia o wymaganym polu

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

Problem

Oznaczanie wymaganych pól jako błędnych natychmiast po wczytaniu strony może być mylące. Najlepiej, aby pole wymagane wyglądało na „nieprawidłowe” tylko wtedy, gdy użytkownik próbował je wypełnić i nie udało mu się to.

Rozwiązanie

Pseudoklasa :user-invalid idealnie rozwiązuje ten problem. W przypadku pola wymaganego nie będzie ono dopasowywane podczas wczytywania strony. Dopasowanie nastąpi tylko wtedy, gdy:

  1. Użytkownik wchodzi w interakcję z polem (np. wpisuje znak i go usuwa), a następnie opuszcza je (blur), pozostawiając puste.
  2. Użytkownik próbuje przesłać formularz, gdy pole jest puste.

Strategia wdrażania

  1. Ograniczenie HTML: dodaj atrybut required do danych wejściowych.
  2. Wizualne informacje zwrotne: użyj :user-invalid, aby nadać obramowaniu czerwony kolor i wyświetlić tekst pomocniczy „Wymagane”.
  3. Czas: polegaj na natywnym czasie przeglądarki, aby uzyskać informacje wizualne. Nie musisz już używać modułów obsługi onBlur, aby dodać klasę touched, ale nadal potrzebujesz kodu JavaScript do synchronizowania atrybutów ARIA (patrz poniżej).

Przewodnik po implementacji

1. Struktura HTML

<form id="feedback-form">
  <div class="field">
    <label for="full-name">Full Name</label>
    <input
      type="text"
      id="full-name"
      name="full-name"
      required
      aria-errormessage="name-error"
    >
    <!-- MANDATORY: Include an icon or distinct non-color indicator alongside error text -->
    <div id="name-error" class="error-msg">
      <span aria-hidden="true">❌</span> This field is required.
    </div>
  </div>
</form>

2. CSS

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

/*
  Only highlight empty required fields AFTER the user visits them.
  MANDATORY: Provide multiple indicators (border shift + helper text/icon) to avoid color-only state communication.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

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

/* Optional: Subtle indicator for required fields that are valid */
input:required:user-valid {
  border-color: #188038;
  border-width: 2px;
}

3. Synchronizacja stanu JavaScriptu

OBOWIĄZKOWE: ponieważ :user-invalid to stan wizualny, MUSISZ udostępnić most JavaScript, aby dynamicznie synchronizować aria-invalid="true" na potrzeby technologii wspomagających osoby z niepełnosprawnością, gdy użytkownik opuści nieprawidłowe pole lub spróbuje przesłać formularz.

const form = document.getElementById('feedback-form');

const syncAriaInvalid = (input) => {
  if (!input.checkValidity()) {
    input.setAttribute('aria-invalid', 'true');
  } else {
    input.removeAttribute('aria-invalid');
  }
};

// Sync on blur when a user finishes interacting
form.addEventListener('blur', (e) => {
  if (e.target.matches('input[required]')) {
    syncAriaInvalid(e.target);
  }
}, true);

// Sync all required fields when submission is attempted
form.addEventListener('submit', () => {
  form.querySelectorAll('input[required]').forEach(syncAriaInvalid);
});

// Remove error state immediately upon correction
form.addEventListener('input', (e) => {
  if (e.target.matches('input[required]') && e.target.checkValidity()) {
    e.target.removeAttribute('aria-invalid');
  }
});

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. Gwiazdki: nadal sprawdzoną metodą jest wizualne oznaczanie wymaganych pól (np. gwiazdką *) w etykiecie, aby użytkownicy wiedzieli, czego się spodziewać, zanim wejdą w interakcję.
  2. Przyciski przesyłania: w przeciwieństwie do przycisków disabled przycisk przesyłania powinien być włączony. Jeśli użytkownik go kliknie, przeglądarka automatycznie uruchomi :user-invalid we wszystkich pustych wymaganych polach i ustawi kursor w pierwszym z nich. To świetne rozwiązanie pod względem ułatwień dostępu i wrażeń użytkownika.
  3. Ułatwienia dostępu: 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);
});