Tổng quan cơ bản về cách tạo các phương thức thu nhỏ và phương thức lớn có khả năng thích ứng màu sắc, có tính phản hồi và dễ tiếp cận bằng phần tử <dialog>
.
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 phương thức mini và mega thích ứng với màu sắc, có khả năng 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, thì đây là 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 bối cảnh trong trang. Hãy cân nhắc thời điểm mà trải nghiệm người dùng có thể hưởng lợi từ mộ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 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ỷ.
Phần tử <dialog>
gần đây đã trở nên ổn định trên các trình duyệt:
Tôi nhận thấy phần tử này còn thiếu một số thứ, vì vậy trong GUI Challenge (Thử thách về giao diện người dùng đồ hoạ) này, tôi sẽ thêm các mục trải nghiệm của nhà phát triển mà tôi mong đợi: các sự kiện bổ sung, tính năng đóng nhanh, ảnh động tuỳ chỉnh, cũng như kiểu chữ mini và mega.
Markup (note: đây là tên ứng dụng)
Các thành phần thiết yếu của phần tử <dialog>
là không bắt buộc. Phần tử này sẽ tự động bị ẩn và có các kiểu được tích hợp sẵn để phủ lên 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 phương thức và thường có thể thay thế tên cho nhau. Ở đây, tôi đã tự ý sử dụng phần tử hộp thoại cho cả cửa sổ bật lên hộp thoại nhỏ (mini) và hộp thoại toàn trang (mega). Tôi đặt tên cho chúng là mega và mini, 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 thường thì các phần tử hộp thoại sẽ được dùng để thu thập một số thông tin 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 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 Mega
Một hộp thoại lớn có 3 phần tử bên trong biểu mẫu: <header>
, <article>
và <footer>
.
Đây là các vùng chứa ngữ nghĩa, cũng như mục tiêu về kiểu cho việc trình bày hộp thoại. Tiêu đề này đặt tên cho cửa sổ 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à một trình xử lý sự kiện nội tuyến onclick
. Thuộc tính autofocus
sẽ nhận tiêu điểm khi hộp thoại mở ra và tôi 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 vô tình.
Hộp thoại nhỏ
Hộp thoại thu nhỏ 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 giúp nút nhỏ hơn và nằm cùng dòng.
<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 một phần tử toàn màn hình 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ố cần thiết này có thể tạo ra một số hoạt động tương tác rất thú vị và hiệu quả trên 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 sẵn rất tốt. Thay vì thêm những tính năng này như tôi thường làm, nhiều tính năng đã có sẵn.
Khôi phục tiêu điểm
Như chúng ta đã làm theo cách thủ công trong phần Tạo thành phần sidenav, điều quan trọng là việc mở và đóng một thành phần 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 sidenav đó mở ra, tiêu điểm sẽ nằm trên 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ở nút đó.
Với phần tử hộp thoại, đây là hành vi mặc định được 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, thì chức năng này sẽ không hoạt động. Trong phần JavaScript, tôi sẽ khôi phục chức năng đó.
Bắt tiêu điểm
Phần tử hộp thoại sẽ 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 đó, JavaScript sẽ chặn và đặt tiêu điểm trở lại.
Sau inert
, mọi phần của tài liệu đều có thể bị "đóng băng" đến mức chúng không còn là mục tiêu lấy tiêu điểm hoặc tương tác bằng chuột nữa. Thay vì giữ tiêu điểm, tiêu điểm sẽ đượ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 tiêu điểm một phần tử
Theo mặc định, phần tử hộp thoại sẽ chỉ định tiêu điểm cho phần tử có thể lấy tiêu điểm đầu tiên trong đánh dấu hộp thoại. Nếu đây không phải là phần tử tốt nhất mà người dùng nên chuyển sang theo mặc định, hãy sử dụng thuộc tính autofocus
. Như đã mô tả trước đó, tôi thấy cách hay nhất là đặ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 bạn xác nhận một cách có chủ ý chứ không phải vô tình.
Đóng bằng phím thoát
Bạn cần tạo điều kiện để người dùng dễ dàng đóng thành phần có khả năng gây gián đoạn này. Rất may là 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 lo lắng về việc điều phối.
Kiểu
Có một cách dễ dàng và một cách khó khăn để tạo kiểu cho phần tử hộp thoại. Đường dẫn dễ dàng đạt được 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 đi theo hướ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 thao tác khác.
Tạo kiểu bằng Open Props
Để tăng tốc độ áp dụng màu sắc thích ứng và tính nhất quán tổng thể của thiết kế, tôi đã không ngần ngại sử dụng thư viện biến CSS Open Props. Ngoài các biến được cung cấp miễn phí, tôi cũng nhập một tệp normalize và một số nút. Cả hai tệp này đều được Open Props cung cấp dưới dạng các tệp nhập không bắt buộc. Những nội dung 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.
Tạo kiểu cho phần tử <dialog>
Sở hữu tài sản hiển thị
Hành vi hiện và ẩn mặc định của một phần tử hộp thoại sẽ chuyển đổi 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 hiệu ứng động cho thành phần này khi xuất hiện và biến mất, mà chỉ có thể tạo hiệu ứng động khi xuất hiện. Tôi muốn tạo hiệu ứng cho cả hai trạng thái xuất hiện và biến mất, và 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à do đó sở hữu giá trị thuộc tính hiển thị, như trong đoạn mã CSS ở trên, bạn cần quản lý một số lượng lớn kiểu để tạo điều kiện cho trải nghiệm người dùng phù hợp. Trước 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á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 này sẽ không xuất hiện 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 cho hộp thoại một giao diện màu thích ứng
Mặc dù color-scheme
chọn tài liệu của bạn vào một giao diện màu thích ứng do trình duyệt cung cấp cho các lựa chọn ưu tiên của 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 thích ứng với các lựa chọn ưu tiên của hệ thống về giao diện sáng và tối, tương tự như khi sử dụng color-scheme
. Những màu này rất phù hợp để 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 cho giao diện của các lớp vùng hiển thị này. Màu nền là var(--surface-1)
; để nằm trên lớp đó, hãy 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);
}
}
Sau này, chúng tôi sẽ thêm nhiều màu sắc thích ứng hơn cho các phần tử con, chẳng hạn như tiêu đề 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.
Điều chỉnh kích thước hộp thoại thích ứng
Theo mặc định, hộp thoại sẽ uỷ quyền kích thước cho nội dung của hộp thoại. Điều này thường rất hữu ích. Mục tiêu của tôi ở đây là giới hạn 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 sẽ không hiển thị tràn viền trên thiết bị di động và sẽ không quá rộng trên màn hình máy tính đến mức khó đọc. Sau đó, tôi thêm 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 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 đó 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ó thấy tôi có max-block-size
hai lần không? Thành phần đầu tiên sử dụng 80vh
, một đơn vị khung nhìn thực. Điều tôi thực sự muốn là giữ hộp thoại trong luồng tương đối, đối với 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 khai báo thứ hai cho thời điểm đơn vị này trở nên ổn định hơn.
Vị trí hộp thoại Mega
Để hỗ trợ việc định vị một phần tử hộp thoại, bạn nên chia phần tử đó thành 2 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ứ, tạo hiệu ứng đổ bóng để hỗ trợ việc hộp thoại này nằm ở phía trước và người dùng 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ự do đặt ở giữa phông nền này và có hình dạng tuỳ theo yêu cầu của nội dung.
Các kiểu sau đây cố định phần tử hộp thoại vào cửa sổ, kéo dài 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 hiển thị nhỏ, tôi tạo kiểu cho mega modal toàn trang này theo cách hơi khác. Tôi đặt lề dưới thành 0
, việ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 về kiểu, tôi có thể biến hộp thoại thành một bảng chọn hành động, gần với ngón tay 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;
}
}
Vị trí hộp thoại nhỏ
Khi sử dụng một khung hiển thị 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 trên phần tử đã gọi chúng. Để làm được 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 kỹ thuật 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.
Làm cho nổi bật
Cuối cùng, hãy thêm một chút đ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 nằm ở phía trên trang. Độ mềm được tạo ra bằng cách bo tròn các góc của hộp thoại. Độ sâu được tạo ra bằng một trong những shadow props (đạo cụ đổ bóng) được chế tạo tỉ mỉ 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 xử lý phông nền rất nhẹ nhàng, 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;
}
Phần phụ tạo kiểu
Tôi gọi phần này là "phần 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.
Giới hạ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 hộp thoại. Tôi không muốn điều này xảy ra:
Thông thường, overscroll-behavior
sẽ là giải pháp thường dùng của tôi, nhưng theo quy cách, giải pháp này không ảnh hưởng đến hộp thoại vì đây không phải là cổng cuộn, tức là không phải là trình 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 trong hướng dẫn này, chẳng hạn như "closed" (đóng) và "opened" (mở), rồ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ả các trình duyệt:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Giờ đây, 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 phần tử rất quan trọng để thu thập thông tin tương tác từ người dùng, tôi sử dụng phần tử này ở đây để bố trí các phần tử tiêu đề, chân trang và bài viết. Với bố cục này, tôi dự định trình bày phần tử con của bài viết dưới dạng một vùng 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 đặt là 1fr
và biểu mẫu 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 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 tràn:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
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à cung cấp một nút đóng dễ tìm. Nút này cũng có màu bề mặt để xuất hiện phía sau nội dung bài viết trong 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 để tiêu đề và nút đóng có một số khoảng trố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, 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ử bài viết có một vai trò đặc biệt trong hộp thoại này: đó là một không gian được thiết kế để 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 ràng buộc để phần tử bài viết này đạt được nếu nó quá cao. Đặt overflow-y: auto
để thanh cuộn chỉ xuất hiện khi cần, chứa nội dung cuộn trong đó bằng overscroll-behavior: contain
và phần còn lại sẽ là 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);
}
}
Tạo kiểu cho hộp thoại <footer>
Vai trò của chân trang là chứa trình đơn gồm các nút hành động. Flexbox được dùng để căn chỉnh nội dung đến cuối trục nội tuyến của chân trang, sau đó thêm một số khoảng cách để các nút có thêm không gian.
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);
}
}
Tạo 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 thao tác cho hộp thoại. Thành phần này sử dụng bố cục flexbox bao bọc bằng gap
để tạo khoảng trống giữa các nút. Các phần tử trong trình đơn có khoảng đệm, chẳng hạn như <ul>
. Tôi cũng xoá kiểu đó vì không cần dùng đế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 phần tử hộp thoại thường được tạo hiệu ứng động vì chúng xuất hiện và biến mất khỏi cửa sổ. Việc thêm một số chuyển động hỗ trợ cho hộp thoại khi xuất hiện và biến mất sẽ giúp người dùng định hướng được bản thân trong luồng.
Thông thường, phần tử hộp thoại chỉ có thể được tạo hiệu ứng động khi xuất hiện chứ không phải khi biến mất. Điều này là do trình duyệt chuyển đổi thuộc tính display
trên phần tử. Trước đó, hướng dẫn đặt màn hình 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 hiệu ứng chuyển động xuất hiện và biến mất.
Open Props có nhiều ảnh khoá hoạt ảnh để sử dụng, giúp việc phối hợp trở nên dễ dàng và dễ đọc. Sau đây là các mục tiêu về ảnh động và phương pháp tiếp cận theo lớp mà tôi đã áp 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.
- Nếu chuyển động ổn, ảnh động trượt và thu phóng sẽ được thêm vào.
- 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 để trượt ra.
Quá trình chuyển đổi mặc định an toàn và có ý nghĩa
Mặc dù Open Props đi kèm với các khung hình chính để làm mờ dần, nhưng tôi thích cách tiếp cận chuyển đổi theo lớp này làm mặc định với ảnh động khung hình chính làm các bản nâng cấp tiềm năng. Trước đó, chúng ta đã tạo kiểu cho khả năng 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 gian và loại hiệu ứng chuyển động mà bạn muốn:
dialog {
transition: opacity .5s var(--ease-3);
}
Thêm chuyển động vào hiệu ứng chuyển cảnh
Nếu người dùng không gặp vấn đề gì 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 xuất hiện và thu nhỏ khi biến mất. Bạn có thể đạt được điều này bằng truy vấn nội dung nghe nhìn prefers-reduced-motion
và một số Open Props:
@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 phù hợp với thiết bị di động để giống với bảng hành động hơn, như thể một tờ giấy nhỏ đã trượt lên từ cuối màn hình và vẫn được gắn vào cuối màn hình. Hoạt ảnh thoát thu nhỏ không phù hợp với thiết kế mới này, và chúng ta có thể điều chỉnh hoạt ảnh này bằng một vài truy vấn nội dung nghe nhìn và một số Open Props:
@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ó một số điều cần thêm bằng 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 điểm bổ sung này xuất phát từ mong muốn có tính năng đóng nhanh (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 phù hợp hơn khi nhận dữ liệu biểu mẫu.
Thêm tính năng đóng nhanh
Đây là một tác 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 tạo hiệu ứng động. Tương tác này đạt được 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 sự kiện lan truyền để đánh giá lượt nhấp 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 này được gọi và một chuỗi được cung cấp.
Các JavaScript khác có thể truy xuất chuỗi này để 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 gần giố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 sẽ phát 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 hiệu ứng cho phần tử này, nên sẽ rất hữu ích nếu có các sự kiện trước và sau khi tạo hiệu ứng, để thay đổi nhằm lấy dữ liệu hoặc đặt lại biểu mẫu hộp thoại. Tôi sử dụng nó ở đây để quản lý việc thêm thuộc tính inert
vào hộp thoại đã đóng và 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 một hình ảnh mới.
Để đạt được điều 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 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 phần Tạo thành phần thông báo nhanh) trả về một lời hứa dựa trên việc hoàn thành lời hứa về ảnh động và lời hứa về hiệu ứng 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 đã trả về và tự tin chuyển sang sự kiện đã đóng.
Thêm sự kiện mở và đã mở
Những sự kiện này không dễ thêm vì phần tử hộp thoại tích hợp không cung cấp sự kiện mở như sự kiện đó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 theo dõi này, tôi sẽ theo dõi các thay đổi đối với thuộc tính open 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
. Trong khi trước đây chúng ta đã lắng nghe sự kiện đóng hộp thoại, lần này hãy sử dụng một trình theo dõi thay đổi đã 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 đối tượng tiếp nhận dữ liệu thay đổi sẽ được gọi khi các thuộc tính của hộp thoại thay đổi, cung cấp danh sách các thay đổi dưới dạng một mảng. Lặp lại các thay đổi về thuộc tính, tìm attributeName
để 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 đã mở hay chưa. Nếu hộp thoại đã mở, hãy xoá thuộc tính inert
, đặt tiêu điểm thành 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 một sự kiện đã bị 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 đường hoặ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 dữ liệu khi một hộp thoại bị xoá.
Bạn có thể đạt được điều này bằng một trình theo dõi thay đổi 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 thay đổi được gọi bất cứ khi nào các phần tử con được thêm hoặc xoá khỏi nội dung của tài liệu. Các đột biến cụ thể đang được theo dõi là đối với removedNodes
có nodeName
của một 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 tuỳ chỉnh bị xoá sẽ được gửi đi.
Xoá thuộc tính đang tải
Để ngăn ảnh động của 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 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 chuyển độ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 chặ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
, sau khi 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à truyề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 này:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
Chỉ cần như vậy, hai hộp thoại sẽ được nâng cấp bằng tính năng đóng nhanh, các bản sửa lỗi tải ảnh động và nhiều sự kiện khác để xử lý.
Nghe 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)
Sau đâ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 một phần tử hình đại diện mới vào danh sách. Thời điểm này phù hợp vì hộp thoại đã hoàn tất ảnh động thoát và 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 điều phối trải nghiệm người dùng có thể diễn ra suôn sẻ hơn.
Thông báo dialog.returnValue
: thông báo 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 là trong sự kiện dialogClosed
, bạn cần biết liệu hộp thoại đã bị đó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 rồi đặt lại biểu mẫu. Thao tác đặt lại này rất hữu ích để khi hộp thoại xuất hiện lại, hộp thoại sẽ trống và sẵn sàng cho một lượt gửi mới.
Kết luận
Giờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào‽ 🙂
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 một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó vào phần bả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 một bản phối lại hay không làm thay đổi thuộc tính
display
. - @geoffrich_ với Svelte và Svelte FLIP đẹp mắt.
Tài nguyên
- Mã nguồn trên GitHub
- Hình đại diện dạng Doodle