Masukan Kolom Wajib Diisi

Masalah

Menandai kolom wajib diisi dengan status error langsung saat pemuatan halaman dapat membingungkan. Idealnya, kolom wajib diisi hanya akan terlihat "tidak valid" jika pengguna telah mencoba mengisinya dan gagal.

Solusi

Class pseudo :user-invalid menyelesaikan masalah ini dengan sempurna. Untuk kolom wajib diisi, kolom tersebut tidak akan cocok saat pemuatan halaman. Kecocokan hanya akan terjadi jika:

  1. Pengguna berinteraksi dengan kolom (misalnya, mengetik karakter dan menghapusnya), lalu meninggalkannya (blur), sehingga kolom tersebut kosong.
  2. Pengguna mencoba mengirimkan formulir saat kolom kosong.

Strategi Penerapan

  1. Batasan HTML: Tambahkan atribut required ke input Anda.
  2. Respons Visual: Gunakan :user-invalid untuk memberi gaya pada batas merah dan menampilkan teks bantuan "Wajib diisi".
  3. Pengaturan waktu: Mengandalkan pengaturan waktu native browser untuk masukan visual. Anda tidak lagi memerlukan pengendali onBlur untuk menambahkan class touched, meskipun beberapa JavaScript masih diperlukan untuk menyinkronkan atribut ARIA (lihat di bawah).

Panduan Penerapan

1. Struktur 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. Sinkronisasi Status JavaScript

WAJIB: Karena :user-invalid adalah status visual, Anda HARUS menyediakan jembatan JavaScript untuk menyinkronkan aria-invalid="true" secara dinamis untuk teknologi pendukung saat pengguna memburamkan kolom yang tidak valid atau mencoba mengirimkan.

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

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. Tanda bintang: Sebaiknya tunjukkan kolom wajib diisi secara visual (misalnya, dengan tanda bintang *) di label, sehingga pengguna tahu apa yang harus dilakukan sebelum berinteraksi.
  2. Tombol Kirim: Tidak seperti tombol disabled, biarkan tombol kirim tetap aktif. Jika pengguna mengkliknya, browser akan otomatis memicu :user-invalid di semua kolom wajib yang kosong dan memfokuskan kolom pertama. Hal ini sangat baik untuk aksesibilitas dan UX.
  3. 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);
});