{"id":"l-a11y-1-2","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"اشرح كيف تشغّل قارئات الشاشة وبرامج التكبير وأجهزة التبديل والتحكّم الصوتي صفحة الويب، واقرأ شجرة إمكانية الوصول التي يبنيها المتصفّح لها.","html":"<p>افتح Ticketly وشاشتك مطفأة. قد يبدو هذا استعراضًا، لكنه ما يفعله سمير كل يوم عمل، وهو أسرع طريقة لتفهم ما يرسله كودك فعلًا إلى من يستخدمونه. قبل أن تُصلح أي شيء، تحتاج إلى صورة واضحة عن كيف تستخرج التقنيات المساعدة (assistive technology، وتُختصر AT) المعلومات من صفحة الويب.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">شجرة إمكانية الوصول</h2>\n<p>لا تنظر التقنيات المساعدة إلى البكسلات. يأخذ المتصفّح الـ DOM، ويدمجه مع CSS، ويبني شجرة ثانية أبسط: <strong>شجرة إمكانية الوصول (accessibility tree)</strong>. لكل عقدة فيها بضع خصائص:</p>\n<ul>\n<li><strong>دور (role)</strong>: زر، رابط، عنوان (المستوى 2)، حقل نصي، مربع اختيار، قائمة، عام (generic)؛</li>\n<li><strong>اسم (name)</strong>: النص الذي يعرّف العنصر، مثل “Book now”؛</li>\n<li><strong>حالات وقيم</strong>: محدَّد، موسَّع، معطَّل، النص الحالي داخل حقل الإدخال.</li>\n</ul>\n<p>يسلّم المتصفّح هذه الشجرة إلى واجهة إمكانية الوصول البرمجية في نظام التشغيل، ومن هناك يقرؤها قارئ الشاشة أو برنامج التكبير أو برنامج التحكّم الصوتي.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-a11y-1-2-t1\"><defs><marker id=\"arrow-l-a11y-1-2-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-1-2-t1\">يغذّي الـ DOM وCSS المتصفّحَ الذي يبني شجرة إمكانية الوصول؛ وتُكشَف الشجرة عبر واجهة إمكانية الوصول في نظام التشغيل لقارئات الشاشة وبرامج التكبير وبرامج التبديل والتحكّم الصوتي.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"70\" width=\"130\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"75\" y=\"102\" text-anchor=\"middle\">DOM + CSS</text>\n  <text class=\"d-label-muted\" x=\"75\" y=\"124\" text-anchor=\"middle\">كودك</text>\n  <path class=\"d-arrow\" d=\"M140 105 L190 105\" marker-end=\"url(#arrow-l-a11y-1-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"195\" y=\"60\" width=\"160\" height=\"90\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"275\" y=\"95\" text-anchor=\"middle\">شجرة</text>\n  <text class=\"d-label-strong\" x=\"275\" y=\"115\" text-anchor=\"middle\">إمكانية الوصول</text>\n  <text class=\"d-label-muted\" x=\"275\" y=\"137\" text-anchor=\"middle\">دور، اسم، حالة</text>\n  <path class=\"d-arrow\" d=\"M355 105 L405 105\" marker-end=\"url(#arrow-l-a11y-1-2-1)\"/>\n  <rect class=\"d-box\" x=\"410\" y=\"70\" width=\"120\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"470\" y=\"102\" text-anchor=\"middle\">واجهة النظام</text>\n  <text class=\"d-label-muted\" x=\"470\" y=\"124\" text-anchor=\"middle\">UIA, AX, AT-SPI</text>\n  <path class=\"d-arrow\" d=\"M530 105 L570 40\" marker-end=\"url(#arrow-l-a11y-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M530 105 L570 105\" marker-end=\"url(#arrow-l-a11y-1-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M530 105 L570 170\" marker-end=\"url(#arrow-l-a11y-1-2-1)\"/>\n  <text class=\"d-label\" x=\"578\" y=\"45\">قارئ الشاشة</text>\n  <text class=\"d-label\" x=\"578\" y=\"110\">برنامج التكبير</text>\n  <text class=\"d-label\" x=\"578\" y=\"175\">الصوت، التبديل</text>\n</svg><figcaption>من الكود الذي تكتبه إلى أذن المستخدم</figcaption></figure>\n<p>هذا جزء من بطاقة الفعالية في Ticketly، وما تحتويه الشجرة له:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><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\">h3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Harbour Lights: Friday&#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\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"btn\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">book</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">('fri')\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Book now&#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\">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\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">book</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">('sat')\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Book now&#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=\"text\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Text</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>heading &quot;Harbour Lights: Friday&quot; (level 3)\ngeneric\n  text &quot;Book now&quot;\nbutton &quot;Book now&quot;</code></pre></div>\n<p>يعرض الـ div النص نفسه، لكن دوره عامّ (generic). يقرأ قارئ الشاشة “Book now” كجملة عادية، بلا أي إشارة إلى أنه يفعل شيئًا، ولا يمكن الوصول إليه بمفتاح Tab. يمكنك فحص هذا بنفسك: في أدوات المطوّر في Chrome أو Edge، حدّد عنصرًا وافتح لوحة <strong>Accessibility</strong>؛ وفي Firefox تبويب مخصّص اسمه Accessibility Inspector.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">أربع طرق يشغّل بها الناس Ticketly</h2>\n<p><strong>قارئات الشاشة</strong> (NVDA وJAWS على Windows، وVoiceOver على أجهزة Apple، وTalkBack على Android) تحوّل الشجرة إلى كلام منطوق أو إلى طريقة برايل. نادرًا ما يستمع المستخدمون إلى الصفحة من أعلاها إلى أسفلها. في استطلاع WebAIM لعام 2026، قال 67.8% من مستخدمي قارئ الشاشة إن أول ما يفعلونه في صفحة طويلة هو القفز بين العناوين. ويستدعون أيضًا قوائم بالروابط والـ landmarks وحقول النماذج. البنية التي تكتبها هي فهرس المحتويات بالنسبة لهم.</p>\n<p><strong>برامج تكبير الشاشة</strong> (Windows Magnifier وZoom في macOS وZoomText) تكبّر منطقة صغيرة تتبع التركيز أو المؤشّر. مها، مصمّمة في فريقي، تعمل بتكبير 400%: ترى نحو جزء من ستة عشر جزءًا من الشاشة في المرة الواحدة. أي شيء يحدث بعيدًا عن تركيزها، مثل إشعار منبثق في الزاوية المقابلة، لم يحدث أصلًا بالنسبة لها.</p>\n<p><strong>أجهزة التبديل (switch access)</strong>: يضغط مستخدموها مفتاحًا ماديًا واحدًا أو اثنين. ينتقل البرنامج بين العناصر القابلة للتركيز واحدًا تلو الآخر، وضغطة المفتاح تفعّل العنصر الحالي. إن لم يكن العنصر قابلًا للتركيز، فلا سبيل إلى الوصول إليه إطلاقًا. يعتمد مستخدمو أجهزة التبديل على قابلية التشغيل نفسها بلوحة المفاتيح التي ستبنيها في القسم 3.</p>\n<p><strong>التحكّم الصوتي</strong> (Voice Control في macOS وiOS، وDragon على Windows) يتيح للناس أن يقولوا “click Book now” أو “show numbers”. يطابق البرنامج الكلمات المنطوقة مع الأسماء المتاحة (accessible names). حين يظهر على الزر “Book seats” لكن عليه <code dir=\"ltr\">aria-label=&quot;cta-primary&quot;</code>، لا تطابق الكلمات التي تقولها دينا الاسم، فلا يحدث شيء. تسمّي WCAG هذا المعيار <strong>Label in Name</strong>: يجب أن يحتوي الاسم المتاح النص المرئي.</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><code dir=\"ltr\">display: none</code> و<code dir=\"ltr\">visibility: hidden</code> وخاصية <code dir=\"ltr\">hidden</code> تزيل المحتوى من شجرة إمكانية الوصول ومن الشاشة معًا. أما <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> فتزيله من الشجرة فقط. يستخدم الناس هذه الأدوات كأنها متبادلة، فإما أن يُخفوا نصًا مهمًّا عن قارئات الشاشة أو يجعلوها تُعلن حشوًا لا فائدة منه. قرّر أولًا من يجب أن يدرك المحتوى، ثم اختر الأداة.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">أن تُظهره للشجرة لا للعين</h2>\n<p>نمطان يغطّيان معظم الحالات في Ticketly.</p>\n<p>حين تحمل إشارة بصرية معنى ليس له نص، أضف نصًا لا تراه إلا الشجرة. يُعلِّم Ticketly الموعد الذي نفدت تذاكره بخطّ يشطب السعر ولون رمادي. فيُعلن قارئ الشاشة “SAR 180” ويحاول سمير بكل سرور أن يشتريه. أضف ملاحظة مخفية بصريًا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><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\">.visually-hidden</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\">  width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</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\">  margin</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-1</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\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  overflow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </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:#0550AE;--shiki-dark:#79C0FF\">  clip-path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">inset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</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:#0550AE;--shiki-dark:#79C0FF\">  white-space</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">nowrap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"3\"><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\">p</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\">\"price sold\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>SAR 180 &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</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\">\"visually-hidden\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(sold out)&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</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<p>وحين يكون العنصر زخرفيًا، أخفِه عن الشجرة. يضع Ticketly أيقونة تقويم صغيرة قبل كل تاريخ. قد تُعلَن عناصر SVG المضمّنة على أنها “image” أو “graphic”، وهذا ضجيج. وتزيلها <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"4\"><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\">p</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\">\"date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">svg</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\">\"icon\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-hidden</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\"> viewBox</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0 0 16 16\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">svg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> 12 June, 8 pm&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\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>الملاحظة المخفية بصريًا صحيحة حين ينقل التصميم البصري المعنى أصلًا. أما إن كان المبصرون قد يفوّتون الخطّ المشطوب أيضًا، فاكتب “Sold out” ظاهرًا للجميع. النص الذي يراه الجميع يفيد عددًا أكبر من الناس من نص لا يسمعه إلا بعضهم.</p>\n</aside>\n<p>الشجرة هي العقد بين كودك وبين كل تقنية مساعدة. في الدرس التالي سترى المعيار الذي يحدّد ما يجب أن يتضمّنه هذا العقد: WCAG 2.2.</p>\n","toc":[{"id":"sec-1","text":"شجرة إمكانية الوصول","level":2},{"id":"sec-2","text":"أربع طرق يشغّل بها الناس Ticketly","level":2},{"id":"sec-3","text":"أن تُظهره للشجرة لا للعين","level":2}],"takeaways":["التقنيات المساعدة لا تقرأ البكسلات؛ بل تقرأ شجرة إمكانية الوصول، حيث لكل عقدة دور (role) واسم وحالات أو قيمة.","يتنقّل مستخدمو قارئ الشاشة بين العناوين والـ landmarks والروابط وحقول النماذج، فالبنية هي فهرس المحتويات بالنسبة لهم.","يعتمد مستخدمو برامج التكبير وأجهزة التبديل والتحكّم الصوتي على تركيز مرئي، وتشغيل كامل بلوحة المفاتيح، وتسميات مرئية تطابق الأسماء المتاحة.","تزيل display none وخاصية hidden وaria-hidden المحتوى من الشجرة؛ أما كلاس visually-hidden فيخفيه عن العين ويُبقيه لقارئات الشاشة."],"further":[{"title":"MDN: Accessibility tree","url":"https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree"},{"title":"W3C WAI: Tools and Techniques used by people with disabilities","url":"https://www.w3.org/WAI/people-use-web/tools-techniques/"},{"title":"MDN: aria-hidden","url":"https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden"}],"quiz":[{"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\">div</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\">\"btn\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">book</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Book now&#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":"الدور “button” والاسم “Book now”","why":"لا يستنتج المتصفّح دور الزر من اسم الكلاس أو من معالج النقر. وحده عنصر button حقيقي (أو دور صريح) يُنتج ذلك."},{"text":"النص العادي “Book now” بلا دور وبلا طريقة لتفعيله","why":"صحيح. دور الـ div عامّ (generic)، فيقرأ قارئ الشاشة النص كمحتوى ثابت، ولا يجد المستخدم أي إشارة إلى أنه يفعل شيئًا."},{"text":"لا شيء على الإطلاق، لأن عناصر div ليست في شجرة إمكانية الوصول","why":"عناصر div التي تحتوي نصًا تظهر فعلًا، كحاويات عامة يمكن قراءة نصها. المشكلة هي غياب الدور، لا غياب العقدة."}],"answer":1},{"q":"<p>تستخدم مها تكبير الشاشة بنسبة 400%، فلا ترى إلا نحو جزء من ستة عشر جزءًا من الصفحة في كل مرة. يعرض Ticketly رسالة «أُضيف إلى السلة» في إشعار منبثق (toast) في الزاوية العليا اليمنى بينما تنقر هي زرًا في الأسفل يسارًا. ما العائق الرئيسي؟</p>","options":[{"text":"الإشعار المنبثق ليس له دور ARIA","why":"الدور يساعد مستخدمي قارئ الشاشة، لكن مها تقرأ بعينيها. حتى الإشعار المُعلَّم بإتقان يبقى غير مرئي لها إن ظهر بعيدًا عن الجزء الذي تراه من الشاشة."},{"text":"نص الإشعار قصير جدًا","why":"الطول ليس المشكلة هنا. الرسالة تظهر في جزء من الشاشة لا تنظر إليه."},{"text":"الرسالة تظهر بعيدًا عن موضع انتباهها وتركيزها","why":"صحيح. يرى مستخدمو برامج التكبير نافذة صغيرة حول التركيز أو المؤشّر، لذلك يجب أن تظهر الاستجابة قرب عنصر التحكّم الذي سبّبها."}],"answer":2},{"q":"<p>تتحكّم دينا في حاسوبها بالصوت. يعرض الزر النص “Book seats” لكن عليه <code dir=\"ltr\">aria-label=&quot;cta-primary&quot;</code>. ماذا يحدث حين تقول “click Book seats”؟</p>","options":[{"text":"يفشل الأمر، لأن الاسم المتاح “cta-primary” لا يحتوي الكلمات التي تراها","why":"صحيح. هذا يخالف معيار WCAG ‏2.5.3 Label in Name. احذف الـ aria-label ليأتي الاسم من النص المرئي."},{"text":"ينجح، لأن التحكّم الصوتي يقرأ النص المرئي","why":"برامج التحكّم الصوتي تطابق الكلمات المنطوقة مع الاسم المتاح، والـ aria-label يحلّ محلّ النص المرئي بوصفه الاسم."},{"text":"ينجح فقط إن كان للزر أيضًا خاصية title","why":"تُتجاهَل الـ title حين يوجد aria-label، ولن تساعد على أي حال اسمًا لا يطابق الكلمات المرئية."}],"answer":0},{"q":"<p>أيّ CSS يُخفي ملاحظة «نفدت التذاكر» عن المستخدمين المبصرين لكنه يُبقيها في شجرة إمكانية الوصول؟</p>","options":[{"text":"<code dir=\"ltr\">display: none</code>","why":"تزيل display none العنصر من شجرة إمكانية الوصول ومن الشاشة معًا، فلا تسمعه قارئات الشاشة أبدًا."},{"text":"<code dir=\"ltr\">visibility: hidden</code>","why":"visibility hidden تزيل المحتوى من شجرة إمكانية الوصول أيضًا."},{"text":"<code dir=\"ltr\">opacity: 0</code> على البطاقة الأم","why":"هذا يُخفي البطاقة كلها عن المبصرين، وهو عكس ما تريده تمامًا، ولا يساعد مستخدمي قارئ الشاشة أيضًا."},{"text":"كلاس visually-hidden يقصّ العنصر إلى مربع بحجم 1px مع <code dir=\"ltr\">position: absolute</code>","why":"صحيح. العنصر ما زال يُرسَم، فيبقى في الشجرة، لكنه لا يشغل أي مساحة مرئية."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h2>Harbour Lights dates</h2>\n<article class=\"event\">\n  <h3>Friday</h3>\n  <p class=\"date\"><svg class=\"icon\" viewBox=\"0 0 16 16\"><rect x=\"1\" y=\"3\" width=\"14\" height=\"12\" rx=\"2\"/></svg> 12 June, 8 pm</p>\n  <p class=\"price sold\" id=\"fri-price\"><svg class=\"icon\" viewBox=\"0 0 16 16\"><circle cx=\"8\" cy=\"8\" r=\"7\"/></svg> SAR 180</p>\n</article>\n<article class=\"event\">\n  <h3>Saturday</h3>\n  <p class=\"date\"><svg class=\"icon\" viewBox=\"0 0 16 16\"><rect x=\"1\" y=\"3\" width=\"14\" height=\"12\" rx=\"2\"/></svg> 13 June, 8 pm</p>\n  <p class=\"price\" id=\"sat-price\"><svg class=\"icon\" viewBox=\"0 0 16 16\"><circle cx=\"8\" cy=\"8\" r=\"7\"/></svg> SAR 180</p>\n</article>","css":".event { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; margin-block-end: 12px; }\n.event h3 { margin: 0 0 4px; }\n.event p { margin: 4px 0; display: flex; align-items: center; gap: 6px; }\n.icon { width: 16px; height: 16px; fill: #5b5670; }\n.sold { text-decoration: line-through; color: #6b6680; }\n\n/* Add a .visually-hidden class here if you need one */\n","js":""},"solution":{"html":"<h2>Harbour Lights dates</h2>\n<article class=\"event\">\n  <h3>Friday</h3>\n  <p class=\"date\"><svg class=\"icon\" aria-hidden=\"true\" viewBox=\"0 0 16 16\"><rect x=\"1\" y=\"3\" width=\"14\" height=\"12\" rx=\"2\"/></svg> 12 June, 8 pm</p>\n  <p class=\"price sold\" id=\"fri-price\"><svg class=\"icon\" aria-hidden=\"true\" viewBox=\"0 0 16 16\"><circle cx=\"8\" cy=\"8\" r=\"7\"/></svg> SAR 180 <span class=\"visually-hidden\">(sold out)</span></p>\n</article>\n<article class=\"event\">\n  <h3>Saturday</h3>\n  <p class=\"date\"><svg class=\"icon\" aria-hidden=\"true\" viewBox=\"0 0 16 16\"><rect x=\"1\" y=\"3\" width=\"14\" height=\"12\" rx=\"2\"/></svg> 13 June, 8 pm</p>\n  <p class=\"price\" id=\"sat-price\"><svg class=\"icon\" aria-hidden=\"true\" viewBox=\"0 0 16 16\"><circle cx=\"8\" cy=\"8\" r=\"7\"/></svg> SAR 180</p>\n</article>","css":".event { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; margin-block-end: 12px; }\n.event h3 { margin: 0 0 4px; }\n.event p { margin: 4px 0; display: flex; align-items: center; gap: 6px; }\n.icon { width: 16px; height: 16px; fill: #5b5670; }\n.sold { text-decoration: line-through; color: #6b6680; }\n\n.visually-hidden {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip-path: inset(50%);\n  white-space: nowrap;\n  border: 0;\n}\n","js":""},"checks":[{"id":"sold-out-text","test":"return /sold out/i.test($('#fri-price').textContent);"},{"id":"sold-out-in-tree","test":"const p = $('#fri-price'); const walker = document.createTreeWalker(p, NodeFilter.SHOW_TEXT); let n; while ((n = walker.nextNode())) { if (!/sold out/i.test(n.textContent)) continue; let el = n.parentElement; let ok = true; while (el) { const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || el.getAttribute('aria-hidden') === 'true' || el.hidden) ok = false; if (el === p) break; el = el.parentElement; } if (ok && !p.closest('[aria-hidden=\"true\"]')) return true; } return false;"},{"id":"icons-hidden","test":"const icons = $$('svg'); return icons.length === 4 && icons.every((s) => s.closest('[aria-hidden=\"true\"]'));"},{"id":"saturday-available","test":"return !/sold out/i.test($('#sat-price').textContent) && /SAR 180/.test($('#sat-price').textContent);"}],"title":"أخبر الشجرة بما تراه العين","prompt":"<p>سعر يوم الجمعة في Ticketly (<code dir=\"ltr\">#fri-price</code>) مشطوب وباهت اللون لأن تذاكر ذلك الموعد نفدت، لكن قارئ الشاشة لا يسمع إلا “SAR 180”. وفي الوقت نفسه قد تُعلَن كل أيقونة SVG زخرفية على أنها رسم بلا تسمية.</p>\n<ol>\n<li>أضف الكلمتين <strong>sold out</strong> إلى <code dir=\"ltr\">#fri-price</code> لتصلا إلى شجرة إمكانية الوصول. يمكنك إظهارهما، أو إخفاؤهما بصريًا باستخدام كلاس <code dir=\"ltr\">.visually-hidden</code> (لا <code dir=\"ltr\">display: none</code>).</li>\n<li>أخفِ أيقونات <code dir=\"ltr\">&lt;svg&gt;</code> الزخرفية الأربع كلها عن التقنيات المساعدة.</li>\n<li>اترك موعد السبت (<code dir=\"ltr\">#sat-price</code>) متاحًا.</li>\n</ol>\n","hints":["النص الذي عليه display: none أو visibility: hidden يُزال من شجرة إمكانية الوصول أيضًا، فلن يُعلَن.","كلاس visually-hidden يقصّ العنصر إلى 1px باستخدام position: absolute وoverflow: hidden وclip-path: inset(50%).","تحصل الأيقونات الزخرفية على aria-hidden=“true” على عنصر <svg> نفسه."],"explanation":"<p>الخطّ المشطوب بصري بحت، لذلك يحتاج المعنى إلى نص. العنصر <code dir=\"ltr\">&lt;span class=&quot;visually-hidden&quot;&gt;(sold out)&lt;/span&gt;</code> يُبقي الكلمات في شجرة إمكانية الوصول بينما يبقى التصميم كما هو؛ والنص المرئي العادي صحيح بالقدر نفسه، وربما أفضل للمبصرين الذين قد يفوّتون الخطّ المشطوب. الأيقونات لا تضيف شيئًا لا يقوله النص أصلًا، لذلك تزيلها <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> من الشجرة وتُبقي الإعلان قصيرًا: “Friday, heading level 3. 12 June, 8 pm. SAR 180 (sold out)”.</p>\n","checkLabels":{"sold-out-text":"سعر الجمعة يحتوي الكلمتين “sold out”","sold-out-in-tree":"نص “sold out” ليس مخفيًا عن التقنيات المساعدة","icons-hidden":"الأيقونات الزخرفية الأربع كلها مخفية باستخدام aria-hidden=“true”","saturday-available":"السبت ما زال يعرض SAR 180 وغير مُعلَّم بأن تذاكره نفدت"}},"words":667,"readMinutes":4}