ภาพรวม
ใช้องค์ประกอบ <dialog> แบบเนทีฟสำหรับกล่องโต้ตอบแบบโมดัลและไม่ใช่โมดัล โดยจะให้ความหมายของการช่วยเหลือพิเศษ การจัดการโฟกัส การแสดงผลเลเยอร์บนสุด และการจัดการ Esc ที่การวางซ้อนที่กำหนดเองต้องสร้างใหม่ด้วยตนเอง
เลือกประเภทการวางซ้อน
เลือกกฎแรกที่ตรงกัน หากไม่แน่ใจ ให้ใช้กฎข้อที่ 1
กฎข้อที่ 1: โฆษณาซ้อนทับขัดจังหวะผู้ใช้และบล็อกหน้าเว็บ (ค่าเริ่มต้น)
ใช้สำหรับการยืนยัน แบบฟอร์ม การลงชื่อเข้าใช้ การดำเนินการที่ส่งผลกระทบ และคำขอ "โมดัล" "กล่องโต้ตอบ" "ไลท์บ็อกซ์" หรือ "ป๊อปอัป" ที่ไม่ได้ระบุเป็นอย่างอื่น
ใช้ <dialog> ที่เปิดเป็นโมดัล โดยใช้ร่วมกับ command="show-modal" (ดูการควบคุมกล่องโต้ตอบและป๊อปโอเวอร์แบบประกาศ) หรือจากสคริปต์ที่มี showModal()
<button type="button" command="show-modal" commandfor="confirm">Delete</button>
<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
เบราว์เซอร์มีหน้าเฉื่อย, การจำกัดโฟกัส, เลเยอร์บนสุด, ::backdrop, Esc และการปิดด้วยท่าทางสัมผัสเพื่อย้อนกลับ และการคืนโฟกัสไปยังผู้เรียกใช้
- อย่าเพิ่ม
role="dialog",aria-modal, กับดักโฟกัส, การสลับinertหรือแฮนเดิลEsc - อย่าเปิดด้วย
show()หรือแอตทริบิวต์open - ควรหยุดการเลื่อนหน้าเว็บที่อยู่ด้านหลังด้วย CSS ด้านล่าง ดูรายละเอียดได้ที่ป้องกันไม่ให้หน้าเว็บใต้กล่องโต้ตอบโมดัลเลื่อน
- อย่าล็อกการเลื่อนด้วย
overflow: hiddenหรือposition: fixedในhtmlหรือbody
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
overscroll-behavior: contain;
}
ไม่ใช้กับเมนูหรือเคล็ดลับเครื่องมือ: ดูหัวข้อกฎข้อที่ 3
กฎข้อที่ 2: ผู้ใช้ยังคงทำงานในหน้าเว็บขณะที่แผงเปิดอยู่
ใช้สำหรับแถบค้นหา ตัวตรวจสอบ จานสี และแผงเขียนที่ยังคงเป็นกล่องโต้ตอบในเชิงความหมาย
ใช้ <dialog popover> สำหรับแผงเลเยอร์บนสุดที่มีการปิดป๊อปโอเวอร์ (auto ปิดแบบเบาๆ ส่วน manual ไม่ปิด) ใช้ dialog.show() เฉพาะเมื่อแผงต้องไม่อยู่ในเลเยอร์บนสุด
<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
<h2 id="find-title">Find</h2>
<input type="search" aria-label="Search">
<button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>
หน้าเว็บจะยังคงโต้ตอบได้และไม่เฉื่อยชา
- ควรใส่ปุ่มปิดที่มองเห็นได้
- อย่าใช้
showModal()ซึ่งจะบล็อกหน้าเว็บ
ไม่ใช้สำหรับการยืนยันการบล็อก: ดูข้อ 1
กฎข้อที่ 3: การวางซ้อนไม่ใช่กล่องโต้ตอบ
ใช้สำหรับเมนู เมนูแบบเลื่อนลง เคล็ดลับเครื่องมือ โฮเวอร์การ์ด และข้อความโทสต์
ใช้องค์ประกอบเชิงความหมายกับแอตทริบิวต์ popover ไม่ใช่ <dialog>
<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
<li><a href="/rename">Rename</a></li>
</ul>
เลือกวิธีปิดข้อความ
โดยค่าเริ่มต้นจะขึ้นอยู่กับวิธีเปิดกล่องโต้ตอบ หากไม่มี closedby หรือไม่ถูกต้อง showModal() จะทำงานเป็น closerequest และวิธีอื่นๆ ในการเปิดจะทำงานเป็น none
- เฉพาะท่าทางสัมผัส Esc และย้อนกลับ (ค่าเริ่มต้นของโมดัล): ไม่ต้องตั้งค่าใดๆ หรือ
closedby="closerequest" - หรือคลิกนอกหน้าต่างเพื่อปิด: ใช้
closedby="any" - เฉพาะนักพัฒนาแอปเท่านั้นที่ปิดได้ (เช่น แบบฟอร์มที่มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก): ให้ใช้
closedby="none"และเพิ่มเส้นทางการยืนยัน ดูหัวข้อ "การปิด" ด้านล่าง
หลักเกณฑ์
ชื่อที่เข้าถึงได้และโฟกัสเริ่มต้น
- ควรตั้งชื่อกล่องโต้ตอบทุกกล่องโดยใช้
aria-labelledbyที่ชี้ไปยังส่วนหัว หรือaria-labelเมื่อไม่มีชื่อที่มองเห็นได้ - ควรใส่
autofocusไว้ในการควบคุมหลักหรือช่องแรก - ควรวาง
autofocusไว้ที่การดำเนินการที่ปลอดภัย (เช่น ยกเลิกหรือเก็บไว้) ในการยืนยันการดำเนินการที่ส่งผลกระทบต่อข้อมูล - อย่าเขียนโค้ดเพื่อคืนโฟกัสเมื่อปิด เบราว์เซอร์จะส่งผลลัพธ์กลับไปยังผู้เรียกใช้
เปิดจากตรงกลาง
- ควรเปิดแบบประกาศด้วย
command="show-modal"และcommandforใน<button>ดูการควบคุมกล่องโต้ตอบและป๊อปโอเวอร์แบบประกาศ - ควรใช้
dialog.showModal()เมื่อเปิดจากสคริปต์
เปิดจากขอบ
- ควรปิดด้วยปุ่ม
<form method="dialog">หรือcommand="close" - ควรเลือก
closedbyอย่างรอบคอบ:any(ปิดเมื่อแตะนอกพื้นที่และEsc),closerequest(Escเท่านั้น ซึ่งเป็นค่าเริ่มต้นของโมดัล) หรือnone(ปิดโดยนักพัฒนาแอปควบคุมเท่านั้น) - ควรใช้
command="request-close"หรือrequestClose()สำหรับปุ่มยกเลิกที่ต้องเป็นไปตามตรรกะการเปลี่ยนแปลงที่ยังไม่ได้บันทึก โดยจะยิงcancelก่อนและไม่สนใจclosedby - ควรถือว่า
closedbyเป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องEscและท่าทางสัมผัสเพื่อย้อนกลับบนอุปกรณ์เคลื่อนที่สามารถปิดโมดัลได้อยู่แล้วโดยไม่ต้องใช้ปุ่มนี้
การดึงดูดความสนใจในช่วงปิดท้าย
- ควรอ่าน
dialog.returnValueใน Listenercloseเพื่อดูว่ามีการใช้ปุ่มใด จากนั้นรีเซ็ตด้วยdialog.returnValue = ''Escและคำขอปิดอื่นๆ ไม่ได้ตั้งค่าไว้ และเบราว์เซอร์จะแตกต่างกันไปตามการล้างค่าก่อนหน้า ดังนั้นหากไม่มีการรีเซ็ต ค่าที่ไม่มีการอัปเดตอาจรั่วไหลได้ ถือว่า''เป็น "ปิดโดยไม่เลือก" - ควรใช้
cancelเพื่อสกัดกั้นEscและrequest-closeแต่ไม่ควรใช้preventDefault()สำหรับการกดEscซ้ำๆ โดยไม่มีการเปิดใช้งานโดยผู้ใช้ - ควรใช้
closedby="none"กับขั้นตอนการยืนยันที่ชัดเจนสำหรับแบบฟอร์มที่มีการแก้ไข - ควรบอกผู้ใช้ถึงสาเหตุที่กล่องโต้ตอบยังคงเปิดอยู่เมื่อมีการป้องกัน
cancelโดยแสดงข้อความที่มองเห็นได้และอ้างอิงด้วยaria-describedbyจากตัวควบคุมที่ได้รับโฟกัส (เช่น ทิ้ง) เพื่อให้โปรแกรมอ่านหน้าจออ่านข้อความเมื่อโฟกัสย้ายไป หากไม่มีป้ายกำกับ การย้ายโฟกัสไปยังปุ่มใหม่จะประกาศเฉพาะชื่อของปุ่มนั้น - อย่าให้การยืนยันย้ายตัวควบคุมไปไว้ใต้เคอร์เซอร์ สำรองพื้นที่ของข้อความ (ซ่อนด้วย
visibilityไม่ใช่displayหรือhidden) และเพิ่มปุ่มที่ทำให้เกิดการเปลี่ยนแปลงในตำแหน่งที่ปุ่มที่มีอยู่จะไม่ย้าย (เช่น ปุ่มแรกในแถวที่จัดชิดขวา) เพื่อไม่ให้การคลิกครั้งที่ 2 ในจุดเดียวกันทิ้งการเปลี่ยนแปลงของผู้ใช้
กล่องโต้ตอบแบบไม่บังคับ (Non-modal Dialog)
- ควรใช้
<dialog popover>(autoหรือmanual) สำหรับแผงเลเยอร์บนสุด และdialog.show()เมื่อไม่ต้องการเลเยอร์บนสุด - ควรใส่ปุ่มปิดที่มองเห็นได้ในกล่องโต้ตอบที่ไม่ใช่โมดัลทุกกล่อง
ฉากหลังและการจัดสไตล์
- ควรจัดสไตล์
::backdropและใช้dialog:modalเพื่อกำหนดเป้าหมายสไตล์เฉพาะโมดัล - ควรปล่อยให้ภาพเคลื่อนไหวทำให้องค์ประกอบเคลื่อนไหวไปยังและจากเลเยอร์บนสุด, ลากเพื่อสร้างองค์ประกอบที่ลากได้ และเปลี่ยนระดับชั้นบนสุดเป็นUI เลเยอร์บนสุดแบบถาวร
- อย่าตั้งค่า
displayเป็นdialogโดยไม่มีเงื่อนไข ซึ่งจะลบล้างสถานะปิด กำหนดขอบเขตด้วยdialog[open]
การช่วยเหลือพิเศษ
- อย่าเพิ่ม
role="dialog",aria-modal, แทรปโฟกัสด้วยตนเอง หรือการสลับinertไปยังโมดัลแบบเนทีฟ - ควรใช้
<button>แบบเนทีฟที่มีชื่อที่เข้าถึงได้สำหรับการควบคุมการปิด - ควรใช้ระดับหัวเรื่องที่สอดคล้องกันสำหรับชื่อกล่องโต้ตอบ
กลยุทธ์สำรอง
closedby (dialog-closedby)
หากไม่มี closedby โมดัลจะยังคงปิดด้วย Esc และท่าทางสัมผัสเพื่อย้อนกลับ มีเพียงการคลิกนอกโมดัลเท่านั้นที่หายไป ดังนั้นนี่จึงเป็นการลดลงอย่างค่อยเป็นค่อยไป closedby="none" จะถูกละเว้นในลักษณะเดียวกัน: Esc จะเริ่มทำงาน cancel แล้วปิดกล่องโต้ตอบ เก็บการป้องกันการเปลี่ยนแปลงที่ยังไม่ได้บันทึกไว้ในเครื่องจัดการ cancel ไม่ใช่ใน closedby="none" เพียงอย่างเดียว เพื่อให้ป้องกันในกรณีที่ไม่มี closedby ด้วย หากเป้าหมาย Baseline ของคุณต้องใช้การปิดเมื่อแตะนอกองค์ประกอบ ให้เพิ่มโค้ดนี้ในตำแหน่งที่ไม่มี closedby
if (!('closedBy' in HTMLDialogElement.prototype)) {
// Test the pointer position, not event.target: the <dialog> is also the target for its own
// padding and the gaps between its children, so a target check would close on those clicks
const outside = (e, dialog) => {
const r = dialog.getBoundingClientRect();
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
};
for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
let pressedOutside = false;
dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
// Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
}
}
overscroll-behavior
พิจารณาการจำกัดการเลื่อนเป็นการเพิ่มประสิทธิภาพแบบต่อเนื่อง อย่าใช้การตรวจหาฟีเจอร์: @supports จะรายงานพร็อพเพอร์ตี้ว่ารองรับแม้ว่าคอนเทนเนอร์ที่เลื่อนไม่ได้จะไม่สนใจก็ตาม ในกรณีที่ไม่ได้บังคับใช้ กล่องโต้ตอบจะยังคงมีการเลื่อนเมื่อเนื้อหาล้น และ showModal() จะยังคงทำให้หน้าไม่มีการตอบสนอง อย่าใช้ overflow: hidden หรือ position: fixed ใน html หรือ body ซึ่งจะนำแถบเลื่อนรูทออกและเปลี่ยนเลย์เอาต์
คำสั่ง Invoker (command/commandfor)
หากเป้าหมาย Baseline ของคุณไม่มีคำสั่ง Invoker ให้ใช้มาร์กอัปเดิมและโหลด Polyfill แบบมีเงื่อนไข (ประสบการณ์การใช้งานแบบเดียวกัน เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่อง) ตรวจหาในอินเทอร์เฟซและไม่โหลดโดยไม่มีเงื่อนไข
// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill'); // npm install invokers-polyfill
}
มีเพียงกฎที่ 2 และ 3 เท่านั้นที่ใช้ป๊อปโอเวอร์
การรองรับเบราว์เซอร์และทางเลือกสำรองสำหรับ 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 หากยอมรับได้สำหรับกรณีการใช้งาน