สร้างเมนูแบบเลื่อนลง
คู่มือนี้ครอบคลุมเมนูป๊อปอัปทุกประเภท ได้แก่ เมนูแบบเลื่อนลง เมนูตามบริบท เมนูรายการเพิ่มเติม และแถบเมนูที่เปิดเมนูป๊อปอัป ตัวอย่างที่ใช้ได้จริงคือเมนูคำสั่งของแอปพลิเคชัน ซึ่งรายการต่างๆ จะเปลี่ยนอินเทอร์เฟซ (สร้าง บันทึก ค่ากำหนด)
คำสั่ง ลิงก์ และองค์ประกอบของเมนู
ตัวเลือกอิสระ 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"บนคอนเทนเนอร์ (ไม่ใช้บนป๊อปโอเวอร์) โดยมีลักษณะเป็นแถบต่อเนื่องแถบเดียวที่ไม่มีช่องว่างระหว่างทริกเกอร์ จุดหยุดพักTab1 จุด,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 เส้นทาง การเปิดด้วยแป้นพิมพ์จะเรียก
trigger.click()ดังนั้นtoggle-popoverของผู้เรียกจึงเป็นวิธีเดียวที่เมนูจะเปิดขึ้น ทำเครื่องหมายที่เมนูเพื่อให้เครื่องจัดการtoggleเข้าสู่เมนู แป้นพิมพ์จะเปิดขึ้นเท่านั้น เมื่อเปิดตัวชี้toggleจะโฟกัสทริกเกอร์โดยไม่มีวงแหวนแทน เนื่องจากpointerdownป้องกันไม่ให้โฟกัสและปุ่มลูกศรยังคงต้องทำงาน - ตัวอย่างที่โฟกัส การโฟกัสทริกเกอร์จะเรียกใช้
showPopover({ source: trigger })เพื่อให้เมนูยึดกับทริกเกอร์ ละเว้นการโฟกัสที่กลับมาจากภายในเมนูของทริกเกอร์เอง หรือจากหน้าต่างอื่น (แฟล็กในหน้าต่างblur, ล้างเครื่องหมายถูกหลังจากหน้าต่างfocusหรือการสลับแอปจะเปิดเมนูล่าสุดอีกครั้ง) ซ่อนเมนูที่แสดงตัวอย่างในblurเว้นแต่จะย้ายโฟกัสไปที่เมนูดังกล่าว - ย้ายโฟกัสใน
toggleไม่ใช่commandcommandจะเริ่มทำงานก่อนที่เมนูจะเปิดขึ้น - คืนค่าโฟกัสไปยังทริกเกอร์ด้วยตนเอง โฟกัสทริกเกอร์ก่อนวันที่
hidePopover()หลังจากเปิดใช้งานตัวชี้ ให้คืนค่าโฟกัสโดยไม่มีวงแหวน (focus({ focusVisible: false })) หรือทริกเกอร์จะแสดงโครงร่างโฟกัสของแป้นพิมพ์หลังจากคลิกเมาส์ การเปิดใช้งานแป้นพิมพ์จะคงวงแหวนไว้ การคลิกด้วยevent.detail === 0มาจากแป้นพิมพ์ - ปิดทีละระดับ เรียก
stopPropagation()ในEscapeและArrowLeftในเมนูย่อย หรือ Listener ของรายการหลักจะปิดด้วย
การจัดการตัวชี้
- การเปิด/ปิดการคลิกตัวชี้ เรียกใช้
preventDefault()ในpointerdownเพื่อไม่ให้การคลิกย้ายโฟกัสและเริ่มแสดงตัวอย่างระหว่างการคลิก จำทริกเกอร์ที่กด การคลิกทริกเกอร์ที่ปิดอยู่หรือแสดงตัวอย่างอยู่จะเปิดหรือปิดทริกเกอร์นั้นโดยตรงและไม่เข้าสู่เมนู มีเพียงการเปิดด้วยแป้นพิมพ์เท่านั้นที่จะเข้าสู่เมนู อย่าสกัดกั้นการคลิก - การวางเมาส์เหนือจะตามโฟกัส จัดรูปแบบ
:hoverและ:focus-visibleแยกกัน เมื่อpointermoveอยู่เหนือรายการ ให้เรียกใช้focus({ focusVisible: false })เบราว์เซอร์ที่ละเว้นตัวเลือกนี้จะไม่แสดงวงแหวนสำหรับการโฟกัสตัวชี้อยู่แล้ว - การวางเมาส์เหนือสวิตช์จะเปิดเมนู เมื่อ
pointerenterทริกเกอร์ระดับบนสุดขณะที่เมนูระดับบนสุดอีกรายการเปิดอยู่: ซ่อนทริกเกอร์ โฟกัสทริกเกอร์ที่วางเมาส์เหนือโดยไม่มีวงแหวน และแสดงตัวอย่างเมื่อโฟกัส (ดูด้านบน) ไม่มีอะไรเปิดอยู่: ไม่ต้องดำเนินการใดๆ - สามเหลี่ยมที่ปลอดภัย บริดจ์ 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 ธรรมดา โดยไม่จำเป็นต้องมีตรรกะสามเหลี่ยม
นอกจากนี้ การโอนช่องยังมีผลในด้านต่างๆ ต่อไปนี้
- หลุดโฟกัส ปิดเมนูที่เปิดอยู่ทั้งหมดเมื่อโฟกัสอยู่นอกระบบเมนู (
documentfocusinListener) - การเปิดใช้งานคำสั่ง ประกาศ จากนั้นซ่อนเมนูรูทและคืนค่าโฟกัสไปยังทริกเกอร์ โดยไม่มีวงแหวนหากการเปิดใช้งานมาจากเคอร์เซอร์ สำหรับรายการที่เลือกได้ (
menuitemcheckbox,menuitemradio) ให้รอประมาณ 300 มิลลิวินาทีก่อนปิด เพื่อให้มองเห็นเครื่องหมายถูกที่เปลี่ยนแปลง จัดรูปแบบทริกเกอร์ที่ปิดเป็นใช้งานอยู่เฉพาะใน:focus-visibleเท่านั้น ไม่เช่นนั้นโฟกัสที่กู้คืน (ไม่มีวงแหวน) จะยังคงไฮไลต์ไว้หลังจากเลือกตัวชี้ aria-expanded. ตั้งค่าใน Listenertoggleเท่านั้น ซึ่งครอบคลุมสถานะตัวอย่างและสถานะเปิด และ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ไว้ ส่วน Listenertoggleจะเก็บ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ยังคงทำงานได้เนื่องจากสร้างการคลิกปุ่ม