Interactie met menu Selecteren

Het probleem

Voor verplichte dropdownmenu's (bijvoorbeeld Kies een land) markeert de standaardvalidatie het veld onmiddellijk als ongeldig als de standaardoptie een lege waarde heeft. Dit kan visuele ruis veroorzaken. We willen de fout alleen tonen als de gebruiker het menu opent en sluit zonder een optie te kiezen of het formulier probeert in te dienen.

De oplossing

De pseudoklasse :user-invalid werkt naadloos met <select>-elementen. Het respecteert de interactiestroom van de gebruiker: alleen de pagina laden of de focus verplaatsen zonder een wijziging aan te brengen, telt niet als een interactie. Het veld blijft dus neutraal totdat de gebruiker actief een selectie probeert te maken.

Implementatiestrategie

  1. HTML-beperking: Gebruik een <select> met required. De eerste optie moet value="" hebben en idealiter worden uitgezet/verborgen om een geldige keuze af te dwingen.
  2. Visuele feedback: Gebruik :user-invalid om de rand van het selectievak op te maken.
  3. Timing: De browser beschouwt het veld als 'interacted' als de gebruiker de waarde wijzigt (zelfs terug naar de standaard ongeldige status) voordat de gebruiker de focus van het bedieningselement haalt of het formulier indient.

Implementatiehandleiding

1. HTML-structuur

De optie 'placeholder' is hierbij essentieel.

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

Terugval en browserondersteuning

Fallbacks en browserondersteuning voor :user-valid en :user-invalid

CSS voor reserve

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

Gebruik een herbruikbaar hulpprogramma dat de interactiestatus bijhoudt met een WeakMap. Zo voorkomt u dat het DOM wordt vervuild met 'vuile' klassen of gegevenskenmerken.

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

Andere overwegingen

  1. Mobiel gedrag: Op mobiele apparaten kan vervaging anders werken, afhankelijk van de OS-kiezer. We raden je aan te testen op echte apparaten.
  2. Toegankelijkheid: Systeemeigen :user-invalid wordt niet automatisch gesynchroniseerd met ARIA-kenmerken. Voeg de volgende JavaScript toe om aria-invalid gesynchroniseerd te houden met de visuele status:
// 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);
});