ड्रॉपडाउन मेन्यू बनाना

ड्रॉपडाउन मेन्यू बनाना

इस गाइड में, किसी भी पॉप-अप मेन्यू के बारे में बताया गया है: ड्रॉपडाउन, कॉन्टेक्स्ट, ओवरफ़्लो, और उन्हें खोलने वाले मेन्यू बार. इसका एक उदाहरण, ऐप्लिकेशन कमांड मेन्यू है. इसमें मौजूद आइटम, इंटरफ़ेस को बदलते हैं. जैसे, बनाएं, सेव करें, और प्राथमिकताएं.

दो अलग-अलग विकल्प. यहां दी गई अन्य सभी जानकारी, दोनों पर लागू होती है.

  • एलिमेंट, हर आइटम के लिए. कमांड एक <button type="button"> है; डेस्टिनेशन एक <a href> है, जो अब भी नए टैब में खुलता है, यूआरएल के तौर पर कॉपी होता है, और स्क्रिप्ट के बिना काम करता है. ड्रॉपडाउन में इन्हें किसी भी तरह से मिक्स किया जा सकता है: दो लिंक और छह कमांड या एक कमांड और छह लिंक. इससे नीचे दिए गए कोड में कोई बदलाव नहीं होता.
  • हर ड्रॉपडाउन के लिए इंटरैक्शन मॉडल. ऐरो बटन, Home/End, और Esc इसे एक मेन्यू बनाते हैं: फ़ोकसग्रुप जोड़ें, इसे हर आइटम पर menuitem का अनुमान लगाने दें, चाहे वह बटन हो या लिंक. साथ ही, इसे एक जैसा बनाएं (एक लगातार बार या ड्रॉपडाउन इंडिकेटर वाला ट्रिगर; नीचे "मेन्यूबार या अलग मेन्यू बटन" देखें). नेविगेशन के लिए, उपयोगकर्ताओं को Tab के ज़रिए लिंक की एक छोटी सूची दिखती है: कोई फ़ोकसग्रुप, कोई role="menu", role="menuitem" या aria-haspopup="menu" नहीं; पॉपओवर, Esc, और फ़ोकस रीस्टोरेशन को बनाए रखें. साथ ही, नेविगेशन बार की गाइड देखें.

कॉन्टेंट से कभी भी भूमिकाएं तय नहीं होतीं. लिंक के "प्रॉडक्ट" ड्रॉपडाउन को मेन्यू माना जा सकता है. हालांकि, निर्देशों के "खाता" ड्रॉपडाउन को नेविगेशन नहीं माना जा सकता.

मेन्यू पैटर्न का इस्तेमाल सिर्फ़ ऐसे कंपोज़िट विजेट के लिए करें जिसमें एक टैब स्टॉप और ऐरो-की नेविगेशन हो. <div focusgroup="menu nomemory"> का इस्तेमाल करें, न कि एचटीएमएल <menu> एलिमेंट का. शेयर किए गए मार्कअप में role="menu" या role="menuitem" न जोड़ें: नेटिव फ़ोकसग्रुप उन भूमिकाओं का अनुमान लगाता है. साथ ही, साफ़ तौर पर बताई गई भूमिकाएं उस अनुमान को ब्लॉक करती हैं. उन्हें सिर्फ़ नीचे दिए गए unsupported-browser फ़ॉलबैक में जोड़ें. 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 का इस्तेमाल करके उसके ट्रिगर की ओर इशारा करें. सीएसएस में, सजावटी सबमेन्यू इंडिकेटर रखें या aria-hidden="true" जोड़ें. aria-expanded="true" के साथ ट्रिगर दें, ताकि उपयोगकर्ताओं को पता चल सके कि कौनसा मेन्यू खुला है या उसकी झलक देखी जा रही है.

टॉप-लेवल ट्रिगर के लिए कोई एक फ़ॉर्म चुनें. भूमिका, कीबोर्ड मॉडल, और स्टाइलिंग मैच होनी चाहिए: अलग-अलग दिखने वाले बटन, हर एक Tab स्टॉप का सिग्नल देते हैं.

  • मेन्यूबार. role="menubar" को कंटेनर पर रखें, न कि पॉपओवर पर. इसे एक लगातार बार के तौर पर स्टाइल करें, जिसमें ट्रिगर के बीच कोई गैप न हो. एक Tab स्टॉप; ArrowLeft/ArrowRight ट्रिगर के बीच स्विच करें; कर्सर को स्विच पर ले जाने से मेन्यू खुलते हैं. ऐप्लिकेशन के कमांड बार के लिए इस्तेमाल करें.
  • अलग-अलग मेन्यू बटन. हर ट्रिगर, अपने-आप में एक Tab स्टॉप होता है और अलग से मौजूद बटन की तरह दिखता है. कोई role="menubar" नहीं, ट्रिगर के बीच कोई ऐरो बटन नहीं, फ़ोकस पर कोई झलक नहीं, कर्सर घुमाकर स्विच करने की सुविधा नहीं. अगर वे नेविगेट करते हैं, तो उन्हें <nav> में रैप करें. ऐसा न होने पर, कोई भूमिका नहीं दी जाएगी. टूलबार और पेज हेडर के लिए इस्तेमाल करें.

इस गाइड के बाकी हिस्से में, मेन्यूबार फ़ॉर्म के बारे में बताया गया है. अलग-अलग बटन के लिए, हर मेन्यू में सब कुछ रखें और मेन्यूबार-लेवल के व्यवहार को छोड़ दें.

सीएसएस ऐंकर पोज़िशनिंग की मदद से मेन्यू को पोज़िशन करना

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 फ़ॉलबैक को प्राथमिकता दी जाती है.

खोलने और बंद करने के लिए ऐनिमेशन

ज़रूरी नहीं. 150 मि॰से॰ से कम, सिर्फ़ opacity और छोटा translate, सबमेन्यू के लिए भी यही, @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 किस ट्रिगर पर वापस जाएगा. एपीजी, पहले या आखिरी ट्रिगर का इस्तेमाल करने की अनुमति देता है. गाइड में पहले विकल्प के बारे में बताया गया है. हालांकि, डेमो में ArrowLeft और ArrowRight का इस्तेमाल करके, टैब स्टॉप को मूव किया जाता है. इसलिए, कीबोर्ड नेविगेशन के बाद Shift+Tab, ट्रिगर किए गए आखिरी एलिमेंट पर वापस ले जाता है. पॉइंटर का इस्तेमाल करने से, वह अपनी जगह से नहीं हटता. इसलिए, एक ही क्रम किसी दूसरे ट्रिगर पर ले जा सकता है. इनमें से कोई भी तरीका अपनाया जा सकता है. हालांकि, एक तरीका चुनें और उसे लगातार इस्तेमाल करें.
  • झलक देखना. टॉप-लेवल ट्रिगर पर फ़ोकस करने से, उसका मेन्यू दिखता है और aria-expanded="true" सेट होता है. फ़ोकस, ट्रिगर पर ही रहता है और किसी आइटम पर फ़ोकस नहीं किया जाता. ट्रिगर को धुंधला करने से, सिर्फ़ झलक दिखाने वाला मेन्यू बंद हो जाता है.
  • झलक पर Enter और Space दबाएं. झलक को बंद करें और ट्रिगर पर फ़ोकस बनाए रखें. नेटिव toggle-popover कार्रवाई इसे मैनेज करती है. इसलिए, स्क्रिप्ट को इन बटन को इंटरसेप्ट नहीं करना चाहिए.
  • झलक पर ArrowDown. इससे मेन्यू खुल जाता है और पहले आइटम पर फ़ोकस हो जाता है.
  • खुले हुए या झलक के तौर पर दिखाए गए ट्रिगर पर पॉइंटर क्लिक करना. यह नेटिव तौर पर बंद हो जाता है; यह मेन्यू में नहीं जाता.
    • सबमेन्यू ट्रिगर के बारे में जानकारी. सबमेन्यू ट्रिगर पर पॉइंटर क्लिक करने से भी, सबमेन्यू को टॉगल किया जा सकता है. इससे, उस सबमेन्यू को बंद किया जा सकता है जिसकी झलक पहले से दिख रही है या उस सबमेन्यू को खोला जा सकता है जो अब तक नहीं दिखा है. नेटिव मेन्यू सिर्फ़ इसे खोलते हैं और कभी बंद नहीं करते. इससे किसी को नुकसान नहीं पहुंचता. इसलिए, गाइड इसे स्वीकार कर लेती है.
  • बंद ट्रिगर पर पॉइंटर क्लिक. यह नेटिव तौर पर खुलता है. फ़ोकस, ट्रिगर पर रहता है; किसी आइटम पर फ़ोकस नहीं किया जाता. सिर्फ़ कीबोर्ड से मेन्यू खोला जा सकता है.
  • किसी आइटम पर पॉइंटर ले जाएं. यह बिना रिंग के फ़ोकस करता है. इसलिए, कर्सर घुमाना और फ़ोकस करना एक ही आइटम है.
  • मेन्यू खुला होने पर, किसी दूसरे टॉप-लेवल ट्रिगर पर पॉइंटर ले जाना. इससे, कर्सर घुमाए गए एलिमेंट की झलक दिखती है और मौजूदा एलिमेंट बंद हो जाता है. साथ ही, बिना रिंग के उसके ट्रिगर पर फ़ोकस किया जाता है. कोई मेन्यू नहीं खुला है: कर्सर को घुमाने से कुछ नहीं होता.
  • सबमेन्यू ट्रिगर पर कर्सर घुमाना. थोड़ी देर बाद, इसके सबमेन्यू की झलक दिखती है. पॉइंटर फ़ोकस करने पर भी, फ़ोकस-ऑन-प्रिव्यू की सुविधा इसी तरह काम करती है. सिबलिंग आइटम इसे बंद कर देता है.
  • मेन्यू में. ArrowUp और ArrowDown, चालू किए गए आइटम के बीच स्विच करने के लिए इस्तेमाल किए जाते हैं. Home और End, चालू किए गए पहले और आखिरी आइटम पर ले जाते हैं. स्क्रिप्ट इन्हें मैनेज करती है.
  • खुले हुए टॉप-लेवल मेन्यू में ArrowLeft या ArrowRight. इससे मेन्यू बंद हो जाता है और उसके बगल में मौजूद मेन्यू की झलक दिखती है. साथ ही, फ़ोकस उसके ट्रिगर पर होता है और वह दोनों में से किसी भी छोर पर रैप हो जाता है. इसके बाद, ArrowDown उसे डालता है. लेफ़्ट और राइट ऐरो का इस्तेमाल करके, कभी भी किसी मेन्यू में नहीं जाया जा सकता.
  • सबमेन्यू ट्रिगर पर ArrowRight. इससे सबमेन्यू खुलता है और उसके पहले आइटम पर फ़ोकस होता है.
  • सबमेन्यू के किसी ऐसे आइटम पर ArrowRight दबाएं जिसमें कोई चाइल्ड सबमेन्यू न हो. इससे हैरारकी बंद हो जाती है और अगले टॉप-लेवल मेन्यू की झलक दिखती है. हालांकि, फ़ोकस उसके ट्रिगर पर ही रहता है. इसके बाद, ArrowDown उसे डालता है.
  • सबमेन्यू में ArrowLeft या Escape. इससे सिर्फ़ वह सबमेन्यू बंद होता है और फ़ोकस उसके ट्रिगर पर वापस चला जाता है. 'माता-पिता के लिए मेन्यू' खुला रहता है.
  • किसी निर्देश पर Enter या Space दबाएं. यह इसे चालू करता है और सभी खुले हुए मेन्यू बंद कर देता है.
  • Tab. इससे मेन्यू सिस्टम बंद हो जाता है. फ़ोकस को ट्रैप न करें या हर निर्देश को एक-एक करके न चलाएं. बंद करने पर, फ़ोकस छिपाए गए कॉन्टेंट पर नहीं जाना चाहिए.

toggle-popover के मेन्यू खोलने से पहले, command इवेंट ट्रिगर होता है. इसलिए, फ़ोकस को मेन्यू में नहीं ले जाया जा सकता. इसके बजाय, फ़ोकस को toggle इवेंट पर ले जाएं. एचटीएमएल, स्टैक में मौजूद सिर्फ़ पहले popover="auto" के लिए फ़ोकस को पहले जैसा करता है. इसलिए, नेस्ट किए गए लेवल को अपने ट्रिगर पर फ़ोकस को साफ़ तौर पर पहले जैसा करना चाहिए.

इंटरैक्शन लागू करना

अपने मार्कअप के लिए, ऊपर दिए गए व्यवहार लागू करें. ऐरो, Home/End, रैप-अराउंड, और Tab-टू-एग्ज़िट हैंडलिंग, सामान्य keydown कोड है. कुछ हिस्सों में गड़बड़ी होने की संभावना ज़्यादा होती है. इन्हें इनपुट टाइप के हिसाब से ग्रुप किया गया है.

कीबोर्ड और फ़ोकस हैंडलिंग

  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. एक बार में एक लेवल बंद करें. सबमेन्यू में Escape और ArrowLeft पर stopPropagation() को कॉल करें. इसके अलावा, पैरंट का लिसनर भी इसे बंद कर देता है.

पॉइंटर मैनेज करना

  1. पॉइंटर क्लिक टॉगल. pointerdown पर preventDefault() को कॉल करें, ताकि क्लिक करने पर फ़ोकस न हटे और क्लिक के बीच में झलक न दिखे. याद रखें कि कौनसा ट्रिगर दबाया गया था. बंद या झलक वाले ट्रिगर पर क्लिक करने से, वह नेटिव तौर पर खुलता या बंद होता है. साथ ही, वह कभी भी मेन्यू में नहीं जाता. सिर्फ़ कीबोर्ड से ही मेन्यू में जाया जा सकता है. क्लिक को इंटरसेप्ट न करें.
  2. होवर करने की सुविधा, फ़ोकस के हिसाब से काम करती है. :hover और :focus-visible को अलग-अलग स्टाइल करें. किसी आइटम पर pointermove होने पर, focus({ focusVisible: false }) को कॉल करें; इस विकल्प को अनदेखा करने वाले ब्राउज़र, पॉइंटर फ़ोकस के लिए कोई रिंग नहीं बनाते हैं.
  3. होवर करने पर, खुले हुए मेन्यू स्विच हो जाते हैं. किसी दूसरे टॉप-लेवल मेन्यू के खुले होने पर, टॉप-लेवल ट्रिगर के pointerenter पर: इसे छिपाएं, कर्सर घुमाए गए ट्रिगर पर फ़ोकस करें, और फ़ोकस पर झलक देखें (ऊपर देखें) इसकी झलक दिखाता है. कोई ऐप्लिकेशन नहीं खुला है: कुछ न करें.
  4. सुरक्षित त्रिकोण. फ़िक्स किया गया सीएसएस ब्रिज बहुत छोटा है: यह सिर्फ़ तब काम करता है, जब पॉइंटर पैरंट एलिमेंट के किनारे के पास से शुरू होता है. सीएसएस को ब्रिज बनाने दें और पॉइंटर की जानकारी देने के लिए, सिर्फ़ स्क्रिप्ट का इस्तेमाल करें:
    • इसे सिर्फ़ तब चलाएँ, जब matchMedia('(hover: hover)').matches; इसका टच और कीबोर्ड से कोई लेना-देना नहीं है.
    • यह तब शुरू होता है, जब पॉइंटर किसी ऐसे सबमेन्यू ट्रिगर को छोड़ता है जिसका सबमेन्यू खुला हो. --below सबमेन्यू को स्किप करें. इसमें कोई साइड एज नहीं है.
    • --bridge-x और --bridge-y (पॉइंटर, सबमेन्यू के हिसाब से पिक्सल में), --bridge-edge (सबमेन्यू के किनारे के पास x: 0 जब पॉइंटर इसके बाईं ओर हो, फ़्लिप होने पर इसकी चौड़ाई), और सबमेन्यू पर --bridge-height सेट करें. साथ ही, ब्रिज को चालू करने के लिए data-bridge सेट करें.
    • यह करीब 300 मिलीसेकंड के बाद, सबमेन्यू में मौजूद किसी आइटम पर पॉइंटर पहुंचने पर या सबमेन्यू बंद होने पर रुक जाता है. लिसनर और एट्रिब्यूट को हटाएं. इसके बाद, कोई भी कोड नहीं चलता.
    • सीएसएस, सबमेन्यू को एक ::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> नहीं.
  • बूलियन विकल्प, aria-checked और साफ़ तौर पर role="menuitemcheckbox" के साथ <button type="button"> होता है. Focusgroup इन भूमिकाओं का अनुमान नहीं लगाता है. इसलिए, यह एक ऐसी जगह है जहां शेयर किया गया मार्कअप, किसी आइटम की भूमिका सेट करता है.
  • एक सेट में, एक <button type="button"> है, जिसमें role="menuitemradio" और aria-checked है. यह role="group" के अंदर है, जिसमें aria-label है. यह सब एक ही फ़ोकस ग्रुप में है.
  • स्क्रिप्ट टॉगल aria-checked.
  • Enter on a checkbox toggles and closes the menu; Space toggles and stays open (APG). रेडियो, दोनों में से किसी भी बटन से बंद हो जाते हैं.
  • सही के निशान को [aria-checked="true"]::before से रेंडर करें, न कि किसी अलग आइकॉन एलिमेंट से. इसके लिए, नीचे दिया गया सीएसएस देखें.

यहां शामिल नहीं है

  • ऐक्टिवेशन के बारे में सुझाव, राय या शिकायत. नेटिव मेन्यू, बंद होने से पहले चुने गए आइटम को फ़्लैश करते हैं. अगर ज़रूरी हो, तो hidePopover() से पहले ~100 मि॰से॰ के लिए विज़ुअल कन्फ़र्मेशन दिखाएं; फ़ोकस को वापस लाने या सूचना देने में देरी न करें.
  • टाइपअहेड. Focusgroup में कोई भी नहीं है. अगर ज़रूरत हो, तो प्रिंट किए जा सकने वाले keydown पर, मौजूदा लेवल के चालू किए गए आइटम लेबल मैच करें. साथ ही, मौजूदा आइटम के बाद अगले मैच पर फ़ोकस करें. Space को इंटरसेप्ट न करें.

प्रोसेस पूरी करने से पहले पुष्टि करना

निर्देशों से इस बात की गारंटी नहीं मिलती कि मॉडल उम्मीद के मुताबिक काम करेगा. इसलिए, नतीजे की जांच करें. पेज खोलें और इनमें से हर एक को असली कीबोर्ड और माउस से जांचें. अगर कोई जांच पूरी नहीं हो पाती है, तो उसे ठीक करें और फिर से जांचें:

  • कीबोर्ड. Tab को दबाने पर, फ़ोकस पहले ट्रिगर पर जाता है; ArrowDown को दबाने पर, फ़ोकस मेन्यू में जाता है; Escape को दबाने पर, एक लेवल बंद हो जाता है और फ़ोकस वापस उसके ट्रिगर पर चला जाता है; Tab को दबाने पर, हर मेन्यू बंद हो जाता है.
  • पॉइंटर खुला है. ट्रिगर पर क्लिक करने से उसका मेन्यू खुल जाता है. इसमें ट्रिगर पर फ़ोकस होता है और कोई रिंग नहीं होती. दोबारा क्लिक करने पर, मेन्यू बंद हो जाता है.
  • पॉइंटर का निर्देश. किसी निर्देश पर क्लिक करने से हर मेन्यू बंद हो जाता है. साथ ही, ट्रिगर पर कोई फ़ोकस रिंग नहीं दिखती.
  • सबमेन्यू. सबमेन्यू ट्रिगर के बीच से सबमेन्यू आइटम पर डायगनल तरीके से जाने पर, सबमेन्यू खुला रहता है. हालांकि, किसी ऐसे सिबलिंग पर जाने से सबमेन्यू बंद हो जाता है जो उससे जुड़ा नहीं है.
  • कम चौड़ी स्क्रीन. 375 पिक्सल पर, कोई भी सबमेन्यू अपने ट्रिगर को कवर नहीं करता.
  • सही के निशान. चुने गए आइटम पर सही का निशान दिखता है.
  • कोई भी खाना न छोड़ें. कंसोल में कोई गड़बड़ी न हो. साथ ही, ट्रिगर पर कोई popovertarget, popovertargetaction या aria-details न हो.

मेन्यू आइटम के कॉन्टेंट को अलाइन करना

जब किसी लाइन में आइकॉन, लेबल, और शॉर्टकट या सबमेन्यू इंडिकेटर हो, तो आइकॉन या सही के निशान के लिए, तय साइज़ का लीडिंग स्लॉट रिज़र्व करें. साथ ही, ट्रेलिंग कॉन्टेंट को इनलाइन के आखिर में पुश करें. आइकॉन का साइज़ पहले से तय होता है. इसलिए, तय स्लॉट की वजह से लेबल अलाइन रहते हैं, भले ही किसी आइटम में आइकॉन हो या न हो. पंक्तियों के लिए subgrid का इस्तेमाल न करें: कोई भी अतिरिक्त इनलाइन चाइल्ड (उदाहरण के लिए, कोई बैज) अपना ग्रिड आइटम बन जाता है और लेआउट को तोड़ देता है. सहायक टेक्नोलॉजी से शॉर्टकट टेक्स्ट को तब छिपाएं, जब वह सिर्फ़ उस बटन को दोहराता हो जो पहले से ही आइटम को ऑपरेट करता है.

[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="menu nomemory" को बनाए रखें. अगर 'focusGroup' in HTMLElement.prototype की वैल्यू false है, तो Up, Down, Home, और End इंस्टॉल करें. साथ ही, हर मेन्यू कंटेनर पर role="menu" और बिना किसी भूमिका वाले बटन पर role="menuitem" सेट करें. इसके अलावा, चेक किए जा सकने वाले आइटम, <hr>, और role="group" को ऐसे ही छोड़ दें. नेटिव फ़ोकसग्रुप मौजूद होने पर, इसे कभी इंस्टॉल न करें. 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 API ज़्यादातर प्रोग्रेसिव एन्हांसमेंट है. हालांकि, इसके मुख्य व्यवहार — टॉप-लेयर प्रमोशन, लाइट-डिसमिस, और popovertarget को शुरू करना — सिर्फ़ सीएसएस के बराबर नहीं हैं. पुराने ब्राउज़र के लिए, polyfill की ज़रूरत होती है. अगर आपको polyfill का इस्तेमाल नहीं करना है, तो मैन्युअल फ़ॉलबैक की ज़रूरत होती है.

पॉलीफ़िल: पुराने ब्राउज़र में popover एट्रिब्यूट का इस्तेमाल करने के लिए, शर्त के मुताबिक @oddbird/popover-polyfill लोड करें. ज़रूरी: HTMLElement.prototype पर popover प्रॉपर्टी की जांच करके, सुविधा का पता लगाएं. साथ ही, पॉलीफ़िल को सिर्फ़ तब लोड करें, जब नेटिव सपोर्ट मौजूद न हो. इसे बिना किसी शर्त के लोड न करें.

बंडलर या इंपोर्ट मैप के साथ:

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

बंडलर के बिना, <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;
}

इसके अलावा, बिना polyfill के लेगसी फ़ॉलबैक के लिए, position: fixed का इस्तेमाल करें और getBoundingClientRect() की मदद से मैन्युअल तरीके से निर्देशांकों का हिसाब लगाएं. इसके अलावा, अगर इस्तेमाल के उदाहरण के लिए यह स्वीकार्य है, तो inset: auto के साथ डिफ़ॉल्ट पोज़िशनिंग का इस्तेमाल करें.

एंकर पोज़िशनिंग के लिए फ़ॉलबैक और ब्राउज़र की सुविधा

सीएसएस ऐंकर पोज़िशनिंग के बिना, पॉपओवर, यूए के बीच में मौजूद व्यूपोर्ट प्लेसमेंट पर वापस चला जाता है. मेन्यू के लिए, कभी भी इस वैल्यू का इस्तेमाल न करें: यह मेन्यू को उसके ट्रिगर से अलग कर देता है. साथ ही, बीच में मौजूद सबमेन्यू, अपने पैरंट मेन्यू को कवर कर लेता है. ऐंकर किए गए नियमों को @supports (position-area: block-end) के पीछे रखें. साथ ही, स्क्रिप्ट के काम न करने पर, मेन्यू को स्क्रिप्ट से पोज़िशन करें:

  • मेन्यू को उसके ट्रिगर के नीचे (टॉप लेवल) या उसके बगल में (सबमेन्यू) रखें. साथ ही, ओवरफ़्लो होने पर उसे दूसरी तरफ़ फ़्लिप करें और व्यूपोर्ट पर क्लैंप करें.
  • मेन्यू खुलने पर, इसे toggle हैंडलर में चलाएं. साथ ही, नतीजे को मेन्यू पर left और top के तौर पर लागू करें. position: fixed का इस्तेमाल inset: auto और margin: 0 के साथ करें, ताकि निर्देशांक व्यूपोर्ट के हिसाब से हों.
  • इन ओवरराइड को @supports not (position-area: block-end) के अंदर रखें, ताकि ऐंकर पोज़िशनिंग वाले ब्राउज़र पर कोई असर न पड़े.

अगर आपको पहले के ऐसे वर्शन को टारगेट करना है जिनमें showPopover({ source }), इंप्लिसिट ऐंकर (ToggleEvent.prototype पर कोई source नहीं) सेट नहीं करता है, तो हर ट्रिगर को एक यूनीक anchor-name दें. साथ ही, हर मेन्यू के लिए सीएसएस नियम लिखने के बजाय, स्क्रिप्ट से हर मेन्यू को मैच होने वाला position-anchor दें.

जिन ब्राउज़र में ऐंकर पोज़िशनिंग की सुविधा नहीं है उनके लिए, पॉलीफ़िल रूट के बारे में जानने के लिए व्यूपोर्ट-अवेयर ड्रॉपडाउन देखें.

इनवोकर कमांड के लिए फ़ॉलबैक और ब्राउज़र की सुविधा

इनवोकर के बिना, command="toggle-popover" कुछ नहीं करता. इसलिए, स्क्रिप्ट को मेन्यू को टॉगल करना होगा.

  • इंटरफ़ेस की सुविधा का पता लगाएं, न कि किसी इंस्टेंस का: 'commandForElement' in HTMLButtonElement.prototype.
  • हर ट्रिगर click पर, preventDefault() को कॉल करें, ताकि इनवोकर वाला ब्राउज़र दो बार टॉगल न कर सके.
  • अगर मेन्यू खुला है, तो उसे closeAndRestore(menu) से बंद करें. अगर ऐसा नहीं है, तो menu.showPopover({ source: trigger }) पर कॉल करें.
  • enterOnOpen में मेन्यू को मार्क करें. हालांकि, अगर क्लिक, ट्रिगर (pointerTrigger) को दबाने से हुआ है, तो सिर्फ़ कीबोर्ड से मेन्यू खोला जा सकता है.
  • Enter और Space अब भी काम करते हैं, क्योंकि वे बटन पर क्लिक जनरेट करते हैं.