إنشاء قائمة منسدلة

إنشاء قائمة منسدلة

يتناول هذا الدليل أي قائمة منبثقة: القائمة المنسدلة وقائمة السياق والقائمة الكاملة وأشرطة القوائم التي تفتحها. المثال العملي هو قائمة أوامر التطبيق، حيث تغيّر العناصر الواجهة (إنشاء، حفظ، الإعدادات المفضّلة).

خياران مستقلان. وينطبق كل ما عدا ذلك هنا على كليهما.

  • العنصر، لكل سلعة الأمر هو <button type="button">، والوجهة هي <a href> لا تزال تفتح في علامة تبويب جديدة، ويتم نسخها كعنوان URL، وتعمل بدون نص برمجي. قد تخلط القائمة المنسدلة بينهما بحرية: رابطان وستة أوامر، أو أمر واحد وستة روابط، لا يغير شيئًا أدناه.
  • نموذج التفاعل، لكل قائمة منسدلة تُحوِّل مفاتيح الأسهم وHome/End وEsc القائمة إلى قائمة منسدلة: أضِف مجموعة التركيز، واجعلها تستنتج menuitem على كل عنصر سواء كان زرًا أو رابطًا، واجعلها تبدو كقائمة منسدلة (شريط مستمر أو زر تشغيل مع مؤشر قائمة منسدلة، راجِع "شريط القوائم أو أزرار القوائم المنفصلة" أدناه). قائمة قصيرة من الروابط التي يتنقّل المستخدمون Tab من خلالها: لا توجد مجموعة تركيز، ولا role="menu" أو role="menuitem" أو aria-haspopup="menu". احتفِظ بالعنصر المنبثق وEsc واستعادة التركيز، واطّلِع على دليل شريط التنقّل.

المحتوى لا يحدّد الأدوار أبدًا. يمكن أن تكون القائمة المنسدلة "المنتجات" للروابط قائمة، ولا يمكن أن تكون القائمة المنسدلة "الحساب" للأوامر تنقلاً.

استخدِم نمط القائمة فقط مع التطبيقات المصغّرة المركّبة التي تتضمّن علامة تبويب واحدة للتوقف والتنقّل باستخدام مفاتيح الأسهم. استخدِم <div focusgroup="menu nomemory">، وليس عنصر HTML‏ <menu>. لا تضِف role="menu" أو role="menuitem" في الترميز المشترَك: يستنتج focusgroup الأصلي هذه الأدوار، وتحظر الأدوار الصريحة هذا الاستنتاج. يجب إضافتها فقط في العنصر الاحتياطي للمتصفّح غير المتوافق أدناه. يؤدي الضغط على nomemory إلى إعادة فتح القائمة بدءًا من العنصر الأول. تنقل السمة Focusgroup التركيز، ويتولّى النص البرمجي المعاينة والتفعيل واستعادة التركيز.

فتح القائمة وتحديد موضعها

اجعل كل عامل تشغيل يستدعي أمرًا باستخدام commandfor وcommand="toggle-popover". يفتح المتصفّح النافذة المنبثقة ويغلقها عند النقر عليها أو الضغط على مفتاحَي Enter وSpace، ويُبقي زر التفعيل داخل مجموعة الإغلاق السريع للنافذة المنبثقة، لذا يؤدي النقر على زر تفعيل مفتوح إلى إغلاقه. لا تستخدِم popovertarget أو popovertargetaction، بل استخدِم commandfor بدلاً منهما.

ضع كل نافذة منبثقة بعد المشغّل مباشرةً. تضيف المتصفّحات علاقة aria-details ضمنية فقط عندما لا يكون العنصر المنبثق هو العنصر الشقيق التالي، لذا يتجنّب هذا الموضع ذلك بدون سمات إضافية.

<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
  commandfor="file-menu" command="toggle-popover">
  File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
  <!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
  <div focusgroup="menu nomemory" aria-labelledby="file-trigger">
    <button type="button">New file</button>
    <button type="button">Save</button>
    <button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
      commandfor="export-menu" command="toggle-popover">
      Export <span aria-hidden="true">›</span>
    </button>
    <!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
    <div id="export-menu" popover="auto">
      <div focusgroup="menu nomemory" aria-labelledby="export-trigger">
        <button type="button">PDF</button>
        <button type="button">Plain Text</button>
      </div>
    </div>
  </div>
</div>

أدخِل اسمًا لكل قائمة باستخدام aria-labelledby يشير إلى عامل التشغيل الخاص بها. يُرجى الاحتفاظ بمؤشرات القائمة الفرعية الزخرفية في CSS أو إضافة aria-hidden="true". امنح المشغّلات التي تحتوي على aria-expanded="true" نمطًا نشطًا مرئيًا حتى يتمكّن المستخدمون من معرفة القائمة المفتوحة أو التي تتم معاينتها.

اختَر نموذجًا واحدًا للمشغّلات ذات المستوى الأعلى. يجب أن يتطابق الدور ونموذج لوحة المفاتيح والتصميم: تشير الأزرار المنفصلة بصريًا إلى توقف واحد Tab لكل منها.

  • شريط القوائم: role="menubar" على الحاوية (وليس على نافذة منبثقة)، ويتم تنسيقه كشريط واحد متواصل بدون فواصل بين المشغّلات. محطة توقّف واحدة في Tab، ويمكنك استخدام ArrowLeft/ArrowRight للتنقّل بين المشغّلات، وتمرير مؤشر الماوس لفتح القوائم. يُستخدم لشريط أوامر التطبيق.
  • أزرار القائمة المنفصلة: كل عامل تشغيل هو نقطة توقّف Tab خاصة به ويظهر كزر مستقل. لا يتوفّر role="menubar"، ولا مفاتيح أسهم بين المشغّلات، ولا معاينة عند التركيز، ولا تبديل عند التمرير. يجب تضمينها في <nav> إذا كانت تتيح التنقّل، وإلا لن يتم تحديد أي دور. يُستخدم لأشرطة الأدوات وعناوين الصفحات.

ويوضّح الجزء المتبقي من هذا الدليل نموذج شريط القوائم. بالنسبة إلى الأزرار المنفصلة، يجب إبقاء كل شيء داخل كل قائمة وإلغاء السلوك على مستوى شريط القوائم.

تحديد موضع القائمة باستخدام ميزة "تحديد موضع الإرساء" في CSS

استخدِم ميزة CSS anchor positioning بدلاً من إحداثيات JavaScript. يضبط كل من command="toggle-popover" وshowPopover({ source: trigger }) علامة ارتساء ضمنية، لذا لا تضِف قواعد anchor-name أو position-anchor لكل قائمة. يجب ألا يعتمد سلوك القائمة على تحديد موضع الارتساء.

/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
  position-area: block-end span-inline-end;
  inset: auto;
}

/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
  position-area: inline-end span-block-end;
  position-try-fallbacks: flip-inline, flip-block, --below;
}

/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
   covers its trigger. */
@position-try --below {
  position-area: block-end span-inline-end;
  margin-inline-start: 1rem;
  /* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
  min-inline-size: 0;
  max-inline-size: calc(100% - 1rem);
}

يجب إبقاء --below في النهاية ليتم تطبيقه فقط عندما لا يتناسب أي من الجانبين. يتم فتحها مباشرةً أسفل المشغّل، لذا على الشاشة الضيقة، تغطي العناصر الشقيقة أسفل المشغّل؛ هذه هي المقايضة المقبولة للبقاء بجوار المشغّل. يمكنك أيضًا السماح بتقليص القائمة الفرعية لتناسب المساحة (min-inline-size: 0، max-inline-size ناقص المسافة البادئة). يتم تخطّي الخيار الاحتياطي الذي لا يتناسب مع المساحة، وإذا لم يتناسب أي خيار، يحتفظ المتصفّح بالموضع الأساسي، وبالتالي فإنّ القائمة الفرعية التي تكون عريضة جدًا ببضع بكسلات تغطي المشغّل بدون إشعار.

لمعرفة آلية عمل العناصر الاحتياطية في القوائم المنسدلة العادية، يُرجى الاطّلاع على القوائم المنسدلة المتوافقة مع إطار العرض. يحدّد هذا الدليل موضع مجموعة أزرار، وليس قائمة. بالنسبة إلى القوائم، تكون الأولوية لموضع القائمة الفرعية والحلّ البديل --below أعلاه.

تحريك الفتح والإغلاق

اختيارية: Under 150 ms, opacity and a small translate only, same for submenus, inside @media (prefers-reduced-motion: no-preference). ينتقل التركيز إلى toggle، وبالتالي لا يتأخر الانتقال أبدًا لمستخدمي لوحة المفاتيح.

@media (prefers-reduced-motion: no-preference) {
  /* Closing: this rule is in effect once :popover-open is removed. */
  [popover] {
    opacity: 0;
    translate: 0 -.25rem;
    transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
  }
  /* Opening: faster. */
  [popover]:popover-open {
    opacity: 1;
    translate: 0 0;
    transition-duration: .1s;
    @starting-style { opacity: 0; translate: 0 -.25rem; }
  }
  /* Switching between open menubar menus is instant, as in native menubars. */
  [role="menubar"]:has(> [popover]:popover-open) > [popover] {
    transition: none;
  }
}

‫"@starting-style" ليس تكرارًا لـ "[popover]": يكون العنصر المنبثق المغلق "display: none"، ولا يتم تشغيل الانتقالات من حالة غير معروضة، لذا يوفّر القيم السابقة للفتح. بدون هذه السمة، يتم فتح القائمة بسرعة وتتحرك فقط عند إغلاقها. يؤدي استخدام display وoverlay مع allow-discrete إلى إبقاء النافذة المنبثقة معروضة وفي الطبقة العليا إلى أن تنتهي عملية الانتقال عند الإغلاق.

لوحة المفاتيح والتركيز

لا تضِف نظام roving-tabindex ثانيًا داخل مجموعة تركيز. يغطي النص البرمجي فقط ما لا يفعله المستدعون ومجموعة التركيز:

  • علامة تبويب واحدة لشريط القوائم يدخل "Tab" عند أول عامل تشغيل على المستوى الأعلى. يؤدي الضغط على ArrowLeft وArrowRight إلى التنقّل بين المشغّلات ذات المستوى الأعلى والالتفاف عند أي من الطرفين. يؤدي الضغط على Home وEnd على مشغّل إلى الدخول إلى قائمته عند العنصر الأول أو الأخير.
    • ملاحظة: يحدّد هذا الخيار المشغّل الذي يعود إليه Tab. يسمح APG باستخدام أول مشغّل أو آخر مشغّل تم استخدامه. يحدّد الدليل الخيار الأول، ولكن العرض التوضيحي ينقل علامة التبويب باستخدام ArrowLeft وArrowRight، لذا بعد التنقّل باستخدام لوحة المفاتيح، يعود Shift+Tab إلى آخر عامل تشغيل تم الوصول إليه. ولا يؤدي استخدام المؤشر إلى تحريكه، لذا يمكن أن يؤدي التسلسل نفسه إلى تفعيل مشغّل مختلف. كلا السلوكين مقبولان، فاختَر أحدهما وحافظ على اتساقه.
  • المعاينة: يؤدي التركيز على مشغّل من المستوى الأعلى إلى عرض قائمته وضبط aria-expanded="true". يظل التركيز على المشغّل، ولا يتم التركيز على أي عنصر. يؤدي تمويه المشغّل إلى إغلاق قائمة المعاينة فقط.
  • مفتاحا Enter وSpace على معاينة يتم إغلاق المعاينة ويظل التركيز على المشغّل. يتولّى إجراء toggle-popover الأصلي معالجة ذلك، لذا يجب ألا يعترض النص البرمجي هذه المفاتيح.
  • ArrowDown على معاينة يؤدي هذا الإجراء إلى تحويله إلى قائمة مفتوحة والتركيز على العنصر الأول.
  • النقر بالمؤشر على عامل تشغيل مفتوح أو تمّت معاينته يتم إغلاقها بشكل تلقائي، ولا يتم الدخول إلى القائمة.
    • اختلاف معروف في عوامل تشغيل القائمة الفرعية يؤدي النقر بالمؤشر على مشغّل قائمة فرعية أيضًا إلى التبديل بشكل أصلي، بحيث يمكنه إغلاق قائمة فرعية تمت معاينتها بالفعل، أو فتح قائمة لم تظهر بعد. تفتح القوائم الأصلية فقط ولا تغلقها أبدًا. هذا غير ضار، لذا يقبله الدليل.
  • النقر بالمؤشر على عامل تشغيل مغلق يتم فتحها بشكلٍ أصلي. يظل التركيز على المشغّل، ولا يتم التركيز على أي عنصر. يمكن فقط لمستخدمي لوحة المفاتيح فتح القائمة.
  • المؤشر فوق عنصر: يتم التركيز عليه بدون حلقة، لذا فإنّ التمرير والتركيز هما عنصر واحد.
  • تمرير المؤشر فوق مشغّل آخر من المستوى الأعلى أثناء فتح قائمة يتم إغلاقها ومعاينة النافذة التي تم تمرير مؤشر الماوس فوقها، مع التركيز على مشغّلها بدون حلقة. ما مِن شيء مفتوح: لا يؤدي التمرير إلى أي إجراء.
  • المؤشر فوق مشغّل قائمة فرعية يعرض معاينة لقائمته الفرعية بعد تأخير قصير (يؤدي تركيز المؤشر إلى تشغيل المعاينة نفسها عند التركيز). يتم إغلاقها من خلال عنصر شقيق.
  • ضمن قائمة يتم استخدام ArrowUp وArrowDown للتنقّل بين العناصر المفعَّلة. ينتقل Home وEnd إلى العنصر الأول والأخير المفعَّلَين، وهو ما يعالجه النص البرمجي.
  • السهم المتّجه لليسار أو السهم المتّجه لليمين في قائمة مفتوحة من المستوى الأعلى يؤدي إلى إغلاق القائمة ومعاينة القائمة المجاورة مع التركيز على مشغّلها، مع الالتفاف عند أي من الطرفين. ثم يدخلها "ArrowDown". لا يؤدي السهمان الأيمن والأيسر إلى الدخول إلى قائمة.
  • ArrowRight على مشغّل قائمة فرعية يفتح القائمة الفرعية ويركّز على العنصر الأول فيها.
  • ArrowRight على عنصر قائمة فرعية بدون قائمة فرعية فرعية. يؤدي هذا الإجراء إلى إغلاق التسلسل الهرمي ومعاينة قائمة المستوى الأعلى التالية مع بقاء التركيز على المشغّل. ثم يدخلها "ArrowDown".
  • ArrowLeft أو Escape في قائمة فرعية يغلق هذه القائمة الفرعية فقط ويعيد التركيز إلى مشغّلها. تظلّ قائمة الوالدَين مفتوحة.
  • اضغط على مفتاح Enter أو مفتاح المسافة على أحد الأوامر. يؤدي ذلك إلى تفعيلها وإغلاق جميع القوائم المفتوحة.
  • علامة التبويب يؤدي هذا الإجراء إلى مغادرة نظام القائمة. لا تحصر التركيز أو تتنقّل بين كل أمر. يجب ألا يؤدي الإغلاق إلى ترك التركيز داخل المحتوى المخفي.

يتم تنشيط الحدث command قبل أن يفتح toggle-popover القائمة، لذا لا يمكن نقل التركيز إلى القائمة هناك. يجب نقل التركيز في الحدث toggle بدلاً من ذلك. يستعيد HTML التركيز فقط لأول popover="auto" في حزمة، لذا يجب أن تستعيد المستويات المتداخلة التركيز إلى المشغّل بشكل صريح.

تنفيذ التفاعل

طبِّق السلوكيات المذكورة أعلاه على الترميز الخاص بك. Arrow, Home/End, wrap-around and Tab-to-exit handling is ordinary keydown code. في ما يلي بعض الأجزاء التي يسهل ارتكاب الأخطاء فيها، وهي مجمّعة حسب نوع الإدخال.

التعامل مع لوحة المفاتيح والتركيز

  1. مسار واحد مفتوح: يؤدي الفتح باستخدام لوحة المفاتيح إلى استدعاء trigger.click()، لذا فإنّ السمة toggle-popover الخاصة بالمستدعي هي الطريقة الوحيدة لفتح القائمة. يجب وضع علامة على القائمة لكي يدخلها معالج toggle، وتُفتح لوحة المفاتيح فقط. عند فتح المؤشر، يركّز toggle على المشغّل بدون حلقة بدلاً من ذلك، لأنّ pointerdown منع التركيز ويجب أن تظل مفاتيح الأسهم تعمل.
  2. معاينة عند التركيز: يؤدي التركيز على عامل تشغيل إلى استدعاء showPopover({ source: trigger }) لكي يتم تثبيت القائمة عليه. تجاهُل عودة التركيز من داخل قائمة المشغّل نفسه أو من نافذة أخرى (تم وضع علامة على النافذة blur، أو تم محو علامة اختيار بعد النافذة focus، أو يؤدي تبديل التطبيقات إلى إعادة فتح القائمة الأخيرة). إخفاء قائمة تمت معاينتها على blur ما لم يتم نقل التركيز إليها
  3. انقل التركيز إلى toggle، وليس command. يتم تنشيط command قبل فتح القائمة.
  4. استعادة التركيز على المشغّل بنفسك: يجب التركيز على المشغّل قبل hidePopover(). بعد تفعيل المؤشر، يمكنك استعادة التركيز بدون حلقة (focus({ focusVisible: false }))، أو يعرض المشغّل مخطط تركيز لوحة المفاتيح بعد النقر بالماوس، مع العلم أنّ عمليات تفعيل لوحة المفاتيح تحتفظ بالحلقة. تم إجراء نقرة باستخدام event.detail === 0 من لوحة المفاتيح.
  5. أغلِق مستوى واحدًا في كل مرة. يتم استدعاء stopPropagation() في Escape وArrowLeft في قائمة فرعية، أو يغلقها المستمع الرئيسي أيضًا.

التعامل مع المؤشرات

  1. تفعيل/إيقاف النقر بالمؤشر يجب استدعاء preventDefault() في pointerdown لكي لا تؤدي النقرة إلى نقل التركيز وبدء معاينة في منتصف النقرة. تذكَّر الزر الذي تم الضغط عليه. يؤدي النقر على عامل تشغيل مغلق أو في وضع المعاينة إلى فتحه أو إغلاقه بشكل أصلي ولا يدخل إلى القائمة مطلقًا؛ يتم إدخال عمليات فتح لوحة المفاتيح فقط. لا تعترض النقرة.
  2. يتّبع التمرير التركيز. يمكنك تنسيق :hover و:focus-visible بشكل منفصل. في pointermove فوق عنصر، يمكنك استدعاء focus({ focusVisible: false })؛ المتصفحات التي تتجاهل الخيار لا ترسم أي حلقة لتركيز المؤشر على أي حال.
  3. يؤدي تمرير مؤشر الماوس إلى تبديل القوائم المفتوحة. عند pointerenter على مشغّل من المستوى الأعلى أثناء فتح قائمة أخرى من المستوى الأعلى: يتم إخفاؤها، والتركيز على المشغّل الذي تم تمرير مؤشر الماوس فوقه بدون حلقة، ومعاينته عند التركيز (انظر أعلاه). عدم فتح أي شيء: عدم اتخاذ أي إجراء
  4. مثلث الأمان: جسر CSS الثابت ضيق جدًا: لا يعمل إلا عندما يبدأ المؤشر بالقرب من حافة العنصر الرئيسي. يمكنك السماح لـ CSS برسم الجسر واستخدام النص البرمجي فقط للإبلاغ عن المؤشر:
    • يجب تشغيل هذا الإجراء فقط عندما يكون matchMedia('(hover: hover)').matches، لأنّه لا علاقة له باللمس ولوحة المفاتيح.
    • يبدأ عندما يغادر المؤشر مشغّل قائمة فرعية تكون مفتوحة. تخطّي قائمة فرعية من النوع --below، والتي لا تحتوي على حافة جانبية
    • يتم ضبط --bridge-x و--bridge-y (المؤشر، بالبكسل بالنسبة إلى القائمة الفرعية)، و--bridge-edge (الحافة القريبة من القائمة الفرعية x: 0 عندما يكون المؤشر على يسارها، وعرضها عند قلبها) و--bridge-height في القائمة الفرعية، وdata-bridge لتفعيل الجسر.
    • توقَّف بعد حوالي 300 ملي ثانية، أو عندما يصل المؤشر إلى عنصر في القائمة الفرعية، أو عندما تُغلق القائمة الفرعية. أزِل أداة الاستماع والسمة. ولا يتم تشغيل أي شيء بعد ذلك.
    • تمنح CSS القائمة الفرعية ::before مقصوصًا على شكل مثلث من المؤشر إلى الزوايا القريبة من الحافة. ويظهر فوق العناصر الشقيقة في مسار المؤشر، لذا لا تتلقى أبدًا التمرير الذي من شأنه إغلاق القائمة الفرعية. يحدّ clip-path أيضًا من اختبار مؤشر الماوس، لذا تظل بقية قائمة الوالدَين تفاعلية.
    • تحتاج القائمة الفرعية إلى overflow: visible (يتم اقتصاص النافذة المنبثقة تلقائيًا)، ويمتد العنصر الزائف إلى ما بعده، لذا يجب إزاحة نقاط الشكل بهذا المقدار.
    • متابعة: المسار المنحني (أوامر منحنى shape()) يتّبع أقواس المؤشر الحقيقية بشكل أفضل من المثلث المستقيم.
[popover] [popover] {
  overflow: visible;

  &[data-bridge]::before {
    content: "";
    position: absolute;
    inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
    clip-path: shape(
      from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
      line to calc(100vw + var(--bridge-edge)) 100vh,
      line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
      close
    );
  }
}
const canHover = matchMedia('(hover: hover)');

// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
  if (!canHover.matches || !submenu.matches(':popover-open')) return;
  const rect = submenu.getBoundingClientRect();
  if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
  const style = submenu.style;
  style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
  style.setProperty('--bridge-height', `${rect.height}px`);
  const update = ({ clientX, clientY, target }) => {
    if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
    style.setProperty('--bridge-x', `${clientX - rect.left}px`);
    style.setProperty('--bridge-y', `${clientY - rect.top}px`);
  };
  const timer = setTimeout(() => stop(), 300);
  const stop = () => {
    clearTimeout(timer);
    document.removeEventListener('pointermove', update);
    submenu.removeEventListener('toggle', stop);
    submenu.removeAttribute('data-bridge');
  };
  update(event);
  submenu.dataset.bridge = '';
  document.addEventListener('pointermove', update);
  submenu.addEventListener('toggle', stop, { once: true });
}

يظلّ خيار "التركيز يتبع التمرير" (العنصر 2) معالجًا عاديًا لـ pointermove: لا يحتاج إلى منطق المثلث.

أيضًا:

  • خارج نطاق التركيز: إغلاق كل قائمة مفتوحة عندما ينتقل التركيز خارج نظام القائمة (مستمع document focusin).
  • تفعيل الطلبات: يتم الإعلان عن ذلك، ثم إخفاء القائمة الجذر واستعادة التركيز إلى مشغّلها، بدون رنين إذا كان التفعيل ناتجًا عن مؤشر. بالنسبة إلى العناصر التي يمكن وضع علامة عليها (menuitemcheckbox وmenuitemradio)، انتظِر لمدة 300 ملي ثانية تقريبًا قبل الإغلاق حتى تظهر علامة الصح التي تم تغييرها. يجب تصميم مشغّل مغلق على أنّه نشط فقط في حال :focus-visible، وإلا سيظلّ التركيز الذي تمت استعادته (بدون حلقة) مميّزًا بعد اختيار المؤشر.
  • aria-expanded: يجب ضبطها فقط في أداة الاستماع toggle. ويغطي حالات المعاينة والفتح، ويظل aria-expanded المشغّل "true" أثناء معاينة قائمته أو فتحها.
  • المساعدون: تفترض الإجراءات الاحتياطية أدناه أنّك حدّدت عناصر مساعدة للترميز: triggers (جميع أزرار [commandfor]) وmenuFor(trigger) وisOpen(menu) وcloseAndRestore(menu, { fromPointer }) (التركيز على المشغّل، بدون حلقة عند fromPointer، ثم menu.hidePopover()) وenterOnOpen (WeakSet من القوائم التي يتم الدخول إليها بعد فتحها) وpointerTrigger (المشغّل الذي تم الضغط عليه مؤخرًا باستخدام مؤشر).
  • استخدِم popover="auto" للقوائم الفرعية، وضَعها بعد مشغّل القائمة الفرعية داخل focusgroup الرئيسي. لا تشارك popover="manual" أبدًا.
  • يتيح التداخل للمتصفّح إبقاء القائمة الرئيسية مفتوحة، وإغلاق القوائم الفرعية المجاورة، وإغلاق القوائم الفرعية الثانوية عند إغلاق القائمة الرئيسية، والتعامل مع الإغلاق السريع عند النقر خارج القائمة.
  • يحتفظ مشغّل القائمة الفرعية بـ aria-haspopup="menu" وaria-expanded، ويحافظ المستمع toggle على مزامنة aria-expanded.
  • يعالج نص لوحة المفاتيح Escape وArrowLeft المتداخلَين كما هو موضّح أعلاه.
  • لا يؤدي مشغّل القائمة الفرعية أيضًا إلى تشغيل أمر: لا يمكن لعنصر تحكم واحد تنشيط وفتح، ولا يمكن أن تعني Enter كليهما. ضع الإجراء التلقائي كأول عنصر في القائمة الفرعية، أو أضِف عنصر أمر منفصل بجانب المشغّل. إذا تم فرض عنصر تحكّم مدمج، فقم بتوثيقه كمقايضة لإمكانية الوصول.

الفواصل والعناصر القابلة للاختيار

  • يجب فصل مجموعات العناصر باستخدام <hr> داخل مجموعة التركيز: عنصر separator أصلي لا يمكن التركيز عليه، ويتم تخطّيه من خلال مجموعة التركيز والحلّ الاحتياطي. لا يتم أبدًا استخدام <div> منمّق.
  • الخيار المنطقي هو <button type="button"> مع aria-checked وrole="menuitemcheckbox" صريح. لا يستنتج Focusgroup هذه الأدوار، لذا هذا هو المكان الوحيد الذي يحدّد فيه الترميز المشترَك دور العنصر.
  • أحد العناصر في المجموعة هو <button type="button"> مع role="menuitemradio" وaria-checked، داخل role="group" مع aria-label، في مجموعة التركيز نفسها.
  • يتم تفعيل النص البرمجي aria-checked أو إيقافه.
  • يؤدي الضغط على Enter على مربّع اختيار إلى تبديل الحالة وإغلاق القائمة، بينما يؤدي الضغط على Space إلى تبديل الحالة وإبقاء القائمة مفتوحة (APG). يتم إغلاق محطات الراديو عند الضغط على أيّ من المفتاحَين.
  • يجب عرض علامة صح من [aria-checked="true"]::before، وليس من عنصر رمز منفصل (راجِع CSS أدناه).

غير مشمولة هنا

  • ملاحظات حول التفعيل: تومض القوائم الأصلية العنصر الذي تم اختياره قبل إغلاقها. يمكنك عرض تأكيد مرئي لمدة 100 ملي ثانية تقريبًا قبل hidePopover()، ولكن لا تؤخِّر استعادة التركيز أو الإعلان.
  • الكتابة الاستباقية: لا تتضمّن مجموعة التركيز أي عناصر. إذا لزم الأمر، يمكنك مطابقة تصنيفات العناصر المفعَّلة في المستوى الحالي على keydown القابلة للطباعة والتركيز على المطابقة التالية بعد العنصر الحالي، مع الالتفاف. لا تعترض Space.

التحقّق قبل الانتهاء

لا يمكن أن تضمن التعليمات سلوكًا معيّنًا، لذا يجب اختبار النتيجة. افتح الصفحة وتحقَّق من كل عنصر من هذه العناصر باستخدام لوحة المفاتيح والماوس الحقيقيين، وأصلِح أي عنصر يتعذّر التحقّق منه وأعِد اختباره:

  • لوحة المفاتيح: يؤدي الضغط على Tab إلى الوصول إلى المشغّل الأول مرة واحدة، ويؤدي الضغط على ArrowDown إلى الدخول، ويؤدي الضغط على Escape إلى إغلاق مستوى واحد وإعادة التركيز إلى المشغّل، ويؤدي الضغط على Tab إلى إغلاق كل قائمة.
  • المؤشر مفتوح. يؤدي النقر على مشغّل إلى فتح قائمته مع التركيز على المشغّل وبدون حلقة، ويؤدي النقر مرة أخرى إلى إغلاقه.
  • أمر المؤشر: يؤدي النقر على أمر إلى إغلاق كل قائمة ولا يعرض أي حلقة تركيز على المشغّل.
  • القائمة الفرعية: يؤدي التحرك قطريًا من منتصف مشغّل القائمة الفرعية إلى عنصر في القائمة الفرعية إلى إبقائها مفتوحة، بينما يؤدي التحرك إلى عنصر شقيق غير ذي صلة إلى إغلاقها.
  • شاشة ضيقة: عند 375 بكسل، لا تغطي أي قائمة فرعية المشغّل الخاص بها.
  • علامات صح تظهر علامة صح بجانب العناصر التي تم وضع علامة عليها.
  • لا تترك أي بقايا. ما مِن أخطاء في وحدة التحكّم، وما مِن popovertarget أو popovertargetaction أو aria-details في المشغّلات.

محاذاة محتوى عنصر القائمة

عندما يحتوي الصف على رمز وتصنيف واختصار أو مؤشر قائمة فرعية، يجب حجز خانة بادئة ذات حجم ثابت للرمز أو علامة صح ودفع المحتوى اللاحق إلى نهاية السطر. تكون الرموز بحجم معروف، لذا يحافظ الموضع الثابت على محاذاة التصنيفات سواء كان العنصر يحتوي على رمز أم لا. Do not use subgrid for rows: any extra inline child (a badge, for example) becomes its own grid item and breaks the layout. إخفاء نص الاختصار من التكنولوجيا المساعِدة عندما يكرّر فقط مفتاحًا يشغّل العنصر حاليًا

[focusgroup~="menu"] {
  --menu-icon-size: 1.25rem;
}

/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
  content: "";
  flex: none;
  inline-size: var(--menu-icon-size);
}

/* Check mark for checked items. This selector must be at least as specific as the base
   ::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
  content: "✓";
}

/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
  margin-inline-start: auto;
}

يجب وضع مؤشر أو اختصار في نهاية <span class="hint" aria-hidden="true"> داخل الزر، لأنّ مشغّل القائمة الفرعية يعلن عن النافذة المنبثقة.

التحسين التدريجي والحلول البديلة

الخيارات الاحتياطية وتوافق المتصفّحات مع focusgroup

يجب إبقاء السمة focusgroup="menu nomemory" في الترميز. إذا كانت قيمة 'focusGroup' in HTMLElement.prototype هي "خطأ"، ثبِّت Up وDown وHome وEnd، واضبط role="menu" على كل حاوية قائمة، وrole="menuitem" على الأزرار التي ليس لها دور، واترك العناصر التي يمكن وضع علامة عليها و<hr> وrole="group" كما هي. لا تثبِّت هذه الحزمة أبدًا في حال توفّر حزمة focusgroup الأصلية. Left, Right, preview, open, and close are author script either way.

if (!('focusGroup' in HTMLElement.prototype)) {
  document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
    group.setAttribute('role', 'menu');
    group.querySelectorAll('button:not([role])').forEach(item => {
      item.setAttribute('role', 'menuitem');
    });
    group.addEventListener('keydown', event => {
      if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
      // Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
      const items = [...group.querySelectorAll('button')]
        .filter(item => item.closest('[focusgroup]') === group)
        .filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
      const current = items.indexOf(document.activeElement);
      if (current < 0) return;
      const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
        (current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
      event.preventDefault();
      event.stopImmediatePropagation();
      items[next].focus({ preventScroll: true });
    }, true);
  });
}

الخيارات الاحتياطية وتوافق المتصفّحات مع Popover

تعتمد واجهة Popover API في الغالب على التحسين التدريجي، ولكن سلوكياتها المحدّدة، مثل الترقية إلى الطبقة العليا والإغلاق البسيط واستدعاء popovertarget، ليس لها مكافئ في CSS فقط. تحتاج المتصفّحات القديمة إلى polyfill أو إلى إجراء احتياطي يدوي إذا كنت تفضّل عدم شحنها.

Polyfill: لإتاحة السمة popover في المتصفّحات القديمة، يمكنك تحميل @oddbird/popover-polyfill بشكل مشروط. إجراء إلزامي: يجب رصد الميزة من خلال التحقّق من السمة popover في HTMLElement.prototype، وتحميل polyfill فقط في حال عدم توفّر الدعم الأصلي، ويجب عدم تحميله بدون شروط.

باستخدام حزمة أو خريطة استيراد:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

بدون حزمة، يمكنك الاستيراد من شبكة توصيل محتوى (CDN) داخل <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

تحذير بشأن التنسيق: لا يمكن لـ polyfill تحديد الفئة الزائفة :popover-open الحقيقية، لذا يتم تطبيق فئة .\:popover-open بدلاً من ذلك. إجراء إلزامي: ادمج السمتَين مع :is() أو :where()، وإلا ستتجاهل المتصفحات التي لا تتضمّن :popover-open القاعدة بأكملها:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

بدلاً من ذلك، يمكنك استخدام position: fixed واحتساب الإحداثيات يدويًا من خلال getBoundingClientRect() أو الاعتماد على تحديد الموضع التلقائي باستخدام inset: auto إذا كان ذلك مقبولاً لحالة الاستخدام، وذلك كحل بديل قديم بدون polyfill.

الخيارات الاحتياطية وتوافق المتصفّحات مع ميزة "تحديد موضع الارتساء"

بدون CSS Anchor Positioning، يعود العنصر المنبثق إلى موضع إطار العرض في وسط UA. لا تستخدم هذا الخيار أبدًا للقائمة: فهو يفصل القائمة عن المشغّل، وتغطي القائمة الفرعية التي تتوسطها القائمة الرئيسية. يمكنك حظر القواعد الثابتة وراء @supports (position-area: block-end) وتحديد موضع القائمة من النص البرمجي في حال تعذّر ذلك:

  • ضَع القائمة أسفل العنصر الذي يفتحها (المستوى الأعلى) أو بجانبه (القائمة الفرعية)، مع قلبها إلى الجانب الآخر وتثبيتها في إطار العرض عندما تتجاوز حدوده.
  • يمكنك تشغيلها في معالج toggle عند فتح القائمة، وتطبيق النتيجة كـ left وtop في القائمة. استخدِم position: fixed مع inset: auto وmargin: 0 لتكون الإحداثيات مرتبطة بإطار العرض.
  • يجب الاحتفاظ بعمليات الإلغاء هذه داخل @supports not (position-area: block-end)، لكي لا تتأثر المتصفّحات التي تتضمّن ميزة تحديد موضع الارتساء.

في حال استهداف عمليات التنفيذ السابقة التي لا ينشئ فيها showPopover({ source }) علامة ارتساء ضمنية (لا يوجد source في ToggleEvent.prototype)، امنح كل عامل تشغيل anchor-name فريدًا وكل قائمة position-anchor مطابقة من النص البرمجي، بدلاً من كتابة قواعد CSS لكل قائمة.

بالنسبة إلى المتصفّحات التي لا تتضمّن ميزة تحديد موضع الارتساء على الإطلاق، يمكنك الاطّلاع على القوائم المنسدلة المستندة إلى إطار العرض لمعرفة مسار polyfill.

الخيارات الاحتياطية وتوافق المتصفّح مع أوامر Invoker

بدون عناصر الاستدعاء، لا يؤدي العنصر command="toggle-popover" أي إجراء، لذا يجب أن يتبدّل النص البرمجي بين القائمة.

  • استخدِم ميزة رصد الواجهة، وليس مثيلاً: 'commandForElement' in HTMLButtonElement.prototype.
  • عند كل عامل تشغيل click، يتم استدعاء preventDefault() لكي لا يتمكّن المتصفّح الذي يتضمّن عناصر استدعاء من التبديل مرّتين.
  • إذا كانت القائمة مفتوحة، أغلِقها باستخدام closeAndRestore(menu). وإلا، يُرجى الاتصال بالرقم ‎menu.showPopover({ source: trigger }).
  • يتم وضع علامة على القائمة في enterOnOpen ما لم تكن النقرة صادرة من المشغّل الذي تم الضغط عليه (pointerTrigger)، لذا يتم فتح القائمة فقط من خلال لوحة المفاتيح.
  • لا يزال بإمكانك استخدام Enter وSpace لأنّهما يؤديان إلى النقر على الأزرار.