أداء الويب وإمكانية الوصول

معايير إمكانية الوصول والاختبار

4 دقيقة للقراءة

إمكانية الوصول (a11y) لم تعد "شيئًا جميلاً" — إنها متطلب قانوني في العديد من الولايات القضائية ومجال تقييم أساسي في مقابلات الواجهات الأمامية. نُشر WCAG 2.2 في 5 أكتوبر 2023، ويجب أن تعرف إضافاته الرئيسية.

WCAG 2.2 AA — ما تحتاج معرفته

يُنظم WCAG في أربعة مبادئ: قابل للإدراك، قابل للتشغيل، مفهوم، متين (POUR). تستهدف معظم الشركات مطابقة المستوى AA كمعيار.

المعايير الجديدة الرئيسية في WCAG 2.2

2.5.8 حجم الهدف (الحد الأدنى) — المستوى AA

يجب أن تكون أهداف التفاعل بحجم 24x24 بكسل CSS على الأقل:

/* ضمان أن جميع العناصر التفاعلية تحقق الحد الأدنى لحجم الهدف */
button, a, input[type="checkbox"], input[type="radio"], select {
  min-width: 24px;
  min-height: 24px;
}

/* أفضل: استخدم أهداف لمس سخية للأجهزة المحمولة */
.btn {
  min-width: 44px;  /* توصية Apple HIG */
  min-height: 44px;
  padding: 12px 16px;
}

/* الروابط المضمنة مُعفاة إذا حققت متطلبات تباعد النص */

2.4.11 عدم إخفاء التركيز (الحد الأدنى) — المستوى AA

عندما يحصل عنصر على تركيز لوحة المفاتيح، يجب ألا يكون مخفيًا بالكامل بواسطة رؤوس لاصقة أو تذييلات أو نوافذ منبثقة أو محتوى متداخل آخر:

/* سيئ: الرأس اللاصق يمكن أن يغطي العناصر المُركزة */
.header {
  position: sticky;
  top: 0;
  z-index: 100;
}

/* جيد: مراعاة الرأس اللاصق عند التمرير للعناصر المُركزة */
:target {
  scroll-margin-top: 80px; /* ارتفاع الرأس اللاصق */
}

/* ضمان تمرير العناصر المُركزة للمنطقة المرئية */
*:focus {
  scroll-margin-top: 80px;
  scroll-margin-bottom: 60px;
}

3.3.7 الإدخال المكرر — المستوى A

لا تُجبر المستخدمين على إعادة إدخال معلومات قدموها بالفعل في نفس الجلسة:

// سيئ: طلب عنوان الشحن بعد أن أدخل المستخدم عنوان الفوترة
function CheckoutForm() {
  return (
    <form>
      <BillingAddressFields />
      {/* يُجبر المستخدم على إعادة كتابة كل شيء */}
      <ShippingAddressFields />
    </form>
  );
}

// جيد: عرض إعادة استخدام البيانات المُدخلة سابقًا
function CheckoutForm() {
  const [sameAsBilling, setSameAsBilling] = useState(true);

  return (
    <form>
      <BillingAddressFields />
      <label>
        <input
          type="checkbox"
          checked={sameAsBilling}
          onChange={(e) => setSameAsBilling(e.target.checked)}
        />
        عنوان الشحن نفس عنوان الفوترة
      </label>
      {!sameAsBilling && <ShippingAddressFields />}
    </form>
  );
}

3.3.8 المصادقة المتاحة (الحد الأدنى) — المستوى AA

يجب ألا تعتمد المصادقة على اختبارات الوظائف المعرفية (مثل تذكر كلمة مرور أو حل لغز) بدون توفير بدائل:

// جيد: توفير طرق مصادقة متعددة
function LoginPage() {
  return (
    <div>
      <h1>تسجيل الدخول</h1>
      {/* السماح لمديري كلمات المرور بالملء التلقائي */}
      <input type="email" autoComplete="email" />
      <input type="password" autoComplete="current-password" />

      {/* بديل: مفتاح المرور/القياسات الحيوية */}
      <button onClick={handlePasskeyLogin}>تسجيل الدخول بمفتاح المرور</button>

      {/* بديل: رابط سحري */}
      <button onClick={handleMagicLink}>أرسل لي رابط تسجيل دخول</button>

      {/* إذا استُخدم CAPTCHA، وفّر بديلاً متاحًا */}
    </div>
  );
}

أدوار ومعرّفات وخصائص ARIA

القاعدة الأولى لـ ARIA: لا تستخدم ARIA إذا كان بإمكانك استخدام HTML الدلالي بدلاً من ذلك.

HTML الدلالي مقابل ARIA

<!-- سيئ: استخدام ARIA لإعادة إنشاء ما يوفره HTML بالفعل -->
<div role="button" tabindex="0" aria-pressed="false"
     onclick="handleClick()" onkeydown="handleKeydown(event)">
  انقر هنا
</div>

<!-- جيد: استخدم العنصر الأصلي — لديه معالجة لوحة مفاتيح مدمجة
     وإدارة تركيز ودعم قارئ شاشة -->
<button onclick="handleClick()">انقر هنا</button>

متى يكون ARIA ضروريًا

<!-- صندوق مركب مخصص — لا يوجد مكافئ HTML أصلي -->
<div role="combobox" aria-expanded="true" aria-haspopup="listbox"
     aria-owns="results-list">
  <input type="text" aria-autocomplete="list"
         aria-controls="results-list" aria-activedescendant="option-3">
</div>
<ul id="results-list" role="listbox">
  <li id="option-1" role="option">النتيجة 1</li>
  <li id="option-2" role="option">النتيجة 2</li>
  <li id="option-3" role="option" aria-selected="true">النتيجة 3</li>
</ul>

<!-- منطقة حية لتحديثات المحتوى الديناميكي -->
<div aria-live="polite" aria-atomic="true">
  تم العثور على 3 نتائج
</div>

<!-- مؤشر تقدم -->
<div role="progressbar" aria-valuenow="65" aria-valuemin="0"
     aria-valuemax="100" aria-label="تقدم الرفع">
  65%
</div>

أنماط التنقل بلوحة المفاتيح

حبس التركيز في النوافذ المنبثقة

عند فتح نافذة منبثقة، يجب أن يبقى التركيز داخلها حتى تُغلق:

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;

    // حفظ العنصر الذي كان مُركزًا قبل فتح النافذة
    previousFocusRef.current = document.activeElement;

    const modal = modalRef.current;
    const focusableElements = modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstFocusable = focusableElements[0];
    const lastFocusable = focusableElements[focusableElements.length - 1];

    // تركيز العنصر الأول في النافذة
    firstFocusable?.focus();

    function handleKeyDown(e) {
      if (e.key === 'Escape') {
        onClose();
        return;
      }
      if (e.key !== 'Tab') return;

      // حبس التركيز داخل النافذة
      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    }

    modal.addEventListener('keydown', handleKeyDown);
    return () => {
      modal.removeEventListener('keydown', handleKeyDown);
      // استعادة التركيز عند إغلاق النافذة
      previousFocusRef.current?.focus();
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div ref={modalRef} role="dialog" aria-modal="true"
           aria-labelledby="modal-title" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
}

إعلانات تغيير المسار

يجب على تطبيقات الصفحة الواحدة إعلان التنقل لقارئات الشاشة:

function RouteAnnouncer() {
  const location = useLocation();
  const [announcement, setAnnouncement] = useState('');

  useEffect(() => {
    // إعلان عنوان الصفحة الجديدة لقارئات الشاشة
    const pageTitle = document.title;
    setAnnouncement(`تم الانتقال إلى ${pageTitle}`);
  }, [location]);

  return (
    <div
      role="status"
      aria-live="assertive"
      aria-atomic="true"
      className="sr-only"
    >
      {announcement}
    </div>
  );
}

روابط التخطي

السماح لمستخدمي لوحة المفاتيح بتخطي التنقل المتكرر:

<!-- أول عنصر قابل للتركيز في الصفحة -->
<a href="#main-content" class="skip-link">تخطي إلى المحتوى الرئيسي</a>

<nav><!-- تنقل طويل --></nav>

<main id="main-content" tabindex="-1">
  <!-- محتوى الصفحة -->
</main>

<style>
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    padding: 8px 16px;
    background: #000;
    color: #fff;
    z-index: 1000;
    transition: top 0.2s;
  }
  .skip-link:focus {
    top: 0; /* مرئي فقط عند التركيز عبر لوحة المفاتيح */
  }
</style>

متطلبات تباين الألوان

نوع النص الحد الأدنى لنسبة التباين (AA)
نص عادي (< 18 نقطة أو < 14 نقطة عريض) 4.5:1
نص كبير (≥ 18 نقطة أو ≥ 14 نقطة عريض) 3:1
مكونات واجهة المستخدم والكائنات الرسومية 3:1
/* جيد: تركيبات عالية التباين */
.text-primary { color: #1a1a1a; } /* على الأبيض: 16.7:1 */
.text-secondary { color: #595959; } /* على الأبيض: 7.0:1 */

/* سيئ: تباين غير كافٍ */
.text-light { color: #aaaaaa; } /* على الأبيض: 2.3:1 — يفشل */

تفضيلات الحركة ونظام الألوان

prefers-reduced-motion

احترم المستخدمين الحساسين للحركة:

/* الافتراضي: الرسوم المتحركة مفعّلة */
.card {
  transition: transform 0.3s ease;
}
.card:hover {
  transform: scale(1.05);
}

/* تقليل أو إزالة الحركة للمستخدمين الذين يفضلون ذلك */
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
  .card:hover {
    transform: none;
  }

  /* استبدال الرسوم المتحركة بتغييرات الشفافية */
  .fade-in {
    animation: none;
    opacity: 1;
  }
}

prefers-color-scheme

دعم تفضيل الوضع الداكن للنظام:

:root {
  --bg-color: #ffffff;
  --text-color: #1a1a1a;
  --border-color: #e0e0e0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: #1a1a1a;
    --text-color: #f0f0f0;
    --border-color: #333333;
  }
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
}

أساسيات اختبار قارئ الشاشة

VoiceOver (macOS)

الإجراء الاختصار
تشغيل/إيقاف Cmd + F5
قراءة العنصر التالي VO + السهم الأيمن (VO = Ctrl + Option)
التفاعل مع العنصر VO + Shift + السهم لأسفل
إيقاف القراءة Ctrl
فتح الدوّار (المعالم والعناوين) VO + U

NVDA (Windows)

الإجراء الاختصار
تشغيل/إيقاف Ctrl + Alt + N
قراءة العنصر التالي السهم لأسفل
قائمة العناوين H (أو Insert + F7)
قائمة المعالم D
إيقاف القراءة Ctrl

شجرة إمكانية الوصول في Chrome DevTools

  1. افتح DevTools → تبويب Elements
  2. انقر على لوحة "Accessibility" في الشريط الجانبي
  3. تفقد أي عنصر لرؤية:
    • الدور: نوع العنصر الذي يراه قارئ الشاشة
    • الاسم: ما يُعلنه قارئ الشاشة (من label أو aria-label أو alt)
    • الحالة: مُحدد، موسّع، مُختار، إلخ.
  4. فعّل "Full accessibility tree" في إعدادات DevTools لاستبدال عرض شجرة DOM

نصيحة للمقابلات: عند مناقشة إمكانية الوصول، قدّمها دائمًا كاهتمام هندسي أساسي، وليس فكرة لاحقة. اذكر أن HTML الدلالي يحل 80% من مشاكل إمكانية الوصول، ARIA يتعامل مع 20% المتبقية، والاختبار الآلي يكتشف حوالي 30% من المشاكل — الاختبار اليدوي بقارئ شاشة أمر ضروري.

تهانينا على إكمال الوحدة 5! خُض الاختبار لاختبار معرفتك بالأداء وإمكانية الوصول. :::

اختبار

الوحدة 5: أداء الويب وإمكانية الوصول

خذ الاختبار