EduFlow

الوحدة 3 · لوحة المفاتيح والتركيز وARIA

ARIA: الأدوار والحالات والـ Live Regions

18 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

طبّق ARIA فقط حيث يقصر HTML، وأبقِ حالات مثل aria-pressed وaria-expanded متزامنة، وأعلن التحديثات الديناميكية عبر live regions من نوع status وalert.

  1. 1القاعدة الأولى في ARIA
  2. 2الأدوار والخصائص والحالات
  3. 3الـ Live Regions: إعلان التغييرات

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

كل ما سبق استخدم HTML عاديًا، وهذا مقصود. عناصر HTML تأتي ومعها الأدوار والأسماء والحالات وسلوك لوحة المفاتيح جاهزة. لكن أحيانًا ينفد ما في HTML. لا يوجد عنصر أصلي لحالة الضغط في زر التبديل، ولا لمجموعة تبويبات، ولا لـ «قل هذا بصوت عالٍ حين تتغيّر السلة». هنا يأتي دور ARIA ‏(Accessible Rich Internet Applications): مجموعة من الخصائص تتيح لك وصف الأدوار والحالات والعلاقات مباشرةً لشجرة إمكانية الوصول.

القاعدة الأولى في ARIA

تبدأ إرشادات W3C نفسها بقاعدة تفاجئ الناس: إن استطعت استخدام عنصر HTML أصلي فيه السلوك الذي تحتاجه، فاستخدمه بدلًا من ARIA. وتظلّ فحوص WebAIM السنوية لأشهر مليون صفحة رئيسية تجد أن الصفحات التي تستخدم ARIA فيها أخطاء مرصودة أكثر، لا أقل. والسبب بسيط. ARIA تغيّر فقط ما يُعلَن. لا تجعل أي شيء قابلًا للتركيز، ولا تضيف معالجة للوحة المفاتيح، ولا تغيّر المظهر. كل خاصية ARIA وعدٌ للتقنيات المساعدة، وعلى كود JavaScript الذي تكتبه أن يفي به.

HTML
<!-- A promise with nothing behind it -->
<span role="button">Add to basket</span>

<!-- The same promise, kept by the browser -->
<button type="button">Add to basket</button>

الأدوار والخصائص والحالات

تنقسم خصائص ARIA (attributes) إلى ثلاث مجموعات.

الأدوار (roles) تقول ما هو الشيء: role="tab"، role="dialog"، role="switch"، role="status". ونادرًا ما تحتاج إليها لأشياء يوفّرها HTML أصلًا.

الخصائص بالمعنى الدقيق (properties) تصف العلاقات أو السمات الثابتة: aria-labelledby وaria-describedby (استخدمتهما كلتيهما في النماذج)، وaria-controls، وaria-required، وaria-haspopup.

الحالات (states) تصف أشياء تتغيّر مع تفاعل المستخدم، ويجب أن يحدّثها السكربت: aria-expanded، aria-pressed، aria-checked، aria-selected، aria-current، aria-disabled، aria-invalid.

قلب قائمة الأمنيات في Ticketly مثال جيد. إنه زر حقيقي أصلًا، لكن لا شيء يخبر قارئ الشاشة إن كانت الفعالية محفوظة أم لا. تحوّله aria-pressed إلى زر تبديل (toggle button):

HTML
<button type="button" class="fav" aria-pressed="false">Save to wishlist</button>
ticketly/wishlist.js
fav.addEventListener('click', () => {
  const saved = fav.getAttribute('aria-pressed') === 'true';
  fav.setAttribute('aria-pressed', String(!saved));
});

صار NVDA يقول “Save to wishlist, toggle button, not pressed”، ثم “pressed” بعد التفعيل. لاحظ أن التسمية تبقى كما هي؛ والحالة هي التي تحمل التغيير. ويمكنك التنسيق انطلاقًا من الحالة مباشرةً، وهذا يمنع المظهر والدلالة من الافتراق:

CSS
.fav[aria-pressed='true'] {
  background: #5b3cc4;
  color: #fff;
}

وللصفحة الحالية في قائمة تنقّل، تخبر aria-current="page" مستخدمي قارئ الشاشة بالرابط الذي هم عليه: “Tickets, current page, link”.

وبضع قواعد أخرى من مذكّرة “Using ARIA” الصادرة عن W3C تُبعدك عن المتاعب:

  • لا تغيّر الدلالات الأصلية إلا إذا اضطررت فعلًا: <h2 role="button"> يدمّر العنوان. ضع زرًّا داخل العنوان بدلًا من ذلك.
  • كل عنصر تحكّم تفاعلي مبني بـ ARIA يجب أن يكون قابلًا للتشغيل بلوحة المفاتيح.
  • كل عنصر تفاعلي يحتاج إلى اسم متاح.

الـ Live Regions: إعلان التغييرات

حين يضيف Ticketly تذكرة، يتغيّر عدّاد السلة في الترويسة من 1 إلى 2. يلاحظ المبصرون ذلك. أما مستخدمو قارئ الشاشة فلا يسمعون شيئًا، لأن تركيزهم ما زال على الزر الذي ضغطوه. الـ live region عنصر تُقرأ تغييرات محتواه تلقائيًا، دون نقل التركيز.

ticketly/index.html
<p id="basket-status" role="status"></p>
JavaScript
function addToBasket() {
  count += 1;
  basketStatus.textContent = `Ticket added. ${count} tickets in your basket.`;
}
عنصر status فارغ موجود منذ تحميل الصفحة. حين يغيّر السكربت نصه، يطلق المتصفّح حدث تغيير عبر واجهة إمكانية الوصول، فينطق قارئ الشاشة النص الجديد بعد الكلام الحالي، بينما يبقى التركيز على الزر. role="status" فارغ، عند التحميل textContent يرى المتصفّح تغييرًا "Ticket added. 2 tickets…" التركيز لا يتحرّك أبدًا؛ تُنطق الرسالة حين يهدأ الكلام
كيف يحدث إعلان الـ live region

ثلاث قواعد تجعل الـ live regions موثوقة:

  1. يجب أن تكون المنطقة موجودة قبل التغيير. تراقب قارئات الشاشة الـ live regions الموجودة بحثًا عن تغييرات. وإدراج عنصر جديد يحتوي نصًا أصلًا كثيرًا ما يفوتها. اعرض المنطقة فارغةً عند تحميل الصفحة.
  2. اختر الدرجة: مهذّبة أم حازمة. ‏role="status" (مثل aria-live="polite") تنتظر حتى ينتهي قارئ الشاشة من جملته الحالية. وrole="alert" (حازمة، assertive) تقاطعه. احتفظ بالـ alert للرسائل العاجلة والنادرة مثل فشل الدفع؛ فإعلان “Ticket added” بحزم مع كل نقرة مُرهِق.
  3. اجعل الرسائل قصيرة وذات معنى. أعلن النتيجة (“2 tickets in your basket”)، لا الآلية (“basket component updated”).

تصبح حالات ARIA وأدوارها في أقوى صورها في الـ widgets المخصّصة، حيث لا يوجد أي عنصر HTML مقابل. يبني الدرس التالي ثلاثة منها وفق دليل ARIA Authoring Practices.

أهم النقاط

  • القاعدة الأولى في ARIA أن تستخدم عنصر HTML أصليًا حين يوجد؛ فـ ARIA تغيّر ما يُعلَن، ولا تغيّر أبدًا كيف يتصرّف أي شيء.
  • تنقسم ARIA إلى أدوار (ما هو العنصر)، وخصائص (العلاقات والسمات الثابتة)، وحالات (قيم تتغيّر، ويجب أن يُبقيها السكربت متزامنة).
  • لا تضع أبدًا aria-hidden=“true” على عنصر قابل للتركيز، ولا تتجاوز دور عنصر أصلي له الدور الصحيح أصلًا.
  • يجب أن تكون الـ live region موجودة في الـ DOM قبل أن يتغيّر نصها؛ استخدم role=“status” للتحديثات المهذّبة، وrole=“alert” للعاجلة فقط.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

ما الذي تغيّره إضافة role="button" إلى عنصر <span>؟

السؤال 2 من 4

يعرض Ticketly الرسالة “Ticket added. 2 tickets in your basket.” في هذا العنصر بعد النقر. لماذا تبقى قارئات الشاشة صامتة؟

JavaScript
const msg = document.createElement('p');
msg.setAttribute('role', 'status');
msg.textContent = 'Ticket added. 2 tickets in your basket.';
basket.append(msg);
السؤال 3 من 4

أيّ هذه الأمثلة يستخدم ARIA بشكل صحيح؟

السؤال 4 من 4

يرفض مزوّد الدفع بطاقة في صفحة الدفع في Ticketly. أيّ دور live region يناسب الرسالة “Payment failed. Your card was declined.”؟

تدريب

بيئة تجريب الكود

أضِف ما لا يستطيع HTML أن يمنحك إياه من ARIA

أربع مهام صغيرة بـ ARIA على بطاقة التذكرة في Ticketly:

  1. علِّم رابط Tickets بأنه الصفحة الحالية باستخدام aria-current="page".
  2. اجعل Save to wishlist زر تبديل: يبدأ بـ aria-pressed="false" ويتبدّل مع كل نقرة. أبقِ نص التسمية كما هو.
  3. اجعل #basket-status ‏live region مهذّبة لتُعلَن رسالة “Ticket added…”.
  4. على زر Add to basket في الشريط الثابت aria-hidden="true"، وهذا يترك محطة Tab صامتة. أصلحه دون حذف الزر (يمكنك تغيير نصه).
<nav aria-label="Site">
  <a href="#home">Home</a>
  <a href="#tickets" class="active">Tickets</a>
  <a href="#faq">FAQ</a>
</nav>

<article class="pass">
  <h2>Friday pass</h2>
  <button type="button" id="add">Add to basket</button>
  <button type="button" class="fav" id="fav">Save to wishlist</button>
</article>

<p id="basket-status"></p>

<div class="sticky-bar">
  <button type="button" id="add-sticky" aria-hidden="true">Add to basket</button>
</div>جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • رابط Tickets وحده عليه aria-current=“page”
  • يبدأ Save to wishlist بـ aria-pressed=“false” ويتبدّل مع كل نقرة
  • ‎#basket-status ‏live region وتُعلن عدد التذاكر في السلة
  • رسالة السلة مهذّبة، لا alert حازمة
  • لا يوجد عنصر قابل للتركيز مخفي بـ aria-hidden