اعلان خطای دسترس‌پذیر

ویژگی‌های وب: :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 Visual & Accessibility Layer: یک ابزار کمکی سبک جاوا اسکریپت ایجاد کنید که به رویدادهای blur و input گوش می‌دهد، بررسی می‌کند که آیا عنصر با :user-invalid مطابقت دارد یا نه، و ویژگی‌های ARIA را براساس آن به‌روز می‌کند.

راهنمای پیاده‌سازی

۱. ساختار 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>

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

۳. 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 و جاوا اسکریپت بررسی کنید.

بررسی جاوا اسکریپت:

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) ترجمه شده باشد. منطق همان است.