Memvalidasi Input Setelah Interaksi

Masalah

Menampilkan error validasi saat pengguna berfokus pada kolom dan mulai mengetik terlalu dini dan mengganggu. Misalnya, saat pengguna mengetik alamat email (misalnya, "user@gm") atau sandi dengan persyaratan yang rumit, kolom tersebut secara teknis tidak valid hingga selesai. Gaya :invalid standar menyebabkan status error langsung muncul, sehingga membuat pengguna frustrasi.

Solusi

Dengan class semu :user-invalid, Anda dapat menunda status error hingga pengguna "berkomitmen" pada suatu nilai (dengan memburamkan kolom) atau mencoba mengirimkan formulir. Hal ini memastikan masukan validasi hanya diberikan setelah pengguna selesai berinteraksi dengan kolom.

Strategi Penerapan

  1. Batasan HTML: GUNAKAN atribut HTML5 standar seperti type="email", pattern, dan required untuk memicu logika validasi bawaan browser.
  2. Respons Visual: GUNAKAN :user-invalid untuk menerapkan gaya error hanya setelah interaksi.
  3. Penguatan Positif: JANGAN ragu untuk menggunakan :user-valid guna memberikan indikator "berhasil" berwarna hijau setelah persyaratan terpenuhi.
  4. Pemulihan yang Lancar: Segera setelah pengguna memperbaiki input ke format yang valid, :user-invalid akan berhenti mencocokkan, sehingga langsung menghapus status error.

Panduan Penerapan

Kasus Penggunaan 1: Validasi Email

WAJIB: Mengandalkan atribut HTML5 standar untuk kolom email. Pesan error disembunyikan secara default dan hanya ditampilkan saat browser menentukan bahwa pengguna telah meninggalkan kolom dalam status tidak valid.

<form>
  <div class="field">
    <label for="email">Email Address</label>
    <!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
    <span id="email-hint" class="hint">Format: you@example.com</span>
    <!-- DO: Use standard HTML validation attributes like type="email" and required -->
    <input
      type="email"
      id="email"
      name="email"
      required
      autocomplete="email"
      aria-describedby="email-hint"
      aria-errormessage="email-error"
    >
    <div id="email-error" class="error-msg">
      <span aria-hidden="true">❌</span> Please enter a valid email address.
    </div>
  </div>
</form>
.hint {
  display: block;
  color: #5f6368;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

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

/*
  DO: Only show error styles after user interaction.
  Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
  display: block;
}

/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
  border-color: #188038;
}

Kasus Penggunaan 2: Kerumitan Sandi

WAJIB: Tentukan aturan kompleksitas menggunakan pola Regex Lookahead di atribut pattern. Daftar aturan ditampilkan di atas input untuk memandu pengguna, dan ditandai jika ada error.

<form>
  <div class="field">
    <label for="password">New Password</label>
    <!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
    <ul id="password-rules" class="rules-list">
      <li>At least 8 characters</li>
      <li>One uppercase letter</li>
      <li>One number</li>
      <li>One special character</li>
    </ul>
    <!-- DO: Use pattern and minlength for complex password validation
         DO: Match all constraints with lookaheads via pattern attribute
     -->
    <input
      type="password"
      id="password"
      autocomplete="new-password"
      required
      pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
      minlength="8"
      aria-describedby="password-rules"
    >
  </div>
</form>
/* DO: State the default styling as neutral */
.rules-list { 
  color: #5f6368; 
  margin-bottom: 0.5rem;
}

/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
  color: #d93025;
  font-weight: 600;
}

/* DO: Add success indications for :user-valid state */
input:user-valid {
  border-color: #188038;
}

/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
  display: none;
}

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. Aksesibilitas:
    • WAJIB: Gunakan aria-describedby untuk menautkan daftar aturan ke input.
    • JANGAN: Menyembunyikan daftar aturan sepenuhnya hingga input valid; pengguna perlu tahu apa yang harus diketik.
  2. Batasan Atribut Pola: WAJIB: Atribut pattern melakukan pencocokan penuh (^...$ tersirat). Pastikan ekspresi reguler sandi Anda memperhitungkan seluruh string.
  3. Keketatan Validasi: Perhatikan bahwa validasi type="email" default browser cukup permisif (misalnya, user@localserver mungkin lulus). Jika memerlukan validasi yang lebih ketat, Anda mungkin perlu menggunakan library validasi yang lebih andal atau fungsi validasi kustom bersama dengan type="email".
  4. Pengelolaan Fokus: WAJIB: Jika pengguna mengirimkan formulir dengan kolom yang tidak valid, browser akan otomatis memfokuskan kolom tidak valid yang pertama. Gaya :user-invalid akan langsung diterapkan karena upaya pengiriman dihitung sebagai interaksi.
  5. Pengalaman ARIA yang Konsisten: :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);
});