ข้อความ

ภาพรวม

ใช้องค์ประกอบ <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 ไว้ที่การดำเนินการที่ปลอดภัย (เช่น ยกเลิกหรือเก็บไว้) ในการยืนยันการดำเนินการที่ส่งผลกระทบต่อข้อมูล
  • อย่าเขียนโค้ดเพื่อคืนโฟกัสเมื่อปิด เบราว์เซอร์จะส่งผลลัพธ์กลับไปยังผู้เรียกใช้

เปิดจากตรงกลาง

เปิดจากขอบ

  • ควรปิดด้วยปุ่ม <form method="dialog"> หรือ command="close"
  • ควรเลือก closedby อย่างรอบคอบ: any (ปิดเมื่อแตะนอกพื้นที่และ Esc), closerequest (Esc เท่านั้น ซึ่งเป็นค่าเริ่มต้นของโมดัล) หรือ none (ปิดโดยนักพัฒนาแอปควบคุมเท่านั้น)
  • ควรใช้ command="request-close" หรือ requestClose() สำหรับปุ่มยกเลิกที่ต้องเป็นไปตามตรรกะการเปลี่ยนแปลงที่ยังไม่ได้บันทึก โดยจะยิง cancel ก่อนและไม่สนใจ closedby
  • ควรถือว่า closedby เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่อง Esc และท่าทางสัมผัสเพื่อย้อนกลับบนอุปกรณ์เคลื่อนที่สามารถปิดโมดัลได้อยู่แล้วโดยไม่ต้องใช้ปุ่มนี้

การดึงดูดความสนใจในช่วงปิดท้าย

  • ควรอ่าน dialog.returnValue ใน Listener close เพื่อดูว่ามีการใช้ปุ่มใด จากนั้นรีเซ็ตด้วย 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() เมื่อไม่ต้องการเลเยอร์บนสุด
  • ควรใส่ปุ่มปิดที่มองเห็นได้ในกล่องโต้ตอบที่ไม่ใช่โมดัลทุกกล่อง

ฉากหลังและการจัดสไตล์

การช่วยเหลือพิเศษ

  • อย่าเพิ่ม 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 หากยอมรับได้สำหรับกรณีการใช้งาน