Interaksi Pilih Menu

Masalah

Untuk dropdown wajib (misalnya, "Pilih Negara"), validasi standar akan langsung menandai kolom sebagai tidak valid jika opsi default memiliki nilai kosong. Hal ini dapat menimbulkan gangguan visual. Kita ingin menampilkan error hanya jika pengguna membuka menu dan menutupnya tanpa memilih opsi, atau mencoba mengirimkan formulir.

Solusi

Class pseudo :user-invalid berfungsi lancar dengan elemen <select>. Hal ini menghormati alur interaksi pengguna: hanya memuat halaman atau memfokuskan/mengaburkan tanpa melakukan perubahan tidak dihitung sebagai interaksi, sehingga kolom tetap netral hingga pengguna secara aktif mencoba melakukan pemilihan.

Strategi Penerapan

  1. Batasan HTML: Gunakan <select> dengan required. Opsi pertama harus memiliki value="" dan idealnya dinonaktifkan/disembunyikan untuk memaksa pilihan yang valid.
  2. Masukan Visual: Gunakan :user-invalid untuk menata gaya batas kotak pilihan.
  3. Waktu: Browser menganggap kolom "berinteraksi" jika pengguna mengubah nilai (bahkan kembali ke status tidak valid default) sebelum mereka memburamkan kontrol, atau setelah pengiriman formulir.

Panduan Penerapan

1. Struktur HTML

Opsi "placeholder" adalah kunci di sini.

<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 & Dukungan Browser

Penggantian & dukungan browser untuk :user-valid dan :user-invalid

CSS untuk Penggantian

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

Penggantian JavaScript

Gunakan utilitas yang dapat digunakan kembali yang melacak status interaksi menggunakan WeakMap. Hal ini menghindari pengotoran DOM dengan class atau atribut data yang "kotor".

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

Pertimbangan Lainnya

  1. Perilaku seluler: Di perangkat seluler, "blur" mungkin terjadi secara berbeda bergantung pada pemilih OS. Sebaiknya lakukan pengujian di perangkat sungguhan.
  2. Aksesibilitas: :user-invalid native tidak otomatis disinkronkan dengan atribut ARIA. Tambahkan JavaScript berikut agar aria-invalid tetap sinkron dengan status visual:
// 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);
});