{"id":"l-ds-3-3","courseId":"design-systems","lang":"ar","kind":"lesson","summary":"ابنِ كل حالات التفاعل وسلوكيات إمكانية الوصول داخل المكوّنات افتراضيًا، بما فيها التركيز المرئي عبر ‎:focus-visible، وحالتي التعطيل والضغط، وأنماط ARIA التي تتبع WAI-ARIA Authoring Practices.","html":"<p>بدا Button الذي أطلقته Northwind عام 2021 مثاليًا في كل لقطة شاشة، لأن اللقطات لا تُظهر إلا الحالة الافتراضية. في الإنتاج لم يكن له تركيز مرئي للوحة المفاتيح، وفشل نمط التعطيل الخاص به في التباين على علامة Tidewater، وكانت حالة «التحميل» تجعل الزر ينكمش والتخطيط يقفز. لم يكن شيء من هذا في ملف Figma. المكوّنات تفشل في حالاتها، لذا يجب أن يصمّمها النظام كلها ويبنيها ويختبرها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">مصفوفة الحالات</h2>\n<p>كل مكوّن تفاعلي يحصل على مصفوفة حالات: صف لكل variant، وعمود لكل حالة. للـ Button:</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>Hover</td>\n<td>المؤشر فوقه</td>\n<td><code dir=\"ltr\">:hover</code></td>\n</tr>\n<tr>\n<td>التركيز المرئي</td>\n<td>تركيز لوحة المفاتيح</td>\n<td><code dir=\"ltr\">:focus-visible</code></td>\n</tr>\n<tr>\n<td>Active</td>\n<td>أثناء الضغط</td>\n<td><code dir=\"ltr\">:active</code></td>\n</tr>\n<tr>\n<td>معطّل</td>\n<td>غير متاح</td>\n<td><code dir=\"ltr\">disabled</code> أو <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code></td>\n</tr>\n<tr>\n<td>تحميل</td>\n<td>إجراء قيد التنفيذ</td>\n<td><code dir=\"ltr\">aria-busy</code>، وعرض ثابت</td>\n</tr>\n<tr>\n<td>مضغوط</td>\n<td>زر التبديل مفعّل</td>\n<td><code dir=\"ltr\">aria-pressed=&quot;true&quot;</code></td>\n</tr>\n</tbody>\n</table></div><p>تصمّم Northwind كل خانة في Figma (كـ variants للمكوّن) قبل أن يكتب أحد سطر كود، والاختبارات البصرية في القسم 4 تعرض كل خانة في كل theme. وحين تصل مساهمة لم يُصمَّم فيها إلا الحالة الافتراضية والـ hover، تعيدها المراجعة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">تركيز يراه الناس</h2>\n<p>مستخدمو لوحة المفاتيح يحتاجون أن يروا أين التركيز؛ وWCAG 2.2 يشترط ذلك في المستوى AA ‏(2.4.7، Focus Visible). أما مستخدمو الفأرة فعادةً لا يريدون حلقة بعد النقر. و<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\">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\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-blue-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-width</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:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-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>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.nw-button: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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-color</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\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-offset</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>الحلقة مبنية من tokens، فيستطيع الوضع الداكن وTidewater تعديلها، وكل المكوّنات تستخدم الحلقة نفسها. وهي <code dir=\"ltr\">outline</code> لا <code dir=\"ltr\">box-shadow</code>، لأن أوضاع forced-colors مثل themes التباين في Windows تحذف الظلال وتُبقي الـ outlines.</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>الـ outline: none على ‎:focus</span></p>\n<p>ما زال <code dir=\"ltr\">:focus { outline: none; }</code> أشيع خلل في إمكانية الوصول في مكتبات المكوّنات، ويُضاف عادةً لأن الحلقة بدت غريبة عند النقر. إنه يحذف الحلقة الافتراضية للمتصفّح عن الجميع، بمن فيهم مستخدمو لوحة المفاتيح. إن حذفت قيمة افتراضية، فضع بديلها في <code dir=\"ltr\">:focus-visible</code> في الـ commit نفسه.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">التعطيل، بطريقتين</h2>\n<p>السمة <code dir=\"ltr\">disabled</code> تُخرج الزر من ترتيب التنقّل بـ Tab ومن التفعيل. وهذا صحيح في معظم الحالات، لكنه يخلق مشكلة: لا يستطيع مستخدمو لوحة المفاتيح وقارئ الشاشة الوصول إلى الزر ليعرفوا <em>لماذا</em> هو معطّل.</p>\n<p>حين تحتاج إلى شرح، استخدم <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code> بدلًا من ذلك. يبقى الزر قابلًا للتركيز ويُعلَن أنه غير متاح؛ وتربط السبب عبر <code dir=\"ltr\">aria-describedby</code>؛ ولأن المتصفّح لن يمنع النقر عنك، يجب أن يفعل المكوّن ذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><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:#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\">  if</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-disabled'</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\">    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:#1F2328;--shiki-dark:#E6EDF3\">    event.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stopImmediatePropagation</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:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>الـ Button في النظام يفعل هذا داخليًا، فتحدّد الفرق prop واحدًا وتحصل على السلوك. وهذا هو جوهر بناء إمكانية الوصول داخل المكوّن: أربعون فريقًا لن يتذكّر كلٌّ منهم <code dir=\"ltr\">stopImmediatePropagation</code>، لكن مكوّنًا واحدًا يستطيع.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">أزرار التبديل، والأهداف، والأنماط</h2>\n<p>الزر الذي يشغّل شيئًا ويطفئه هو <strong>زر تبديل</strong> (toggle button). علّم حالته بـ <code dir=\"ltr\">aria-pressed=&quot;true&quot;</code> أو <code dir=\"ltr\">&quot;false&quot;</code>، ونسّق هذه السمة نفسها حتى لا تتباعد الحالة المرئية عن الحالة المُعلَنة:</p>\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\">.nw-button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">aria-pressed</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:#953800;--shiki-dark:#FFA657\">  --nw-button-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-bg-subtle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-button-border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-bg</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>والحجم مهمّ أيضًا: معيار النجاح 2.5.8 في WCAG 2.2 يطلب أهدافًا للمؤشر لا تقل عن 24 في 24 بكسل CSS في المستوى AA، مع بعض الاستثناءات. وأصغر ارتفاع لعنصر تحكّم في Northwind هو 32px، تفرضه tokens الحجم <code dir=\"ltr\">sm</code>.</p>\n<p>ولأي شيء أبعد من الزر، استخدم العناصر الأصلية أولًا: <code dir=\"ltr\">&lt;button&gt;</code> و<code dir=\"ltr\">&lt;dialog&gt;</code> و<code dir=\"ltr\">&lt;details&gt;</code> و<code dir=\"ltr\">&lt;select&gt;</code>. وحين تبني عنصرًا مخصّصًا مثل التبويبات أو زر القائمة أو الـ combobox، اتبع النمط المقابل في WAI-ARIA Authoring Practices Guide: أدواره وحالاته وتفاعلات لوحة المفاتيح فيه. مستخدمو التقنيات المساعدة يتوقّعون هذه السلوكيات، والدليل يوثّقها بدقّة.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا يهمّ هذا</span></p>\n<p>في Northwind كشف تدقيق لإمكانية الوصول على منتج مبني بالنظام 31 مشكلة. ستٌّ وعشرون منها كانت في مكوّنات منفردة بناها الفريق بنفسه؛ وخمس في مكوّنات النظام، وإصلاح هذه الخمس في النظام أصلحها في كل المنتجات دفعة واحدة. إمكانية الوصول المبنية في مكوّن مشترك تكبر مع النظام؛ أما المتروكة لذاكرة كل فريق فلا.</p>\n</aside>\n<p>في التمرين تصلح مجموعة أزرار تخفي تركيز لوحة المفاتيح وتتجاهل حالة التعطيل الخاصة بها. وبعدها تكتب التوثيق الذي يخبر الفرق كيف تستخدم كل هذا.</p>\n","toc":[{"id":"sec-1","text":"مصفوفة الحالات","level":2},{"id":"sec-2","text":"تركيز يراه الناس","level":2},{"id":"sec-3","text":"التعطيل، بطريقتين","level":2},{"id":"sec-4","text":"أزرار التبديل، والأهداف، والأنماط","level":2}],"takeaways":["صمّم وابنِ مصفوفة حالات كاملة لكل مكوّن تفاعلي، تشمل التركيز والتعطيل والتحميل والضغط، لا الحالة الافتراضية والـ hover وحدهما.","نسّق تركيز لوحة المفاتيح بـ <code dir=\"ltr\">:focus-visible</code> وبـ outline مأخوذ من token؛ ولا تحذف الـ outline أبدًا دون بديل مرئي.","استخدم العناصر الأصلية أولًا، واتبع WAI-ARIA Authoring Practices في العناصر المخصّصة مثل التبويبات والقوائم والـ comboboxes.","السمة <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code> تُبقي العنصر قابلًا للتركيز حتى تشرح سبب عدم توفّره، لكن عليك أن تمنع إجراءه بنفسك.","حين تعيش إمكانية الوصول داخل المكوّن يحصل عليها 40 فريقًا مجانًا؛ وحين تعيش في التوثيق يفوّتها معظمهم."],"further":[{"title":":focus-visible (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible"},{"title":"WAI-ARIA Authoring Practices Guide patterns (W3C)","url":"https://www.w3.org/WAI/ARIA/apg/patterns/"},{"title":"Understanding SC 2.5.8 Target Size (Minimum) (W3C)","url":"https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html"},{"title":"aria-pressed (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-pressed"}],"quiz":[{"q":"<p>أطلق أحد الفرق <code dir=\"ltr\">.nw-button:focus { outline: none; }</code> لأن الحلقة «تبدو قبيحة عند النقر». ما الإصلاح الصحيح في النظام؟</p>","options":[{"text":"إبقاء <code dir=\"ltr\">outline: none</code> والاعتماد على لون الـ hover لمستخدمي لوحة المفاتيح.","why":"مستخدمو لوحة المفاتيح لا يمرّرون المؤشر فوق العناصر، فيفقدون أي إشارة إلى مكانهم."},{"text":"نقل الحلقة إلى <code dir=\"ltr\">:focus-visible</code> حتى تظهر مع تركيز لوحة المفاتيح لا مع معظم نقرات الفأرة.","why":"صحيح. تطبّق المتصفّحات <code dir=\"ltr\">:focus-visible</code> حين يجب أن يكون التركيز مرئيًا، وهذا ما تريده المجموعتان فعلًا."},{"text":"استبدال الـ outline بحلقة <code dir=\"ltr\">box-shadow</code> على <code dir=\"ltr\">:focus</code>.","why":"ستظلّ تظهر عند النقر، والظلال تُحذف في أوضاع forced-colors، فيفقد مستخدمو التباين العالي الحلقة."},{"text":"إضافة <code dir=\"ltr\">tabindex=&quot;-1&quot;</code> إلى الأزرار حتى لا تستقبل التركيز.","why":"هذا يُخرج الأزرار من ترتيب لوحة المفاتيح كليًا، فتصبح غير قابلة للاستخدام دون فأرة."}],"answer":1},{"q":"<p>زر الحفظ غير متاح حتى يُختار سائق. يريد فريق المنتج tooltip يشرح السبب. أيّ نهج ينجح؟</p>","options":[{"text":"استخدام السمة <code dir=\"ltr\">disabled</code> وربط الـ tooltip بالزر.","why":"الزر المعطّل لا يستقبل التركيز، فلا يصل مستخدمو لوحة المفاتيح وقارئ الشاشة إلى الـ tooltip أبدًا."},{"text":"إخفاء الزر حتى يُختار سائق.","why":"الإخفاء يزيل أي تلميح بأن الحفظ ممكن أصلًا، ويقفز التخطيط حين يظهر الزر."},{"text":"تحديد <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code>، وإبقاء الزر قابلًا للتركيز، ومنع النقر في الكود، وربط الشرح عبر <code dir=\"ltr\">aria-describedby</code>.","why":"صحيح. يبقى العنصر قابلًا للوصول ويُعلَن أنه غير متاح، والسبب متاح للجميع."}],"answer":2},{"q":"<p>تحتاج Northwind إلى زر تبديل «Show only late shipments». أيّ markup يوصل حالته بشكل صحيح؟</p>","options":[{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; aria-pressed=&quot;true&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"صحيح. السمة <code dir=\"ltr\">aria-pressed</code> تحوّل الزر إلى زر تبديل، وقارئات الشاشة تعلن أنه مضغوط أو غير مضغوط."},{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; class=&quot;is-active&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"الـ class يغيّر الشكل لكنه لا يخبر التقنيات المساعدة بشيء عن الحالة."},{"text":"<code dir=\"ltr\">&lt;div role=&quot;button&quot; data-on=&quot;true&quot;&gt;Show only late shipments&lt;/div&gt;</code>","why":"الـ div يحتاج إضافة التعامل مع لوحة المفاتيح وقابلية التركيز يدويًا، و<code dir=\"ltr\">data-on</code> لا تصل إلى التقنيات المساعدة."},{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; aria-label=&quot;on&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"الـ label يستبدل الاسم المرئي بـ «on»، وهذا أسوأ من ألّا تقول شيئًا."}],"answer":0},{"q":"<p>لماذا ترسم Northwind حلقات التركيز بـ <code dir=\"ltr\">outline</code> بدل <code dir=\"ltr\">box-shadow</code>؟</p>","options":[{"text":"لأن الـ outlines أسرع في الرسم.","why":"تكلفة الرسم ليست فرقًا ذا معنى هنا."},{"text":"لأن الظلال لا تستطيع استخدام الـ custom properties.","why":"تستطيع؛ أي قيمة CSS يمكن أن تأتي من custom property."},{"text":"لأن الـ outlines تُرسم دائمًا خارج صندوق الحدود، فتستحيل الإزاحة مع الظلال.","why":"كلاهما يستطيع إنشاء فجوة؛ <code dir=\"ltr\">outline-offset</code> أسهل فقط. السبب الحاسم في مكان آخر."},{"text":"لأن أوضاع forced-colors مثل themes التباين في Windows تحذف الظلال وتُبقي الـ outlines.","why":"صحيح. الحلقة المبنية على outline تصمد أمام إعدادات التباين العالي، فيظلّ هؤلاء المستخدمون يرون التركيز."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<div class=\"toolbar\">\n  <button class=\"nw-button\" type=\"button\" id=\"assign\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" id=\"late\" aria-pressed=\"false\">Show only late</button>\n  <button class=\"nw-button\" type=\"button\" id=\"save\" aria-disabled=\"true\" aria-describedby=\"save-why\">Save route</button>\n  <p id=\"save-why\">Select a driver before saving.</p>\n</div>\n<p id=\"log\">Saved 0 times</p>","css":":root {\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-50: #eaf0fd;\n  --nw-gray-200: #d9dee3;\n  --nw-gray-500: #7b8794;\n  --nw-color-action-bg: var(--nw-blue-600);\n  --nw-color-action-bg-subtle: var(--nw-blue-50);\n  --nw-color-text-disabled: var(--nw-gray-500);\n  --nw-color-border: var(--nw-gray-200);\n  --nw-focus-ring-color: var(--nw-blue-600);\n  --nw-focus-ring-width: 2px;\n  --nw-focus-ring-offset: 2px;\n}\n.nw-button {\n  --nw-button-bg: #ffffff;\n  --nw-button-text: #1f2933;\n  --nw-button-border: var(--nw-color-border);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: 8px 16px;\n  border-radius: 6px;\n  font: inherit;\n  cursor: pointer;\n}\n.nw-button:focus {\n  outline: none;\n}\n","js":"let saves = 0;\nconst log = document.querySelector('#log');\nconst save = document.querySelector('#save');\nconst late = document.querySelector('#late');\n\nsave.addEventListener('click', () => {\n  saves += 1;\n  log.textContent = `Saved ${saves} times`;\n});\n\nlate.addEventListener('click', () => {\n  // Toggle the filter here\n});\n"},"solution":{"html":"<div class=\"toolbar\">\n  <button class=\"nw-button\" type=\"button\" id=\"assign\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" id=\"late\" aria-pressed=\"false\">Show only late</button>\n  <button class=\"nw-button\" type=\"button\" id=\"save\" aria-disabled=\"true\" aria-describedby=\"save-why\">Save route</button>\n  <p id=\"save-why\">Select a driver before saving.</p>\n</div>\n<p id=\"log\">Saved 0 times</p>","css":":root {\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-50: #eaf0fd;\n  --nw-gray-200: #d9dee3;\n  --nw-gray-500: #7b8794;\n  --nw-color-action-bg: var(--nw-blue-600);\n  --nw-color-action-bg-subtle: var(--nw-blue-50);\n  --nw-color-text-disabled: var(--nw-gray-500);\n  --nw-color-border: var(--nw-gray-200);\n  --nw-focus-ring-color: var(--nw-blue-600);\n  --nw-focus-ring-width: 2px;\n  --nw-focus-ring-offset: 2px;\n}\n.nw-button {\n  --nw-button-bg: #ffffff;\n  --nw-button-text: #1f2933;\n  --nw-button-border: var(--nw-color-border);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: 8px 16px;\n  border-radius: 6px;\n  font: inherit;\n  cursor: pointer;\n}\n.nw-button:focus-visible {\n  outline: var(--nw-focus-ring-width) solid var(--nw-focus-ring-color);\n  outline-offset: var(--nw-focus-ring-offset);\n}\n.nw-button[aria-pressed=\"true\"] {\n  --nw-button-bg: var(--nw-color-action-bg-subtle);\n  --nw-button-border: var(--nw-color-action-bg);\n}\n.nw-button[aria-disabled=\"true\"] {\n  --nw-button-text: var(--nw-color-text-disabled);\n  cursor: not-allowed;\n}\n","js":"let saves = 0;\nconst log = document.querySelector('#log');\nconst save = document.querySelector('#save');\nconst late = document.querySelector('#late');\n\nsave.addEventListener('click', () => {\n  if (save.getAttribute('aria-disabled') === 'true') return;\n  saves += 1;\n  log.textContent = `Saved ${saves} times`;\n});\n\nlate.addEventListener('click', () => {\n  const pressed = late.getAttribute('aria-pressed') === 'true';\n  late.setAttribute('aria-pressed', String(!pressed));\n});\n"},"checks":[{"id":"focus-visible-rule","test":"const rules = [...document.styleSheets].flatMap((s) => [...s.cssRules]).filter((r) => r.selectorText); const fv = rules.filter((r) => r.selectorText.includes(':focus-visible')); const plainFocus = rules.filter((r) => /:focus(?![-\\w])/.test(r.selectorText) && /outline/.test(r.style.cssText) && /none|0px/.test(r.style.outlineStyle + r.style.outline + r.style.outlineWidth)); return fv.length > 0 && plainFocus.length === 0;"},{"id":"ring","test":"const b = $('#assign'); b.focus(); const ok = b.matches(':focus-visible') && style(b, 'outline-style') === 'solid' && style(b, 'outline-width') === '2px' && style(b, 'outline-color') === 'rgb(31, 90, 224)' && style(b, 'outline-offset') === '2px'; b.blur(); return ok;"},{"id":"toggle","test":"const b = $('#late'); click(b); await tick(); const on = b.getAttribute('aria-pressed') === 'true' && style(b, 'background-color') === 'rgb(234, 240, 253)'; click(b); await tick(); return on && b.getAttribute('aria-pressed') === 'false' && style(b, 'background-color') === 'rgb(255, 255, 255)';"},{"id":"blocked","test":"click($('#save')); await tick(); return text('#log') === 'Saved 0 times';"},{"id":"disabled-look","test":"return style('#save', 'color') === 'rgb(123, 135, 148)' && style('#save', 'cursor') === 'not-allowed';"}],"title":"ابنِ حالات التركيز والتبديل والتعطيل في المكوّن","prompt":"<p>شريط الأدوات هذا يخفي تركيز لوحة المفاتيح ويتجاهل حالاته. أصلحه في CSS وJS:</p>\n<ol>\n<li>احذف القاعدة <code dir=\"ltr\">:focus { outline: none }</code> وأضف حلقة <code dir=\"ltr\">:focus-visible</code>: ‏outline صلبًا يستخدم <code dir=\"ltr\">--nw-focus-ring-width</code> و<code dir=\"ltr\">--nw-focus-ring-color</code> مع <code dir=\"ltr\">outline-offset</code> بقيمة <code dir=\"ltr\">--nw-focus-ring-offset</code>.</li>\n<li>اجعل <strong>Show only late</strong> زر تبديل حقيقيًا: كل نقرة تقلب <code dir=\"ltr\">aria-pressed</code> بين <code dir=\"ltr\">&quot;true&quot;</code> و<code dir=\"ltr\">&quot;false&quot;</code>. وحين يكون مضغوطًا تكون خلفيته <code dir=\"ltr\">--nw-color-action-bg-subtle</code> وحدّه <code dir=\"ltr\">--nw-color-action-bg</code>.</li>\n<li>الزر <strong>Save route</strong> يحمل <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code>: يجب ألّا يحفظ عند النقر، ويستخدم نصّه <code dir=\"ltr\">--nw-color-text-disabled</code>، ويكون المؤشر فوقه <code dir=\"ltr\">not-allowed</code>.</li>\n</ol>\n","hints":["نسّق الحالات بسماتها: <code dir=\"ltr\">.nw-button[aria-pressed=&quot;true&quot;]</code> و<code dir=\"ltr\">.nw-button[aria-disabled=&quot;true&quot;]</code>. وخصّص الـ component tokens داخلها.","في معالج النقر لزر التبديل، اقرأ <code dir=\"ltr\">aria-pressed</code> ثم اجعله عكس قيمته بـ <code dir=\"ltr\">setAttribute('aria-pressed', String(!pressed))</code>.","في أول معالج الحفظ، استخدم <code dir=\"ltr\">return</code> مبكرًا حين يكون <code dir=\"ltr\">save.getAttribute('aria-disabled') === 'true'</code>."],"explanation":"<p>صارت الحلقة تظهر مع تركيز لوحة المفاتيح عبر <code dir=\"ltr\">:focus-visible</code>، وهي مأخوذة من tokens، فيستطيع كل مكوّن وكل theme مشاركتها. وشكل زر التبديل تقوده <code dir=\"ltr\">aria-pressed</code>، فلا يمكن أن تختلف الحالة المرئية عن الحالة المُعلَنة أبدًا. و<code dir=\"ltr\">aria-disabled</code> تُبقي زر الحفظ قابلًا للتركيز حتى يمكن الوصول إلى شرحه عبر <code dir=\"ltr\">aria-describedby</code>، ولهذا يجب أن يحرس معالج النقر الإجراء بنفسه. في النظام الحقيقي يفعل مكوّن Button كل هذا داخليًا، وتحصل عليه الفرق افتراضيًا.</p>\n","checkLabels":{"focus-visible-rule":"تنسيق التركيز يستخدم <code dir=\"ltr\">:focus-visible</code>، ولا توجد قاعدة <code dir=\"ltr\">:focus</code> تحذف الـ outline","ring":"تركيز لوحة المفاتيح يُظهر حلقة صلبة بعرض 2px بلون التركيز مع إزاحة 2px","toggle":"النقر على <strong>Show only late</strong> يقلب <code dir=\"ltr\">aria-pressed</code> ونمط الضغط معه","blocked":"النقر على زر Save المعلَّم بـ <code dir=\"ltr\">aria-disabled</code> لا يحفظ","disabled-look":"يستخدم زر Save لون النص المعطّل ومؤشر <code dir=\"ltr\">not-allowed</code>"}},"words":607,"readMinutes":4}