EduFlow

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

أنماط الـ Widgets: الـ Disclosure والتبويبات وزر القائمة

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

الدرس في لمحة

ابنِ widgets من نوع disclosure والتبويبات وزر القائمة وفق دليل ARIA Authoring Practices، بالأدوار والحالات وسلوك لوحة المفاتيح الصحيح، واعرف متى لا تستخدمها.

  1. 1دليل ARIA Authoring Practices
  2. 2الـ Disclosure: الإظهار والإخفاء
  3. 3التبويبات
  4. 4زر القائمة، ومتى لا تستخدمه

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

في قسم قائمة الفنانين في Ticketly ثلاثة تبويبات، Friday وSaturday وSunday، مبنية من أزرار منسّقة تبدّل اللوحات عند النقر. بالفأرة كل شيء على ما يرام. أما بقارئ الشاشة فهي صفّ من ثلاثة أزرار لا علاقة بينها، ولا شيء يقول أيّ يوم معروض، واللوحات تتبدّل بصمت. لا يوجد عنصر <tabs> في HTML، وهذا هو ملعب ARIA.

دليل ARIA Authoring Practices

يصف دليل ARIA Authoring Practices ‏(APG)، الذي تنشره W3C، نحو ثلاثين نمطًا شائعًا للـ widgets: الـ accordion، والـ combobox، ونافذة الحوار، والـ disclosure، والـ listbox، وزر القائمة، والتبويبات، والشجرة وغيرها. ولكل نمط يسرد الأدوار والحالات والخصائص، وتفاعل لوحة المفاتيح الدقيق الذي يتوقّعه المستخدمون. يتعلّم مستخدمو قارئ الشاشة هذه الأعراف عبر آلاف المواقع، فاتّباعها يعني أن الـ widget الذي تبنيه يتصرّف بالطريقة التي يعرفونها أصلًا.

تحذيران. أمثلة APG عروض توضيحية لا components جاهزة للإنتاج، فاختبرها في سياقك. وAPG نفسه يقول بتفضيل HTML الأصلي حيث يوجد. وهذا يستبعد عددًا مفاجئًا من الـ widgets المخصّصة.

الـ Disclosure: الإظهار والإخفاء

أبسط نمط هو زر يُظهر منطقة ويُخفيها، مثل إجابات الأسئلة الشائعة في Ticketly. إن سمح التصميم بذلك، فعنصرا <details> و<summary> الأصليان يمنحانك الأمر كله دون أي سكربت:

HTML
<details>
  <summary>How do refunds work?</summary>
  <p>You can get a full refund up to 14 days before the festival.</p>
</details>

وحين تحتاج إلى نسخة مخصّصة، فالحالة الأساسية هي aria-expanded على الزر:

HTML
<h3>
  <button type="button" aria-expanded="false" aria-controls="refunds">How do refunds work?</button>
</h3>
<div id="refunds" hidden>
  <p>You can get a full refund up to 14 days before the festival.</p>
</div>
JavaScript
button.addEventListener('click', () => {
  const open = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', String(!open));
  panel.hidden = open;
});

يُعلن قارئ الشاشة “How do refunds work?, button, collapsed”، ثم “expanded”. ولفّ الزر بعنوان يُبقي أسئلة الصفحة ضمن التنقّل بين العناوين.

التبويبات

لنمط التبويبات أجزاء متحرّكة أكثر:

  • حاوية عليها role="tablist" (واسم، مثل aria-label="Festival days").
  • كل تبويب عليه role="tab"، وaria-selected="true" أو "false"، وaria-controls تشير إلى لوحته.
  • كل لوحة عليها role="tabpanel" وaria-labelledby تشير إلى تبويبها.
  • لوحة المفاتيح: السهمان الأيسر والأيمن ينقلان بين التبويبات (مع الالتفاف عند الطرفين)، وHome وEnd يقفزان إلى الأول والأخير، وTab ينقل خارج الـ tablist إلى اللوحة.

النقطة الأخيرة تستخدم تقنية اسمها roving tabindex (الـ tabindex المتنقّل). وحده التبويب المحدَّد له tabindex="0"؛ والبقية لها tabindex="-1". فيكون الـ tablist كله محطة Tab واحدة، وينقل معالج مفاتيح الأسهم التركيز والقيمة 0 بين التبويبات.

ثلاثة تبويبات: Friday وعليه tabindex بقيمة سالب واحد، وSaturday المحدَّد وعليه tabindex بقيمة صفر، وSunday وعليه tabindex بقيمة سالب واحد. مفاتيح الأسهم تنقل بين التبويبات؛ ومفتاح Tab ينتقل من التبويب المحدَّد نزولًا إلى لوحة Saturday. Friday tabindex=-1 Saturday tabindex=0 Sunday tabindex=-1 ← → الأسهم Tab tabpanel "Saturday" (الفنانون ومواعيد العروض)
الـ roving tabindex في تبويبات الأيام في Ticketly

وهذه نسخة Ticketly:

ticketly/lineup.html
<div role="tablist" aria-label="Festival days">
  <button type="button" role="tab" id="tab-fri" aria-selected="true" aria-controls="panel-fri">Friday</button>
  <button type="button" role="tab" id="tab-sat" aria-selected="false" aria-controls="panel-sat" tabindex="-1">Saturday</button>
  <button type="button" role="tab" id="tab-sun" aria-selected="false" aria-controls="panel-sun" tabindex="-1">Sunday</button>
</div>
<div role="tabpanel" id="panel-fri" aria-labelledby="tab-fri" tabindex="0">…</div>
<div role="tabpanel" id="panel-sat" aria-labelledby="tab-sat" tabindex="0" hidden>…</div>
<div role="tabpanel" id="panel-sun" aria-labelledby="tab-sun" tabindex="0" hidden>…</div>
ticketly/tabs.js
const tabs = [...document.querySelectorAll('[role="tab"]')];

function select(tab) {
  for (const t of tabs) {
    const on = t === tab;
    t.setAttribute('aria-selected', String(on));
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  }
  tab.focus();
}

tabs.forEach((tab) => tab.addEventListener('click', () => select(tab)));

document.querySelector('[role="tablist"]').addEventListener('keydown', (event) => {
  const i = tabs.indexOf(event.target);
  const moves = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };
  if (!(event.key in moves) || i === -1) return;
  event.preventDefault();
  select(tabs[(moves[event.key] + tabs.length) % tabs.length]);
});

يستخدم هذا الكود التفعيل التلقائي (automatic activation): الانتقال بالأسهم إلى تبويب يُظهر لوحته فورًا، وهو ما يوصي به APG حين تُعرض اللوحات دون تأخير. أما إن كانت اللوحة تحتاج إلى تحميل بيانات، فاستخدم التفعيل اليدوي بدلًا منه: الأسهم تنقل التركيز فقط، وEnter أو Space يحدّد التبويب. تحصل اللوحات على tabindex="0" لأنها لا تحتوي عناصر قابلة للتركيز خاصة بها؛ وهذا يمنح Tab مكانًا يصل إليه.

زر القائمة، ومتى لا تستخدمه

نمط زر القائمة (menu button) في APG زر عليه aria-haspopup="menu" وaria-expanded يفتح عنصرًا عليه role="menu" يضمّ أبناء عليهم role="menuitem". مفاتيح الأسهم تتنقّل بين العناصر، وEscape يُغلق القائمة ويعيد التركيز إلى الزر. وهو مناسب لقوائم الأفعال على طريقة التطبيقات، مثل قائمة “⋯” في Ticketly على حجز ما، وفيها “Download tickets” و"Transfer" و"Cancel booking".

لكنه خاطئ لتنقّل المواقع. يخبر الدور menu قارئات الشاشة بأن تتوقّع سلوك تطبيقات سطح المكتب: فتبدّل أنماط التفاعل وتُعلن العناصر على أنها عناصر قائمة لا روابط. أما لقائمة “Events” المنسدلة في الترويسة المليئة بالروابط، فاستخدم disclosure: زرًّا عليه aria-expanded يكشف قائمة عادية من الروابط. كود أبسط، ويتصرّف كما يتوقّع مستخدمو المواقع.

جعل القسم 3 موقع Ticketly قابلًا للتشغيل. وينتقل القسم 4 إلى ما يراه الناس ويقرؤونه، بدءًا باللون والتباين.

أهم النقاط

  • يوثّق دليل ARIA Authoring Practices ‏(APG) الأدوار والحالات وسلوك لوحة المفاتيح الذي يتوقّعه المستخدمون من كل نمط widget.
  • الـ disclosure زر عليه aria-expanded يُظهر المحتوى ويُخفيه؛ وعنصرا details وsummary الأصليان يمنحانك واحدًا مجانًا.
  • تستخدم التبويبات الأدوار tablist وtab وtabpanel، وaria-selected، وroving tabindex بحيث لا يكون محطة لـ Tab إلا التبويب النشط، ومفاتيح الأسهم للتنقّل بين التبويبات.
  • استخدم role=“menu” فقط لقوائم الأفعال على طريقة التطبيقات؛ أما القوائم المنسدلة لتنقّل الموقع فهي disclosures تضمّ روابط عادية.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

في widget تبويبات مبني بشكل صحيح، ماذا يحدث حين يضغط مستخدم لوحة المفاتيح Tab والتركيز على التبويب المحدَّد؟

السؤال 2 من 4

ما الخطأ في زر الأسئلة الشائعة هذا؟

HTML
<button type="button" aria-controls="refund-answer">How do refunds work?</button>
<div id="refund-answer" hidden>…</div>
السؤال 3 من 4

في ترويسة Ticketly قائمة منسدلة باسم “Events” تضمّ ستة روابط إلى صفحات الفعاليات. أيّ نمط يناسبها أكثر؟

السؤال 4 من 4

لماذا يعطي نمط التبويبات في APG التبويبات غير النشطة tabindex="-1"؟

تدريب

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

ابنِ تبويبات أيام سهلة الوصول

تبويبات قائمة الفنانين في Ticketly لا تعمل إلا بالفأرة ولا تقول شيئًا لقارئات الشاشة. حوّلها إلى نمط التبويبات في APG:

  • role="tablist" على الحاوية (مع اسم لها، مثل aria-label="Festival days")، وrole="tab" على كل زر، وrole="tabpanel" على كل لوحة.
  • لكل تبويب aria-controls تشير إلى لوحته؛ ولكل لوحة aria-labelledby تشير إلى تبويبها.
  • قيمة aria-selected هي "true" على التبويب المحدَّد و"false" على البقية. يبدأ Friday محدَّدًا.
  • roving tabindex: للتبويب المحدَّد tabindex="0"، وللبقية -1.
  • نقر تبويب يحدّده. وArrowRight وArrowLeft يحدّدان التبويب التالي أو السابق وينقلان التركيز إليه، مع الالتفاف عند الطرفين.
<h2>Line-up</h2>
<div class="tabs">
  <button type="button" class="tab active" id="tab-fri" data-panel="panel-fri">Friday</button>
  <button type="button" class="tab" id="tab-sat" data-panel="panel-sat">Saturday</button>
  <button type="button" class="tab" id="tab-sun" data-panel="panel-sun">Sunday</button>
</div>
<div class="panel" id="panel-fri">
  <p>Nour Quartet, 8 pm. The Saltmarsh Band, 10 pm.</p>
</div>
<div class="panel" id="panel-sat" hidden>
  <p>Layla &amp; the Tides, 7 pm. Harbour Brass, 9 pm.</p>
</div>
<div class="panel" id="panel-sun" hidden>
  <p>Family matinee, 3 pm. Closing set by DJ Marwa, 8 pm.</p>
</div>جارٍ تحميل المحرّر…

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

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • tablist واحد يضمّ ثلاثة تبويبات، وتوجد ثلاث tabpanels
  • كل تبويب يتحكّم في لوحته، وكل لوحة مسمّاة بتبويبها
  • يبدأ Friday محدَّدًا ولوحته وحدها مرئية
  • التبويب المحدَّد وحده له tabindex 0؛ والبقية لها -1
  • نقر Saturday يحدّده ويُظهر لوحته وينقل الـ tabindex
  • مفاتيح الأسهم تحدّد التبويب التالي أو السابق وتنقل التركيز إليه، مع الالتفاف عند الطرفين