Interaktion mit Auswahlmenü

Das Problem

Bei obligatorischen Drop-down-Menüs (z.B. „Land auswählen“) wird das Feld bei der Standardvalidierung sofort als ungültig gekennzeichnet, wenn die Standardoption einen leeren Wert hat. Das kann zu visuellem Rauschen führen. Der Fehler soll nur angezeigt werden, wenn der Nutzer das Menü öffnet und schließt, ohne eine Option auszuwählen, oder wenn er versucht, das Formular zu senden.

Die Lösung

Die Pseudoklasse :user-invalid funktioniert nahtlos mit <select>-Elementen. Der Interaktionsfluss des Nutzers wird berücksichtigt: Das einfache Laden der Seite oder das Fokussieren/Unfokussieren ohne Änderung zählt nicht als Interaktion. Das Feld bleibt also neutral, bis der Nutzer aktiv versucht, eine Auswahl zu treffen.

Implementierungsstrategie

  1. HTML-Einschränkung: Verwenden Sie ein <select> mit required. Die erste Option sollte value="" enthalten und idealerweise deaktiviert/ausgeblendet sein, um eine gültige Auswahl zu erzwingen.
  2. Visuelles Feedback: Verwenden Sie :user-invalid, um den Rahmen des Auswahlfelds zu gestalten.
  3. Zeitpunkt: Der Browser betrachtet das Feld als „interagiert“, wenn der Nutzer den Wert ändert (auch zurück in den ungültigen Standardzustand), bevor er das Steuerelement verlässt, oder wenn das Formular gesendet wird.

Implementierungsleitfaden

1. HTML-Struktur

Die Option „placeholder“ ist hier entscheidend.

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

Fallback und Browserunterstützung

Fallbacks und Browserunterstützung für :user-valid und :user-invalid

CSS für Fallback

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-Fallback

Verwenden Sie ein wiederverwendbares Dienstprogramm, das den Interaktionsstatus mithilfe eines WeakMap verfolgt. So wird verhindert, dass das DOM mit „schmutzigen“ Klassen oder Datenattributen verunreinigt wird.

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

Weitere Überlegungen

  1. Verhalten auf Mobilgeräten: Auf Mobilgeräten kann die Unkenntlichmachung je nach Betriebssystemauswahl unterschiedlich erfolgen. Es wird empfohlen, die Tests auf echten Geräten durchzuführen.
  2. Barrierefreiheit im Internet: Native :user-invalid-Elemente werden nicht automatisch mit ARIA-Attributen synchronisiert. Fügen Sie den folgenden JavaScript-Code hinzu, um aria-invalid mit dem visuellen Status synchron zu halten:
// 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);
});