Xây dựng thành phần hộp thoại

Thông tin tổng quan cơ bản về cách tạo các cửa sổ nhỏ và lớn dễ tiếp cận bằng phần tử <dialog> để tạo khả năng thích ứng màu sắc, phản hồi nhanh và dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ của mình về cách tạo ra khả năng thích ứng màu sắc, các cửa sổ nhỏ và lớn cũng như dễ tiếp cận với phần tử <dialog>. Thử bản minh hoạxem nguồn!

Hình minh hoạ các hộp thoại lớn và hộp thoại nhỏ trong giao diện sáng và tối.

Nếu bạn thích xem video hơn, sau đây là phiên bản của bài đăng này trên YouTube:

Tổng quan

Chiến lược phát hành đĩa đơn <dialog> là yếu tố tuyệt vời cho hành động hoặc thông tin theo ngữ cảnh trong trang. Cân nhắc thời điểm trải nghiệm người dùng có thể hưởng lợi từ cùng một hành động trên trang thay vì nhiều trang hành động: có thể do biểu mẫu nhỏ hoặc hành động duy nhất được yêu cầu từ xác nhận hoặc huỷ.

Gần đây, phần tử <dialog> đã trở nên ổn định trên các trình duyệt:

Hỗ trợ trình duyệt

  • 37
  • 79
  • 98
  • 15,4

Nguồn

Tôi thấy phần tử này bị thiếu một vài thứ, vì vậy trong GUI (Giao diện người dùng đồ hoạ) này Thử thách Tôi thêm trải nghiệm của nhà phát triển các mục tôi mong đợi: sự kiện bổ sung, loại bỏ ánh sáng, ảnh động tuỳ chỉnh và hình đại diện và loại mega.

Markup (note: đây là tên ứng dụng)

Các thành phần cơ bản của phần tử <dialog> khá khiêm tốn. Phần tử này sẽ tự động bị ẩn và có các kiểu được tích hợp để phủ nội dung của bạn.

<dialog>
  …
</dialog>

Chúng tôi có thể cải thiện đường cơ sở này.

Theo truyền thống, phần tử hộp thoại sẽ có nhiều điểm chung với thể thức và thường tên có thể thay thế cho nhau. Tôi muốn sử dụng phần tử hộp thoại cho cả cửa sổ hộp thoại bật lên nhỏ (nhỏ) cũng như hộp thoại toàn trang (mega). Tôi đã đặt tên cho cả hai hộp thoại lớn và nhỏ, với cả hai hộp thoại được điều chỉnh đôi chút cho phù hợp với các trường hợp sử dụng khác nhau. Tôi đã thêm thuộc tính modal-mode để bạn có thể chỉ định loại:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

Ảnh chụp màn hình cả hộp thoại mini và lớn trong cả giao diện sáng lẫn tối.

Không phải lúc nào cũng vậy, nhưng thường thì các thành phần hộp thoại sẽ được dùng để thu thập tương tác. Biểu mẫu bên trong phần tử hộp thoại được tạo để sử dụng khi kết hợp cùng nhau. Bạn nên có một phần tử biểu mẫu bao bọc nội dung hộp thoại để JavaScript có thể truy cập vào dữ liệu mà người dùng đã nhập. Hơn nữa, các nút bên trong một biểu mẫu sử dụng method="dialog" có thể đóng hộp thoại mà không cần JavaScript và truyền .

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Hộp thoại siêu lớn

Một hộp thoại lớn có 3 phần tử bên trong biểu mẫu: <header>! <article>, và <footer>. Các đối tượng này đóng vai trò là vùng chứa ngữ nghĩa cũng như mục tiêu kiểu cho bản trình bày hộp thoại. Tiêu đề đặt tiêu đề cho cửa sổ phụ và đưa ra đoạn kết . Bài viết này dành cho thông tin và thông tin về biểu mẫu. Chân trang có <menu> / các nút hành động.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

Nút trình đơn đầu tiên có autofocus và một trình xử lý sự kiện cùng dòng onclick. Thuộc tính autofocus sẽ nhận được đặt tiêu điểm khi hộp thoại mở ra và tôi thấy cách hay nhất là đặt tiêu điểm này nút huỷ, chứ không phải nút xác nhận. Điều này đảm bảo rằng việc xác nhận có chủ ý chứ không phải tình cờ.

Hộp thoại nhỏ

Hộp thoại mini rất giống với hộp thoại lớn, chỉ thiếu một Phần tử <header>. Điều này cho phép kích thước nhỏ hơn và cùng dòng hơn.

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

Phần tử hộp thoại cung cấp một nền tảng vững chắc cho phần tử khung nhìn toàn diện có thể thu thập dữ liệu và tương tác của người dùng. Những yếu tố thiết yếu này có thể giúp các tương tác thú vị và mạnh mẽ trong trang web hoặc ứng dụng của bạn.

Hỗ trợ tiếp cận

Phần tử hộp thoại có khả năng hỗ trợ tiếp cận tích hợp rất tốt. Thay vì thêm các như tôi thường làm, nhiều tính năng đã có sẵn.

Đang khôi phục tiêu điểm

Như chúng tôi đã làm thủ công trong bài viết Xây dựng trang điều hướng bên , điều quan trọng là mở và đóng nội dung nào đó đúng cách sẽ tập trung vào điểm mở và đóng có liên quan các nút. Khi điều hướng bên đó mở ra, tiêu điểm sẽ được đặt vào nút đóng. Khi người dùng nhấn nút đóng, tiêu điểm sẽ được khôi phục về nút mở ra.

Với phần tử hộp thoại, đây là hành vi mặc định tích hợp sẵn:

Rất tiếc, nếu bạn muốn tạo hiệu ứng động cho hộp thoại, hãy sử dụng chức năng này bị mất. Trong phần JavaScript, tôi sẽ khôi phục phần đó của Google.

Tập trung vào bẫy

Phần tử hộp thoại quản lý inert cho bạn trên tài liệu. Trước inert, JavaScript được dùng để theo dõi tiêu điểm rời khỏi một phần tử, khi đó phần tử sẽ chặn và đặt phần tử trở lại.

Hỗ trợ trình duyệt

  • 102
  • 102
  • 112
  • 15,5

Nguồn

Sau inert, bất kỳ phần nào của tài liệu đều có thể bị "đóng băng" chúng không còn là mục tiêu lấy tiêu điểm hoặc có tính tương tác bằng chuột. Thay vì mắc bẫy tiêu điểm sẽ được dẫn vào phần tương tác duy nhất của tài liệu.

Mở và tự động lấy nét một phần tử

Theo mặc định, phần tử hộp thoại sẽ gán tiêu điểm cho phần tử đầu tiên có thể làm tâm điểm trong phần đánh dấu hộp thoại. Nếu đây không phải là phần tử phù hợp nhất để người dùng đặt mặc định, hãy sử dụng thuộc tính autofocus. Như đã mô tả trước đó, tôi cho rằng cách hay nhất để đặt thông tin này lên nút huỷ chứ không phải nút xác nhận. Điều này đảm bảo rằng việc xác nhận là có chủ ý chứ không phải tình cờ.

Đóng bằng phím Escape

Bạn phải đảm bảo dễ dàng đóng phần tử có thể gây gián đoạn này. Rất may là phần tử hộp thoại sẽ xử lý phím Escape cho bạn, giúp bạn giải phóng trách nhiệm điều phối.

Kiểu

Có một đường dẫn đơn giản để tạo kiểu cho phần tử hộp thoại và một đường dẫn cứng. Cách dễ dàng đường dẫn đó đạt được do không thay đổi thuộc tính hiển thị của hộp thoại và hoạt động nhưng còn nhiều hạn chế. Tôi đi theo con đường cố định để cung cấp ảnh động tuỳ chỉnh cho mở và đóng hộp thoại, chiếm quyền kiểm soát thuộc tính display và nhiều thao tác khác.

Tạo kiểu bằng đạo cụ mở

Để đẩy nhanh tốc độ màu sắc thích ứng và tính nhất quán tổng thể trong thiết kế, tôi đã tự tin đưa vào thư viện biến CSS của tôi Open Prop (Mở đạo cụ). Ngang bằng ngoài các biến được cung cấp miễn phí, tôi cũng nhập chuẩn hoá tệp và một số nút, cả hai đều mở đạo cụ cung cấp dưới dạng lệnh nhập không bắt buộc. Các lệnh nhập này giúp tôi tập trung vào việc tuỳ chỉnh hộp thoại và bản minh hoạ mà không cần nhiều kiểu để hỗ trợ và tạo giao diện tốt.

Tạo kiểu cho phần tử <dialog>

Sở hữu thuộc tính hiển thị

Hành vi hiện và ẩn mặc định của phần tử hộp thoại bật/tắt chế độ hiển thị từ block đến none. Rất tiếc, điều này có nghĩa là video không thể tạo ảnh động vào và ra, chỉ ở vào. Tôi muốn tạo ảnh động cả trong lẫn ngoài, bước đầu tiên là để tự thiết lập display:

dialog {
  display: grid;
}

Bằng cách thay đổi và theo đó sở hữu, giá trị thuộc tính hiển thị, như được thể hiện trong đoạn mã CSS nêu trên, nên cần phải quản lý một số lượng đáng kể kiểu kiểu để giúp mang lại trải nghiệm phù hợp cho người dùng. Đầu tiên, trạng thái mặc định của hộp thoại là đã đóng. Bạn có thể biểu thị trạng thái này một cách trực quan và ngăn hộp thoại nhận được lượt tương tác có các kiểu sau:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

Giờ đây, hộp thoại sẽ không xuất hiện và bạn không thể tương tác khi không mở. Để sau Tôi sẽ thêm một số JavaScript để quản lý thuộc tính inert trên hộp thoại, đảm bảo rằng người dùng bàn phím và trình đọc màn hình cũng không thể truy cập vào hộp thoại bị ẩn.

Cung cấp chủ đề màu sắc thích ứng cho hộp thoại

Hộp thoại siêu lớn hiển thị giao diện sáng và tối, thể hiện màu sắc của bề mặt.

Mặc dù color-scheme chọn đưa tài liệu của bạn vào một trình duyệt do trình duyệt cung cấp chủ đề màu sắc thích ứng theo lựa chọn ưu tiên của hệ thống sáng và tối, tôi muốn tuỳ chỉnh phần tử hộp thoại nhiều hơn thế. Open Props cung cấp một số bề mặt màu có khả năng tự động điều chỉnh cho phù hợp với các lựa chọn ưu tiên về hệ thống sáng và tối, tương tự như sử dụng color-scheme. Các thật tuyệt vời khi tạo các lớp trong một thiết kế và tôi thích sử dụng màu sắc để giúp hỗ trợ trực quan giao diện này của các bề mặt lớp. Màu nền là var(--surface-1); để đặt trên lớp đó, hãy sử dụng var(--surface-2):

dialog {
  …
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

Các màu thích ứng khác sẽ được thêm vào sau cho các phần tử con, chẳng hạn như tiêu đề và chân trang. Tôi xem đó là yếu tố bổ sung cho yếu tố hộp thoại, nhưng thực sự quan trọng đối với tạo nên thiết kế hộp thoại hấp dẫn và được thiết kế tốt.

Kích thước hộp thoại thích ứng

Hộp thoại mặc định uỷ quyền kích thước của hộp thoại cho nội dung của hộp thoại, thường là tuyệt vời. Mục tiêu của tôi ở đây là ràng buộc max-inline-size thành kích thước dễ đọc (--size-content-3 = 60ch) hoặc 90% chiều rộng khung nhìn. Chiến dịch này đảm bảo hộp thoại sẽ không hiển thị tràn viền trên thiết bị di động và sẽ không rộng trên màn hình máy tính đến nỗi khó đọc. Sau đó, tôi thêm một max-block-size để hộp thoại không vượt quá chiều cao của trang. Điều này cũng có nghĩa là chúng tôi cần chỉ định vị trí của vùng có thể cuộn của hộp thoại, trong trường hợp khu vực đó cao phần tử hộp thoại.

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

Bạn có nhận thấy cách tôi có max-block-size hai lần không? Hàm đầu tiên sử dụng 80vh, một thẻ đơn vị khung nhìn. Điều tôi thực sự muốn là giữ cho hộp thoại ở trong luồng tương đối, cho người dùng quốc tế, nên tôi sử dụng thuật toán hợp lý, mới hơn và chỉ một phần hỗ trợ đơn vị dvb trong phần khai báo thứ hai khi mã này trở nên ổn định hơn.

Định vị hộp thoại siêu lớn

Để hỗ trợ định vị phần tử hộp thoại, bạn nên chia nhỏ hai phần tử phần: phông nền toàn màn hình và vùng chứa hộp thoại. Phông nền phải che phủ mọi thứ, cung cấp hiệu ứng bóng đổ để hỗ trợ hộp thoại này phía trước và nội dung phía sau không thể truy cập được. Vùng chứa hộp thoại miễn phí căn giữa phông nền này và có hình dạng bất kỳ mà nội dung yêu cầu.

Các kiểu sau đây sẽ cố định phần tử hộp thoại với cửa sổ, kéo giãn phần tử theo từng góc và sử dụng margin: auto để căn giữa nội dung:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Kiểu hộp thoại lớn trên thiết bị di động

Trên các khung nhìn nhỏ, tôi tạo kiểu cho cửa sổ lớn cho toàn bộ trang này hơi khác một chút. N đặt lề dưới thành 0, thao tác này sẽ đưa nội dung hộp thoại xuống cuối khung nhìn. Với một vài điều chỉnh kiểu, tôi có thể biến hộp thoại thành biểu đồ hành động, gần với ngón cái của người dùng hơn:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Ảnh chụp màn hình khoảng cách lề lớp phủ của công cụ cho nhà phát triển 
  trên cả hộp thoại siêu lớn dành cho máy tính và thiết bị di động trong khi mở.

Định vị hộp thoại nhỏ

Khi sử dụng một khung nhìn lớn hơn, chẳng hạn như trên máy tính, tôi đã chọn đặt các hộp thoại nhỏ trên là phần tử gọi chúng. Để thực hiện việc này, tôi cần JavaScript. Bạn có thể tìm thấy kỹ thuật tôi sử dụng đây, nhưng tôi cho rằng nội dung đó nằm ngoài phạm vi của bài viết này. Nếu không có JavaScript, mini sẽ xuất hiện ở giữa màn hình, giống như hộp thoại lớn.

Làm nổi bật

Cuối cùng, hãy thêm điểm nhấn cho hộp thoại để hộp thoại trông giống như một bề mặt mềm mại phía trên trang. Độ mềm đạt được bằng cách bo tròn các góc của hộp thoại. Độ sâu đạt được với một trong những bóng đổ được chế tác cẩn thận của Open Props đạo cụ:

dialog {
  …
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

Tuỳ chỉnh phần tử giả phông nền

Tôi chọn xử lý phông nền rất nhẹ, chỉ thêm hiệu ứng làm mờ bằng backdrop-filter vào hộp thoại lớn:

Hỗ trợ trình duyệt

  • 76
  • 79
  • 103
  • 18

Nguồn

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

Tôi cũng chọn tiến hành chuyển đổi vào backdrop-filter với hy vọng các trình duyệt sẽ cho phép chuyển đổi phần tử phông nền trong tương lai:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Ảnh chụp màn hình của hộp thoại lớn phủ lên nền được làm mờ gồm các hình đại diện đầy màu sắc.

Tạo kiểu bổ sung

Tôi gọi phần này là "thông tin bổ sung" vì vấn đề này có liên quan nhiều đến thành phần hộp thoại của tôi bản minh hoạ khác so với phần tử hộp thoại nói chung.

Ngăn cuộn

Khi hộp thoại xuất hiện, người dùng vẫn có thể cuộn trang phía sau nó, mà tôi không muốn:

Thông thường, overscroll-behavior sẽ là giải pháp thông thường của tôi, nhưng theo quy cách, nó không ảnh hưởng đến hộp thoại vì đó không phải là cổng cuộn nên không có gì để ngăn chặn. Tôi có thể dùng JavaScript để theo dõi các sự kiện mới từ hướng dẫn này, chẳng hạn như "đã đóng" và "opened" (đã mở) và bật/tắt overflow: hidden trên tài liệu, hoặc tôi có thể đợi :has() ổn định trong tất cả trình duyệt:

Hỗ trợ trình duyệt

  • 105
  • 105
  • 121
  • 15,4

Nguồn

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

Bây giờ, khi một hộp thoại lớn đang mở, tài liệu html sẽ có overflow: hidden.

Bố cục <form>

Ngoài việc là một yếu tố rất quan trọng để thu thập hoạt động tương tác từ người dùng, tôi sử dụng thông tin đó ở đây để bố trí đầu trang, chân trang và bài viết. Với bố cục này, tôi dự định trình bày rõ bài viết con là một khu vực có thể cuộn. Tôi đạt được điều này bằng grid-template-rows. Phần tử bài viết được cung cấp 1fr và bản thân biểu mẫu có cùng giá trị tối đa chiều cao làm phần tử hộp thoại. Đặt chiều cao cố định và kích thước hàng cố định này cho phép phần tử bài viết bị hạn chế và cuộn khi bị tràn:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Ảnh chụp màn hình công cụ cho nhà phát triển phủ thông tin bố cục lưới lên các hàng.

Tạo kiểu cho hộp thoại <header>

Vai trò của phần tử này là cung cấp tiêu đề cho nội dung hộp thoại và ưu đãi một nút đóng dễ tìm. Thiết bị cũng được gán màu cho bề mặt để làm cho thiết bị xuất hiện ở phía sau nội dung bài viết hộp thoại. Những yêu cầu này dẫn đến hộp linh hoạt vùng chứa, các mục được căn chỉnh theo chiều dọc được giãn cách với các cạnh và một số khoảng đệm và khoảng trống để tạo khoảng trống cho tiêu đề và nút đóng:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

Ảnh chụp màn hình Chrome Devtools che phủ thông tin bố cục hộp linh hoạt trên tiêu đề hộp thoại.

Tạo kiểu cho nút đóng tiêu đề

Vì bản minh hoạ sử dụng các nút Open Props (Mở đạo cụ) nên nút đóng được tuỳ chỉnh vào một nút có biểu tượng hình tròn làm tâm điểm như sau:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

Ảnh chụp màn hình Chrome Devtools che phủ thông tin về kích thước và khoảng đệm cho nút đóng tiêu đề.

Tạo kiểu cho hộp thoại <article>

Phần tử bài viết có vai trò đặc biệt trong hộp thoại này: đó là một không gian nhằm cuộn trong trường hợp hộp thoại cao hoặc dài.

Để thực hiện điều này, phần tử biểu mẫu gốc đã thiết lập một số mức tối đa cho Bản thân nó cung cấp các hạn chế để phần tử bài viết này tiếp cận nếu nó được quá cao. Đặt overflow-y: auto để thanh cuộn chỉ hiển thị khi cần, chứa thao tác cuộn trong đó bằng overscroll-behavior: contain và phần còn lại sẽ là kiểu bản trình bày tuỳ chỉnh:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

Vai trò của chân trang là chứa các trình đơn gồm các nút hành động. Hộp linh hoạt được dùng để căn chỉnh nội dung với phần cuối của trục cùng dòng chân trang, sau đó giãn cách để hãy căn chỉnh cho các nút.

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

Ảnh chụp màn hình Chrome Devtools che phủ thông tin bố cục hộp linh hoạt trên phần tử chân trang.

menu được dùng để chứa các nút hành động cho hộp thoại. Hàm này sử dụng tính năng xuống dòng tự động bố cục hộp linh hoạt với gap để tạo không gian giữa các nút. Thành phần trình đơn có khoảng đệm như <ul>. Tôi cũng xoá kiểu đó vì không cần đến.

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

Ảnh chụp màn hình Chrome Devtools che phủ thông tin hộp linh hoạt trên các phần tử của trình đơn chân trang.

Hoạt ảnh

Các phần tử hộp thoại thường có ảnh động vì các phần tử này đi vào và thoát khỏi cửa sổ. Đưa ra hộp thoại một số chuyển động hỗ trợ cho việc ra vào này sẽ giúp người dùng tự xác định hướng trong luồng nội dung.

Thông thường, thành phần hộp thoại chỉ có thể là ảnh động trong, chứ không phải ảnh động. Điều này là do trình duyệt sẽ bật/tắt thuộc tính display trên phần tử đó. Phần trước, hướng dẫn đặt màn hình thành lưới và không bao giờ đặt màn hình thành không có. Điều này mở ra khả năng tạo hiệu ứng động.

Đạo cụ mở đi kèm với nhiều khung hình chính ảnh động để sử dụng, giúp tạo để nội dung sắp xếp trở nên dễ đọc và dễ đọc. Sau đây là mục tiêu ảnh động và các lớp mà tôi đã áp dụng:

  1. Chuyển động giảm dần là hiệu ứng chuyển đổi mặc định, độ mờ đơn giản hiện dần và rõ dần.
  2. Nếu chuyển động được, ảnh động trượt và tỷ lệ sẽ được thêm.
  3. Bố cục thích ứng trên thiết bị di động cho hộp thoại lớn được điều chỉnh thành dạng trượt ra.

Quá trình chuyển đổi mặc định an toàn và hiệu quả

Mặc dù Open Prop đi kèm với các khung hình chính để làm mờ và mờ dần, nhưng tôi thích tính năng này hơn phương pháp chuyển đổi phân lớp làm mặc định với ảnh động khung hình chính như các bản nâng cấp tiềm năng. Trước đó, chúng ta đã định kiểu chế độ hiển thị của hộp thoại bằng độ mờ, đang điều phối 1 hoặc 0 tuỳ thuộc vào thuộc tính [open]. Người nhận chuyển đổi từ 0% đến 100%, cho trình duyệt biết thời gian và loại tốc độ bạn muốn:

dialog {
  transition: opacity .5s var(--ease-3);
}

Thêm chuyển động vào hiệu ứng chuyển động

Nếu người dùng vẫn đồng ý với chuyển động, thì cả hộp thoại lớn và hộp thoại nhỏ đều sẽ trượt tăng dần kích thước vào và mở rộng ra làm lối ra. Bạn có thể đạt được điều này bằng cách prefers-reduced-motion truy vấn nội dung nghe nhìn và một số mở đạo cụ:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

Điều chỉnh ảnh động thoát cho thiết bị di động

Ở phần trước của phần tạo kiểu, kiểu hộp thoại lớn được điều chỉnh cho phù hợp với thiết bị di động thiết bị giống như một trang tính hành động, giống như thể một mảnh giấy nhỏ trượt từ cuối màn hình lên và vẫn được gắn vào cuối màn hình. Cân hoạt ảnh thoát không phù hợp với thiết kế mới này và chúng ta có thể điều chỉnh điều này bằng một vài truy vấn về nội dung đa phương tiện và một số Open Prop:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

Có khá nhiều nội dung cần thêm với JavaScript:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

Những bổ sung này xuất phát từ mong muốn loại bỏ nhẹ (nhấp vào hộp thoại phông nền), ảnh động và một số sự kiện bổ sung để đẩy nhanh thời gian tải dữ liệu biểu mẫu.

Đang thêm thao tác đóng

Nhiệm vụ này đơn giản và là nội dung bổ sung tuyệt vời cho phần tử hộp thoại không phải được tạo hiệu ứng động. Bạn có thể tương tác bằng cách xem các lượt nhấp vào hộp thoại và tận dụng sự kiện sôi nổi để đánh giá nội dung được nhấp vào và sẽ chỉ close() nếu đó là phần tử trên cùng:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Lưu ý dialog.close('dismiss'). Sự kiện được gọi và một chuỗi được cung cấp. Chuỗi này có thể được JavaScript khác truy xuất để biết thông tin chi tiết về cách đã đóng hộp thoại. Bạn sẽ thấy tôi cũng cung cấp các chuỗi gần mỗi lần tôi gọi chức năng từ nhiều nút khác nhau để cung cấp ngữ cảnh cho ứng dụng về tương tác của người dùng.

Thêm sự kiện đóng cửa và đóng cửa

Phần tử hộp thoại đi kèm với một sự kiện đóng: nó phát ra ngay lập tức khi hàm close() của hộp thoại được gọi. Khi chúng ta đang tạo ảnh động cho phần tử này, Thật tuyệt khi có các sự kiện ở trước và sau ảnh động, cũng như để thay đổi nhằm thu hút hoặc đặt lại biểu mẫu hộp thoại. Tôi sử dụng hồ sơ này ở đây để quản lý việc thêm Thuộc tính inert trên hộp thoại đã đóng và trong bản minh hoạ, tôi dùng các thuộc tính này để sửa đổi danh sách hình đại diện nếu người dùng đã gửi hình ảnh mới.

Để làm việc này, hãy tạo 2 sự kiện mới có tên là closingclosed. Sau đó theo dõi sự kiện đóng được tích hợp sẵn trong hộp thoại. Tại đây, hãy đặt hộp thoại thành inert rồi gửi sự kiện closing. Nhiệm vụ tiếp theo là chờ các ảnh động và hiệu ứng chuyển đổi để hoàn tất quá trình chạy trên hộp thoại, sau đó gửi Sự kiện closed.

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  …
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

Hàm animationsComplete, cũng được dùng trong Tạo thông báo ngắn thành phần, trả về hứa hẹn dựa trên hứa hẹn hoàn tất hoạt ảnh và chuyển tiếp. Đó là lý do dialogClose là một không đồng bộ hàm; sau đó ứng dụng có thể await lời hứa được trả về và tự tin tiến đến sự kiện đã kết thúc.

Thêm sự kiện khai trương và sự kiện đã mở cửa

Những sự kiện này không dễ thêm vào vì phần tử hộp thoại tích hợp sẵn không cung cấp sự kiện mở giống như với sự kiện kết thúc. Tôi sử dụng MutationObserver để cung cấp thông tin chi tiết về thay đổi đối với thuộc tính của hộp thoại. Trong trình quan sát này, Tôi sẽ theo dõi những thay đổi đối với thuộc tính mở và quản lý các sự kiện tuỳ chỉnh cho phù hợp.

Tương tự như cách chúng tôi bắt đầu sự kiện bế mạc và sự kiện kết thúc, hãy tạo hai sự kiện mới có tên là openingopened. Vị trí trước đây chúng ta nghe hộp thoại đóng lần này, hãy sử dụng trình quan sát đột biến đã tạo để theo dõi .

…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  …
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

Hàm gọi lại trình quan sát đột biến sẽ được gọi khi hộp thoại các thuộc tính được thay đổi, cung cấp danh sách các thay đổi dưới dạng mảng. Lặp lại thuộc tính thay đổi, đang tìm attributeName đang mở. Tiếp theo, hãy kiểm tra liệu phần tử có thuộc tính này hay không: thông tin này cho biết hộp thoại có đã trở nên mở. Nếu cửa sổ này đã mở, hãy xoá thuộc tính inert, đặt tiêu điểm cho một phần tử yêu cầu autofocus hoặc phần tử button đầu tiên tìm thấy trong hộp thoại. Cuối cùng, tương tự như lời kết và sự kiện đã kết thúc, hãy gửi sự kiện khai mạc ngay lập tức, chờ ảnh động để kết thúc, sau đó gửi sự kiện đã mở.

Thêm sự kiện đã xóa

Trong các ứng dụng trang đơn, hộp thoại thường được thêm và xoá dựa trên tuyến đường hoặc các nhu cầu và trạng thái khác của ứng dụng. Bạn nên dọn dẹp các sự kiện hoặc khi hộp thoại bị xoá.

Bạn có thể thực hiện việc này bằng một trình quan sát đột biến khác. Lần này, thay vì quan sát các thuộc tính trên một phần tử hộp thoại, chúng ta sẽ quan sát các thành phần con của phần tử và theo dõi các phần tử hộp thoại bị xoá.

…
const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  …
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

Lệnh gọi lại trình quan sát đột biến được gọi bất cứ khi nào phần tử con được thêm hoặc bị xoá khỏi phần nội dung của tài liệu. Các đột biến cụ thể được theo dõi là để removedNodesnodeName trong số một hộp thoại. Nếu hộp thoại đã bị xoá, sự kiện nhấp và đóng cũng sẽ bị xoá giải phóng bộ nhớ rồi gửi sự kiện tuỳ chỉnh đã xoá.

Đang xoá thuộc tính đang tải

Để ngăn ảnh động hộp thoại phát ảnh động thoát khi được thêm vào trang hoặc khi tải trang, thuộc tính tải đã được thêm vào hộp thoại. Chiến lược phát hành đĩa đơn tập lệnh sau đây chờ các ảnh động của hộp thoại chạy xong, rồi xoá các ảnh động này thuộc tính đó. Giờ đây, hộp thoại này có thể tuỳ ý tạo hiệu ứng động, và chúng ta đã ẩn hiệu quả hoạt ảnh gây mất tập trung.

export default async function (dialog) {
  …
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Tìm hiểu thêm về vấn đề chặn hoạt ảnh khung hình chính khi tải trang tại đây.

Tất cả ở cùng một nơi

Sau đây là toàn bộ dialog.js, vì chúng tôi đã giải thích từng phần riêng lẻ:

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Sử dụng mô-đun dialog.js

Hàm đã xuất từ mô-đun dự kiến sẽ được gọi và truyền một hộp thoại muốn thêm những sự kiện và chức năng mới này:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Cứ như vậy, hai hộp thoại được nâng cấp bằng tính năng đóng gói nhẹ, ảnh động tải bản sửa lỗi và nhiều sự kiện khác để làm việc.

Theo dõi các sự kiện tuỳ chỉnh mới

Giờ đây, mỗi phần tử hộp thoại được nâng cấp có thể theo dõi 5 sự kiện mới, chẳng hạn như:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

Dưới đây là 2 ví dụ về cách xử lý các sự kiện đó:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

Trong bản minh hoạ mà tôi tạo bằng phần tử hộp thoại, tôi sử dụng sự kiện đã đóng đó và dữ liệu biểu mẫu để thêm phần tử hình đại diện mới vào danh sách. Thời điểm thích hợp trong hộp thoại đã hoàn tất hoạt ảnh thoát, sau đó một số tập lệnh tạo hiệu ứng động trong hình đại diện mới. Nhờ những sự kiện mới, điều chỉnh trải nghiệm người dùng mượt mà hơn.

Lưu ý dialog.returnValue: đoạn mã này chứa chuỗi đóng được truyền khi sự kiện close() của hộp thoại được gọi. Điều quan trọng trong sự kiện dialogClosed là biết hộp thoại đã được đóng, huỷ hay xác nhận. Nếu mã được xác nhận, sau đó lấy giá trị biểu mẫu và đặt lại biểu mẫu. Việc đặt lại sẽ hữu ích, khi hộp thoại hiện lại, sẽ trống và sẵn sàng để gửi nội dung mới.

Kết luận

Giờ bạn đã biết cách tôi thực hiện điều đó, bạn sẽ làm cách nào‽ 🙂

Hãy đa dạng hoá phương pháp tiếp cận và tìm hiểu tất cả các cách xây dựng ứng dụng trên web.

Tạo một bản minh hoạ, tweet cho tôi các đường liên kết và tôi sẽ thêm vào vào phần bản phối lại của cộng đồng dưới đây!

Bản phối lại của cộng đồng

Tài nguyên