منوِ کرکره‌ای بساز

منوِ کرکره‌ای بساز

این راهنما همه منوهای بالاپر را پوشش می‌دهد: منو کرکره‌ای، منو زمینه‌ای، منو سرریز، و نوارهای منویی که آن‌ها را باز می‌کنند. نمونه کارشده منوی فرمان برنامه است که در آن موارد میانجی را تغییر می‌دهند (ایجاد، ذخیره، اولویت‌ها).

دو انتخاب مستقل. همه موارد دیگر در اینجا برای هر دو اعمال می‌شود.

  • عنصر، برای هر مورد. فرمان یک <button type="button"> است؛ مقصد یک <a href> است که همچنان در برگه جدیدی باز می‌شود، به‌عنوان نشانی وب کپی می‌شود، و بدون دستورگان کار می‌کند. منو کرکره‌ای می‌تواند آن‌ها را آزادانه ترکیب کند: دو پیوند و شش فرمان، یا یک فرمان و شش پیوند، هیچ‌چیز را در زیر تغییر نمی‌دهد.
  • مدل تعامل، برای هر منو کرکره‌ای. کلیدهای جهت‌نما، Home/End، و Esc آن را به منو تبدیل می‌کنند: focusgroup را اضافه کنید، اجازه دهید menuitem را در هر مورد، چه دکمه باشد چه پیوند، استنباط کند و آن را شبیه به یک منو کنید (یک نوار پیوسته یا یک راه‌انداز با نشانگر منو کرکره‌ای؛ به «نوار منو یا دکمه‌های منو جداگانه» در زیر مراجعه کنید). فهرست کوتاهی از پیوندها که کاربران ازطریق آن Tab می‌کنند پیمایش است: بدون focusgroup، بدون role="menu"، role="menuitem"، یا aria-haspopup="menu"؛ popover،‏ Esc، و بازیابی تمرکز را نگه دارید و راهنمای navbar را ببینید.

محتوا هرگز نقش‌ها را تعیین نمی‌کند. منو کرکره‌ای «محصولات» از پیوندها می‌تواند منو باشد؛ منو کرکره‌ای «حساب» از فرمان‌ها نمی‌تواند پیمایش باشد.

از الگوی منو فقط برای ابزاره ترکیبی با یک توقف کلید جهش و پیمایش کلید جهت‌نما استفاده کنید. از <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 استفاده کنید، نه مختصات جاوا اسکریپت. ‫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 دوم را به داخل focusgroup اضافه نکنید. اسکریپت فقط آنچه را که فراخوان‌ها و گروه کانونی انجام نمی‌دهند پوشش می‌دهد:

  • یک توقف زبانه برای نوار منو. Tab در اولین راه‌انداز سطح بالا وارد می‌شود. ArrowLeft و ArrowRight بین راه‌اندازهای سطح بالا حرکت می‌کنند و در هر دو انتها می‌پیچند. Home و End در راه‌انداز، منو آن را در اولین یا آخرین مورد وارد کنید.
    • توجه: کدام راه‌انداز Tab به آن برمی‌گردد. APG یا اولین راه‌انداز یا آخرین راه‌انداز استفاده‌شده را مجاز می‌کند. راهنما اولی را مشخص می‌کند، اما نسخه نمایشی توقف زبانه را با ArrowLeft و ArrowRight جابه‌جا می‌کند، بنابراین پس‌از پیمایش صفحه‌کلید، Shift+Tab به آخرین راه‌انداز رسیده برمی‌گردد. استفاده از اشاره‌گر آن را جابه‌جا نمی‌کند، بنابراین همان توالی می‌تواند روی راه‌انداز دیگری قرار بگیرد. هر دو رفتار قابل‌قبول است؛ یکی را انتخاب کنید و آن را ثابت نگه دارید.
  • پیش‌نمایش. تمرکز روی راه‌انداز سطح بالا منو آن را نشان می‌دهد و aria-expanded="true" را تنظیم می‌کند. تمرکز روی راه‌انداز باقی می‌ماند و هیچ موردی متمرکز نمی‌شود. محو کردن راه‌انداز منوِ فقط پیش‌نمایش را می‌بندد.
  • کلیدهای Enter و Space در پیش‌نمایش. پیش‌نمایش را می‌بندند و تمرکز را روی راه‌انداز نگه می‌دارند. کنش بومی toggle-popover این کار را انجام می‌دهد، بنابراین متن نباید این کلیدها را رهگیری کند.
  • کلید پایین‌بَر روی پیش‌نمایش. آن را به منو باز تبدیل می‌کند و روی اولین مورد تمرکز می‌کند.
  • کلیک اشاره‌گر روی راه‌انداز باز یا پیش‌نمایش‌شده. آن را به‌طور بومی می‌بندد؛ وارد منو نمی‌شود.
    • تفاوت شناخته‌شده در راه‌اندازهای منو فرعی. کلیک اشاره‌گر روی راه‌انداز منو فرعی نیز به‌طور بومی تغییر وضعیت می‌دهد، بنابراین می‌تواند منو فرعی‌ای را که قبلاً پیش‌نمایش شده است ببندد، یا منو فرعی‌ای را که هنوز ظاهر نشده است باز کند. منوهای بومی فقط آن را باز می‌کنند و هرگز آن را نمی‌بندند. این بی‌ضرر است، بنابراین راهنما آن را می‌پذیرد.
  • کلیک اشاره‌گر روی راه‌انداز بسته. آن را به‌صورت بومی باز می‌کند. تمرکز روی راه‌انداز باقی می‌ماند؛ هیچ موردی متمرکز نمی‌شود. فقط باز شدن صفحه‌کلید وارد منو می‌شود.
  • اشاره‌گر روی موردی قرار دارد. بدون حلقه روی آن تمرکز می‌کند، بنابراین نگه داشتن نشانگر و تمرکز یک مورد هستند.
  • اشاره‌گر روی راه‌انداز سطح بالای دیگری قرار دارد درحالی‌که منو باز است. آن را می‌بندد و پیش‌نمایش موردی را که روی آن نگه داشته شده است نشان می‌دهد، و بدون حلقه روی راه‌انداز آن تمرکز می‌کند. چیزی باز نیست: نگه داشتن نشانگر کاری انجام نمی‌دهد.
  • اشاره‌گر روی راه‌انداز منو فرعی. زیرمنو را پس‌از تأخیری کوتاه پیش‌نمایش می‌کند (تمرکز اشاره‌گر همان پیش‌نمایش در تمرکز را اجرا می‌کند). مورد هم‌نیا آن را می‌بندد.
  • درون منو. ArrowUp و ArrowDown بین موارد فعال‌شده جابه‌جا می‌شوند. Home و End به اولین و آخرین مورد فعال‌شده می‌روند که اسکریپت آن را مدیریت می‌کند.
  • کلید چپ‌بَر یا کلید راست‌بَر در منو سطح بالای باز. آن را می‌بندد و منو مجاور را با تمرکز روی راه‌انداز آن پیش‌نمایش می‌کند، و در هر دو انتها می‌پیچد. ‫ArrowDown سپس آن را وارد می‌کند. کلیدهای چپ و راست هرگز در منو قرار نمی‌گیرند.
  • ArrowRight روی راه‌انداز منو فرعی. زیرمنو را باز می‌کند و روی اولین مورد آن تمرکز می‌کند.
  • کلید ArrowRight روی گزینه منو فرعی بدون منو فرعی فرزند. سلسله‌مراتب را می‌بندد و منو سطح بالای بعدی را پیش‌نمایش می‌کند، درحالی‌که تمرکز روی راه‌انداز آن باقی می‌ماند. ‫ArrowDown سپس آن را وارد می‌کند.
  • کلید چپ‌بَر یا کلید گریز در زیرمنو. فقط آن زیرمنو را می‌بندد و تمرکز را به راه‌انداز آن برمی‌گرداند. منوِ ولی باز می‌ماند.
  • کلید Enter یا «فاصله» را روی فرمان فشار دهید. آن را فعال می‌کند و همه منوهای باز را می‌بندد.
  • Tab. از سیستم منو خارج می‌شود. تمرکز را به‌تله نیندازید یا همه فرمان‌ها را گام‌به‌گام اجرا نکنید. بستن نباید تمرکز را در محتوای پنهان رها کند.

رویداد command قبل‌از اینکه toggle-popover منو را باز کند فعال می‌شود، بنابراین تمرکز نمی‌تواند به منو منتقل شود. درعوض، تمرکز را در رویداد toggle جابه‌جا کنید. HTML تمرکز را فقط برای اولین popover="auto" در پشته بازیابی می‌کند، بنابراین سطوح تودرتو باید تمرکز را به‌طور صریح به راه‌انداز خود بازیابی کنند.

پیاده‌سازی تعامل

رفتارهای بالا را برای نشان‌گذاری خودتان پیاده‌سازی کنید. Arrow،‏ Home/End، wrap-around و Tab-to-exit handling is ordinary keydown code. چند بخش وجود دارد که ممکن است به‌راحتی اشتباه شود، این بخش‌ها در زیر براساس نوع ورودی گروه‌بندی شده‌اند.

مدیریت کردن صفحه‌کلید و ناحیه تمرکز

  1. یک مسیر باز. باز کردن با صفحه‌کلید trigger.click() را فراخوانی می‌کند، بنابراین toggle-popover فراخواننده تنها راه باز شدن منو است. Mark the menu so the toggle handler enters it, keyboard opens only. در باز شدن اشاره‌گر، toggle به‌جای آن روی راه‌انداز بدون حلقه تمرکز می‌کند، زیرا pointerdown از تمرکز جلوگیری کرده است و کلیدهای جهت‌نما باید همچنان کار کنند.
  2. پیش‌نمایش در کانون. تمرکز روی راه‌انداز showPopover({ source: trigger }) را فرا می‌خواند تا منو به آن متصل شود. Ignore focus returning from inside the trigger's own menu, or from another window (flag on window blur, cleared a tick after window focus, or app-switching reopens the last menu). پنهان کردن منو پیش‌نمایش‌شده در blur، مگراینکه تمرکز به آن منتقل شده باشد.
  3. تمرکز را به toggle منتقل کنید، نه command. command قبل‌از باز شدن منو فعال می‌شود.
  4. تمرکز را خودتان به راه‌انداز برگردانید. ماشه را قبل‌از hidePopover() متمرکز کنید. پس‌از فعال‌سازی اشاره‌گر، تمرکز را بدون حلقه بازیابی کنید (focus({ focusVisible: false }))، یا راه‌انداز طرح کلی تمرکز صفحه‌کلید را پس‌از کلیک موشواره نشان می‌دهد؛ فعال‌سازی‌های صفحه‌کلید حلقه را حفظ می‌کنند. کلیک با event.detail === 0 از صفحه‌کلید انجام شد.
  5. هر بار یک سطح را ببندید. Call stopPropagation() on Escape and ArrowLeft in a submenu, or the parent's listener closes it too.

مدیریت اشاره‌گر

  1. کلیک اشاره‌گر جابه‌جا می‌شود. ‫preventDefault() را در pointerdown فراخوانی کنید تا کلیک تمرکز را جابه‌جا نکند و پیش‌نمایش در میان کلیک شروع نشود. به‌خاطر بسپارید کدام ماشه فشار داده شده است. کلیک کردن روی راه‌انداز بسته یا پیش‌نمایش‌شده آن را به‌طور بومی باز یا بسته می‌کند و هرگز وارد منو نمی‌شود؛ فقط باز کردن با صفحه‌کلید وارد آن می‌شود. کلیک را رهگیری نکنید.
  2. نگه داشتن نشانگر تمرکز را دنبال می‌کند. :hover و :focus-visible را جداگانه سبک‌بندی کنید. در pointermove روی یک مورد، focus({ focusVisible: false }) را فراخوانی کنید؛ مرورگرهایی که این گزینه را نادیده می‌گیرند، در هرصورت برای تمرکز اشاره‌گر حلقه‌ای رسم نمی‌کنند.
  3. نگه داشتن کلیدها منوهای باز را تغییر می‌دهد. در pointerenter راه‌انداز سطح بالا درحالی‌که منو سطح بالای دیگری باز است: آن را پنهان کنید، روی راه‌انداز نگه داشته‌شده بدون حلقه تمرکز کنید، و پیش‌نمایش در تمرکز (بالا را ببینید) آن را پیش‌نمایش می‌کند. هیچ‌چیز باز نیست: کاری انجام ندهید.
  4. مثلث ایمن. پل CSS ثابت خیلی باریک است: فقط زمانی کار می‌کند که اشاره‌گر نزدیک لبه والد شروع شود. اجازه دهید CSS پل را بکشد و از اسکریپت فقط برای گزارش اشاره‌گر استفاده کنید:
    • این را فقط وقتی matchMedia('(hover: hover)').matches اجرا کنید؛ به لمس و صفحه‌کلید مربوط نیست.
    • وقتی اشاره‌گر راه‌انداز زیرمنویی را که زیرمنو آن باز است ترک می‌کند، شروع می‌شود. رد شدن از زیرمنو --below که لبه کناری ندارد.
    • Set --bridge-x and --bridge-y (the pointer, in px relative to the submenu), --bridge-edge (the submenu's near-edge x: 0 when the pointer is to its left, its width when it flipped) and --bridge-height on the submenu, and data-bridge to switch the bridge on.
    • پس‌از حدود ۳۰۰ میلی‌ثانیه، وقتی اشاره‌گر به موردی در زیرمنو می‌رسد، یا وقتی زیرمنو بسته می‌شود، متوقف می‌شود. شنونده و مشخصه را بردارید. بعداز آن چیزی اجرا نمی‌شود.
    • 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 });
}

«نگه داشتن نشانگر روی کانون» (مورد ۲) یک گرداننده pointermove ساده باقی می‌ماند: به منطق مثلثی نیاز ندارد.

همچنین،

  • خارج از کانون. وقتی تمرکز خارج از سیستم منو قرار می‌گیرد، همه منوهای باز را ببندید (شنونده document focusin).
  • فعال‌سازی فرمان. آن را اعلام کنید، سپس منو ریشه را پنهان کنید و تمرکز را به راه‌انداز آن برگردانید، بدون حلقه اگر فعال‌سازی از اشاره‌گر آمده باشد. برای موارد علامت‌زدنی (menuitemcheckbox، menuitemradio)، حدود ۳۰۰ میلی‌ثانیه قبل‌از بستن صبر کنید تا علامت تأیید تغییریافته قابل‌مشاهده باشد. Style a closed trigger as active only on :focus-visible; otherwise the restored (ringless) focus leaves it highlighted after a pointer selection.
  • 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> در focusgroup جدا کنید: separator بومی، غیرقابل‌تمرکز، نادیده‌گرفته‌شده توسط focusgroup و بازگشت. هرگز <div> سبک‌دار.
  • گزینه بولی <button type="button"> با aria-checked و role="menuitemcheckbox" صریح است. 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 رندر کنید، نه از عنصر نماد جداگانه (CSS زیر را ببینید).

اینجا پوشش داده نشده است

  • بازخورد فعال‌سازی. منوهای بومی قبل‌از بسته شدن، مورد انتخاب‌شده را به‌صورت چشمک‌زن نشان می‌دهند. درصورت تمایل، پیش‌از hidePopover()، تأیید تصویری حدود ۱۰۰ میلی‌ثانیه‌ای نشان دهید؛ بازگرداندن تمرکز یا اعلام را به‌تأخیر نیندازید.
  • تایپ پیش‌نگر. Focusgroup هیچ‌کدام را ندارد. درصورت نیاز، در keydown قابل‌چاپ، برچسب‌های مورد فعال‌شده سطح فعلی را مطابقت دهید و روی مطابقت بعدی پس‌از مورد فعلی تمرکز کنید، پیچیدن. Space را رهگیری نکنید.

درستی‌سنجی قبل‌از اتمام

دستورالعمل نمی‌تواند رفتار را تضمین کند، بنابراین نتیجه را آزمایش کنید. صفحه را باز کنید و هرکدام از این موارد را با صفحه‌کلید و موشواره واقعی بررسی کنید؛ هرکدام را که ناموفق بود اصلاح کنید و دوباره آزمایش کنید:

  • صفحه‌کلید. Tab یک‌بار روی اولین راه‌انداز قرار می‌گیرد؛ ArrowDown وارد می‌شود؛ Escape یک سطح را می‌بندد و تمرکز را به راه‌انداز آن برمی‌گرداند؛ Tab همه منوها را می‌بندد.
  • اشاره‌گر باز است. با کلیک کردن روی راه‌انداز، منو آن باز می‌شود و راه‌انداز کانونی می‌شود و حلقه‌ای وجود ندارد؛ با کلیک کردن مجدد، منو بسته می‌شود.
  • فرمان اشاره‌گر. با کلیک کردن روی فرمان، همه منوها بسته می‌شود و هیچ حلقه تمرکزی روی راه‌انداز نشان داده نمی‌شود.
  • منو فرعی. حرکت مورب از وسط راه‌انداز منو فرعی به موردی در منو فرعی آن را باز نگه می‌دارد؛ حرکت به هم‌نیای نامرتبط آن را می‌بندد.
  • صفحه باریک. در ۳۷۵ پیکسل، هیچ زیرمنویی راه‌انداز خودش را نمی‌پوشاند.
  • علامت‌های تأیید. موارد علامت‌گذاری‌شده با علامت تأیید نشان داده می‌شوند.
  • هیچ غذایی باقی نمانده است. خطای کنسول وجود ندارد و هیچ 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

‫focusgroup="menu nomemory" را در نشانه‌گذاری نگه دارید. اگر 'focusGroup' in HTMLElement.prototype نادرست است، «بالا»، «پایین»، «صفحه اصلی»، و «پایان» را نصب کنید، 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

«میانای برنامه‌سازی کاربردی Popover» عمدتاً ارتقای پیش‌رو است، اما رفتارهای تعریف‌کننده آن (ارتقای لایه بالایی، رد کردن با کلیک در خارج از عنصر، و فراخوانی popovertarget) معادل CSS-only ندارند. مرورگرهای قدیمی‌تر به شبیه‌ساز مرورگر نیاز دارند، یا اگر ترجیح می‌دهید شبیه‌ساز مرورگر ارسال نکنید، به بازگشت دستی نیاز دارند.

شبیه‌ساز مرورگر: برای پشتیبانی از مشخصه popover در مرورگرهای قدیمی‌تر، @oddbird/popover-polyfill را به‌صورت شرطی بار کنید. اجباری: ویژگی را با بررسی کردن ویژگی popover در HTMLElement.prototype شناسایی کنید و شبیه‌ساز مرورگر را فقط زمانی بار کنید که پشتیبانی بومی وجود ندارد — آن را بدون قیدوشرط بار نکنید.

با بسته‌بندی یا نقشه واردات:

// 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>

هشدار سبک‌بندی: شبیه‌ساز مرورگر نمی‌تواند کلاس کاذب واقعی :popover-open را تعریف کند، بنابراین به‌جای آن کلاس .\:popover-open را اعمال می‌کند. اجباری: این دو را با :is() یا :where() ترکیب کنید، درغیراین‌صورت مرورگرهایی که فاقد :popover-open هستند کل قانون را نادیده می‌گیرند:

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

یا برای بازگشت به نسخه قدیمی بدون شبیه‌ساز مرورگر، از position: fixed استفاده کنید و مختصات را به‌صورت دستی ازطریق getBoundingClientRect() محاسبه کنید یا اگر برای مورد استفاده قابل‌قبول است، به موقعیت‌یابی پیش‌فرض با inset: auto تکیه کنید.

پشتیبانی مرورگر و بازگشت‌ها برای «موقعیت‌یابی لنگر»

بدون «موقعیت‌یابی لنگر CSS»، یک popover به جای‌گذاری نمای پدیدار متمرکز UA برمی‌گردد. هرگز این کار را برای منو انجام ندهید: این کار منو را از راه‌انداز آن جدا می‌کند و زیرمنوِ مرکزی منوِ مادر را می‌پوشاند. قوانین لنگرشده را پشت @supports (position-area: block-end) قرار دهید و منو را از اسکریپت درصورت ناموفق بودن قرار دهید:

  • منو را زیر راه‌انداز آن (سطح بالا) یا کنار آن (زیرمنو) قرار دهید، درصورت سرریز شدن، به طرف دیگر بروید و به نمای هدف بچسبید.
  • وقتی منو باز می‌شود، آن را در گرداننده toggle اجرا کنید و نتیجه را به‌عنوان left و top در منو اعمال کنید. از position: fixed با inset: auto و margin: 0 استفاده کنید تا مختصات نسبت‌به نمای هدف باشد.
  • این لغو کردن‌ها را در @supports not (position-area: block-end) نگه دارید تا مرورگرهای دارای موقعیت‌یابی لنگر تحت‌تأثیر قرار نگیرند.

If targeting earlier implementations where showPopover({ source }) does not establish an implicit anchor (no source on ToggleEvent.prototype), give each trigger a unique anchor-name and each menu a matching position-anchor from script, instead of writing per-menu CSS rules.

برای مرورگرهایی که اصلاً موقعیت‌یابی لنگر ندارند، به منوهای کرکره‌ای آگاه از نمای هدف برای مسیر شبیه‌ساز مرورگر مراجعه کنید.

پشتیبانی مرورگر و بازگشت به حالت قبلی برای فرمان‌های Invoker

بدون فراخواننده، command="toggle-popover" کاری انجام نمی‌دهد، بنابراین متن باید منو را تغییر دهد.

  • ویژگی تشخیص میانای، نه یک نمونه: 'commandForElement' in HTMLButtonElement.prototype.
  • در هر راه‌انداز click، با preventDefault() تماس بگیرید تا مرورگر با فراخوان‌ها نتواند دو بار تغییر وضعیت دهد.
  • اگر منو باز است، آن را با closeAndRestore(menu) ببندید. درغیراین‌صورت با menu.showPopover({ source: trigger }) تماس بگیرید.
  • Mark the menu in enterOnOpen unless the click came from the pressed trigger (pointerTrigger), so only keyboard opens enter the menu.
  • Enter و Space همچنان کار می‌کنند چون کلیک روی دکمه‌ها را تولید می‌کنند.