สร้างเมนูแบบเลื่อนลง

สร้างเมนูแบบเลื่อนลง

คู่มือนี้ครอบคลุมเมนูป๊อปอัปทุกประเภท ได้แก่ เมนูแบบเลื่อนลง เมนูตามบริบท เมนูรายการเพิ่มเติม และแถบเมนูที่เปิดเมนูป๊อปอัป ตัวอย่างที่ใช้ได้จริงคือเมนูคำสั่งของแอปพลิเคชัน ซึ่งรายการต่างๆ จะเปลี่ยนอินเทอร์เฟซ (สร้าง บันทึก ค่ากำหนด)

ตัวเลือกอิสระ 2 รายการ ส่วนอื่นๆ ทั้งหมดที่นี่จะมีผลกับทั้ง 2 รายการ

  • องค์ประกอบต่อรายการ คำสั่งคือ <button type="button"> ส่วนปลายทางคือ <a href> ที่ยังคงเปิดในแท็บใหม่ คัดลอกเป็น URL และทำงานได้โดยไม่ต้องใช้สคริปต์ เมนูแบบเลื่อนลงอาจผสมผสานลิงก์และคำสั่งได้อย่างอิสระ เช่น ลิงก์ 2 รายการและคำสั่ง 6 รายการ หรือคำสั่ง 1 รายการและลิงก์ 6 รายการ โดยไม่มีผลต่อรายการด้านล่าง
  • รูปแบบการโต้ตอบตามเมนูแบบเลื่อนลง ปุ่มลูกศร Home/End และ Esc ทำให้เป็นเมนู: เพิ่ม focusgroup, ให้ระบบอนุมาน menuitem ในทุกรายการไม่ว่าจะเป็นปุ่มหรือลิงก์ และทำให้ดูเหมือนเป็นเมนู (แถบต่อเนื่องหรือทริกเกอร์ที่มีตัวบ่งชี้เมนูแบบเลื่อนลง โปรดดู "แถบเมนูหรือปุ่มเมนูแยกต่างหาก" ด้านล่าง) รายการลิงก์สั้นๆ ที่ผู้ใช้ที่ลิงก์แล้วTabผ่านคือการนำทาง: ไม่มี focusgroup, ไม่มี role="menu", role="menuitem" หรือ aria-haspopup="menu"; เก็บป๊อปโอเวอร์, Esc และการคืนค่าโฟกัสไว้ และดูคู่มือแถบนําทาง

เนื้อหาไม่ได้เป็นตัวกำหนดบทบาท เมนูแบบเลื่อนลง "ผลิตภัณฑ์" ของลิงก์อาจเป็นเมนูได้ ส่วนเมนูแบบเลื่อนลง "บัญชี" ของคำสั่งจะใช้เป็นการนำทางไม่ได้

ใช้รูปแบบเมนูสำหรับวิดเจ็ตแบบผสมที่มีจุดหยุดแท็บเดียวและการไปยังส่วนต่างๆ ด้วยปุ่มลูกศรเท่านั้น ใช้ <div focusgroup="menu nomemory"> ไม่ใช่องค์ประกอบ <menu> ของ HTML อย่าเพิ่ม 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 ครั้งละ 1 จุด

  • แถบเมนู role="menubar"บนคอนเทนเนอร์ (ไม่ใช้บนป๊อปโอเวอร์) โดยมีลักษณะเป็นแถบต่อเนื่องแถบเดียวที่ไม่มีช่องว่างระหว่างทริกเกอร์ จุดหยุดพัก Tab 1 จุด, ArrowLeft/ArrowRight ย้ายไปมาระหว่างทริกเกอร์, วางเมาส์เหนือสวิตช์เพื่อเปิดเมนู ใช้สำหรับแถบคำสั่งของแอปพลิเคชัน
  • ปุ่มเมนูแยกต่างหาก ทริกเกอร์แต่ละรายการจะเป็นจุดหยุด Tab ของตัวเองและมีลักษณะเหมือนปุ่มแบบสแตนด์อโลน ไม่มี role="menubar", ไม่มีปุ่มลูกศรระหว่างทริกเกอร์, ไม่มีการแสดงตัวอย่างเมื่อโฟกัส, ไม่มีการสลับเมื่อวางเมาส์เหนือ รวมไว้ใน <nav> หากมีการไปยังส่วนต่างๆ ไม่เช่นนั้นจะไม่มีบทบาท ใช้สำหรับแถบเครื่องมือและส่วนหัวของหน้า

ส่วนที่เหลือของคู่มือนี้จะอธิบายแบบฟอร์มแถบเมนู สำหรับปุ่มแยก ให้เก็บทุกอย่างไว้ในแต่ละเมนูและยกเลิกการทำงานระดับแถบเมนู

จัดตำแหน่งเมนูด้วยการกำหนดตำแหน่ง Anchor ของ CSS

ใช้การจัดตำแหน่ง Anchor ของ CSS ไม่ใช่พิกัด JavaScript ทั้ง command="toggle-popover" และ showPopover({ source: trigger }) จะตั้งค่า Anchor โดยนัย ดังนั้นอย่าเพิ่มกฎ anchor-name หรือ position-anchor ต่อเมนู ลักษณะการทำงานของเมนูต้องไม่ขึ้นอยู่กับการวางตำแหน่ง 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 ลบการเยื้อง) ระบบจะข้ามโฆษณาสำรองที่ไม่พอดี และหากไม่มีโฆษณาใดพอดี เบราว์เซอร์จะเก็บตำแหน่งฐานไว้ ดังนั้นเมนูย่อยที่กว้างเกินไป 2-3 พิกเซลจะครอบคลุมทริกเกอร์โดยไม่มีการแจ้งเตือน

ดูวิธีการทำงานของเส้นทางที่แสดงสำรองแบบพลิกในป๊อปโอเวอร์ธรรมดาได้ที่เมนูแบบเลื่อนลงที่รับรู้วิวพอร์ต คำแนะนำดังกล่าวจะจัดตำแหน่งกลุ่มปุ่ม ไม่ใช่เมนู สำหรับเมนู การวางตำแหน่งเมนูย่อยและ--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 ระบบที่ 2 ภายใน focusgroup สคริปต์จะครอบคลุมเฉพาะสิ่งที่ Invoker และ Focusgroup ไม่ครอบคลุมเท่านั้น ดังนี้

  • การหยุดแท็บ 1 ครั้งสำหรับแถบเมนู Tab เข้าสู่ทริกเกอร์ระดับบนสุดรายการแรก ArrowLeft และ ArrowRight จะเลื่อนไปมาระหว่างทริกเกอร์ระดับบนสุดและตัดข้อความที่ปลายด้านใดด้านหนึ่ง Home และ End บนทริกเกอร์จะเข้าสู่เมนูที่รายการแรกหรือรายการสุดท้าย
    • หมายเหตุ: ทริกเกอร์ที่ Tab จะกลับไปใช้ APG อนุญาตให้ใช้ทริกเกอร์แรกหรือทริกเกอร์สุดท้ายที่ใช้ คู่มือระบุรายการแรก แต่การสาธิตจะย้ายจุดหยุดแท็บด้วย ArrowLeft และ ArrowRight ดังนั้นหลังจากใช้แป้นพิมพ์เพื่อไปยังส่วนต่างๆ Shift+Tab จะกลับไปยังทริกเกอร์สุดท้ายที่เข้าถึง การใช้ตัวชี้จะไม่ย้ายตัวชี้ ดังนั้นลำดับเดียวกันจึงอาจไปอยู่บนทริกเกอร์อื่นได้ ลักษณะการทำงานทั้ง 2 แบบเป็นที่ยอมรับได้ ให้เลือกแบบใดแบบหนึ่งและใช้ให้สอดคล้องกัน
  • ดูตัวอย่าง การโฟกัสทริกเกอร์ระดับบนสุดจะแสดงเมนูและตั้งค่า aria-expanded="true" โฟกัสจะยังคงอยู่ที่ทริกเกอร์ และไม่มีการโฟกัสรายการใดๆ การเบลอทริกเกอร์จะปิดเมนูตัวอย่างเท่านั้น
  • กด Enter และ Space บนตัวอย่าง ผู้ใช้ปิดตัวอย่างและมุ่งเน้นที่ทริกเกอร์ต่อไป การดำเนินการ toggle-popover แบบเนทีฟจะจัดการเรื่องนี้ ดังนั้นสคริปต์ต้องไม่ขัดจังหวะคีย์เหล่านี้
  • ArrowDown บนตัวอย่าง แปลงเป็นเมนูแบบเปิดและโฟกัสที่รายการแรก
  • คลิกตัวชี้บนทริกเกอร์ที่เปิดอยู่หรือที่แสดงตัวอย่าง ปิดโดยตรง ไม่เข้าสู่เมนู
    • ความแตกต่างที่ทราบเกี่ยวกับทริกเกอร์เมนูย่อย การคลิกเคอร์เซอร์บนทริกเกอร์เมนูย่อยจะสลับโดยอัตโนมัติด้วย จึงสามารถปิดเมนูย่อยที่แสดงตัวอย่างเมื่อวางเมาส์เหนือเมนู หรือเปิดเมนูย่อยที่ยังไม่ปรากฏได้ เมนูที่มาพร้อมเครื่องจะเปิดเมนูเท่านั้นและไม่ปิดเมนู ซึ่งไม่เป็นอันตราย ไกด์จึงยอมรับ
  • คลิกเคอร์เซอร์บนทริกเกอร์ที่ปิดอยู่ เปิดในเครื่อง โฟกัสจะยังคงอยู่ที่ทริกเกอร์ โดยไม่มีการโฟกัสรายการใดๆ เฉพาะการเปิดแป้นพิมพ์เท่านั้นที่จะเข้าสู่เมนู
  • วางเคอร์เซอร์เหนือรายการ โฟกัสโดยไม่มีวงแหวน ดังนั้นการวางเมาส์เหนือและการโฟกัสจึงเป็นรายการเดียวกัน
  • วางเคอร์เซอร์เหนือทริกเกอร์ระดับบนสุดอื่นขณะที่เมนูเปิดอยู่ ปิดหน้าต่างและแสดงตัวอย่างหน้าต่างที่วางเคอร์เซอร์ไว้ โดยโฟกัสที่ทริกเกอร์โดยไม่มีวงแหวน ไม่มีอะไรเปิดอยู่: การวางเมาส์เหนือองค์ประกอบจะไม่เกิดอะไรขึ้น
  • ตัวชี้เหนือทริกเกอร์เมนูย่อย แสดงตัวอย่างเมนูย่อยหลังจากหน่วงเวลาสั้นๆ (โฟกัสตัวชี้จะเรียกใช้การแสดงตัวอย่างเมื่อโฟกัสแบบเดียวกัน) รายการที่อยู่ระดับเดียวกันจะปิดรายการนั้น
  • ภายในเมนู ArrowUp และ ArrowDown จะเลื่อนไปมาระหว่างรายการที่เปิดใช้ Home และ End จะย้ายไปยังรายการแรกและรายการสุดท้ายที่เปิดใช้ ซึ่งสคริปต์จะจัดการ
  • ปุ่มลูกศรซ้ายหรือลูกศรขวาในเมนูระดับบนสุดที่เปิดอยู่ ปิดเมนูและแสดงตัวอย่างเมนูที่อยู่ติดกันโดยโฟกัสที่ทริกเกอร์ โดยตัดข้อความที่ปลายด้านใดด้านหนึ่ง ArrowDown แล้วป้อนรหัส การปัดซ้ายและขวาจะไม่นำคุณไปยังเมนู
  • ArrowRight บนทริกเกอร์เมนูย่อย เปิดเมนูย่อยและโฟกัสที่รายการแรก
  • ArrowRight บนรายการในเมนูย่อยที่ไม่มีเมนูย่อยระดับล่าง ปิดลำดับชั้นและแสดงตัวอย่างเมนูระดับบนสุดถัดไปขณะที่โฟกัสยังคงอยู่ที่ทริกเกอร์ ArrowDown แล้วป้อนรหัส
  • ArrowLeft หรือ Escape ในเมนูย่อย ปิดเฉพาะเมนูย่อยนั้นและคืนค่าโฟกัสไปยังทริกเกอร์ เมนูสำหรับผู้ปกครองจะเปิดอยู่
  • กด Enter หรือ Space บนคำสั่ง เปิดใช้งานและปิดเมนูที่เปิดอยู่ทั้งหมด
  • Tab ออกจากระบบเมนู อย่าดักจับโฟกัสหรือทำตามคำสั่งทุกคำสั่ง การปิดต้องไม่ปล่อยให้โฟกัสอยู่ภายในเนื้อหาที่ซ่อนอยู่

เหตุการณ์ command เริ่มทำงานก่อนที่ toggle-popover จะเปิดเมนู ดังนั้นโฟกัสจึงย้ายเข้าไปในเมนูไม่ได้ ย้ายโฟกัสไปที่เหตุการณ์ toggle แทน HTML จะคืนค่าโฟกัสเฉพาะ popover="auto" แรกในสแต็กเท่านั้น ดังนั้นระดับที่ซ้อนกันจึงต้องคืนค่าโฟกัสไปยังทริกเกอร์อย่างชัดเจน

การใช้การโต้ตอบ

ใช้ลักษณะการทำงานข้างต้นสำหรับมาร์กอัปของคุณเอง ลูกศร, Home/End, การตัดขึ้นบรรทัดใหม่ และการจัดการ Tab เพื่อออกเป็นโค้ด keydown ธรรมดา มีบางส่วนที่อาจทำให้เข้าใจผิดได้ง่าย ซึ่งจัดกลุ่มไว้ด้านล่างตามประเภทอินพุต

การจัดการแป้นพิมพ์และโฟกัส

  1. เส้นทางเปิด 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 ในเมนูย่อย หรือ Listener ของรายการหลักจะปิดด้วย

การจัดการตัวชี้

  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 มิลลิวินาที เมื่อเคอร์เซอร์ไปถึงรายการในเมนูย่อย หรือเมื่อเมนูย่อยปิดลง นำ Listener และแอตทริบิวต์ออก หลังจากนั้นจะไม่มีการดำเนินการใดๆ
    • 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 Listener)
  • การเปิดใช้งานคำสั่ง ประกาศ จากนั้นซ่อนเมนูรูทและคืนค่าโฟกัสไปยังทริกเกอร์ โดยไม่มีวงแหวนหากการเปิดใช้งานมาจากเคอร์เซอร์ สำหรับรายการที่เลือกได้ (menuitemcheckbox, menuitemradio) ให้รอประมาณ 300 มิลลิวินาทีก่อนปิด เพื่อให้มองเห็นเครื่องหมายถูกที่เปลี่ยนแปลง จัดรูปแบบทริกเกอร์ที่ปิดเป็นใช้งานอยู่เฉพาะใน :focus-visible เท่านั้น ไม่เช่นนั้นโฟกัสที่กู้คืน (ไม่มีวงแหวน) จะยังคงไฮไลต์ไว้หลังจากเลือกตัวชี้
  • aria-expanded. ตั้งค่าใน Listener 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 ไว้ ส่วน Listener toggle จะเก็บ aria-expanded ไว้ให้ซิงค์กัน
  • สคริปต์แป้นพิมพ์จะจัดการ Escape และ ArrowLeft ที่ฝังอยู่ตามที่อธิบายไว้ข้างต้น
  • ทริกเกอร์เมนูย่อยจะไม่เรียกใช้คำสั่งด้วย โดยการควบคุม 1 รายการไม่สามารถทั้งเปิดใช้งานและเปิดได้ และ Enter ไม่สามารถหมายถึงทั้ง 2 อย่าง กำหนดให้การดำเนินการเริ่มต้นเป็นรายการเมนูย่อยรายการแรก หรือเพิ่มรายการคำสั่งแยกต่างหากข้างทริกเกอร์ หากจำเป็นต้องใช้การควบคุมแบบรวม ให้บันทึกไว้เป็นข้อดีข้อเสียด้านการช่วยเหลือพิเศษ

ตัวคั่นและรายการที่เลือกได้

  • แยกกลุ่มรายการด้วย <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 บนช่องทำเครื่องหมายจะสลับและปิดเมนู Space จะสลับและเปิดค้างไว้ (APG) วิทยุจะปิดเมื่อกดปุ่มใดปุ่มหนึ่ง
  • แสดงผลเครื่องหมายถูกจาก [aria-checked="true"]::before ไม่ใช่องค์ประกอบไอคอนแยกต่างหาก (ดู CSS ด้านล่าง)

ไม่ครอบคลุมที่นี่

  • ความคิดเห็นเกี่ยวกับการเปิดใช้งาน เมนูที่มาพร้อมเครื่องจะกะพริบรายการที่เลือกก่อนปิด หากต้องการ ให้แสดงการยืนยันด้วยภาพประมาณ 100 มิลลิวินาทีก่อน hidePopover() อย่าหน่วงเวลาการคืนค่าโฟกัสหรือการประกาศ
  • Typeahead Focusgroup ไม่มี หากจำเป็น ใน keydown ที่พิมพ์ได้ ให้จับคู่ป้ายกำกับรายการที่เปิดใช้ของระดับปัจจุบัน และโฟกัสการจับคู่ถัดไปหลังจากรายการปัจจุบัน โดยตัดขึ้นบรรทัดใหม่ อย่าขัดขวาง Space

ยืนยันก่อนดำเนินการให้เสร็จสิ้น

คำสั่งไม่สามารถรับประกันลักษณะการทำงานได้ ดังนั้นโปรดทดสอบผลลัพธ์ เปิดหน้าเว็บและตรวจสอบแต่ละรายการต่อไปนี้ด้วยแป้นพิมพ์และเมาส์จริง แก้ไขและทดสอบซ้ำรายการที่ไม่ผ่าน

  • แป้นพิมพ์ Tab จะไปยังทริกเกอร์แรก 1 ครั้ง ArrowDown จะเข้าสู่เมนู Escape จะปิดเมนู 1 ระดับและกลับไปโฟกัสที่ทริกเกอร์ของเมนูนั้น Tab จะปิดเมนูทั้งหมด
  • Pointer open. การคลิกทริกเกอร์จะเปิดเมนูโดยโฟกัสที่ทริกเกอร์และไม่มีวงแหวน การคลิกอีกครั้งจะเป็นการปิด
  • คำสั่งเคอร์เซอร์ การคลิกคำสั่งจะปิดเมนูทั้งหมดและไม่แสดงวงแหวนโฟกัสบนทริกเกอร์
  • เมนูย่อย การเลื่อนในแนวทแยงจากตรงกลางของทริกเกอร์เมนูย่อยไปยังรายการเมนูย่อยจะทำให้เมนูย่อยเปิดอยู่ ส่วนการเลื่อนไปยังรายการระดับเดียวกันที่ไม่เกี่ยวข้องจะปิดเมนูย่อย
  • หน้าจอแคบ ที่ความกว้าง 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

เก็บ focusgroup="menu nomemory" ไว้ในมาร์กอัป หาก 'focusGroup' in HTMLElement.prototype เป็นเท็จ ให้ติดตั้งปุ่มขึ้น ลง หน้าแรก และสิ้นสุด ตั้งค่า role="menu" ในคอนเทนเนอร์เมนูแต่ละรายการ และ role="menuitem" ในปุ่มที่ไม่มีบทบาท โดยปล่อยรายการที่เลือกได้ <hr> และ role="group" ไว้ตามเดิม อย่าติดตั้งเมื่อมี FocusGroup แบบเนทีฟอยู่ ซ้าย ขวา ตัวอย่าง เปิด และปิดเป็นสคริปต์ของผู้เขียนไม่ว่าจะด้วยวิธีใดก็ตาม

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 เฉพาะเมื่อไม่มีการรองรับแบบเนทีฟ อย่าโหลดโดยไม่มีเงื่อนไข

เมื่อใช้ Bundler หรือแผนที่การนำเข้า ให้ทำดังนี้

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

หากไม่มี Bundler ให้นำเข้าจาก 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 แทน จำเป็น: รวมทั้ง 2 รายการเข้าด้วยกันโดยใช้ :is() หรือ :where() ไม่เช่นนั้นเบราว์เซอร์ที่ไม่มี :popover-open จะทิ้งกฎทั้งหมด

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

หรือสำหรับทางเลือกสำรองแบบเดิมที่ไม่มี Polyfill ให้ใช้ position: fixed และคำนวณพิกัดด้วยตนเองผ่าน getBoundingClientRect() หรือใช้การกำหนดตำแหน่งเริ่มต้นด้วย inset: auto หากยอมรับได้สำหรับกรณีการใช้งาน

การใช้โฆษณาสำรองและการรองรับเบราว์เซอร์สำหรับการจัดตำแหน่ง Anchor

หากไม่มีการกำหนดตำแหน่ง Anchor ของ CSS ป๊อปโอเวอร์จะกลับไปใช้ตำแหน่งวิวพอร์ตที่อยู่ตรงกลางของ UA อย่าใช้การจัดตำแหน่งแบบนี้กับเมนู เพราะจะทำให้เมนูแยกออกจากทริกเกอร์ และเมนูย่อยที่อยู่ตรงกลางจะบดบังเมนูหลัก กำหนดกฎที่ยึดไว้หลัง @supports (position-area: block-end) และจัดตำแหน่งเมนูจากสคริปต์เมื่อดำเนินการไม่สำเร็จ

  • วางเมนูไว้ใต้ทริกเกอร์ (ระดับบนสุด) หรือข้างๆ (เมนูย่อย) โดยพลิกไปอีกด้านหนึ่งและยึดไว้กับวิวพอร์ตเมื่อเมนูล้น
  • เรียกใช้ในเครื่องจัดการ toggle เมื่อเมนูเปิดขึ้น และใช้ผลลัพธ์เป็น left และ top ในเมนู ใช้ position: fixed กับ inset: auto และ margin: 0 เพื่อให้พิกัดสัมพันธ์กับวิวพอร์ต
  • เก็บการลบล้างเหล่านี้ไว้ใน @supports not (position-area: block-end) เพื่อไม่ให้เบราว์เซอร์ที่มีการจัดตำแหน่ง Anchor ได้รับผลกระทบ

หากกำหนดเป้าหมายเป็นการใช้งานก่อนหน้านี้ที่ showPopover({ source }) ไม่ได้สร้าง Anchor โดยนัย (ไม่มี source ใน ToggleEvent.prototype) ให้ทริกเกอร์แต่ละรายการมี anchor-name ที่ไม่ซ้ำกัน และเมนูแต่ละรายการมี position-anchor ที่ตรงกันจากสคริปต์ แทนที่จะเขียนกฎ CSS ต่อเมนู

สำหรับเบราว์เซอร์ที่ไม่มีการกำหนดตำแหน่ง Anchor เลย โปรดดูเมนูแบบเลื่อนลงที่รับรู้วิวพอร์ตสำหรับเส้นทาง Polyfill

การสนับสนุนเบราว์เซอร์และฟอลแบ็กสำหรับคำสั่ง Invoker

หากไม่มีตัวเรียกใช้ command="toggle-popover" จะไม่ทำงาน ดังนั้นสคริปต์จึงต้องสลับเมนู

  • ตรวจหาฟีเจอร์ของอินเทอร์เฟซ ไม่ใช่อินสแตนซ์: 'commandForElement' in HTMLButtonElement.prototype
  • ในทริกเกอร์ click แต่ละรายการ ให้เรียกใช้ preventDefault() เพื่อไม่ให้เบราว์เซอร์ที่มี Invoker สลับได้ 2 ครั้ง
  • หากเมนูเปิดอยู่ ให้ปิดด้วย closeAndRestore(menu) หรือโทรหา menu.showPopover({ source: trigger })
  • ทำเครื่องหมายเมนูใน enterOnOpen เว้นแต่การคลิกจะมาจากทริกเกอร์ที่กด (pointerTrigger) เพื่อให้เฉพาะการเปิดแป้นพิมพ์เท่านั้นที่เข้าสู่เมนู
  • Enter และ Space ยังคงทำงานได้เนื่องจากสร้างการคลิกปุ่ม