التحقّق من صحة البيانات المُدخَلة بعد التفاعل

ميزات الويب: ‎:user-valid و‎ :user-invalid

المشكلة

إنّ عرض أخطاء التحقّق من الصحة في اللحظة التي يركّز فيها المستخدم على حقل ويبدأ الكتابة هو أمر سابق لأوانه ومشتّت للانتباه. على سبيل المثال، عندما يكتب المستخدم عنوان بريد إلكتروني (مثل "user@gm") أو كلمة مرور تتطلّب شروطًا معقّدة، يكون الحقل غير صالح من الناحية الفنية إلى أن يكتمل. يؤدي نمط :invalid العادي إلى ظهور حالة خطأ على الفور، ما يزعج المستخدم.

الحل

تتيح لك الفئة الزائفة :user-invalid تأجيل حالة الخطأ إلى أن يلتزم المستخدم بقيمة (من خلال تمويه الحقل) أو يحاول إرسال النموذج. يضمن ذلك تقديم ملاحظات التحقّق فقط بعد انتهاء المستخدم من التفاعل مع الحقل.

استراتيجية التنفيذ

  1. قيود HTML: استخدِم سمات HTML5 العادية، مثل type="email" وpattern وrequired لتفعيل منطق التحقّق المضمّن في المتصفّح.
  2. الملاحظات المرئية: استخدِم :user-invalid لتطبيق نمط الخطأ بعد التفاعل فقط.
  3. التعزيز الإيجابي: استخدِم :user-valid اختياريًا لإظهار مؤشر "نجاح" باللون الأخضر بعد استيفاء المتطلبات.
  4. التعافي السلس: بمجرد أن يصحح المستخدم الإدخال إلى تنسيق صالح، يتوقف :user-invalid عن المطابقة، مما يزيل حالة الخطأ على الفور.

دليل الاستخدام

حالة الاستخدام 1: التحقّق من صحة عنوان البريد الإلكتروني

إجراء إلزامي: يجب الاعتماد على سمات HTML5 العادية لحقول البريد الإلكتروني. يتم إخفاء رسالة الخطأ تلقائيًا ولا تظهر إلا عندما يحدّد المتصفّح أنّ المستخدم قد غادر الحقل في حالة غير صالحة.

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

حالة الاستخدام 2: تعقيد كلمة المرور

إلزامي: حدِّد قاعدة التعقيد باستخدام نمط Regex Lookahead في السمة pattern. تظهر قائمة القواعد فوق حقل الإدخال لإرشاد المستخدم، ويتم تمييزها في حال حدوث خطأ.

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

العودة إلى الإعدادات التلقائية والتوافق مع المتصفّح

الخيارات الاحتياطية وتوافق المتصفّح مع :user-valid و :user-invalid

CSS للإجراء الاحتياطي

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

استخدِم أداة مساعدة قابلة لإعادة الاستخدام لتتبُّع حالة التفاعل باستخدام WeakMap. يؤدي ذلك إلى تجنُّب تلويث نموذج العناصر في المستند (DOM) بفئات أو سمات بيانات "غير صالحة".

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

اعتبارات أخرى

  1. أدوات تسهيل الاستخدام:
    • MANDATORY: Use aria-describedby to link the rules list to the input.
    • لا تخفِ قوائم القواعد بالكامل إلى أن يصبح الإدخال صالحًا، فالمستخدمون بحاجة إلى معرفة ما يجب كتابته.
  2. حدود سمة النمط: سمة pattern إلزامية وتُجري مطابقة كاملة (ضمنية ^...$). تأكَّد من أنّ التعبير النمطي لكلمة المرور يراعي السلسلة بأكملها.
  3. صرامة التحقّق من الصحة: يُرجى العِلم أنّ عملية التحقّق من صحة type="email" التلقائية في المتصفّح تكون متساهلة إلى حدّ كبير (على سبيل المثال، قد يتم قبول user@localserver). إذا كنت بحاجة إلى عملية تحقّق أكثر صرامة، قد تحتاج إلى استخدام مكتبة تحقّق أكثر فعالية أو دالة تحقّق مخصّصة إلى جانب type="email".
  4. إدارة التركيز: إلزامي: إذا أرسل المستخدم النموذج مع حقل غير صالح، سيركّز المتصفّح تلقائيًا على أول حقل غير صالح. سيتم تطبيق أنماط :user-invalid على الفور لأنّ محاولة الإرسال تُعدّ تفاعلاً.
  5. تجربة ARIA متّسقة: لا تتم مزامنة سمات :user-invalid الأصلية تلقائيًا مع سمات ARIA. أضِف رمز JavaScript التالي لإبقاء aria-invalid متزامنًا مع الحالة المرئية:
// 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);
});