أداء الويب وإمكانية الوصول
معايير إمكانية الوصول والاختبار
إمكانية الوصول (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
- افتح DevTools → تبويب Elements
- انقر على لوحة "Accessibility" في الشريط الجانبي
- تفقد أي عنصر لرؤية:
- الدور: نوع العنصر الذي يراه قارئ الشاشة
- الاسم: ما يُعلنه قارئ الشاشة (من label أو aria-label أو alt)
- الحالة: مُحدد، موسّع، مُختار، إلخ.
- فعّل "Full accessibility tree" في إعدادات DevTools لاستبدال عرض شجرة DOM
نصيحة للمقابلات: عند مناقشة إمكانية الوصول، قدّمها دائمًا كاهتمام هندسي أساسي، وليس فكرة لاحقة. اذكر أن HTML الدلالي يحل 80% من مشاكل إمكانية الوصول، ARIA يتعامل مع 20% المتبقية، والاختبار الآلي يكتشف حوالي 30% من المشاكل — الاختبار اليدوي بقارئ شاشة أمر ضروري.
تهانينا على إكمال الوحدة 5! خُض الاختبار لاختبار معرفتك بالأداء وإمكانية الوصول. :::