{"id":"l-a11y-3-2","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"انقل تركيز لوحة المفاتيح عن قصد حين يتغيّر المحتوى دون تحميل صفحة جديدة، باستخدام عنصر dialog الأصلي وخاصية inert وإدارة التركيز عند تغيّر المسار.","html":"<p>في موقع الويب التقليدي، يتولّى المتصفّح أمر التركيز نيابةً عنك. كل نقرة على رابط تحمّل صفحة جديدة، فيعود التركيز إلى الأعلى، ويُعلن قارئ الشاشة العنوان الجديد. لكن Ticketly تطبيق أحادي الصفحة (single-page app) فيه نوافذ منبثقة وأدراج وتوجيه في جهة العميل، فلا يحدث شيء من ذلك. حين يغيّر كودك الشاشة، يقرّر كودك أين يذهب التركيز. وإن لم يقرّر، يبقى التركيز حيث كان، وكثيرًا ما يكون ذلك على شيء صار مخفيًا أو مغطّى أو محذوفًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">النافذة التي لا تحبس أحدًا</h2>\n<p>يفتح زر “Book now” في Ticketly نافذة حجز منبثقة: <code dir=\"ltr\">div</code> عليه طبقة معتمة. راقب ما يحدث بلوحة المفاتيح. تظهر النافذة، لكن التركيز ما زال على “Book now” خلفها. ينتقل Tab إلى رابط المهرجان التالي في الصفحة التي تحتها. ولا يقول قارئ شاشة سمير شيئًا عن أي نافذة؛ فبالنسبة له لم يحدث شيء.</p>\n<p>نافذة الحوار المشروطة (modal dialog) الصحيحة تحتاج إلى خمسة سلوكيات:</p>\n<ol>\n<li><strong>ينتقل</strong> التركيز إلى داخل النافذة حين تُفتح.</li>\n<li><strong>يبقى</strong> Tab وShift+Tab داخلها ما دامت مفتوحة.</li>\n<li><strong>تُخفى</strong> الصفحة التي خلفها عن التقنيات المساعدة.</li>\n<li><strong>يُغلقها</strong> مفتاح Escape.</li>\n<li><strong>يعود</strong> التركيز إلى عنصر التحكّم الذي فتحها.</li>\n</ol>\n<p>لسنوات كان هذا يتطلّب مكتبة لحبس التركيز وخمسين سطرًا من الكود. أما اليوم فعنصر <code dir=\"ltr\">&lt;dialog&gt;</code> الأصلي المفتوح بـ <code dir=\"ltr\">showModal()</code> يفعل السلوكيات الخمسة كلها.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">ticketly/booking.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\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"book\"</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>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">dialog</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\">\"booking\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"booking-title\"</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\">h2</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\">\"booking-title\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Book Friday pass&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h2</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\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"qty\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Quantity&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</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\">input</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\">\"qty\"</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\">\"number\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> min</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"1\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> max</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"6\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</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\">></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\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"confirm\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Confirm&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"close\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Close&#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\">dialog</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=\"1\"><div class=\"code-head\"><span class=\"code-title\">ticketly/booking.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> dialog</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\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#booking'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#book'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'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\"> dialog.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">showModal</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">());</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#close'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'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\"> dialog.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">close</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">());</span></span></code></pre></div>\n<p>يضع <code dir=\"ltr\">showModal()</code> النافذة في <strong>الطبقة العليا (top layer)</strong> فوق كل شيء، ويجعل بقية المستند <strong>inert</strong> (خاملًا): غير قابل للتركيز ولا للنقر وغائبًا عن شجرة إمكانية الوصول. يذهب التركيز إلى العنصر الذي عليه <code dir=\"ltr\">autofocus</code> إن وُجد، وإلا فإلى أول عنصر قابل للتركيز داخلها (هنا حقل الكمية)، وإلا فإلى النافذة نفسها. ويُغلقها Escape. وحين تُغلق، يعيد المتصفّح التركيز إلى العنصر الذي كان عليه قبلها، أي زر “Book now”. وتنسّق الـ pseudo-element ‏<code dir=\"ltr\">::backdrop</code> الطبقة المعتمة.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 190\" role=\"img\" aria-labelledby=\"l-a11y-3-2-t1\"><defs><marker id=\"arrow-l-a11y-3-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-3-2-t1\">يبدأ التركيز على زر Book now. ينقله showModal إلى أول حقل في النافذة، حيث يدور Tab داخل النافذة بينما الصفحة inert. ويعيد الإغلاق أو Escape التركيز إلى زر Book now.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"60\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"95\" y=\"95\" text-anchor=\"middle\">\"Book now\"</text>\n  <rect class=\"d-box-primary\" x=\"270\" y=\"40\" width=\"200\" height=\"100\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"370\" y=\"70\" text-anchor=\"middle\">dialog (الطبقة العليا)</text>\n  <text class=\"d-label\" x=\"370\" y=\"96\" text-anchor=\"middle\">Quantity → Confirm</text>\n  <text class=\"d-label\" x=\"370\" y=\"118\" text-anchor=\"middle\">→ Close → Quantity</text>\n  <path class=\"d-arrow\" d=\"M170 75 L265 65\" marker-end=\"url(#arrow-l-a11y-3-2-1)\"/>\n  <text class=\"d-code\" x=\"215\" y=\"55\" text-anchor=\"middle\">showModal()</text>\n  <path class=\"d-arrow\" d=\"M265 120 L170 108\" marker-end=\"url(#arrow-l-a11y-3-2-1)\"/>\n  <text class=\"d-code\" x=\"215\" y=\"140\" text-anchor=\"middle\">close / Esc</text>\n  <rect class=\"d-box-warn\" x=\"520\" y=\"60\" width=\"140\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"590\" y=\"88\" text-anchor=\"middle\">الصفحة خلفها</text>\n  <text class=\"d-label-muted\" x=\"590\" y=\"108\" text-anchor=\"middle\">inert</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"180\" text-anchor=\"middle\">لا يغادر Tab النافذة ما دامت مفتوحة</text>\n</svg><figcaption>التركيز خلال حياة نافذة الحوار</figcaption></figure>\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>استدعاء show() بدلًا من showModal()</span></p>\n<p>يفتح <code dir=\"ltr\">dialog.show()</code> نافذة حوار <strong>غير مشروطة (non-modal)</strong>: لا طبقة عليا، ولا خلفية inert، ولا معالجة لمفتاح Escape. وهو مناسب لأشياء مثل نافذة منبثقة في شريط أدوات يعمل المستخدمون معها إلى جانب الصفحة. أما لأي شيء فيه طبقة معتمة تحجب الصفحة، فاستخدم <code dir=\"ltr\">showModal()</code>. وتجنّب أيضًا تبديل خاصية <code dir=\"ltr\">open</code> مباشرةً؛ فهي تتصرّف مثل <code dir=\"ltr\">show()</code>.</p>\n</aside>\n<p>أعطِ كل نافذة حوار اسمًا بـ <code dir=\"ltr\">aria-labelledby</code> تشير إلى عنوانها. حين تُفتح، يقرأ NVDA ‏&quot;Book Friday pass, dialog, Quantity, spin button, 1&quot;، وهذا يخبر سمير بالضبط أين هو. وإن لم يكن أول حقل في النافذة أفضل نقطة بداية، كنافذة شروط طويلة مثلًا، فضع <code dir=\"ltr\">autofocus</code> على العنصر الذي يجب أن يتلقّى التركيز، مثل زر الإغلاق فيها، أو عنوانها بعد جعله قابلًا للتركيز بـ <code dir=\"ltr\">tabindex=&quot;-1&quot;</code> لتبدأ القراءة من الأعلى.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">inert لكل ما سوى ذلك</h2>\n<p>ليست كل لوحة مخفية نافذة حوار. تنزلق قائمة الهاتف في Ticketly خارج الشاشة حين تُغلق، لكن روابطها ما زالت في الـ DOM وما زالت قابلة للتركيز، فيمرّ مستخدمو لوحة المفاتيح على ثمانية روابط غير مرئية. خاصية <code dir=\"ltr\">inert</code> تُطفئ شجرة فرعية كاملة: لا تركيز، ولا نقر، ولا شجرة إمكانية وصول.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"2\"><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\">nav</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\">\"mobile-menu\"</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\">\"drawer\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> inert</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">nav</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=\"3\"><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\"> setMenuOpen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">open</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  menu.inert </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">open;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  menu.classList.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toggle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'drawer--open'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, open);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  toggle.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-expanded'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(open));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>خاصية <code dir=\"ltr\">inert</code> مدعومة في كل المتصفّحات الرئيسية منذ 2023. استخدمها للأدراج المغلقة، وشرائح الـ carousel الواقعة خارج الشاشة، والمحتوى الذي خلف طبقة مخصّصة لا تستطيع تحويلها إلى <code dir=\"ltr\">&lt;dialog&gt;</code>.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">التركيز بعد تغيّر المسار والحذف</h2>\n<p>حين يستبدل الـ router في Ticketly الصفحةَ الرئيسية بصفحة الدفع، يختفي الرابط الذي فعّله المستخدم، فيسقط التركيز إلى <code dir=\"ltr\">&lt;body&gt;</code>. تبدأ ضغطة Tab التالية من أعلى المستند، ولا يُعلن قارئ الشاشة شيئًا. افعل ما كان تحميل الصفحة سيفعله:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">ticketly/router.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> afterNavigate</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\"> heading</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\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'main h1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  document.title </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">heading</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">textContent</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} · Ticketly`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  heading.</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\">'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\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  heading.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>التركيز على <code dir=\"ltr\">h1</code> يُعلن اسم الصفحة الجديدة ويضع المستخدم في بداية محتواها. و<code dir=\"ltr\">tabindex=&quot;-1&quot;</code> تجعل العنوان قابلًا للتركيز من السكربت دون إضافة محطة لـ Tab. بعض الـ frameworks تفعل جزءًا من هذا نيابةً عنك؛ فتحقّق مما يفعله الـ framework الذي تستخدمه قبل أن تضيف كودك.</p>\n<p>والمبدأ نفسه ينطبق على الحذف. حين يحذف المستخدم تذكرة من السلة، يختفي زر “Remove” الذي ضغطه. انقل التركيز إلى مكان منطقي، مثل زر Remove للعنصر التالي أو عنوان السلة، بدلًا من تركه يسقط إلى أعلى الصفحة.</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>اختبر وعينك على document.activeElement</span></p>\n<p>في أدوات المطوّر، أنشئ Live Expression لـ <code dir=\"ltr\">document.activeElement</code>. وأنت تفتح نوافذ الحوار وتغلقها أو تتنقّل، يمكنك أن تراقب بالضبط أين التركيز، وهذا أسرع بكثير من التخمين من الشاشة.</p>\n</aside>\n<p>العناصر الأصلية وبضعة أسطر لإدارة التركيز تغطّي معظم Ticketly. وحين لا يوجد عنصر أصلي، تحتاج إلى ARIA، وهذا موضوع الدرس التالي.</p>\n","toc":[{"id":"sec-1","text":"النافذة التي لا تحبس أحدًا","level":2},{"id":"sec-2","text":"inert لكل ما سوى ذلك","level":2},{"id":"sec-3","text":"التركيز بعد تغيّر المسار والحذف","level":2}],"takeaways":["حين يغيّر السكربت ما يظهر على الشاشة، يصبح هو المسؤول عن وجهة التركيز؛ فالمتصفّح لا يدير التركيز إلا عند تحميل صفحة كاملة.","نافذة الحوار التي تُفتح بـ showModal() تنقل التركيز إلى داخلها، وتجعل بقية الصفحة inert، وتُغلق بمفتاح Escape، وتعيد التركيز إلى العنصر الذي فتحها حين تُغلق.","خاصية inert تُخرج شجرة فرعية كاملة من التركيز والنقر وشجرة إمكانية الوصول، وهي مثالية للأدراج المغلقة واللوحات الواقعة خارج الشاشة.","بعد تغيّر المسار في جهة العميل، حدّث document.title وانقل التركيز إلى عنوان h1 في الصفحة الجديدة (مع tabindex=“-1”)."],"further":[{"title":"MDN: The Dialog element","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog"},{"title":"MDN: HTMLElement inert property","url":"https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert"},{"title":"W3C APG: Dialog (Modal) Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/"}],"quiz":[{"q":"<p>يضغط مستخدم لوحة المفاتيح Enter على “Book now”، فيُشغَّل هذا الكود. أين يكون التركيز بعد ذلك؟</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:#1F2328;--shiki-dark:#E6EDF3\">bookButton.</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:#1F2328;--shiki-dark:#E6EDF3\">  document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.modal'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// a div styled as a modal</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n","options":[{"text":"ما زال على زر “Book now”، خلف النافذة","why":"صحيح. إظهار div لا ينقل شيئًا. ضغطة Tab التالية تستكمل التنقّل في الصفحة خلف الطبقة المعتمة، وقد لا يذكر قارئ الشاشة النافذة إطلاقًا."},{"text":"على أول حقل داخل النافذة، لأن النوافذ تتلقّى التركيز تلقائيًا","why":"وحده عنصر dialog الأصلي المفتوح بـ showModal() ينقل التركيز. أما الـ div الذي حُذفت منه hidden فمجرد محتوى إضافي."},{"text":"على عنصر body، لأن النقر نقل التركيز بعيدًا","why":"تفعيل الزر لا ينقل التركيز بعيدًا عنه. التركيز لا يتغيّر إلا إذا غيّره كودك."}],"answer":0},{"q":"<p>أيّ هذه الأشياء لا يفعله <code dir=\"ltr\">dialog.showModal()</code> نيابةً عنك؟</p>","options":[{"text":"جعل المحتوى خارج النافذة inert","why":"يضع showModal() النافذة في الطبقة العليا (top layer) ويجعل بقية المستند inert، فلا يستطيع Tab الهروب إلى الصفحة خلفها."},{"text":"إغلاق النافذة حين يضغط المستخدم Escape","why":"نوافذ الحوار المشروطة (modal) تُغلق بـ Escape افتراضيًا، وتطلق حدث cancel يمكنك الاستماع إليه."},{"text":"منح النافذة اسمًا متاحًا","why":"صحيح. ما زلت تحتاج إلى تسميتها، عادةً بـ aria-labelledby تشير إلى عنوانها، لتُعلن قارئات الشاشة “Book Friday pass, dialog”."},{"text":"نقل التركيز إلى داخل النافذة","why":"يركّز showModal() على عنصر عليه autofocus، وإلا فعلى أول عنصر قابل للتركيز داخلها، وإلا فعلى النافذة نفسها."}],"answer":2},{"q":"<p>تنزلق قائمة الهاتف في Ticketly خارج الشاشة حين تُغلق، لكن Tab ما زال يزور روابطها الثمانية وهي غير مرئية. ما أبسط إصلاح متين؟</p>","options":[{"text":"أضف <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> إلى القائمة حين تكون مغلقة","why":"aria-hidden تُخفي المحتوى عن التقنيات المساعدة لكنها تتركه قابلًا للتركيز، فيظلّ مستخدمو لوحة المفاتيح ينتقلون إلى روابط غير مرئية، وهذه المرة دون أي إعلان."},{"text":"أضف <code dir=\"ltr\">tabindex=&quot;-1&quot;</code> إلى كل رابط من الروابط الثمانية","why":"يعمل هذا إلى أن يضيف أحدهم رابطًا تاسعًا، وعليك أن تتذكّر التراجع عنه عند الفتح. أما inert فتعالج الشجرة الفرعية كلها بخاصية واحدة."},{"text":"اضبط خاصية <code dir=\"ltr\">inert</code> على القائمة ما دامت مغلقة","why":"صحيح. تُخرج inert الشجرة الفرعية كلها من ترتيب Tab وأحداث المؤشّر وشجرة إمكانية الوصول، وحذف الخاصية يعيد كل شيء."}],"answer":2},{"q":"<p>بعد تنقّل في جهة العميل من الصفحة الرئيسية إلى ‎&quot;/checkout&quot;، ماذا يجب أن يفعل الـ router في Ticketly لمستخدمي قارئ الشاشة؟</p>","options":[{"text":"لا شيء؛ قارئات الشاشة تكتشف تغيّرات عنوان URL تلقائيًا","why":"قارئات الشاشة تتفاعل مع تحميل الصفحات الكاملة. أما التنقّل عبر pushState فيغيّر الـ DOM بصمت، ويبقى التركيز على الرابط الذي نُقر، والذي ربما لم يعد موجودًا."},{"text":"إعادة تحميل الصفحة ليُعلنها المتصفّح","why":"هذا يهدر فوائد التوجيه في جهة العميل. وإدارة التركيز والعنوان تعطي النتيجة نفسها دون إعادة تحميل."},{"text":"نقل التركيز إلى أول رابط في ترويسة الموقع","why":"هذا يجبر المستخدم على المرور بالترويسة كلها من جديد في كل تنقّل، ولا يُعلن الصفحة التي هو فيها."},{"text":"تحديث document.title ونقل التركيز إلى عنوان h1 في صفحة الدفع، وعليه tabindex=“-1”","why":"صحيح. يسمع المستخدم العنوان الجديد، ويستكمل Tab من بداية المحتوى الجديد، ويطابق عنوان النافذة الصفحة."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<main>\n  <h1>Harbour Lights: Friday</h1>\n  <p>Standing tickets, SAR 180 each.</p>\n  <button type=\"button\" id=\"book\">Book now</button>\n  <a href=\"#faq\">Festival FAQ</a>\n</main>\n\n<div class=\"modal\" id=\"booking\" hidden>\n  <h2 id=\"booking-title\">Book Friday pass</h2>\n  <label for=\"qty\">Quantity</label>\n  <input id=\"qty\" type=\"number\" min=\"1\" max=\"6\" value=\"1\">\n  <button type=\"button\" id=\"confirm\">Confirm</button>\n  <button type=\"button\" id=\"close\">Close</button>\n</div>","css":".modal { border: 0; border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 40px rgb(0 0 0 / 0.3); }\ndiv.modal:not([hidden]) { position: fixed; inset: 10% 10% auto; background: #fff; }\n.modal::backdrop { background: rgb(20 16 40 / 0.6); }\n.modal h2 { margin-block-start: 0; }\n.modal input { display: block; margin-block: 4px 12px; font: inherit; }\n","js":"const modal = document.querySelector('#booking');\n\ndocument.querySelector('#book').addEventListener('click', () => {\n  modal.hidden = false;\n});\n\ndocument.querySelector('#close').addEventListener('click', () => {\n  modal.hidden = true;\n});\n"},"solution":{"html":"<main>\n  <h1>Harbour Lights: Friday</h1>\n  <p>Standing tickets, SAR 180 each.</p>\n  <button type=\"button\" id=\"book\">Book now</button>\n  <a href=\"#faq\">Festival FAQ</a>\n</main>\n\n<dialog class=\"modal\" id=\"booking\" aria-labelledby=\"booking-title\">\n  <h2 id=\"booking-title\">Book Friday pass</h2>\n  <label for=\"qty\">Quantity</label>\n  <input id=\"qty\" type=\"number\" min=\"1\" max=\"6\" value=\"1\">\n  <button type=\"button\" id=\"confirm\">Confirm</button>\n  <button type=\"button\" id=\"close\">Close</button>\n</dialog>","css":".modal { border: 0; border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 40px rgb(0 0 0 / 0.3); }\ndiv.modal:not([hidden]) { position: fixed; inset: 10% 10% auto; background: #fff; }\n.modal::backdrop { background: rgb(20 16 40 / 0.6); }\n.modal h2 { margin-block-start: 0; }\n.modal input { display: block; margin-block: 4px 12px; font: inherit; }\n","js":"const modal = document.querySelector('#booking');\n\ndocument.querySelector('#book').addEventListener('click', () => {\n  modal.showModal();\n});\n\ndocument.querySelector('#close').addEventListener('click', () => {\n  modal.close();\n});\n"},"checks":[{"id":"named-dialog","test":"const d = $('#booking'); if (!d || d.tagName !== 'DIALOG') return false; const ids = (d.getAttribute('aria-labelledby') || '').split(/\\s+/).filter(Boolean); const viaLabelledby = ids.some((id) => document.getElementById(id)?.textContent.includes('Book Friday pass')); return viaLabelledby || /Book Friday pass/.test(d.getAttribute('aria-label') || '');"},{"id":"opens-modal","test":"const b = $('#book'); b.focus(); await click(b); const d = $('#booking'); return !!d.open && d.matches(':modal');"},{"id":"focus-inside","test":"return $('#booking').contains(document.activeElement) && document.activeElement !== document.body;"},{"id":"page-inert","test":"const inside = document.activeElement; $('#book').focus(); const escaped = document.activeElement === $('#book'); inside.focus(); return !escaped;"},{"id":"focus-returns","test":"const d = $('#booking'); if (d.tagName !== 'DIALOG' || !d.open || !d.contains(document.activeElement)) return false; await click('#close'); await tick(30); return !d.open && document.activeElement === $('#book');"}],"title":"حوّل نافذة الحجز إلى نافذة حوار حقيقية","prompt":"<p>نافذة الحجز في Ticketly عنصر <code dir=\"ltr\">div</code> يظهر حين تضغط <strong>Book now</strong>. يبقى التركيز خلفها، وتبقى الصفحة التي تحتها قابلة للوصول، ولا تُبلَّغ قارئات الشاشة بأن نافذة حوار فُتحت.</p>\n<p>حوّل <code dir=\"ltr\">#booking</code> إلى عنصر <code dir=\"ltr\">&lt;dialog&gt;</code> أصلي (أبقِ الـ id) له اسم متاح مأخوذ من عنوانه “Book Friday pass”. افتحه <strong>كنافذة مشروطة (modal)</strong> من زر Book now وأغلقه من زر Close. وحين يُغلق، يجب أن يعود التركيز إلى Book now.</p>\n","hints":["غيّر الـ div إلى <dialog> واحذف خاصية hidden؛ فنافذة الحوار المغلقة مخفية أصلًا.","سمِّها بـ aria-labelledby=“booking-title”.","استخدم modal.showModal() للفتح وmodal.close() للإغلاق. ينقل المتصفّح التركيز إلى الداخل، ويجعل الصفحة inert، ويعيد التركيز عند الإغلاق."],"explanation":"<p>يؤدّي <code dir=\"ltr\">showModal()</code> العمل الذي كان يحتاج إلى مكتبة لحبس التركيز: ينقل التركيز إلى أول عنصر قابل للتركيز (حقل الكمية)، ويجعل كل ما هو خارج النافذة inert فلا يستطيع Tab ولا المؤشّر الافتراضي لقارئ الشاشة التجوّل خلفها، ويُغلقها بـ Escape، ويعيد التركيز إلى Book now حين تُغلق. و<code dir=\"ltr\">aria-labelledby</code> تجعل NVDA يُعلن “Book Friday pass, dialog” عند الفتح. لاحظ أن كود JavaScript بالكاد تغيّر؛ فالتحسّن يأتي من اختيار العنصر والدالة الصحيحين.</p>\n","checkLabels":{"named-dialog":"‎#booking عنصر <dialog> اسمه “Book Friday pass”","opens-modal":"زر Book now يفتحه كنافذة حوار مشروطة","focus-inside":"ينتقل التركيز إلى داخل النافذة حين تُفتح","page-inert":"لا تستطيع الصفحة التي خلفها تلقّي التركيز ما دامت النافذة مفتوحة","focus-returns":"زر Close يُغلق النافذة ويعود التركيز إلى Book now"}},"words":669,"readMinutes":4}