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 các cửa sổ bật lên mini và mega thích ứng với màu sắc, phản hồi và dễ tiếp cận bằng phần tử <dialog>
.
Dùng thử bản minh hoạ và xem nguồn!
Nếu bạn thích xem video, hãy xem phiên bản video của bài đăng này trên YouTube:
Tổng quan
Phần tử <dialog>
rất phù hợp với thông tin hoặc hành động theo ngữ cảnh trong trang. Hãy cân nhắc thời điểm trải nghiệm người dùng có thể hưởng lợi từ thao tác trên cùng một trang thay vì thao tác trên nhiều trang: có thể là do biểu mẫu có kích thước nhỏ hoặc thao tác duy nhất mà người dùng cần thực hiện là 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:
Tôi nhận thấy phần tử này thiếu một vài tính năng, vì vậy, trong Thử thách giao diện người dùng này, tôi sẽ thêm các mục trải nghiệm dành cho nhà phát triển mà tôi mong đợi: các sự kiện bổ sung, loại bỏ ánh sáng, ảnh động tuỳ chỉnh và loại mini và mega.
Markup (note: đây là tên ứng dụng)
Phần tử <dialog>
có các thành phần thiết yếu khá đơn giả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 ta có thể cải thiện đường cơ sở này.
Theo truyền thống, phần tử hộp thoại có nhiều điểm chung với một cửa sổ bật lên và thường thì tên của các phần tử này có thể thay thế cho nhau. Tôi đã tự do sử dụng phần tử hộp thoại cho cả cửa sổ bật lên của hộp thoại nhỏ (mini) cũng như hộp thoại toàn trang (mega). Tôi đặt tên cho các hộp thoại này là mega và mini, trong đó cả hai hộp thoại đều được điều chỉnh một chút cho các trường hợp sử dụng khác nhau.
Tôi đã thêm một thuộc tính modal-mode
để cho phép bạn chỉ định loại:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Không phải lúc nào cũng vậy, nhưng các phần tử hộp thoại thường sẽ được dùng để thu thập một số thông tin về tương tác. Các biểu mẫu bên trong phần tử hộp thoại được tạo để đi 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 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 dữ liệu.
<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
Hộp thoại lớn có 3 phần tử bên trong biểu mẫu: <header>
, <article>
và <footer>
.
Các thành phần này đóng vai trò là vùng chứa ngữ nghĩa, cũng như mục tiêu kiểu để trình bày hộp thoại. Tiêu đề của tiêu đề là tiêu đề của phương thức và cung cấp một nút đóng. Bài viết này dành cho thông tin và dữ liệu đầu vào của biểu mẫu. Phần chân trang chứa một <menu>
gồm 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à trình xử lý sự kiện nội tuyến onclick
. Thuộc tính autofocus
sẽ nhận được tiêu điểm khi hộp thoại mở ra và tôi nhận thấy cách tốt nhất là đặt thuộc tính này trê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 do vô tình.
Hộp thoại thu nhỏ
Hộp thoại thu nhỏ rất giống với hộp thoại lớn, chỉ thiếu phần tử <header>
. Điều này cho phép tệp có kích thước nhỏ hơn và phù hợp 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 nền tảng vững chắc cho phần tử khung nhìn đầy đủ có thể thu thập dữ liệu và hoạt động tương tác của người dùng. Những yếu tố thiết yếu này có thể tạo ra một số hoạt động tương tác rất 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ó tính năng hỗ trợ tiếp cận tích hợp rất tốt. Thay vì thêm các tính năng này như tôi thường làm, nhiều tính năng hiện đã có sẵn.
Đang khôi phục tiêu điểm
Như chúng ta đã làm thủ công trong bài viết Xây dựng thành phần điều hướng bên, điều quan trọng là việc mở và đóng nội dung nào đó đúng cách sẽ đặt tiêu điểm vào các nút mở và đóng có liên quan. Khi thanh điều hướng bên đó mở ra, tiêu điểm sẽ được đặt vào nút đóng. Khi nhấn vào nút đóng, tiêu điểm sẽ được khôi phục về nút đã mở nút đó.
Với phần tử hộp thoại, đây là hành vi mặc định tích hợp:
Rất tiếc, nếu bạn muốn tạo ảnh động cho hộp thoại vào và ra, chức năng này sẽ bị mất. Trong phần JavaScript, tôi sẽ khôi phục chức năng đó.
Tập trung vào việc bắt giữ
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ử, tại thời điểm đó, tiêu điểm sẽ chặn và đặt lại tiêu điểm.
Sau inert
, mọi phần của tài liệu đều có thể bị "đóng băng" đến mức không còn là mục tiêu lấy tiêu điểm hoặc không tương tác được bằng chuột. Thay vì giữ tiêu điểm, tiêu điểm được hướng đến 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ử có thể lấy tiêu điểm đầu tiên trong phần đánh dấu hộp thoại. Nếu đây không phải là phần tử tốt nhất để người dùng sử dụng theo mặc định, hãy sử dụng thuộc tính autofocus
. Như đã mô tả ở trên, tôi thấy tốt nhất là bạn nên đặt thông báo này trê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 ngẫu nhiên.
Đóng bằng phím thoát
Điều quan trọng là bạn phải giúp người dùng dễ dàng đóng phần tử có thể gây gián đoạn này. May mắn thay, phần tử hộp thoại sẽ xử lý phím thoát cho bạn, giúp bạn không phải chịu gánh nặng điều phối.
Kiểu
Có một cách dễ dàng để tạo kiểu cho phần tử hộp thoại và một cách khó. Bạn có thể đạt được đường dẫn dễ dàng bằng cách không thay đổi thuộc tính hiển thị của hộp thoại và làm việc với các giới hạn của hộp thoại đó. Tôi sẽ đi theo con đường khó để cung cấp ảnh động tuỳ chỉnh cho việc mở và đóng hộp thoại, tiếp quản thuộc tính display
và nhiều tính năng khác.
Tạo kiểu bằng đạo cụ mở
Để tăng tốc độ màu sắc thích ứng và tính nhất quán tổng thể của thiết kế, tôi đã rất tự hào khi đưa Open Props vào thư viện biến CSS của mình. Ngoài các biến được cung cấp miễn phí, tôi cũng nhập tệp normalize và một số nút, cả hai đều do Open Props cung cấp dưới dạng tệp 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à làm cho hộp thoại trông đẹp mắt.
Định kiểu cho phần tử <dialog>
Sở hữu thuộc tính hiển thị
Hành vi hiển thị và ẩn mặc định của một phần tử hộp thoại sẽ bật/tắt thuộc tính hiển thị từ block
thành none
. Rất tiếc, điều này có nghĩa là bạn không thể tạo ảnh động vào và ra, chỉ có thể tạo ảnh động vào. Tôi muốn tạo ảnh động cả trong và ngoài, bước đầu tiên là đặt thuộc tính display của riêng tôi:
dialog {
display: grid;
}
Bằng cách thay đổi và sở hữu, giá trị thuộc tính hiển thị, như minh hoạ trong đoạn mã CSS ở trên, một số lượng kiểu định dạng cần được quản lý đáng kể để mang lại trải nghiệm phù hợp cho người dùng. Trước tiên, trạng thái mặc định của hộp thoại là đóng. Bạn có thể thể hiện trạng thái này một cách trực quan và ngăn hộp thoại nhận được các lượt tương tác bằng các kiểu sau:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Giờ đây, hộp thoại sẽ không hiển thị và 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 ẩn.
Cung cấp chủ đề màu sắc thích ứng cho hộp thoại
Mặc dù color-scheme
chọn tài liệu của bạn thành giao diện màu thích ứng do trình duyệt cung cấp cho các tuỳ chọn ưu tiên về hệ thống sáng và tối, nhưng 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ố màu sắc bề mặt tự động điều chỉnh theo các tuỳ chọn hệ thống sáng và tối, tương tự như khi sử dụng color-scheme
. Đây là những định dạng tuyệt vời để tạo các lớp trong một thiết kế và tôi thích sử dụng màu sắc để 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)
; để nằm 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ư đầu trang và chân trang. Tôi coi chúng là phần bổ sung cho một phần tử hộp thoại, nhưng thực sự quan trọng trong việc tạo ra một thiết kế hộp thoại hấp dẫn và được thiết kế tốt.
Định cỡ hộp thoại thích ứng
Theo mặc định, hộp thoại này sẽ uỷ quyền kích thước của hộp thoại cho nội dung trong đó, nhìn chung là rất tốt. Mục tiêu của tôi ở đây là ràng buộc max-inline-size
ở kích thước có thể đọc được (--size-content-3
= 60ch
) hoặc 90% chiều rộng khung nhìn. Điều này đảm bảo hộp thoại không tràn viền trên thiết bị di động và không quá rộng trên màn hình máy tính khiến người dùng 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 ta sẽ cần chỉ định vị trí của vùng cuộn của hộp thoại, trong trường hợp đó là một phần tử hộp thoại cao.
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 tôi có max-block-size
hai lần không? Phương thức đầu tiên sử dụng 80vh
, một đơn vị khung nhìn thực tế. Điều tôi thực sự muốn là giữ cho hộp thoại nằm trong luồng tương đối, cho người dùng quốc tế, vì vậy, tôi sử dụng đơn vị dvb
hợp lý, mới hơn và chỉ được hỗ trợ một phần trong phần khai báo thứ hai khi nó 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ỏ thành hai 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 bao phủ mọi thứ, tạo hiệu ứng bóng để hỗ trợ cho việc hộp thoại này ở phía trước và không thể truy cập vào nội dung ở phía sau. Vùng chứa hộp thoại có thể tự căn giữa trên phông nền này và có bất kỳ hình dạng nào mà nội dung của vùng chứa yêu cầu.
Các kiểu sau đây sẽ cố định phần tử hộp thoại vào cửa sổ, kéo giãn phần tử đó đến 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. Tôi đặt lề dưới cùng thành 0
để đư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ể chuyển hộp thoại thành một bảng 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 vị hộp thoại thu nhỏ
Khi sử dụng khung nhìn lớn hơn, chẳng hạn như trên máy tính để bàn, tôi đã chọn đặt các hộp thoại nhỏ lên phần tử đã gọi các hộp thoại đó. Để làm việc này, tôi cần JavaScript. Bạn có thể tìm thấy kỹ thuật mà tôi sử dụng tại đây, nhưng tôi cảm thấy điều này nằm ngoài phạm vi của bài viết này. Nếu không có JavaScript, hộp thoại nhỏ sẽ xuất hiện ở giữa màn hình, giống như hộp thoại lớn.
Tạo nội dung nổi bật
Cuối cùng, hãy thêm một chút tinh tế vào hộp thoại để hộp thoại trông giống như một bề mặt mềm nằm xa phía trên trang. Bạn có thể tạo hiệu ứng mềm mại bằng cách bo tròn các góc của hộp thoại. Độ sâu này đạt được bằng một trong những đạo cụ bóng được chế tác cẩn thận của Open Props:
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 làm việc rất nhẹ nhàng với phông nền, chỉ thêm hiệu ứng làm mờ bằng backdrop-filter
vào hộp thoại lớn:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Tôi cũng chọn đặt hiệu ứng chuyển đổi trên backdrop-filter
, với hy vọng rằ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;
}
Tạo kiểu bổ sung
Tôi gọi phần này là "bổ sung" vì phần này liên quan nhiều hơn đến bản minh hoạ phần tử hộp thoại của tôi so với phần tử hộp thoại nói chung.
Chứa cuộn
Khi hộp thoại hiển thị, người dùng vẫn có thể cuộn trang phía sau hộp thoại, điều này là không mong 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 thông số kỹ thuật, giải pháp này không ảnh hưởng đến hộp thoại vì không phải là cổng cuộn, tức là không phải là thanh cuộn nên không có gì để ngăn chặn. Tôi có thể sử dụng JavaScript để theo dõi các sự kiện mới trong hướng dẫn này, chẳng hạn như "đã đóng" và "đã mở", đồng thời 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:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Bây giờ, khi một hộp thoại lớn mở ra, tài liệu html sẽ có overflow: hidden
.
Bố cục <form>
Ngoài việc là một thành phần rất quan trọng để thu thập thông tin tương tác từ người dùng, tôi còn sử dụng thành phần này ở đây để bố trí các thành phần cho tiêu đề, chân trang và
bài viết. Với bố cục này, tôi dự định sẽ trình bày rõ phần con bài viết là một khu vực có thể cuộn. Tôi thực hiện việc này bằng grid-template-rows
.
Phần tử bài viết được cung cấp 1fr
và chính biểu mẫu này có cùng chiều cao tối đa với phần tử hộp thoại. Việc thiết lập chiều cao và kích thước hàng cố định này cho phép phần tử bài viết bị ràng buộc và cuộn khi phần tử đó tràn:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Định kiểu 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à cung cấp một nút đóng dễ tìm. Bài viết cũng được gán màu cho bề mặt để làm cho nó trông giống như phía sau nội dung bài viết trên hộp thoại. Các yêu cầu này dẫn đến một vùng chứa flexbox, các mục được căn chỉnh theo chiều dọc và có khoảng cách đến các cạnh, cũng như 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);
}
}
Tạo kiểu cho nút đóng tiêu đề
Vì bản minh hoạ đang sử dụng các nút Open Props (Mở thành phần), nên nút đóng được tuỳ chỉnh thành một nút có biểu tượng tròn ở giữa 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;
}
Tạo kiểu cho hộp thoại <article>
Phần tử article có vai trò đặc biệt trong hộp thoại này: đó là một không gian dùng để cuộn trong trường hợp hộp thoại cao hoặc dài.
Để thực hiện việc này, phần tử biểu mẫu mẹ đã thiết lập một số giá trị tối đa cho chính nó, cung cấp các quy tắc ràng buộc để phần tử bài viết này đạt được nếu phần tử đó quá cao. Đặt overflow-y: auto
để thanh cuộn chỉ hiển thị khi cần, chứa tính năng cuộn trong đó bằng overscroll-behavior: contain
và phần còn lại sẽ là các kiểu 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);
}
}
Định kiểu hộp thoại <footer>
Chức năng của chân trang là chứa các trình đơn của nút hành động. Flexbox được dùng để căn chỉnh nội dung với cuối trục cùng dòng của chân trang, sau đó là một số khoảng trống để tạo không gian 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 kiểu cho trình đơn chân trang của hộp thoại
Phần tử menu
được dùng để chứa các nút hành động cho hộp thoại. Ứng dụng này sử dụng bố cục flexbox bao bọc với gap
để tạo khoảng trống giữa các nút. Các thành phần trong 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;
}
Hoạt ảnh
Các thành phần của hộp thoại thường được tạo ảnh động vì chúng sẽ xuất hiện và thoát khỏi cửa sổ. Việc cung cấp cho hộp thoại một số chuyển động hỗ trợ cho lối vào và lối ra này giúp người dùng tự định hướng trong luồng.
Thông thường, phần tử hộp thoại chỉ có thể tạo ảnh động vào chứ không thể tạo ảnh động ra. Điều này là do trình duyệt bật/tắt thuộc tính display
trên phần tử. Trước đó, hướng dẫn này đã đặt chế độ hiển thị thành lưới và không bao giờ đặt thành không có. Điều này giúp bạn có thể tạo ảnh động vào và ra.
Open Props đi kèm với nhiều ảnh động khung hình chính để sử dụng, giúp việc điều phối trở nên dễ dàng và dễ đọc. Dưới đây là các mục tiêu ảnh động và phương pháp phân lớp mà tôi đã sử dụng:
- Giảm chuyển động là hiệu ứng chuyển đổi mặc định, một hiệu ứng mờ đơn giản xuất hiện và biến mất.
- Nếu chuyển động được, ảnh động trượt và tỷ lệ sẽ được thêm.
- Bố cục thích ứng cho thiết bị di động của hộp thoại lớn được điều chỉnh để trượt ra ngoài.
Một chuyển đổi mặc định an toàn và có ý nghĩa
Mặc dù Đạo cụ mở đi kèm với các khung hình chính để làm mờ và làm mờ, nhưng tôi thích phương pháp chuyển đổi theo lớp này làm mặc định, trong đó ảnh động khung hình chính là các bản nâng cấp tiềm năng. Trước đó, chúng ta đã tạo kiểu cho chế độ hiển thị của hộp thoại bằng độ mờ, điều phối 1
hoặc 0
tuỳ thuộc vào thuộc tính [open]
. Để chuyển đổi giữa 0% và 100%, hãy cho trình duyệt biết thời lượng và loại hiệu ứng làm dịu mà 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 đồng ý với chuyển động, cả hộp thoại lớn và hộp thoại nhỏ đều sẽ trượt lên khi mở và thu nhỏ khi đóng. Bạn có thể thực hiện việc này bằng truy vấn nội dung đa phương tiện prefers-reduced-motion
và một số thành phần mở:
@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
Trước đó trong phần tạo kiểu, kiểu hộp thoại lớn được điều chỉnh cho các thiết bị di động để giống như một trang hành động, như thể một mảnh giấy nhỏ đã trượt lên từ cuối màn hình và vẫn được gắn ở dưới cùng. Ảnh động thoát thu nhỏ không phù hợp với thiết kế mới này. Chúng ta có thể điều chỉnh ảnh động này bằng một số truy vấn nội dung nghe nhìn và một số thành phần mở:
@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 nội dung bổ sung này xuất phát từ mong muốn loại bỏ ánh sáng (nhấp vào phông nền hộp thoại), ảnh động và một số sự kiện bổ sung để có thời gian tốt hơn khi nhận dữ liệu biểu mẫu.
Thêm thao tác đóng sáng
Đây là một nhiệm vụ đơn giản và là một bổ sung tuyệt vời cho phần tử hộp thoại không có ảnh động. Hoạt động tương tác được thực hiện bằng cách theo dõi các lượt nhấp vào phần tử hộp thoại và tận dụng tính năng bùng nổ sự kiện để đánh giá nội dung đã được nhấp vào, đồng thời 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 cá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 rằng tôi cũng cung cấp các chuỗi đóng mỗi khi gọi hàm từ nhiều nút để cung cấp ngữ cảnh cho ứng dụng của tôi về hoạt động tương tác của người dùng.
Thêm sự kiện đóng và đã đóng
Phần tử hộp thoại đi kèm với một sự kiện đóng: sự kiện này phát ra ngay lập tức khi hàm close()
của hộp thoại được gọi. Vì chúng ta đang tạo ảnh động cho phần tử này, nên bạn nên có các sự kiện trước và sau ảnh động để thay đổi việc lấy dữ liệu hoặc đặt lại biểu mẫu hộp thoại. Tôi sử dụng thuộc tính này ở đây để quản lý việc thêm thuộc tính inert
vào hộp thoại đã đóng. Trong bản minh hoạ, tôi sử 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à closing
và closed
. Sau đó, hãy lắng nghe sự kiện đóng tích hợp sẵn trên hộp thoại. Từ đây, hãy đặt hộp thoại thành inert
và gửi sự kiện closing
. Nhiệm vụ tiếp theo là đợi ảnh động và hiệu ứng chuyển đổi chạy xong 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 thành phần Tạo thông báo ngắn, trả về một lời hứa dựa trên việc hoàn tất ảnh động và lời hứa chuyển đổi. Đó là lý do dialogClose
là một hàm không đồng bộ; sau đó, hàm này có thể await
lời hứa được trả về và tự tin chuyển sang sự kiện đã đóng.
Thêm sự kiện sắp mở và đã mở
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 không cung cấp sự kiện mở như khi đóng. Tôi sử dụng MutationObserver để cung cấp thông tin chi tiết về việc thay đổi các thuộc tính của hộp thoại. Trong trình quan sát này, tôi sẽ theo dõi các 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 ta bắt đầu các sự kiện đóng và đã đóng, hãy tạo hai sự kiện mới có tên là opening
và opened
. Trước đây, chúng ta đã nghe sự kiện đóng hộp thoại, lần này, hãy sử dụng trình quan sát đột biến đã tạo để theo dõi các thuộc tính của hộp thoạ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 callback của trình quan sát đột biến sẽ được gọi khi thuộc tính hộp thoại thay đổi, cung cấp danh sách các thay đổi dưới dạng mảng. Lặp lại các thay đổi về thuộc tính, tìm attributeName
đang mở. Tiếp theo, hãy kiểm tra xem phần tử có thuộc tính hay không: điều này cho biết liệu hộp thoại có mở hay không. Nếu hộp thoại đã được mở, hãy xoá thuộc tính inert
, đặt tiêu điểm vào 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ư sự kiện đóng và đã đóng, hãy gửi sự kiện mở ngay lập tức, đợi ảnh động kết thúc, sau đó gửi sự kiện đã mở.
Thêm sự kiện đã xoá
Trong các ứng dụng trang đơn, hộp thoại thường được thêm và xoá dựa trên các tuyến hoặc nhu cầu và trạng thái ứng dụng khác. Việc dọn dẹp các sự kiện hoặc dữ liệu có thể hữu ích khi một 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 phần tử con của phần tử nội dung 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 của trình quan sát đột biến được gọi bất cứ khi nào các phần tử con được thêm hoặc xoá khỏi phần nội dung của tài liệu. Các đột biến cụ thể đang được theo dõi là dành cho removedNodes
có nodeName
của hộp thoại. Nếu một hộp thoại đã bị xoá, các sự kiện nhấp và đóng sẽ bị xoá để giải phóng bộ nhớ và sự kiện xoá tuỳ chỉnh sẽ được gửi.
Đ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, một thuộc tính tải đã được thêm vào hộp thoại. Tập lệnh sau đây sẽ chờ ảnh động hộp thoại chạy xong, sau đó xoá thuộc tính. Giờ đây, hộp thoại có thể tự do tạo ảnh động vào và ra, đồng thời chúng ta đã ẩn hiệu quả một ảnh động 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 đề ngăn ảnh động khung hình chính khi tải trang tại đây.
Tất cả cùng nhau
Sau đây là toàn bộ dialog.js
, giờ đây chúng ta đã 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 được xuất từ mô-đun dự kiến sẽ được gọi và chuyển một phần tử hộp thoại muốn thêm các sự kiện và chức năng mới sau đây:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
Như vậy, hai hộp thoại được nâng cấp với tính năng đóng nhẹ, sửa lỗi tải ảnh động và nhiều sự kiện khác để xử lý.
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 này rất thích hợp vì hộp thoại đã hoàn tất ảnh động thoát, sau đó một số tập lệnh sẽ tạo ảnh động trong hình đại diện mới. Nhờ các sự kiện mới, việc sắp xếp trải nghiệm người dùng có thể suôn sẻ hơn.
Lưu ý dialog.returnValue
: thuộc tính này chứa chuỗi đóng được truyền khi sự kiện close()
của hộp thoại được gọi. Trong sự kiện dialogClosed
, điều quan trọng là phải biết liệu hộp thoại đã được đóng, huỷ hay xác nhận. Nếu được xác nhận, tập lệnh sẽ lấy các giá trị biểu mẫu và đặt lại biểu mẫu. Thao tác đặt lại này rất hữu ích vì khi hộp thoại xuất hiện lại, hộp thoại sẽ trống và sẵn sàng để gửi nội dung mới.
Kết luận
Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂
Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách xây dựng trên web.
Hãy tạo bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phần phối lại của cộng đồng ở bên dưới!
Bản phối lại của cộng đồng
- @GrimLink với hộp thoại 3 trong 1.
- @mikemai2awesome với bản phối lại hay không làm thay đổi thuộc tính
display
. - @geoffrich_ với Svelte và đánh bóng Svelte FLIP đẹp mắt.
Tài nguyên
- Mã nguồn trên GitHub
- Hình đại diện Doodle