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ạ và xem
nguồn!
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:
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>
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.
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 giúp đả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ụ). Trong 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
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 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:
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;
}
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:
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;
}
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);
}
}
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;
}
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);
}
}
Tạo kiểu cho hộp thoại <footer>
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);
}
}
Tạo kiểu trình đơn chân trang của hộp thoại
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;
}
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:
- 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.
- Nếu chuyển động được, ảnh động trượt và tỷ lệ sẽ được thêm.
- 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à closing
và closed
. 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à opening
và opened
. 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à để
removedNodes
có
nodeName
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
- @GrimLink với tính năng 3 trong 1 .
- @mikemai2awesome với
bản phối lại không làm thay đổi
thuộc tính
display
. - @geoffrich_ với Mỏng nhẵn và đẹp Trau chuốt Svelte FLIP.
Tài nguyên
- Mã nguồn trên GitHub
- Hình đại diện của Doodle