Hộp thoại

Tổng quan

Sử dụng phần tử <dialog> gốc cho hộp thoại phương thức và không theo phương thức. Phần tử này cung cấp ngữ nghĩa hỗ trợ tiếp cận, xử lý tiêu điểm, kết xuất lớp trên cùng và xử lý Esc mà các lớp phủ tuỳ chỉnh phải tạo lại theo cách thủ công.

Chọn loại lớp phủ

Chọn quy tắc đầu tiên phù hợp. Nếu bạn không chắc chắn, hãy sử dụng Quy tắc 1.

Quy tắc 1: Lớp phủ làm gián đoạn người dùng và chặn trang (mặc định)

Sử dụng cho các yêu cầu xác nhận, biểu mẫu, đăng nhập, hành động huỷ bỏ và mọi yêu cầu "phương thức", "hộp thoại", "hộp đèn" hoặc "cửa sổ bật lên" không có quy định khác.

Sử dụng <dialog> được mở dưới dạng phương thức, theo cách khai báo bằng command="show-modal" (xem Hộp thoại khai báo và Kiểm soát cửa sổ bật lên) hoặc từ tập lệnh bằng 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>

Trình duyệt cung cấp: trang không hoạt động, vùng chứa tiêu điểm, lớp trên cùng, ::backdrop, Esc và đóng bằng cử chỉ quay lại, đồng thời trả lại tiêu điểm cho trình gọi.

  • KHÔNG thêm role="dialog", aria-modal, bẫy tiêu điểm, bật/tắt inert hoặc trình xử lý Esc.
  • KHÔNG mở bằng show() hoặc thuộc tính open.
  • NÊN dừng thao tác cuộn trang phía sau bằng CSS bên dưới. Hãy xem bài viết Ngăn trang bên dưới hộp thoại phụ cuộn để biết thông tin chi tiết.
  • KHÔNG khoá thao tác cuộn bằng overflow: hidden hoặc position: fixed trên html hoặc 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;
}

Không dùng cho trình đơn hoặc chú thích: xem Quy tắc 3.

Quy tắc 2: Người dùng tiếp tục làm việc trên trang trong khi bảng điều khiển đang mở

Dùng cho thanh tìm kiếm, trình kiểm tra, bảng màu và ngăn soạn thư vẫn là hộp thoại về mặt ngữ nghĩa.

Sử dụng <dialog popover> cho bảng điều khiển lớp trên cùng có tính năng đóng cửa sổ bật lên (auto đóng nhẹ, manual không đóng nhẹ). Chỉ sử dụng dialog.show() khi bảng điều khiển không được nằm ở lớp trên cùng.

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

Trang vẫn có thể tương tác và không bị vô hiệu hoá.

  • NÊN có nút đóng dễ thấy.
  • KHÔNG sử dụng showModal() vì lệnh này sẽ chặn trang.

Không dùng để chặn thông báo xác nhận: xem Quy tắc 1.

Quy tắc 3: Lớp phủ không phải là hộp thoại

Dùng cho trình đơn, trình đơn thả xuống, chú thích, thẻ di chuột và thông báo ngắn.

Sử dụng phần tử ngữ nghĩa có thuộc tính popover, không phải <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Chọn cách đóng thông báo

Chế độ mặc định tuỳ thuộc vào cách mở hộp thoại. Nếu closedby không có hoặc không hợp lệ, showModal() sẽ hoạt động như closerequest và mọi cách mở khác sẽ hoạt động như none.

  • Chỉ phím Esc và cử chỉ quay lại (mặc định cho phương thức): không đặt gì hoặc đặt closedby="closerequest".
  • Cũng nhấp vào bên ngoài để đóng: sử dụng closedby="any".
  • Chỉ nhà phát triển mới đóng được (ví dụ: biểu mẫu có các thay đổi chưa lưu): sử dụng closedby="none" và thêm đường dẫn xác nhận. Xem phần Đóng bên dưới.

Nguyên tắc

Tên thành phần hỗ trợ tiếp cận và tiêu điểm ban đầu

  • NÊN đặt tên cho mọi hộp thoại bằng aria-labelledby trỏ đến tiêu đề của hộp thoại hoặc aria-label khi không có tiêu đề hiển thị.
  • NÊN đặt autofocus trên thành phần điều khiển chính hoặc trường đầu tiên.
  • NÊN đặt autofocus vào hành động an toàn (chẳng hạn như Huỷ hoặc Giữ lại) trong một thông báo xác nhận có tính huỷ diệt.
  • KHÔNG viết mã để trả lại tiêu điểm khi đóng. Trình duyệt sẽ trả về giá trị này cho trình gọi.

Từ giữa ra

Closing (Đang đóng)

  • NÊN đóng bằng nút <form method="dialog"> hoặc command="close".
  • NÊN chọn closedby một cách có chủ đích: any (loại bỏ nhẹ và Esc), closerequest (chỉ Esc, mặc định của phương thức) hoặc none (chỉ đóng do nhà phát triển kiểm soát).
  • NÊN sử dụng command="request-close" hoặc requestClose() cho các nút Huỷ phải tuân theo logic thay đổi chưa lưu. Họ kích hoạt cancel trước và bỏ qua closedby.
  • NÊN coi closedby là tính năng cải tiến tăng dần. Esc và cử chỉ quay lại trên thiết bị di động đã đóng các phương thức mà không cần đến nó.

Móc vào để đóng

  • NÊN đọc dialog.returnValue trong trình nghe close để tìm hiểu nút nào đã được sử dụng, sau đó đặt lại nút đó bằng dialog.returnValue = ''. Esc và các yêu cầu đóng khác không đặt giá trị nào, và các trình duyệt khác nhau về việc có xoá giá trị trước đó hay không, vì vậy, nếu không đặt lại, giá trị cũ có thể bị rò rỉ. Coi '' là "bị loại bỏ mà không chọn".
  • NÊN sử dụng cancel để chặn Esc và request-close, nhưng không dựa vào preventDefault() để nhấn Esc nhiều lần mà không có hoạt động kích hoạt của người dùng.
  • NÊN sử dụng closedby="none" với bước xác nhận rõ ràng cho các biểu mẫu chưa được lưu.
  • NÊN cho người dùng biết lý do hộp thoại vẫn mở khi cancel bị chặn: hiển thị một thông báo rõ ràng và tham chiếu thông báo đó bằng aria-describedby từ thành phần điều khiển nhận tiêu điểm (chẳng hạn như Huỷ), để trình đọc màn hình đọc thông báo đó khi tiêu điểm di chuyển. Nếu không có thuộc tính này, thì khi tiêu điểm di chuyển đến một nút mới, trình đọc màn hình chỉ thông báo tên của nút đó.
  • KHÔNG để nút xác nhận di chuyển xuống dưới con trỏ. Dành không gian cho thông báo (ẩn thông báo bằng visibility, không phải display hoặc hidden) và thêm nút huỷ bỏ ở vị trí mà các nút hiện có không di chuyển (ví dụ: nút đầu tiên trong hàng được căn lề phải), để lượt nhấp thứ hai tại cùng một vị trí không thể loại bỏ các thay đổi của người dùng.

Hộp thoại không phụ thuộc

  • NÊN sử dụng <dialog popover> (auto hoặc manual) cho bảng điều khiển lớp trên cùng và dialog.show() khi bạn không muốn có lớp trên cùng.
  • NÊN thêm nút đóng dễ thấy trong mọi hộp thoại không phụ thuộc.

Phông nền và kiểu dáng

Hỗ trợ tiếp cận

  • KHÔNG thêm role="dialog", aria-modal, bẫy lấy nét thủ công hoặc bật/tắt inert vào một phương thức gốc.
  • NÊN sử dụng <button> gốc có tên thành phần hỗ trợ tiếp cận cho các nút điều khiển đóng.
  • NÊN sử dụng một cấp tiêu đề nhất quán cho tiêu đề hộp thoại.

Chiến lược dự phòng

closedby (dialog-closedby)

Nếu không có closedby, một phương thức vẫn đóng bằng Esc và cử chỉ quay lại; chỉ mất thao tác nhấp bên ngoài, vì vậy, đây là sự xuống cấp nhẹ. closedby="none" cũng bị bỏ qua theo cách tương tự: Esc kích hoạt cancel rồi đóng hộp thoại. Giữ trình bảo vệ các thay đổi chưa lưu trong trình xử lý cancel, chứ không chỉ trong closedby="none", để trình bảo vệ này cũng bảo vệ những nơi thiếu closedby. Nếu mục tiêu Baseline của bạn yêu cầu đóng nhanh, hãy thêm mục này vào nơi thiếu 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

Hãy coi việc chứa cuộn là một tính năng cải tiến tăng dần. Đừng phát hiện tính năng: @supports báo cáo thuộc tính này là được hỗ trợ ngay cả khi các vùng chứa không thể cuộn bỏ qua thuộc tính này. Khi không được thực thi, hộp thoại vẫn chứa thao tác cuộn bất cứ khi nào nội dung của hộp thoại bị tràn và showModal() vẫn khiến trang không hoạt động. Đừng quay lại overflow: hidden hoặc position: fixed trên html hoặc body, vì thao tác này sẽ xoá thanh cuộn gốc và thay đổi bố cục.

Lệnh gọi (command/commandfor)

Nếu mục tiêu Baseline của bạn không bao gồm Lệnh của trình gọi, hãy giữ nguyên mã đánh dấu và tải polyfill theo điều kiện (cùng một trải nghiệm, dưới dạng cải tiến tăng dần). Phát hiện trên giao diện và không bao giờ tải giao diện đó vô điều kiện:

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

Chỉ Quy tắc 2 và 3 sử dụng Popover.

Phương án dự phòng và hỗ trợ trình duyệt cho Popover

Popover API chủ yếu là cải tiến tăng dần, nhưng các hành vi xác định của API này (quảng bá lớp trên cùng, đóng nhẹ và gọi popovertarget) không có hành vi tương đương chỉ dùng CSS. Các trình duyệt cũ cần có polyfill hoặc phương án dự phòng thủ công nếu bạn không muốn cung cấp polyfill.

Polyfill: Để hỗ trợ thuộc tính popover trong các trình duyệt cũ, hãy tải @oddbird/popover-polyfill theo điều kiện. BẮT BUỘC: Phát hiện tính năng bằng cách kiểm tra thuộc tính popover trên HTMLElement.prototype và chỉ tải polyfill khi thiếu tính năng hỗ trợ gốc — KHÔNG tải polyfill vô điều kiện.

Với trình đóng gói hoặc bản đồ nhập:

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

Nếu không có trình đóng gói, hãy nhập từ CDN bên trong <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Lưu ý về kiểu: Polyfill không thể xác định lớp giả :popover-open thực, vì vậy, lớp này sẽ áp dụng lớp .\:popover-open. BẮT BUỘC: Kết hợp cả hai bằng :is() hoặc :where(), nếu không, các trình duyệt thiếu :popover-open sẽ loại bỏ toàn bộ quy tắc:

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

Ngoài ra, đối với phương án dự phòng cũ không có polyfill, hãy sử dụng position: fixed và tính toán toạ độ theo cách thủ công thông qua getBoundingClientRect() hoặc dựa vào vị trí mặc định bằng inset: auto nếu điều đó phù hợp với trường hợp sử dụng.