{"id":"l-a11y-3-3","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"طبّق ARIA فقط حيث يقصر HTML، وأبقِ حالات مثل aria-pressed وaria-expanded متزامنة، وأعلن التحديثات الديناميكية عبر live regions من نوع status وalert.","html":"<p>كل ما سبق استخدم HTML عاديًا، وهذا مقصود. عناصر HTML تأتي ومعها الأدوار والأسماء والحالات وسلوك لوحة المفاتيح جاهزة. لكن أحيانًا ينفد ما في HTML. لا يوجد عنصر أصلي لحالة الضغط في زر التبديل، ولا لمجموعة تبويبات، ولا لـ «قل هذا بصوت عالٍ حين تتغيّر السلة». هنا يأتي دور <strong>ARIA</strong> ‏(Accessible Rich Internet Applications): مجموعة من الخصائص تتيح لك وصف الأدوار والحالات والعلاقات مباشرةً لشجرة إمكانية الوصول.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">القاعدة الأولى في ARIA</h2>\n<p>تبدأ إرشادات W3C نفسها بقاعدة تفاجئ الناس: <strong>إن استطعت استخدام عنصر HTML أصلي فيه السلوك الذي تحتاجه، فاستخدمه بدلًا من ARIA.</strong> وتظلّ فحوص WebAIM السنوية لأشهر مليون صفحة رئيسية تجد أن الصفحات التي تستخدم ARIA فيها أخطاء مرصودة أكثر، لا أقل. والسبب بسيط. ARIA تغيّر فقط ما <strong>يُعلَن</strong>. لا تجعل أي شيء قابلًا للتركيز، ولا تضيف معالجة للوحة المفاتيح، ولا تغيّر المظهر. كل خاصية ARIA وعدٌ للتقنيات المساعدة، وعلى كود JavaScript الذي تكتبه أن يفي به.</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:#6E7781;--shiki-dark:#8B949E\">&#x3C;!-- A promise with nothing behind it --></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</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\">\"button\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">&#x3C;!-- The same promise, kept by the browser --></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:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<h2 id=\"sec-2\" tabindex=\"-1\">الأدوار والخصائص والحالات</h2>\n<p>تنقسم خصائص ARIA (attributes) إلى ثلاث مجموعات.</p>\n<p><strong>الأدوار (roles)</strong> تقول ما هو الشيء: <code dir=\"ltr\">role=&quot;tab&quot;</code>، <code dir=\"ltr\">role=&quot;dialog&quot;</code>، <code dir=\"ltr\">role=&quot;switch&quot;</code>، <code dir=\"ltr\">role=&quot;status&quot;</code>. ونادرًا ما تحتاج إليها لأشياء يوفّرها HTML أصلًا.</p>\n<p><strong>الخصائص بالمعنى الدقيق (properties)</strong> تصف العلاقات أو السمات الثابتة: <code dir=\"ltr\">aria-labelledby</code> و<code dir=\"ltr\">aria-describedby</code> (استخدمتهما كلتيهما في النماذج)، و<code dir=\"ltr\">aria-controls</code>، و<code dir=\"ltr\">aria-required</code>، و<code dir=\"ltr\">aria-haspopup</code>.</p>\n<p><strong>الحالات (states)</strong> تصف أشياء تتغيّر مع تفاعل المستخدم، ويجب أن يحدّثها السكربت: <code dir=\"ltr\">aria-expanded</code>، <code dir=\"ltr\">aria-pressed</code>، <code dir=\"ltr\">aria-checked</code>، <code dir=\"ltr\">aria-selected</code>، <code dir=\"ltr\">aria-current</code>، <code dir=\"ltr\">aria-disabled</code>، <code dir=\"ltr\">aria-invalid</code>.</p>\n<p>قلب قائمة الأمنيات في Ticketly مثال جيد. إنه زر حقيقي أصلًا، لكن لا شيء يخبر قارئ الشاشة إن كانت الفعالية محفوظة أم لا. تحوّله <code dir=\"ltr\">aria-pressed</code> إلى زر تبديل (toggle button):</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\">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\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"fav\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Save to wishlist&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</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\">ticketly/wishlist.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:#1F2328;--shiki-dark:#E6EDF3\">fav.</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\"> saved</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fav.</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-pressed'</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\">  fav.</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-pressed'</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\">saved));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>صار NVDA يقول “Save to wishlist, toggle button, not pressed”، ثم “pressed” بعد التفعيل. لاحظ أن التسمية تبقى كما هي؛ والحالة هي التي تحمل التغيير. ويمكنك التنسيق انطلاقًا من الحالة مباشرةً، وهذا يمنع المظهر والدلالة من الافتراق:</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:#0550AE;--shiki-dark:#79C0FF\">.fav</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:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </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\">  color</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>وللصفحة الحالية في قائمة تنقّل، تخبر <code dir=\"ltr\">aria-current=&quot;page&quot;</code> مستخدمي قارئ الشاشة بالرابط الذي هم عليه: “Tickets, current page, link”.</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>aria-hidden على عنصر قابل للتركيز</span></p>\n<p>رأى مطوّر أن زر “Add to basket” يُعلَن مرتين (مرة في البطاقة ومرة في شريط ثابت)، فأضاف <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> إلى أحدهما. الزر ما زال في ترتيب Tab، فيصل إليه مستخدمو لوحة المفاتيح، ولا يقول قارئ الشاشة شيئًا لأنه مخفي عن الشجرة. لا تُخفِ العناصر القابلة للتركيز أبدًا. إن كان العنصر مكرّرًا فعلًا، فاحذفه، أو اجعله <code dir=\"ltr\">inert</code>.</p>\n</aside>\n<p>وبضع قواعد أخرى من مذكّرة “Using ARIA” الصادرة عن W3C تُبعدك عن المتاعب:</p>\n<ul>\n<li>لا تغيّر الدلالات الأصلية إلا إذا اضطررت فعلًا: <code dir=\"ltr\">&lt;h2 role=&quot;button&quot;&gt;</code> يدمّر العنوان. ضع زرًّا داخل العنوان بدلًا من ذلك.</li>\n<li>كل عنصر تحكّم تفاعلي مبني بـ ARIA يجب أن يكون قابلًا للتشغيل بلوحة المفاتيح.</li>\n<li>كل عنصر تفاعلي يحتاج إلى اسم متاح.</li>\n</ul>\n<h2 id=\"sec-3\" tabindex=\"-1\">الـ Live Regions: إعلان التغييرات</h2>\n<p>حين يضيف Ticketly تذكرة، يتغيّر عدّاد السلة في الترويسة من 1 إلى 2. يلاحظ المبصرون ذلك. أما مستخدمو قارئ الشاشة فلا يسمعون شيئًا، لأن تركيزهم ما زال على الزر الذي ضغطوه. الـ <strong>live region</strong> عنصر تُقرأ تغييرات محتواه تلقائيًا، دون نقل التركيز.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">ticketly/index.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\">p</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\">\"basket-status\"</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\">\"status\"</span><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\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"5\"><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:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> addToBasket</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  count </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\">  basketStatus.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Ticket added. ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">count</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} tickets in your basket.`</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<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 180\" role=\"img\" aria-labelledby=\"l-a11y-3-3-t1\"><defs><marker id=\"arrow-l-a11y-3-3-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-3-t1\">عنصر status فارغ موجود منذ تحميل الصفحة. حين يغيّر السكربت نصه، يطلق المتصفّح حدث تغيير عبر واجهة إمكانية الوصول، فينطق قارئ الشاشة النص الجديد بعد الكلام الحالي، بينما يبقى التركيز على الزر.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"60\" width=\"170\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"105\" y=\"88\" text-anchor=\"middle\">role=\"status\"</text>\n  <text class=\"d-label-muted\" x=\"105\" y=\"108\" text-anchor=\"middle\">فارغ، عند التحميل</text>\n  <path class=\"d-arrow\" d=\"M190 90 L260 90\" marker-end=\"url(#arrow-l-a11y-3-3-1)\"/>\n  <text class=\"d-code\" x=\"225\" y=\"78\" text-anchor=\"middle\">textContent</text>\n  <rect class=\"d-box-primary\" x=\"265\" y=\"60\" width=\"170\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"350\" y=\"88\" text-anchor=\"middle\">يرى المتصفّح</text>\n  <text class=\"d-label\" x=\"350\" y=\"108\" text-anchor=\"middle\">تغييرًا</text>\n  <path class=\"d-arrow\" d=\"M435 90 L500 90\" marker-end=\"url(#arrow-l-a11y-3-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"505\" y=\"60\" width=\"160\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"585\" y=\"85\" text-anchor=\"middle\">\"Ticket added.</text>\n  <text class=\"d-label\" x=\"585\" y=\"105\" text-anchor=\"middle\">2 tickets…\"</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"160\" text-anchor=\"middle\">التركيز لا يتحرّك أبدًا؛ تُنطق الرسالة حين يهدأ الكلام</text>\n</svg><figcaption>كيف يحدث إعلان الـ live region</figcaption></figure>\n<p>ثلاث قواعد تجعل الـ live regions موثوقة:</p>\n<ol>\n<li><strong>يجب أن تكون المنطقة موجودة قبل التغيير.</strong> تراقب قارئات الشاشة الـ live regions الموجودة بحثًا عن تغييرات. وإدراج عنصر جديد يحتوي نصًا أصلًا كثيرًا ما يفوتها. اعرض المنطقة فارغةً عند تحميل الصفحة.</li>\n<li><strong>اختر الدرجة: مهذّبة أم حازمة.</strong> ‏<code dir=\"ltr\">role=&quot;status&quot;</code> (مثل <code dir=\"ltr\">aria-live=&quot;polite&quot;</code>) تنتظر حتى ينتهي قارئ الشاشة من جملته الحالية. و<code dir=\"ltr\">role=&quot;alert&quot;</code> (حازمة، assertive) تقاطعه. احتفظ بالـ alert للرسائل العاجلة والنادرة مثل فشل الدفع؛ فإعلان “Ticket added” بحزم مع كل نقرة مُرهِق.</li>\n<li><strong>اجعل الرسائل قصيرة وذات معنى.</strong> أعلن النتيجة (“2 tickets in your basket”)، لا الآلية (“basket component updated”).</li>\n</ol>\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>الـ live regions مخصّصة لتحديثات الحالة بينما يواصل المستخدم عمله. أما إن كان على المستخدم أن يتصرّف بناءً على المحتوى الجديد، كما في نافذة حوار أو خطأ في حقل نموذج، فانقل التركيز إليه بدلًا من ذلك. ونقل التركيز مع إعلان الرسالة نفسها سيجعل قارئ الشاشة يقولها مرتين.</p>\n</aside>\n<p>تصبح حالات ARIA وأدوارها في أقوى صورها في الـ widgets المخصّصة، حيث لا يوجد أي عنصر HTML مقابل. يبني الدرس التالي ثلاثة منها وفق دليل ARIA Authoring Practices.</p>\n","toc":[{"id":"sec-1","text":"القاعدة الأولى في ARIA","level":2},{"id":"sec-2","text":"الأدوار والخصائص والحالات","level":2},{"id":"sec-3","text":"الـ Live Regions: إعلان التغييرات","level":2}],"takeaways":["القاعدة الأولى في ARIA أن تستخدم عنصر HTML أصليًا حين يوجد؛ فـ ARIA تغيّر ما يُعلَن، ولا تغيّر أبدًا كيف يتصرّف أي شيء.","تنقسم ARIA إلى أدوار (ما هو العنصر)، وخصائص (العلاقات والسمات الثابتة)، وحالات (قيم تتغيّر، ويجب أن يُبقيها السكربت متزامنة).","لا تضع أبدًا aria-hidden=“true” على عنصر قابل للتركيز، ولا تتجاوز دور عنصر أصلي له الدور الصحيح أصلًا.","يجب أن تكون الـ live region موجودة في الـ DOM قبل أن يتغيّر نصها؛ استخدم role=“status” للتحديثات المهذّبة، وrole=“alert” للعاجلة فقط."],"further":[{"title":"W3C: Using ARIA (the rules of ARIA use)","url":"https://www.w3.org/TR/using-aria/"},{"title":"MDN: ARIA live regions","url":"https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions"},{"title":"W3C APG: Read Me First","url":"https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/"}],"quiz":[{"q":"<p>ما الذي تغيّره إضافة <code dir=\"ltr\">role=&quot;button&quot;</code> إلى عنصر <code dir=\"ltr\">&lt;span&gt;</code>؟</p>","options":[{"text":"تجعل الـ span قابلًا للتركيز وللنقر بمفتاحي Enter وSpace","why":"ARIA لا تغيّر شيئًا في السلوك. ستظلّ تحتاج إلى tabindex=“0” ومعالجات للمفاتيح، وهي أشياء يمنحها الزر الحقيقي مجانًا."},{"text":"تغيّر تنسيق الـ span ليطابق تنسيق الأزرار في المتصفّح","why":"ليس لـ ARIA أي أثر في التنسيق. يبدو الـ span تمامًا كما كان."},{"text":"فقط طريقة إعلان التقنيات المساعدة له، على أنه “button”","why":"صحيح. الأدوار تغيّر شجرة إمكانية الوصول ولا شيء غيرها، ولهذا فإن زر ARIA بلا دعم للوحة المفاتيح وعدٌ مكسور."}],"answer":2},{"q":"<p>يعرض Ticketly الرسالة “Ticket added. 2 tickets in your basket.” في هذا العنصر بعد النقر. لماذا تبقى قارئات الشاشة صامتة؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><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:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> msg</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\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'p'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">msg.</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\">'role'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'status'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">msg.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Ticket added. 2 tickets in your basket.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">basket.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(msg);</span></span></code></pre></div>\n","options":[{"text":"role=“status” لا يُعلَن إلا للأخطاء","why":"status هو دور الـ live region المهذّبة ويُعلَن مع أي تغيير؛ أما alert فهو الدور الحازم للرسائل العاجلة."},{"text":"عناصر الفقرات لا يمكن أن تكون live regions","why":"أي عنصر يمكن أن يكون live region. المشكلة في التوقيت، لا في نوع العنصر."},{"text":"يحتاج أيضًا إلى aria-live=“assertive” ليُعلَن أصلًا","why":"المناطق المهذّبة تُعلَن أيضًا، بعد أن ينتهي الكلام الحالي للمستخدم. أما assertive فتغيّر الأولوية فقط."},{"text":"أُنشئت الـ live region مع نصها في اللحظة نفسها، فلم يكن هناك تغيير يرصده قارئ الشاشة","why":"صحيح. ضع عنصر role=“status” فارغًا في الصفحة عند التحميل، ثم اضبط textContent فيه حين يحدث شيء."}],"answer":3},{"q":"<p>أيّ هذه الأمثلة يستخدم ARIA بشكل صحيح؟</p>","options":[{"text":"<code dir=\"ltr\">&lt;button aria-pressed=&quot;false&quot;&gt;Save to wishlist&lt;/button&gt;</code>، ويُحدَّث إلى “true” عند الحفظ","why":"صحيح. زر التبديل يكشف حالته عبر aria-pressed، والسكربت يُبقيها متزامنة، فيسمع المستخدمون “Save to wishlist, toggle button, pressed”."},{"text":"<code dir=\"ltr\">&lt;h2 role=&quot;button&quot; onclick=&quot;toggle()&quot;&gt;Refunds&lt;/h2&gt;</code>","why":"هذا يستبدل دور العنوان، فيختفي من التنقّل بين العناوين، ويبقى «الزر» بلا دعم للوحة المفاتيح."},{"text":"<code dir=\"ltr\">&lt;a href=&quot;/faq&quot; aria-hidden=&quot;true&quot;&gt;FAQ&lt;/a&gt;</code>","why":"الرابط ما زال قابلًا للتركيز لكنه صار بلا اسم ولا دور في الشجرة. فيصل مستخدمو لوحة المفاتيح إلى محطة صامتة."},{"text":"<code dir=\"ltr\">&lt;nav role=&quot;navigation&quot;&gt;</code>","why":"ليس ضارًا، لكنه مكرّر. لعنصر nav دور navigation أصلًا، فالخاصية لا تضيف شيئًا."}],"answer":0},{"q":"<p>يرفض مزوّد الدفع بطاقة في صفحة الدفع في Ticketly. أيّ دور live region يناسب الرسالة “Payment failed. Your card was declined.”؟</p>","options":[{"text":"<code dir=\"ltr\">role=&quot;status&quot;</code>","why":"status تنتظر بتهذيب حتى ينتهي قارئ الشاشة من الكلام. أما لإخفاق يوقف المستخدم ويجب أن يسمعه الآن، فـ alert أنسب."},{"text":"<code dir=\"ltr\">role=&quot;log&quot;</code>","why":"log مخصّص لتسلسلات الرسائل مثل سجلّ المحادثة، حيث تُضاف المدخلات الجديدة في النهاية."},{"text":"<code dir=\"ltr\">role=&quot;alert&quot;</code>","why":"صحيح. alert حازمة: تقاطع الكلام الحالي، وهذا مناسب لرسالة نادرة وعاجلة يجب أن يتصرّف المستخدم بناءً عليها."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<nav aria-label=\"Site\">\n  <a href=\"#home\">Home</a>\n  <a href=\"#tickets\" class=\"active\">Tickets</a>\n  <a href=\"#faq\">FAQ</a>\n</nav>\n\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <button type=\"button\" id=\"add\">Add to basket</button>\n  <button type=\"button\" class=\"fav\" id=\"fav\">Save to wishlist</button>\n</article>\n\n<p id=\"basket-status\"></p>\n\n<div class=\"sticky-bar\">\n  <button type=\"button\" id=\"add-sticky\" aria-hidden=\"true\">Add to basket</button>\n</div>","css":"nav { display: flex; gap: 12px; margin-block-end: 12px; }\n.active { font-weight: 700; }\n.pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.pass h2 { margin: 0; flex-basis: 100%; font-size: 1.2rem; }\nbutton { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #5b3cc4; background: #fff; color: #5b3cc4; }\n.fav.on, .fav[aria-pressed=\"true\"] { background: #5b3cc4; color: #fff; }\n.sticky-bar { margin-block-start: 16px; padding: 8px; background: #f1eefb; }\n","js":"let count = 0;\nconst status = document.querySelector('#basket-status');\nconst fav = document.querySelector('#fav');\n\nfunction addToBasket() {\n  count += 1;\n  status.textContent = `Ticket added. ${count} tickets in your basket.`;\n}\ndocument.querySelector('#add').addEventListener('click', addToBasket);\ndocument.querySelector('#add-sticky').addEventListener('click', addToBasket);\n\nfav.addEventListener('click', () => {\n  fav.classList.toggle('on');\n});\n"},"solution":{"html":"<nav aria-label=\"Site\">\n  <a href=\"#home\">Home</a>\n  <a href=\"#tickets\" class=\"active\" aria-current=\"page\">Tickets</a>\n  <a href=\"#faq\">FAQ</a>\n</nav>\n\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <button type=\"button\" id=\"add\">Add to basket</button>\n  <button type=\"button\" class=\"fav\" id=\"fav\" aria-pressed=\"false\">Save to wishlist</button>\n</article>\n\n<p id=\"basket-status\" role=\"status\"></p>\n\n<div class=\"sticky-bar\">\n  <button type=\"button\" id=\"add-sticky\">Add Friday pass to basket</button>\n</div>","css":"nav { display: flex; gap: 12px; margin-block-end: 12px; }\n.active { font-weight: 700; }\n.pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.pass h2 { margin: 0; flex-basis: 100%; font-size: 1.2rem; }\nbutton { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #5b3cc4; background: #fff; color: #5b3cc4; }\n.fav.on, .fav[aria-pressed=\"true\"] { background: #5b3cc4; color: #fff; }\n.sticky-bar { margin-block-start: 16px; padding: 8px; background: #f1eefb; }\n","js":"let count = 0;\nconst status = document.querySelector('#basket-status');\nconst fav = document.querySelector('#fav');\n\nfunction addToBasket() {\n  count += 1;\n  status.textContent = `Ticket added. ${count} tickets in your basket.`;\n}\ndocument.querySelector('#add').addEventListener('click', addToBasket);\ndocument.querySelector('#add-sticky').addEventListener('click', addToBasket);\n\nfav.addEventListener('click', () => {\n  const saved = fav.getAttribute('aria-pressed') === 'true';\n  fav.setAttribute('aria-pressed', String(!saved));\n  fav.classList.toggle('on', !saved);\n});\n"},"checks":[{"id":"current-page","test":"const links = $$('nav a'); const cur = links.filter((a) => a.hasAttribute('aria-current')); return cur.length === 1 && cur[0].textContent.trim() === 'Tickets' && cur[0].getAttribute('aria-current') === 'page';"},{"id":"pressed-toggles","test":"const fav = $('#fav'); if (fav.tagName !== 'BUTTON' || fav.getAttribute('aria-pressed') !== 'false') return false; await click(fav); const on = fav.getAttribute('aria-pressed') === 'true'; await click(fav); return on && fav.getAttribute('aria-pressed') === 'false' && fav.textContent.trim() === 'Save to wishlist';"},{"id":"live-region","test":"const s = $('#basket-status'); const role = s.getAttribute('role'); const live = s.getAttribute('aria-live'); const isLive = role === 'status' || role === 'alert' || live === 'polite' || live === 'assertive'; if (!isLive) return false; await click('#add'); return /tickets? in your basket/.test(s.textContent);"},{"id":"polite","test":"const s = $('#basket-status'); return s.getAttribute('role') !== 'alert' && s.getAttribute('aria-live') !== 'assertive';"},{"id":"no-hidden-focusable","test":"const focusable = $$('a[href], button, input, select, textarea, [tabindex]').filter((el) => el.tabIndex >= 0 && !el.inert && !el.closest('[inert]')); return focusable.every((el) => !el.closest('[aria-hidden=\"true\"]')) && $$('button').length >= 3;"}],"title":"أضِف ما لا يستطيع HTML أن يمنحك إياه من ARIA","prompt":"<p>أربع مهام صغيرة بـ ARIA على بطاقة التذكرة في Ticketly:</p>\n<ol>\n<li>علِّم رابط <strong>Tickets</strong> بأنه الصفحة الحالية باستخدام <code dir=\"ltr\">aria-current=&quot;page&quot;</code>.</li>\n<li>اجعل <strong>Save to wishlist</strong> زر تبديل: يبدأ بـ <code dir=\"ltr\">aria-pressed=&quot;false&quot;</code> ويتبدّل مع كل نقرة. أبقِ نص التسمية كما هو.</li>\n<li>اجعل <code dir=\"ltr\">#basket-status</code> ‏live region <strong>مهذّبة</strong> لتُعلَن رسالة “Ticket added…”.</li>\n<li>على زر Add to basket في الشريط الثابت <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code>، وهذا يترك محطة Tab صامتة. أصلحه دون حذف الزر (يمكنك تغيير نصه).</li>\n</ol>\n","hints":["aria-current وaria-pressed خاصيتان عاديتان في HTML؛ وaria-pressed تحتاج أيضًا إلى تحديث في معالج النقر.","role=“status” هي live region مهذّبة. والعنصر موجود أصلًا عند التحميل، وهذا بالضبط ما تحتاجه الـ live regions.","احذف aria-hidden من الزر الثابت. وإن أزعجتك التسمية المكرّرة، فاجعلها أكثر تحديدًا، مثل “Add Friday pass to basket”."],"explanation":"<p>تضيف <code dir=\"ltr\">aria-current=&quot;page&quot;</code> عبارة “current page” إلى إعلان رابط Tickets. ويُبقي زر قائمة الأمنيات تسميته ويكشف حالته عبر <code dir=\"ltr\">aria-pressed</code>، التي يُبقيها معالج النقر متزامنة؛ ويمكن للـ CSS التنسيق انطلاقًا من الخاصية نفسها. و<code dir=\"ltr\">role=&quot;status&quot;</code> على الفقرة الموجودة والفارغة تعني أن كل رسالة جديدة تُقرأ بتهذيب دون نقل التركيز. وعاد الزر الثابت مرئيًا وقابلًا للتركيز في شجرة إمكانية الوصول: لا يجوز أبدًا أن يقع عنصر قابل للتركيز داخل <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code>، لأن مستخدمي لوحة المفاتيح يصلون إليه بينما تبقى قارئات الشاشة صامتة.</p>\n","checkLabels":{"current-page":"رابط Tickets وحده عليه aria-current=“page”","pressed-toggles":"يبدأ Save to wishlist بـ aria-pressed=“false” ويتبدّل مع كل نقرة","live-region":"‎#basket-status ‏live region وتُعلن عدد التذاكر في السلة","polite":"رسالة السلة مهذّبة، لا alert حازمة","no-hidden-focusable":"لا يوجد عنصر قابل للتركيز مخفي بـ aria-hidden"}},"words":667,"readMinutes":4}