إشعار الخطأ الذي يمكن الوصول إليه

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

المشكلة

توفّر عملية التحقّق من صحة HTML5 العادية ملاحظات مرئية (من خلال :invalid أو :user-invalid)، ولكنّها لا تتم مزامنتها تلقائيًا مع سمات تسهيل الاستخدام، مثل aria-invalid.

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

الحل

نريد تطبيق الحالة البرمجية (aria-invalid="true") في اللحظة نفسها التي يتم فيها تطبيق الحالة المرئية (:user-invalid). بما أنّ :user-invalid يعتمد على علامة "تفاعل المستخدم" الداخلية في المتصفّح، يمكننا استخدام JavaScript للتحقّق من تطابق هذا المحدّد أثناء أحداث التفاعل العادية.

لمزيد من التفاصيل، يُرجى الاطّلاع على MDN aria-invalid.

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

  1. الطبقة المرئية: استخدِم CSS :user-invalid لعرض الحدود أو الرموز.
  2. طبقة تسهيل الاستخدام: استخدِم aria-invalid وaria-errormessage لإبلاغ التكنولوجيا المساعِدة (AT) بالحالة.
  3. طبقة تسهيل الاستخدام والعرض المرئي في Bridge: أنشِئ أداة JavaScript خفيفة تستمع إلى أحداث blur وinput، وتتحقّق مما إذا كان العنصر يتطابق مع :user-invalid، وتعدّل سمات ARIA وفقًا لذلك.

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

1. بنية HTML

اربط حقل الإدخال برسالة الخطأ الخاصة به باستخدام aria-errormessage (أو aria-describedby لتوفير دعم أوسع).

<form>
  <div class="field">
    <label for="email">Email</label>
    <input 
      type="email" 
      id="email" 
      required 
      aria-errormessage="email-error"
    >
    <span id="email-error" class="error-msg">
      Please enter a valid email address.
    </span>
  </div>
</form>

2. CSS

يمكنك التحكّم في ظهور رسالة الخطأ باستخدام الفئة الزائفة الأصلية :user-invalid.

.error-msg {
  display: none;
  color: #d93025;
}

/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
  display: block;
}

/* Optional: Visual cues on the input itself */
input:user-invalid {
  border-color: #d93025;
}

3- JavaScript

بما أنّه لا يوجد حدث "UserInvalidChanged"، يمكنك ربط أحداث النماذج العادية للتحقّق من الحالة.

const updateAriaState = (event) => {
  const input = event.target;
  if (!input.matches?.('input, textarea, select')) return;

  // Check if the browser currently considers this input "user-invalid"
  const isUserInvalid = input.matches(':user-invalid');
  
  if (isUserInvalid) {
    input.setAttribute('aria-invalid', 'true');
  } else {
    input.removeAttribute('aria-invalid');
  }
};

// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);

// Also update on input if we've already shown the error, 
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
  const input = event.target;
  if (!input.matches?.('input, textarea, select')) return;

  const hasAriaInvalid = input.hasAttribute('aria-invalid');
  const ariaInvalid = input.getAttribute('aria-invalid');
  if (hasAriaInvalid && ariaInvalid === 'true') {
    updateAriaState(event);
  }
});

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

رصد الميزات

يمكنك التحقّق من توافق CSS وJavaScript.

التحقّق من JavaScript:

if (!CSS.supports('selector(:user-invalid)')) {
  // Fallback logic here
}

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

لضمان عدم تمييز منطق الاحتياط بصريًا عن السلوك الأصلي، يجب تطبيق أنماط الأخطاء على كل من الفئة الزائفة وفئة الاحتياط.

/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
  display: block;
}

المنطق الاحتياطي

إذا لم يكن :user-invalid متوفّرًا، يمكنك تتبُّع حالة التفاعل يدويًا باستخدام WeakMap.

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' && input.matches?.('form')) {
      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.matches?.('input, textarea, select')) 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 = () => {
    if (CSS.supports('selector(:user-invalid)')) return;

    document.addEventListener('blur', handleEvent, true); // Capture phase required
    document.addEventListener('input', handleEvent, true);
    document.addEventListener('change', handleEvent, true);
    document.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize globally
UserInvalidFallback.init();

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

  1. aria-live مقابل aria-errormessage:

    • يربط السمة aria-errormessage الإدخال بالنص، ولكن قد لا تقرأه برامج قراءة الشاشة بصوت عالٍ فور ظهوره (فقط عند التركيز على الإدخال).
    • إذا كنت بحاجة إلى إعلان فوري عند ظهور الخطأ (على سبيل المثال، عند التمويه)، ننصحك بإضافة role="alert" أو aria-live="polite" إلى حاوية رسالة الخطأ، ولكن عليك إجراء اختبار شامل لتجنُّب "الإعلان المزدوج" عندما يركّز المستخدم على الحقل لإصلاحه.
  2. التوسع الدولي:

    • تأكَّد من ترجمة المحتوى النصي لرسالة الخطأ (#email-error). يبقى المنطق كما هو.