{"id":"l-a11y-3-4","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"ابنِ widgets من نوع disclosure والتبويبات وزر القائمة وفق دليل ARIA Authoring Practices، بالأدوار والحالات وسلوك لوحة المفاتيح الصحيح، واعرف متى لا تستخدمها.","html":"<p>في قسم قائمة الفنانين في Ticketly ثلاثة تبويبات، Friday وSaturday وSunday، مبنية من أزرار منسّقة تبدّل اللوحات عند النقر. بالفأرة كل شيء على ما يرام. أما بقارئ الشاشة فهي صفّ من ثلاثة أزرار لا علاقة بينها، ولا شيء يقول أيّ يوم معروض، واللوحات تتبدّل بصمت. لا يوجد عنصر <code dir=\"ltr\">&lt;tabs&gt;</code> في HTML، وهذا هو ملعب ARIA.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">دليل ARIA Authoring Practices</h2>\n<p>يصف <strong>دليل ARIA Authoring Practices ‏(APG)</strong>، الذي تنشره W3C، نحو ثلاثين نمطًا شائعًا للـ widgets: الـ accordion، والـ combobox، ونافذة الحوار، والـ disclosure، والـ listbox، وزر القائمة، والتبويبات، والشجرة وغيرها. ولكل نمط يسرد الأدوار والحالات والخصائص، و<strong>تفاعل لوحة المفاتيح</strong> الدقيق الذي يتوقّعه المستخدمون. يتعلّم مستخدمو قارئ الشاشة هذه الأعراف عبر آلاف المواقع، فاتّباعها يعني أن الـ widget الذي تبنيه يتصرّف بالطريقة التي يعرفونها أصلًا.</p>\n<p>تحذيران. أمثلة APG عروض توضيحية لا components جاهزة للإنتاج، فاختبرها في سياقك. وAPG نفسه يقول بتفضيل HTML الأصلي حيث يوجد. وهذا يستبعد عددًا مفاجئًا من الـ widgets المخصّصة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ Disclosure: الإظهار والإخفاء</h2>\n<p>أبسط نمط هو زر يُظهر منطقة ويُخفيها، مثل إجابات الأسئلة الشائعة في Ticketly. إن سمح التصميم بذلك، فعنصرا <code dir=\"ltr\">&lt;details&gt;</code> و<code dir=\"ltr\">&lt;summary&gt;</code> الأصليان يمنحانك الأمر كله دون أي سكربت:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">details</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">summary</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">summary</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>You can get a full refund up to 14 days before the festival.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">details</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>وحين تحتاج إلى نسخة مخصّصة، فالحالة الأساسية هي <code dir=\"ltr\">aria-expanded</code> على الزر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-expanded</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refunds\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refunds\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>You can get a full refund up to 14 days before the festival.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'click'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> open</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-expanded'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'true'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-expanded'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">open));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  panel.hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> open;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يُعلن قارئ الشاشة “How do refunds work?, button, collapsed”، ثم “expanded”. ولفّ الزر بعنوان يُبقي أسئلة الصفحة ضمن التنقّل بين العناوين.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">التبويبات</h2>\n<p>لنمط التبويبات أجزاء متحرّكة أكثر:</p>\n<ul>\n<li>حاوية عليها <code dir=\"ltr\">role=&quot;tablist&quot;</code> (واسم، مثل <code dir=\"ltr\">aria-label=&quot;Festival days&quot;</code>).</li>\n<li>كل تبويب عليه <code dir=\"ltr\">role=&quot;tab&quot;</code>، و<code dir=\"ltr\">aria-selected=&quot;true&quot;</code> أو <code dir=\"ltr\">&quot;false&quot;</code>، و<code dir=\"ltr\">aria-controls</code> تشير إلى لوحته.</li>\n<li>كل لوحة عليها <code dir=\"ltr\">role=&quot;tabpanel&quot;</code> و<code dir=\"ltr\">aria-labelledby</code> تشير إلى تبويبها.</li>\n<li><strong>لوحة المفاتيح:</strong> السهمان الأيسر والأيمن ينقلان بين التبويبات (مع الالتفاف عند الطرفين)، وHome وEnd يقفزان إلى الأول والأخير، وTab ينقل خارج الـ tablist إلى اللوحة.</li>\n</ul>\n<p>النقطة الأخيرة تستخدم تقنية اسمها <strong>roving tabindex</strong> (الـ tabindex المتنقّل). وحده التبويب المحدَّد له <code dir=\"ltr\">tabindex=&quot;0&quot;</code>؛ والبقية لها <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>. فيكون الـ tablist كله محطة Tab واحدة، وينقل معالج مفاتيح الأسهم التركيز والقيمة <code dir=\"ltr\">0</code> بين التبويبات.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-a11y-3-4-t1\"><defs><marker id=\"arrow-l-a11y-3-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-a11y-3-4-t1\">ثلاثة تبويبات: Friday وعليه tabindex بقيمة سالب واحد، وSaturday المحدَّد وعليه tabindex بقيمة صفر، وSunday وعليه tabindex بقيمة سالب واحد. مفاتيح الأسهم تنقل بين التبويبات؛ ومفتاح Tab ينتقل من التبويب المحدَّد نزولًا إلى لوحة Saturday.</title>\n  <rect class=\"d-box\" x=\"40\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"115\" y=\"52\" text-anchor=\"middle\">Friday</text>\n  <text class=\"d-code\" x=\"115\" y=\"72\" text-anchor=\"middle\">tabindex=-1</text>\n  <rect class=\"d-box-primary\" x=\"210\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"285\" y=\"52\" text-anchor=\"middle\">Saturday</text>\n  <text class=\"d-code\" x=\"285\" y=\"72\" text-anchor=\"middle\">tabindex=0</text>\n  <rect class=\"d-box\" x=\"380\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"455\" y=\"52\" text-anchor=\"middle\">Sunday</text>\n  <text class=\"d-code\" x=\"455\" y=\"72\" text-anchor=\"middle\">tabindex=-1</text>\n  <path class=\"d-arrow\" d=\"M360 45 L378 45\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M210 45 L192 45\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <text class=\"d-label-muted\" x=\"600\" y=\"60\" text-anchor=\"middle\">← → الأسهم</text>\n  <path class=\"d-arrow\" d=\"M285 80 L285 125\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <text class=\"d-label-muted\" x=\"320\" y=\"108\">Tab</text>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"130\" width=\"490\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"285\" y=\"160\" text-anchor=\"middle\">tabpanel \"Saturday\" (الفنانون ومواعيد العروض)</text>\n</svg><figcaption>الـ roving tabindex في تبويبات الأيام في Ticketly</figcaption></figure>\n<p>وهذه نسخة Ticketly:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">ticketly/lineup.html</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tablist\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Festival days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-fri\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-fri\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Friday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Saturday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Sunday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tabpanel\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-fri\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-fri\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tabpanel\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tabpanel\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">ticketly/tabs.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabs</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelectorAll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'[role=\"tab\"]'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">tab</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> on</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tab;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    t.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-selected'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(on));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    t.tabIndex </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> on </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getElementById</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(t.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-controls'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">on;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  tab.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">tabs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">forEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">tab</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tab.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'click'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tab)));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'[role=\"tablist\"]'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> i</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">indexOf</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event.target);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> moves</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { ArrowRight: i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, ArrowLeft: i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Home: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, End: tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> moves) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  event.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">preventDefault</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tabs[(moves[event.key] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يستخدم هذا الكود <strong>التفعيل التلقائي (automatic activation)</strong>: الانتقال بالأسهم إلى تبويب يُظهر لوحته فورًا، وهو ما يوصي به APG حين تُعرض اللوحات دون تأخير. أما إن كانت اللوحة تحتاج إلى تحميل بيانات، فاستخدم التفعيل اليدوي بدلًا منه: الأسهم تنقل التركيز فقط، وEnter أو Space يحدّد التبويب. تحصل اللوحات على <code dir=\"ltr\">tabindex=&quot;0&quot;</code> لأنها لا تحتوي عناصر قابلة للتركيز خاصة بها؛ وهذا يمنح Tab مكانًا يصل إليه.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>التبويبات للتنقّل</span></p>\n<p>إن كان كل «تبويب» يحمّل عنوان URL مختلفًا، فهو ليس widget تبويبات؛ بل تنقّل يشبه التبويبات. استخدم روابط داخل <code dir=\"ltr\">nav</code> مع <code dir=\"ltr\">aria-current=&quot;page&quot;</code> على الرابط الحالي. عندها يستطيع المستخدمون فتح الصفحات في تبويبات جديدة ويعمل زر الرجوع.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">زر القائمة، ومتى لا تستخدمه</h2>\n<p>نمط <strong>زر القائمة (menu button)</strong> في APG زر عليه <code dir=\"ltr\">aria-haspopup=&quot;menu&quot;</code> و<code dir=\"ltr\">aria-expanded</code> يفتح عنصرًا عليه <code dir=\"ltr\">role=&quot;menu&quot;</code> يضمّ أبناء عليهم <code dir=\"ltr\">role=&quot;menuitem&quot;</code>. مفاتيح الأسهم تتنقّل بين العناصر، وEscape يُغلق القائمة ويعيد التركيز إلى الزر. وهو مناسب لقوائم الأفعال على طريقة التطبيقات، مثل قائمة “⋯” في Ticketly على حجز ما، وفيها “Download tickets” و&quot;Transfer&quot; و&quot;Cancel booking&quot;.</p>\n<p>لكنه خاطئ لتنقّل المواقع. يخبر الدور <code dir=\"ltr\">menu</code> قارئات الشاشة بأن تتوقّع سلوك تطبيقات سطح المكتب: فتبدّل أنماط التفاعل وتُعلن العناصر على أنها عناصر قائمة لا روابط. أما لقائمة “Events” المنسدلة في الترويسة المليئة بالروابط، فاستخدم <strong>disclosure</strong>: زرًّا عليه <code dir=\"ltr\">aria-expanded</code> يكشف قائمة عادية من الروابط. كود أبسط، ويتصرّف كما يتوقّع مستخدمو المواقع.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الـ popover يساعد في المظهر لا في الدلالة</span></p>\n<p>خاصية <code dir=\"ltr\">popover</code> في HTML مع <code dir=\"ltr\">popovertarget</code> تمنحك العرض في الطبقة العليا والإغلاق الخفيف (light dismiss) مجانًا. وحين يفتح الزرُّ الـ popover بطريقة تصريحية عبر <code dir=\"ltr\">popovertarget</code>، تكشف Chrome وEdge وFirefox وSafari حالة التوسيع أو الطيّ لذلك الزر تلقائيًا، فلا تُضِف <code dir=\"ltr\">aria-expanded</code> بنفسك. أما إن فتح السكربت الـ popover بـ <code dir=\"ltr\">showPopover()</code>، فلا يُكشف شيء وعليك أن تدير <code dir=\"ltr\">aria-expanded</code> يدويًا. وفي الحالتين لا يقرّر الـ popover النمط الذي تبنيه: ما زلت أنت من يختار دلالة الـ menu أو الـ disclosure ويضيف دعم لوحة المفاتيح المطابق لها.</p>\n</aside>\n<p>جعل القسم 3 موقع Ticketly قابلًا للتشغيل. وينتقل القسم 4 إلى ما يراه الناس ويقرؤونه، بدءًا باللون والتباين.</p>\n","toc":[{"id":"sec-1","text":"دليل ARIA Authoring Practices","level":2},{"id":"sec-2","text":"الـ Disclosure: الإظهار والإخفاء","level":2},{"id":"sec-3","text":"التبويبات","level":2},{"id":"sec-4","text":"زر القائمة، ومتى لا تستخدمه","level":2}],"takeaways":["يوثّق دليل ARIA Authoring Practices ‏(APG) الأدوار والحالات وسلوك لوحة المفاتيح الذي يتوقّعه المستخدمون من كل نمط widget.","الـ disclosure زر عليه aria-expanded يُظهر المحتوى ويُخفيه؛ وعنصرا details وsummary الأصليان يمنحانك واحدًا مجانًا.","تستخدم التبويبات الأدوار tablist وtab وtabpanel، وaria-selected، وroving tabindex بحيث لا يكون محطة لـ Tab إلا التبويب النشط، ومفاتيح الأسهم للتنقّل بين التبويبات.","استخدم role=“menu” فقط لقوائم الأفعال على طريقة التطبيقات؛ أما القوائم المنسدلة لتنقّل الموقع فهي disclosures تضمّ روابط عادية."],"further":[{"title":"W3C APG: Tabs Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/tabs/"},{"title":"W3C APG: Disclosure (Show/Hide) Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/"},{"title":"W3C APG: Menu Button Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/"},{"title":"MDN: The Details disclosure element","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details"}],"quiz":[{"q":"<p>في widget تبويبات مبني بشكل صحيح، ماذا يحدث حين يضغط مستخدم لوحة المفاتيح Tab والتركيز على التبويب المحدَّد؟</p>","options":[{"text":"ينتقل التركيز إلى التبويب التالي في الـ tablist","why":"التنقّل بين التبويبات مهمة مفاتيح الأسهم. لو زار Tab كل تبويب، لاحتاج المستخدمون إلى ضغطات كثيرة لتجاوز الـ widget."},{"text":"ينتقل التركيز إلى التبويب التالي ويحدّده","why":"لا يغيّر Tab التحديد أبدًا في نمط APG؛ مفاتيح الأسهم هي التي تفعل ذلك."},{"text":"لا شيء، لأن الـ tablist يلتقط مفتاح Tab","why":"التقاط Tab سيُنشئ مصيدة للوحة المفاتيح. يجب أن يسمح الـ widget لمفتاح Tab بالمغادرة بشكل طبيعي."},{"text":"يغادر التركيز الـ tablist وينتقل إلى لوحة التبويب النشط (أو إلى العنصر التالي القابل للتركيز فيها)","why":"صحيح. مع roving tabindex لا يكون في تسلسل Tab إلا التبويب المحدَّد، فتذهب ضغطة Tab التالية إلى محتوى اللوحة."}],"answer":3},{"q":"<p>ما الخطأ في زر الأسئلة الشائعة هذا؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refund-answer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refund-answer\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"يجب أن يكون رابطًا لأنه يكشف محتوى","why":"كشف المحتوى في مكانه فعلٌ لا تنقّل، فالزر هو الصحيح."},{"text":"يحتاج إلى aria-expanded، تُحدَّث مع كل تبديل، ليعرف المستخدمون إن كانت الإجابة مفتوحة","why":"صحيح. aria-expanded هي ما يجعل قارئات الشاشة تقول “collapsed” أو “expanded”. أما aria-controls وحدها فبالكاد تُعلَن."},{"text":"يحتاج إلى role=“menu” لأنه يفتح شيئًا","why":"الدور menu مخصّص لقوائم أفعال التطبيقات. أما زر الإظهار والإخفاء فهو نمط الـ disclosure."}],"answer":1},{"q":"<p>في ترويسة Ticketly قائمة منسدلة باسم “Events” تضمّ ستة روابط إلى صفحات الفعاليات. أيّ نمط يناسبها أكثر؟</p>","options":[{"text":"زر disclosure عليه aria-expanded يُظهر قائمة من الروابط العادية","why":"صحيح. القوائم المنسدلة للتنقّل هي disclosures. ينتقل المستخدمون بين الروابط بـ Tab كالمعتاد، وتُعلنها قارئات الشاشة على أنها روابط."},{"text":"زر قائمة مع role=“menu” وrole=“menuitem” على كل رابط","why":"الدور menu يجعل قارئات الشاشة تنتقل إلى نمط التفاعل الخاص بالتطبيقات وتتوقّع التنقّل بالأسهم، وهذا يفاجئ المستخدمين في تنقّل المواقع."},{"text":"tablist يكون فيه كل فعالية تبويبًا","why":"التبويبات تبدّل بين لوحات من المحتوى في الصفحة نفسها. أما هذه فروابط إلى صفحات أخرى."}],"answer":0},{"q":"<p>لماذا يعطي نمط التبويبات في APG التبويبات غير النشطة <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>؟</p>","options":[{"text":"لإخفاء التبويبات غير النشطة عن قارئات الشاشة","why":"الـ tabindex لا يؤثّر في شجرة إمكانية الوصول. التبويبات غير النشطة ما زالت تُعلَن، مثلًا “Saturday, tab, 2 of 3”."},{"text":"ليكون الـ tablist كله محطة Tab واحدة، بينما تستطيع معالجات مفاتيح الأسهم نقل التركيز إلى كل تبويب من السكربت","why":"صحيح. هذه تقنية roving tabindex: للتبويب النشط tabindex=“0”، وللبقية “-1”، ومفاتيح الأسهم تنقل التركيز والقيمة 0 بينها."},{"text":"لأن الأزرار داخل الـ tablist لا يمكن التركيز عليها بغير ذلك","why":"الأزرار قابلة للتركيز افتراضيًا. القيمة -1 موجودة لإخراجها من تسلسل Tab، لا لجعلها قابلة للتركيز."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h2>Line-up</h2>\n<div class=\"tabs\">\n  <button type=\"button\" class=\"tab active\" id=\"tab-fri\" data-panel=\"panel-fri\">Friday</button>\n  <button type=\"button\" class=\"tab\" id=\"tab-sat\" data-panel=\"panel-sat\">Saturday</button>\n  <button type=\"button\" class=\"tab\" id=\"tab-sun\" data-panel=\"panel-sun\">Sunday</button>\n</div>\n<div class=\"panel\" id=\"panel-fri\">\n  <p>Nour Quartet, 8 pm. The Saltmarsh Band, 10 pm.</p>\n</div>\n<div class=\"panel\" id=\"panel-sat\" hidden>\n  <p>Layla &amp; the Tides, 7 pm. Harbour Brass, 9 pm.</p>\n</div>\n<div class=\"panel\" id=\"panel-sun\" hidden>\n  <p>Family matinee, 3 pm. Closing set by DJ Marwa, 8 pm.</p>\n</div>","css":".tabs { display: flex; gap: 4px; border-block-end: 2px solid #5b3cc4; }\n.tab { font: inherit; padding: 8px 16px; border: 1px solid #c9c4dc; border-block-end: 0; border-radius: 8px 8px 0 0; background: #f4f2fb; color: #1b1830; }\n.tab.active, .tab[aria-selected=\"true\"] { background: #5b3cc4; color: #fff; }\n.panel { padding: 12px 4px; }\n:focus-visible { outline: 3px solid #1b1830; outline-offset: 2px; }\n","js":"const tabs = [...document.querySelectorAll('.tab')];\n\ntabs.forEach((tab) => {\n  tab.addEventListener('click', () => {\n    for (const t of tabs) {\n      const on = t === tab;\n      t.classList.toggle('active', on);\n      document.getElementById(t.dataset.panel).hidden = !on;\n    }\n  });\n});\n"},"solution":{"html":"<h2>Line-up</h2>\n<div class=\"tabs\" role=\"tablist\" aria-label=\"Festival days\">\n  <button type=\"button\" class=\"tab active\" role=\"tab\" id=\"tab-fri\" aria-selected=\"true\" aria-controls=\"panel-fri\">Friday</button>\n  <button type=\"button\" class=\"tab\" role=\"tab\" id=\"tab-sat\" aria-selected=\"false\" aria-controls=\"panel-sat\" tabindex=\"-1\">Saturday</button>\n  <button type=\"button\" class=\"tab\" role=\"tab\" id=\"tab-sun\" aria-selected=\"false\" aria-controls=\"panel-sun\" tabindex=\"-1\">Sunday</button>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-fri\" aria-labelledby=\"tab-fri\" tabindex=\"0\">\n  <p>Nour Quartet, 8 pm. The Saltmarsh Band, 10 pm.</p>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-sat\" aria-labelledby=\"tab-sat\" tabindex=\"0\" hidden>\n  <p>Layla &amp; the Tides, 7 pm. Harbour Brass, 9 pm.</p>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-sun\" aria-labelledby=\"tab-sun\" tabindex=\"0\" hidden>\n  <p>Family matinee, 3 pm. Closing set by DJ Marwa, 8 pm.</p>\n</div>","css":".tabs { display: flex; gap: 4px; border-block-end: 2px solid #5b3cc4; }\n.tab { font: inherit; padding: 8px 16px; border: 1px solid #c9c4dc; border-block-end: 0; border-radius: 8px 8px 0 0; background: #f4f2fb; color: #1b1830; }\n.tab.active, .tab[aria-selected=\"true\"] { background: #5b3cc4; color: #fff; }\n.panel { padding: 12px 4px; }\n:focus-visible { outline: 3px solid #1b1830; outline-offset: 2px; }\n","js":"const tabs = [...document.querySelectorAll('[role=\"tab\"]')];\n\nfunction select(tab) {\n  for (const t of tabs) {\n    const on = t === tab;\n    t.classList.toggle('active', on);\n    t.setAttribute('aria-selected', String(on));\n    t.tabIndex = on ? 0 : -1;\n    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;\n  }\n  tab.focus();\n}\n\ntabs.forEach((tab) => tab.addEventListener('click', () => select(tab)));\n\ndocument.querySelector('[role=\"tablist\"]').addEventListener('keydown', (event) => {\n  const i = tabs.indexOf(event.target);\n  const moves = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };\n  if (i === -1 || !(event.key in moves)) return;\n  event.preventDefault();\n  select(tabs[(moves[event.key] + tabs.length) % tabs.length]);\n});\n"},"checks":[{"id":"roles","test":"const list = $$('[role=\"tablist\"]'); if (list.length !== 1) return false; const tabs = $$('[role=\"tab\"]'); return tabs.length === 3 && tabs.every((t) => list[0].contains(t)) && $$('[role=\"tabpanel\"]').length === 3;"},{"id":"linked","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); return tabs.length === 3 && tabs.every((t) => { const p = panelOf(t); return p && p.getAttribute('role') === 'tabpanel' && (p.getAttribute('aria-labelledby') || '').split(/\\s+/).includes(t.id); });"},{"id":"initial-state","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); return tabs.length === 3 && selectedIs('Friday');"},{"id":"roving","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); const sel = tabs.find((t) => t.getAttribute('aria-selected') === 'true'); return tabs.length === 3 && !!sel && tabs.every((t) => t.tabIndex === (t === sel ? 0 : -1));"},{"id":"click-selects","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); if (tabs.length !== 3) return false; await click(byName('Saturday')); return selectedIs('Saturday') && byName('Saturday').tabIndex === 0 && byName('Friday').tabIndex === -1;"},{"id":"arrow-keys","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); if (tabs.length !== 3) return false; await press(byName('Saturday'), 'ArrowRight'); if (!selectedIs('Sunday') || document.activeElement !== byName('Sunday')) return false; await press(byName('Sunday'), 'ArrowRight'); if (!selectedIs('Friday') || document.activeElement !== byName('Friday')) return false; await press(byName('Friday'), 'ArrowLeft'); return selectedIs('Sunday') && document.activeElement === byName('Sunday');"}],"title":"ابنِ تبويبات أيام سهلة الوصول","prompt":"<p>تبويبات قائمة الفنانين في Ticketly لا تعمل إلا بالفأرة ولا تقول شيئًا لقارئات الشاشة. حوّلها إلى نمط التبويبات في APG:</p>\n<ul>\n<li><code dir=\"ltr\">role=&quot;tablist&quot;</code> على الحاوية (مع اسم لها، مثل <code dir=\"ltr\">aria-label=&quot;Festival days&quot;</code>)، و<code dir=\"ltr\">role=&quot;tab&quot;</code> على كل زر، و<code dir=\"ltr\">role=&quot;tabpanel&quot;</code> على كل لوحة.</li>\n<li>لكل تبويب <code dir=\"ltr\">aria-controls</code> تشير إلى لوحته؛ ولكل لوحة <code dir=\"ltr\">aria-labelledby</code> تشير إلى تبويبها.</li>\n<li>قيمة <code dir=\"ltr\">aria-selected</code> هي <code dir=\"ltr\">&quot;true&quot;</code> على التبويب المحدَّد و<code dir=\"ltr\">&quot;false&quot;</code> على البقية. يبدأ Friday محدَّدًا.</li>\n<li>roving tabindex: للتبويب المحدَّد <code dir=\"ltr\">tabindex=&quot;0&quot;</code>، وللبقية <code dir=\"ltr\">-1</code>.</li>\n<li>نقر تبويب يحدّده. و<strong>ArrowRight</strong> و<strong>ArrowLeft</strong> يحدّدان التبويب التالي أو السابق وينقلان التركيز إليه، مع الالتفاف عند الطرفين.</li>\n</ul>\n","hints":["أبقِ نص التبويبات Friday وSaturday وSunday تمامًا؛ فالفحوص تجد التبويبات بنصها.","اكتب دالة واحدة select(tab) تحدّث aria-selected وtabIndex وخاصية hidden في اللوحات، ثم تستدعي tab.focus().","في مستمع keydown على الـ tablist، جِد موقع event.target في مصفوفة tabs، وأضِف 1 أو اطرحه، والتفّ بـ (index + tabs.length) % tabs.length."],"explanation":"<p>تحوّل الأدوار ثلاثة أزرار متفرّقة إلى widget واحد تُعلنه قارئات الشاشة هكذا: “Festival days, tab list, Friday, tab, selected, 1 of 3”. و<code dir=\"ltr\">aria-controls</code> و<code dir=\"ltr\">aria-labelledby</code> تربطان كل تبويب بلوحته في الاتجاهين. ولأن التبويب المحدَّد وحده له <code dir=\"ltr\">tabindex=&quot;0&quot;</code>، يكون الـ tablist محطة Tab واحدة؛ وينقل معالج مفاتيح الأسهم التحديد والتركيز والقيمة <code dir=\"ltr\">0</code> معًا، ويلتفّ عند الطرفين كما يصف APG. وجود دالة <code dir=\"ltr\">select()</code> واحدة تُبقي كل الحالات متزامنة هو ما يجعل الـ widget جديرًا بالثقة: المظهر (<code dir=\"ltr\">.active</code>) والدلالة (<code dir=\"ltr\">aria-selected</code>) والتركيز لا يمكن أن تختلف أبدًا.</p>\n","checkLabels":{"roles":"tablist واحد يضمّ ثلاثة تبويبات، وتوجد ثلاث tabpanels","linked":"كل تبويب يتحكّم في لوحته، وكل لوحة مسمّاة بتبويبها","initial-state":"يبدأ Friday محدَّدًا ولوحته وحدها مرئية","roving":"التبويب المحدَّد وحده له tabindex 0؛ والبقية لها -1","click-selects":"نقر Saturday يحدّده ويُظهر لوحته وينقل الـ tabindex","arrow-keys":"مفاتيح الأسهم تحدّد التبويب التالي أو السابق وتنقل التركيز إليه، مع الالتفاف عند الطرفين"}},"words":667,"readMinutes":4}