ملاحظات حول الحقول المطلوبة

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

المشكلة

قد يكون وضع علامة على الحقول المطلوبة بحالة خطأ فور تحميل الصفحة أمرًا مربكًا. من الناحية المثالية، يجب أن يظهر الحقل المطلوب على أنّه "غير صالح" فقط إذا حاول المستخدم ملأه وفشل في ذلك.

الحل

وتحلّ الفئة الصورية :user-invalid هذه المشكلة بشكل مثالي. بالنسبة إلى الحقل المطلوب، لن يتطابق عند تحميل الصفحة. وستتم المطابقة فقط في الحالات التالية:

  1. يتفاعل المستخدم مع الحقل (مثلاً، يكتب حرفًا ويحذفه) ثم يغادره (يصبح غير مركَّز عليه)، ما يتركه فارغًا.
  2. يحاول المستخدم إرسال النموذج بينما الحقل فارغ.

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

  1. قيود HTML: أضِف السمة required إلى حقول الإدخال.
  2. الملاحظات المرئية: استخدِم :user-invalid لتنسيق الحدود باللون الأحمر وعرض نص المساعدة "مطلوب".
  3. التوقيت: يعتمد على التوقيت الأصلي للمتصفّح لتقديم ملاحظات مرئية. لم تعُد بحاجة إلى معالِجات onBlur لإضافة فئة touched، ولكن لا تزال هناك حاجة إلى بعض رموز JavaScript لمزامنة سمات ARIA (انظر أدناه).

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

1. بنية 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- مزامنة حالة JavaScript

MANDATORY: Because :user-invalid is a visual state, you MUST provide a JavaScript bridge to sync aria-invalid="true" dynamically for assistive technologies when a user blurs an invalid field or attempts submission.

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

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

الخيارات الاحتياطية وتوافق المتصفّح مع :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. علامات النجمة: من أفضل الممارسات الإشارة إلى الحقول المطلوبة بشكل مرئي (مثلاً، باستخدام علامة النجمة *) في التصنيف، لكي يعرف المستخدمون ما يمكن توقّعه قبل التفاعل.
  2. أزرار الإرسال: على عكس أزرار disabled، يجب إبقاء زر الإرسال مفعَّلاً. إذا نقر المستخدم عليه، سيشغّل المتصفّح تلقائيًا :user-invalid في جميع الحقول المطلوبة الفارغة ويركّز على الحقل الأول. هذا ممتاز لأدوات تسهيل الاستخدام وتجربة المستخدم.
  3. تسهيل الاستخدام: لا تتم مزامنة سمات :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);
});