{"id":"l-a11y-3-1","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"اجعل كل عنصر تحكّم في Ticketly قابلًا للوصول بلوحة المفاتيح بترتيب منطقي، وأظهر مؤشّر تركيز واضحًا، وأضِف رابط تخطٍّ (skip link)، وامنع الأشرطة الثابتة من إخفاء التركيز.","html":"<p>ضع الفأرة في الدرج وحاول شراء تذكرة الجمعة في Ticketly. ستكتشف ثلاثة أشياء في نحو ثلاثين ثانية. مفتاح Tab يمرّ بك على أربعين رابطًا ونيّف في الترويسة والتذييل قبل أن يصل إلى المهرجان. ثم يقفز التركيز من زر السبت إلى زر الجمعة ويعود إلى الأعلى. ومعظم الوقت لا ترى أين التركيز أصلًا، لأن أحدهم كتب <code dir=\"ltr\">outline: none</code> عام 2019 ليتخلّص من «ذلك المربع الأزرق القبيح».</p>\n<p>الوصول بلوحة المفاتيح ليس حالة نادرة. كل من يستخدم قارئ شاشة على حاسوب مكتبي يعتمد على لوحة المفاتيح، وكذلك مستخدمو أجهزة التبديل، والمصابون بالرعاش، ومن تعطّلت لوحة اللمس لديهم، والمطوّرون الذين يفضّلونها ببساطة. يشترط معيار WCAG ‏2.1.1 Keyboard ‏(A) أن يعمل كل شيء من لوحة المفاتيح، ومعيار 2.1.2 ‏(A) ألا يُحبس التركيز أبدًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">المفاتيح التي يستخدمها الناس</h2>\n<ul>\n<li><strong>Tab / Shift+Tab</strong>: التنقّل بين العناصر التفاعلية (الروابط والأزرار وحقول النماذج).</li>\n<li><strong>Enter</strong>: اتّباع رابط، أو تفعيل زر، أو إرسال نموذج.</li>\n<li><strong>Space</strong>: تفعيل زر، أو تبديل مربّع اختيار، أو تمرير الصفحة حين لا يكون أي عنصر عليه التركيز.</li>\n<li><strong>مفاتيح الأسهم</strong>: التنقّل داخل widget، مثل أزرار radio أو قائمة select أو التبويبات أو القوائم.</li>\n<li><strong>Escape</strong>: إغلاق نافذة حوار أو قائمة أو نافذة منبثقة.</li>\n</ul>\n<p>العناصر الأصلية تدعم كل هذا أصلًا. ومهمّتك في الغالب ألا تكسره، وأن تلتزم بهذه الأعراف حين تبني widgets مخصّصة في درس <a href=\"/ar/lesson/web-accessibility/l-a11y-3-4\">أنماط الـ Widgets</a>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">ترتيب التركيز يتبع الـ DOM</h2>\n<p>ينتقل Tab بين العناصر القابلة للتركيز <strong>بترتيب المصدر</strong>. إن وضع التخطيط المرئي زر “Add Saturday” قبل “Add Friday” لكن الكود يضعهما بالعكس، فسيبدو التركيز كأنه يقفز إلى الخلف. والإصلاح أن تضع الكود بالترتيب الذي تريد أن يقابله الناس به. واحذر من CSS الذي يعيد ترتيب العناصر بصريًا، مثل <code dir=\"ltr\">order</code> أو <code dir=\"ltr\">flex-direction: row-reverse</code> أو التموضع الصريح في الـ grid، لأن التركيز سيظلّ يتبع الكود (2.4.3 Focus Order).</p>\n<p>تضبط <code dir=\"ltr\">tabindex</code> قابلية التركيز:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>القيمة</th>\n<th>الأثر</th>\n<th>استخدمها لـ</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">0</code></td>\n<td>قابل للتركيز، بترتيب الـ DOM</td>\n<td>عنصر تحكّم مخصّص مبني من عنصر غير تفاعلي</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">-1</code></td>\n<td>قابل للتركيز من السكربت فقط، لا بمفتاح Tab</td>\n<td>عناوين أو حاويات تنقل التركيز إليها</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">1</code> أو أكثر</td>\n<td>يتقدّم على كل شيء آخر</td>\n<td>لا تستخدمها أبدًا</td>\n</tr>\n</tbody>\n</table></div><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>الـ tabindex الموجب</span></p>\n<p>كان في Ticketly ‏<code dir=\"ltr\">tabindex=&quot;1&quot;</code> على زر السبت و<code dir=\"ltr\">tabindex=&quot;3&quot;</code> على الجمعة «لإصلاح الترتيب». القيم الموجبة تُنشئ طابورًا منفصلًا يُزار قبل بقية الصفحة، فصار التركيز يبدأ من منتصف الصفحة، وينكسر الترتيب كلما أضاف أحدهم عنصر تحكّم. احذف كل القيم الموجبة وأصلح ترتيب الـ DOM بدلًا منها.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">اجعل التركيز مرئيًا</h2>\n<p>مستخدم لوحة المفاتيح بلا مؤشّر تركيز مرئي كمستخدم الفأرة بمؤشّر غير مرئي. يشترط معيار WCAG ‏2.4.7 Focus Visible ‏(AA) وجود مؤشّر؛ ويعني معيار 1.4.11 Non-text Contrast أن تباينه يجب ألا يقلّ عن <strong>3:1</strong> مع ما يحيط به.</p>\n<p>الأداة الحديثة هي <code dir=\"ltr\">:focus-visible</code>. تطبّقها المتصفّحات حين يجب أن يظهر التركيز: دائمًا عند التنقّل بلوحة المفاتيح وفي الحقول النصية، لكن عادةً لا حين يُنقر زر بالفأرة. هذا يجيب عن شكوى المصمّمة الحقيقية دون أن يؤذي أحدًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">ticketly/styles.css</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:#0550AE;--shiki-dark:#79C0FF\">:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> #5b3cc4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</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>استخدم <code dir=\"ltr\">outline</code> لا <code dir=\"ltr\">box-shadow</code> للحلقة. سمات التباين في Windows (وضع forced colors) تحذف الظلال لكنها تُبقي الـ outline، فالمؤشّر المبني على الظلّ وحده يختفي عند أكثر الناس حاجة إليه.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">روابط التخطّي والأشرطة الثابتة</h2>\n<p>في ترويسة Ticketly شعار، وعشرة روابط قائمة، وحقل بحث، وقائمة للحساب: ثلاث عشرة محطة لمفتاح Tab. على مستخدم لوحة المفاتيح أن يمرّ عليها كلها بمفتاح Tab في كل صفحة. يطلب معيار WCAG ‏2.4.1 Bypass Blocks ‏(A) طريقة لتجاوز المحتوى المتكرّر، والحلّ الكلاسيكي هو <strong>رابط التخطّي (skip link)</strong>: أول عنصر قابل للتركيز في الصفحة، مخفي حتى يتلقّى التركيز.</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\">body</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\">a</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"skip-link\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> href</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#main\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Skip to main content&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">a</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\">header</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">header</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\">main</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\">\"main\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</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\">body</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</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:#0550AE;--shiki-dark:#79C0FF\">.skip-link</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">absolute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset-block-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset-inline-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#fff</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transform</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translateY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-150</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.skip-link:focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transform</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</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>لا تُخفِه بـ <code dir=\"ltr\">display: none</code>، فهذا يُخرجه من ترتيب Tab. نقله خارج الشاشة وإعادته عند التركيز يُبقيه قابلًا للوصول ويُظهره تمامًا حين يحتاجه مستخدم لوحة المفاتيح.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-a11y-3-1-t1\"><defs><marker id=\"arrow-l-a11y-3-1-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-1-t1\">من دون رابط تخطٍّ، تصل أول ضغطة Tab إلى الشعار وتعبر اثنتا عشرة ضغطة أخرى الترويسة قبل الوصول إلى المحتوى الرئيسي. ومع رابط التخطّي، تُظهر أول ضغطة Tab الرابط Skip to main content، وينقل Enter مباشرة إلى main.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"30\">من دون رابط تخطٍّ</text>\n  <rect class=\"d-box\" x=\"20\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"90\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"160\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"230\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"340\" y=\"67\" text-anchor=\"middle\">… 13 محطة</text>\n  <rect class=\"d-box-primary\" x=\"400\" y=\"45\" width=\"140\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"470\" y=\"67\" text-anchor=\"middle\">main</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"130\">مع رابط تخطٍّ</text>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"145\" width=\"200\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"120\" y=\"167\" text-anchor=\"middle\">Skip to main content</text>\n  <path class=\"d-arrow\" d=\"M220 162 L395 162\" marker-end=\"url(#arrow-l-a11y-3-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"305\" y=\"152\" text-anchor=\"middle\">Enter</text>\n  <rect class=\"d-box-primary\" x=\"400\" y=\"145\" width=\"140\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"470\" y=\"167\" text-anchor=\"middle\">main</text>\n</svg><figcaption>أول ضغطة Tab في Ticketly</figcaption></figure>\n<p>الترويسات الثابتة وأشرطة ملفات تعريف الارتباط تجلب المشكلة المعاكسة: ينتقل التركيز إلى عنصر مخفي تحتها، وهذا يخالف معيار 2.4.11 Focus Not Obscured ‏(AA). أخبر المتصفّح بمقدار ما تغطّيه الأجزاء الثابتة من الـ viewport، فيمرّر العناصر التي عليها التركيز بعيدًا عنها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</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:#116329;--shiki-dark:#7EE787\">html</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  scroll-padding-block-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">96</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* sticky header height plus a little air */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>أما شريط ملفات تعريف الارتباط، فالإصلاح الحقيقي ألا تتركه يطفو فوق المحتوى إلى ما لا نهاية: ضعه في تدفّق الصفحة، أو تأكّد أنه يمكن إغلاقه قبل الوصول إلى أي شيء آخر.</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>فحص لوحة المفاتيح في دقيقتين</span></p>\n<p>تنقّل بمفتاح Tab من شريط العنوان إلى أسفل الصفحة. وعند كل محطة اسأل: هل أرى أين أنا، وهل هذا هو المكان الذي توقّعته، وهل يفعل Enter أو Space ما تفعله النقرة؟ ثم عُد صعودًا بـ Shift+Tab. ستجد معظم أخطاء لوحة المفاتيح بهذه الطريقة قبل أن تجدها أي أداة.</p>\n</aside>\n<p>صار بالإمكان عبور Ticketly من أعلاه إلى أسفله بلوحة المفاتيح. يتناول الدرس التالي الحالات الأصعب، حيث يجب على كودك أن ينقل التركيز بنفسه: نوافذ الحوار والتنقّل في التطبيقات أحادية الصفحة.</p>\n","toc":[{"id":"sec-1","text":"المفاتيح التي يستخدمها الناس","level":2},{"id":"sec-2","text":"ترتيب التركيز يتبع الـ DOM","level":2},{"id":"sec-3","text":"اجعل التركيز مرئيًا","level":2},{"id":"sec-4","text":"روابط التخطّي والأشرطة الثابتة","level":2}],"takeaways":["ترتيب التركيز يتبع ترتيب الـ DOM، فضع الكود بالترتيب الذي يجب أن يقابله الناس به، ولا تعاكسه بإعادة الترتيب عبر CSS.","استخدم tabindex=“0” لإضافة عنصر تحكّم مخصّص إلى ترتيب Tab، وtabindex=“-1” للعناصر التي تنقل إليها التركيز من السكربت؛ ولا تستخدم أبدًا tabindex موجبًا.","لا تحذف إطار التركيز أبدًا دون بديل؛ نسّق :focus-visible بمؤشّر لا يقلّ تباينه عن 3:1.","رابط التخطّي كأول عنصر قابل للتركيز يتيح لمستخدمي لوحة المفاتيح تجاوز التنقّل المتكرّر، وscroll-padding يمنع الأشرطة الثابتة من إخفاء التركيز."],"further":[{"title":"MDN: :focus-visible","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible"},{"title":"MDN: tabindex","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex"},{"title":"W3C: Understanding 2.4.7 Focus Visible","url":"https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html"},{"title":"W3C APG: Developing a Keyboard Interface","url":"https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/"}],"quiz":[{"q":"<p>بأيّ ترتيب يزور مفتاح Tab هذه الأزرار؟</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:#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\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2\"</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\"> 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\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Weekend&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"Friday، Saturday، Sunday، Weekend","why":"هذا ترتيب الـ DOM، وهو ما تحصل عليه حين لا يكون لأي عنصر tabindex موجب. أما القيم الموجبة فتتخطّى الطابور."},{"text":"Sunday، Saturday، Friday، Weekend","why":"صحيح. العناصر ذات الـ tabindex الموجب تأتي أولًا بترتيب تصاعدي، ثم كل ما سواها بترتيب الـ DOM. ولهذا تسبّب القيم الموجبة الفوضى."},{"text":"Saturday، Sunday، Friday، Weekend","why":"قيم الـ tabindex الموجبة تُزار بترتيب تصاعدي، فالقيمة 1 ‏(Sunday) تأتي قبل 2 ‏(Saturday)."},{"text":"Friday، Weekend، Sunday، Saturday","why":"عناصر الـ tabindex الموجب تُزار قبل العناصر ذات tabindex 0، لا بعدها."}],"answer":1},{"q":"<p>تجد مصمّمة أن حلقة التركيز الافتراضية قبيحة. أيّ CSS هو أفضل ردّ؟</p>","options":[{"text":"<code dir=\"ltr\">:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }</code>","why":"صحيح. يستبدل المؤشّر الافتراضي بمؤشّر قوي بألوان العلامة التجارية يراه مستخدمو لوحة المفاتيح، بينما لا يظهر عند نقر الأزرار بالفأرة."},{"text":"<code dir=\"ltr\">*:focus { outline: none; }</code>","why":"هذا يحذف المؤشّر عن الجميع، فيفقد مستخدمو لوحة المفاتيح معرفة مكانهم. ويخالف معيار 2.4.7 Focus Visible."},{"text":"<code dir=\"ltr\">:focus { outline: 1px dotted #ddd; }</code>","why":"الخط الرمادي الفاتح الرفيع على صفحة بيضاء يكاد لا يُرى. المؤشّرات تحتاج إلى تباين لا يقلّ عن 3:1 مع ما يحيط بها."}],"answer":0},{"q":"<p>متى يجب أن يحصل عنصر على <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>؟</p>","options":[{"text":"حين تريد إخراج زر من ترتيب Tab لأنه أقل أهمية","why":"كل عنصر تحكّم تفاعلي يجب أن يكون قابلًا للوصول بلوحة المفاتيح. وإخراج زر يعمل من ترتيب Tab يجعله غير قابل للاستخدام لمستخدمي لوحة المفاتيح."},{"text":"على الصور الزخرفية لتتخطّاها قارئات الشاشة","why":"الـ tabindex يؤثّر في تركيز لوحة المفاتيح فقط. الصور الزخرفية تحتاج إلى alt=“” أو aria-hidden، وهي أصلًا غير قابلة للتركيز."},{"text":"على كل عنصر داخل منطقة main، لإبقاء ترتيب التركيز مرتّبًا","why":"هذا سيُخرج الروابط والأزرار في main من ترتيب Tab، فيستحيل استخدام الصفحة بلوحة المفاتيح."},{"text":"على عنصر تحتاج إلى نقل التركيز إليه من السكربت، مثل عنوان بعد تغيّر المسار، ولا ينبغي أن يكون محطة لمفتاح Tab","why":"صحيح. tabindex=“-1” يجعل العنصر قابلًا للتركيز عبر element.focus() دون إضافته إلى تسلسل Tab."}],"answer":3},{"q":"<p>ارتفاع الترويسة الثابتة في Ticketly ‏80px. حين يتنقّل مستخدم لوحة المفاتيح إلى الخلف صعودًا في الصفحة، تنزلق الروابط التي عليها التركيز تحتها. أيّ إصلاح يعالج معيار 2.4.11 Focus Not Obscured؟</p>","options":[{"text":"إعطاء الترويسة <code dir=\"ltr\">z-index: -1</code> حين يكون أي عنصر عليه التركيز","why":"إرسال الترويسة خلف المحتوى سيجعلها تتداخل بصريًا بطرق غريبة، ولا يُبقي التركيز في مجال الرؤية بشكل موثوق."},{"text":"إضافة <code dir=\"ltr\">html { scroll-padding-block-start: 96px; }</code> ليمرّر المتصفّح العناصر التي عليها التركيز بعيدًا عن الترويسة","why":"صحيح. يخبر scroll-padding المتصفّح بالجزء المغطّى من الـ viewport، فيترك التمرير مساحة للترويسة الثابتة."},{"text":"إضافة <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> إلى الترويسة","why":"aria-hidden تؤثّر في التقنيات المساعدة فقط. ستظلّ الترويسة تغطّي الرابط الذي عليه التركيز بصريًا، وستختفي روابطها من الشجرة."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<header class=\"site-header\">\n  <a href=\"#\" class=\"logo\">Ticketly</a>\n  <nav aria-label=\"Site\">\n    <a href=\"#events\">Events</a>\n    <a href=\"#faq\">FAQ</a>\n    <a href=\"#account\">Account</a>\n  </nav>\n</header>\n<main id=\"main\">\n  <h1>Harbour Lights 2026</h1>\n  <div class=\"passes\">\n    <button tabindex=\"3\">Add Friday pass</button>\n    <button tabindex=\"1\">Add Saturday pass</button>\n    <button>Add weekend pass</button>\n  </div>\n</main>","css":".site-header { display: flex; gap: 16px; align-items: center; }\nnav { display: flex; gap: 12px; }\n.passes { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #5b3cc4; color: #fff; }\n\n/* \"Remove the ugly blue box\" (2019) */\n*:focus { outline: none; }\n","js":""},"solution":{"html":"<a class=\"skip-link\" href=\"#main\">Skip to main content</a>\n<header class=\"site-header\">\n  <a href=\"#\" class=\"logo\">Ticketly</a>\n  <nav aria-label=\"Site\">\n    <a href=\"#events\">Events</a>\n    <a href=\"#faq\">FAQ</a>\n    <a href=\"#account\">Account</a>\n  </nav>\n</header>\n<main id=\"main\">\n  <h1>Harbour Lights 2026</h1>\n  <div class=\"passes\">\n    <button>Add Friday pass</button>\n    <button>Add Saturday pass</button>\n    <button>Add weekend pass</button>\n  </div>\n</main>","css":".site-header { display: flex; gap: 16px; align-items: center; }\nnav { display: flex; gap: 12px; }\n.passes { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #5b3cc4; color: #fff; }\n\n:focus-visible {\n  outline: 3px solid #1b1830;\n  outline-offset: 2px;\n}\n\n.skip-link {\n  position: absolute;\n  inset-block-start: 8px;\n  inset-inline-start: 8px;\n  padding: 8px 12px;\n  background: #fff;\n  color: #1b1830;\n  transform: translateY(-150%);\n}\n.skip-link:focus {\n  transform: none;\n}\n","js":""},"checks":[{"id":"no-positive","test":"return $$('[tabindex]').every((el) => Number(el.getAttribute('tabindex')) <= 0);"},{"id":"skip-first","test":"const focusable = $$('a[href], button, input, select, textarea, [tabindex]').filter((el) => el.tabIndex >= 0 && !el.disabled && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden'); const first = focusable[0]; return !!first && first.tagName === 'A' && /skip/i.test(first.textContent) && first.getAttribute('href') === '#' + $('main').id;"},{"id":"skip-visible","test":"const link = $$('a').find((a) => /skip/i.test(a.textContent)); if (!link) return false; link.focus(); await tick(50); const r = link.getBoundingClientRect(); return document.activeElement === link && r.width > 1 && r.height > 1 && r.bottom > 0 && r.top < window.innerHeight;"},{"id":"focus-ring","test":"const ok = (el) => { el.focus(); const cs = getComputedStyle(el); const outline = cs.outlineStyle !== 'none' && parseFloat(cs.outlineWidth) >= 2; return outline; }; const items = [...$$('main button'), ...$$('nav a')]; const result = items.every(ok); document.activeElement.blur(); return result;"}],"title":"اجعل Ticketly صديقًا للوحة المفاتيح","prompt":"<p>ثلاث مشكلات في لوحة المفاتيح على صفحة صغيرة واحدة:</p>\n<ol>\n<li>أزرار التذاكر تستخدم قيم <code dir=\"ltr\">tabindex</code> موجبة، فيقفز ترتيب التركيز هنا وهناك. احذفها؛ فترتيب الـ DOM صحيح أصلًا.</li>\n<li>قاعدة من عام 2019 تُخفي كل مؤشّرات التركيز. استبدلها بـ outline مرئي عبر <code dir=\"ltr\">:focus-visible</code> لا يقلّ عرضه عن 2px.</li>\n<li>لا توجد طريقة لتجاوز الترويسة. أضِف رابط تخطٍّ يكون <strong>أول عنصر قابل للتركيز</strong>، نصه يحتوي “Skip” و<code dir=\"ltr\">href=&quot;#main&quot;</code>. يمكن أن يكون مخفيًا خارج الشاشة، لكن يجب أن يكون مرئيًا حين يكون عليه التركيز (لا تستخدم <code dir=\"ltr\">display: none</code>).</li>\n</ol>\n","hints":["احذف خصائص tabindex من الأزرار بالكامل.","استبدل *:focus { outline: none; } بـ :focus-visible { outline: 3px solid …; outline-offset: 2px; }","ضع <a class=\"skip-link\" href=\"#main\">Skip to main content</a> قبل الترويسة، وارفعه بـ transform: translateY(-150%)، وأعِد الـ transform إلى وضعه في ‎.skip-link:focus."],"explanation":"<p>من دون قيم tabindex موجبة، يتبع Tab الكود: رابط التخطّي، ثم الشعار، ثم القائمة، ثم التذاكر الثلاث بترتيب ظهورها. صارت قاعدة التركيز تضيف مؤشّرًا بدلًا من أن تحذفه، و<code dir=\"ltr\">:focus-visible</code> تمنعه من الوميض عند النقر بالفأرة. رابط التخطّي هو أول محطة لـ Tab؛ ويبقى قابلًا للوصول لأنه يُنقل خارج الشاشة بدلًا من حذفه، وينزلق إلى مجال الرؤية عند التركيز فيراه مستخدمو لوحة المفاتيح المبصرون أيضًا. وضغط Enter عليه ينقل التركيز إلى <code dir=\"ltr\">#main</code>، متجاوزًا الترويسة.</p>\n","checkLabels":{"no-positive":"لا يوجد عنصر له tabindex موجب","skip-first":"أول عنصر قابل للتركيز رابط تخطٍّ يشير إلى ‎#main","skip-visible":"رابط التخطّي مرئي على الشاشة حين يكون عليه التركيز","focus-ring":"الروابط والأزرار تُظهر outline لا يقلّ عرضه عن 2px عند التركيز"}},"words":737,"readMinutes":4}