סקירה כללית בסיסית של אופן בניית מודלים קטנים וגדולים שמותאמים לצבעים, רספונסיביים ונגישים באמצעות רכיב <dialog>
.
בפוסט הזה אני רוצה לשתף את המחשבות שלי על בניית מודלים קטנים וגדולים שמותאמים לצבעים, רספונסיביים ונגישים באמצעות הרכיב <dialog>
.
רוצים לנסות את ההדגמה? אפשר גם לראות את המקור.
אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:
סקירה כללית
האלמנט
<dialog>
מתאים מאוד להצגת מידע הקשרי או פעולות בדף. כדאי לשקול מתי חוויית המשתמש יכולה להרוויח מפעולה באותו דף במקום פעולה בכמה דפים: למשל, אם הטופס קטן או שהפעולה היחידה שנדרשת מהמשתמש היא אישור או ביטול.
האלמנט <dialog>
הפך לאחרונה ליציב בדפדפנים:
גיליתי שחסרים כמה דברים באלמנט, אז באתגר ממשק המשתמש הגרפי הזה הוספתי את הפריטים שציפיתי להם בחוויית המפתחים: אירועים נוספים, סגירה קלה, אנימציות בהתאמה אישית וסוגים של מיני ומגה.
Markup
הדרישות הבסיסיות של רכיב <dialog>
הן צנועות. הרכיב יוסתר אוטומטית ויש לו סגנונות מובנים להצגת התוכן בשכבת-על.
<dialog>
…
</dialog>
אפשר לשפר את נקודת הבסיס הזו.
באופן מסורתי, רכיב דיאלוג דומה מאוד לתיבת דו-שיח מודאלית, ולעתים קרובות השמות הם בני חילוף. השתמשתי כאן באלמנט dialog גם לחלונות קופצים קטנים (מיני) וגם לחלונות דו-שיח בגודל מלא (מגה). נתתי להם את השמות mega ו-mini, ושני הדיאלוגים הותאמו קצת לתרחישי שימוש שונים.
הוספתי מאפיין modal-mode
כדי שתוכלו לציין את הסוג:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
לא תמיד, אבל בדרך כלל רכיבי דיאלוג ישמשו לאיסוף מידע על אינטראקציות. טפסים בתוך רכיבי תיבת דו-שיח נועדו להיות ביחד.
מומלץ להשתמש ברכיב טופס כדי לעטוף את תוכן תיבת הדו-שיח, כדי ש-JavaScript יוכל לגשת לנתונים שהמשתמש הזין. בנוסף, לחצנים בתוך טופס שמשתמש ב-method="dialog"
יכולים לסגור תיבת דו-שיח בלי JavaScript ולהעביר נתונים.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
תיבת דו-שיח גדולה
בתיבת דו-שיח גדולה יש שלושה רכיבים בתוך הטופס:
<header>
,
<article>
,
ו-
<footer>
.
הם משמשים כמאגרי מידע סמנטיים, וגם כמטרות סגנון להצגת תיבת הדו-שיח. הכותרת מופיעה בחלון הקופץ ויש בה לחצן לסגירה. המאמר מתייחס לקלט ולמידע בטפסים. הכותרת התחתונה כוללת <menu>
של כפתורי פעולה.
<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>
ללחצן התפריט הראשון יש
autofocus
ומטפל באירועים מוטבע onclick
. המאפיין autofocus
יקבל את המיקוד כשתיבת הדו-שיח תיפתח, ולדעתי מומלץ להשתמש בו בלחצן הביטול ולא בלחצן האישור. כך אפשר לוודא שהאישור נעשה במכוון ולא בטעות.
תיבת דו-שיח קטנה
תיבת הדו-שיח הקטנה דומה מאוד לתיבת הדו-שיח הגדולה, רק שחסר בה רכיב <header>
. כך הוא יכול להיות קטן יותר ולהתאים יותר לשורה.
<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>
רכיב תיבת הדו-שיח מספק בסיס חזק לרכיב מלא של אזור התצוגה שיכול לאסוף נתונים ואינטראקציות של משתמשים. השימוש ברכיבים האלה יכול ליצור אינטראקציות מעניינות ויעילות באתר או באפליקציה.
נגישות
לאלמנט הדיאלוג יש נגישות מובנית טובה מאוד. במקום להוסיף את התכונות האלה כמו שאני עושה בדרך כלל, הרבה מהן כבר קיימות.
שחזור המיקוד
כמו שעשינו באופן ידני ביצירת רכיב של סרגל ניווט צדדי, חשוב שפתיחה וסגירה של רכיב מסוים ימקדו את תשומת הלב בלחצני הפתיחה והסגירה הרלוונטיים. כשסרגל הצד הזה נפתח, המיקוד עובר ללחצן הסגירה. כשלוחצים על לחצן הסגירה, המיקוד חוזר ללחצן שפתח את החלון.
באלמנט dialog, זו התנהגות ברירת מחדל מובנית:
לצערנו, אם רוצים להוסיף אנימציה לכניסה וליציאה של תיבת הדו-שיח, הפונקציונליות הזו לא זמינה. בקטע JavaScript אשחזר את הפונקציונליות הזו.
הגבלת המיקוד
רכיב תיבת הדו-שיח מנהל את
inert
בשבילכם במסמך. לפני inert
, נעשה שימוש ב-JavaScript כדי לעקוב אחרי יציאה ממיקוד של רכיב, ובשלב הזה המערכת מיירטת את המיקוד ומחזירה אותו.
אחרי inert
, אפשר "להקפיא" חלקים במסמך כך שהם לא יהיו יותר יעדי מיקוד או אינטראקטיביים עם העכבר. במקום ללכוד את המיקוד, המיקוד מועבר לחלק האינטראקטיבי היחיד במסמך.
פתיחה של אלמנט והעברת המיקוד אליו באופן אוטומטי
כברירת מחדל, רכיב תיבת הדו-שיח יקצה מיקוד לרכיב הראשון שניתן להתמקד בו בתגי העיצוב של תיבת הדו-שיח. אם זה לא הרכיב הכי טוב שהמשתמש יכול להגדיר כברירת מחדל, צריך להשתמש במאפיין autofocus
. כמו שציינתי קודם, לדעתי הכי טוב לשים את זה על לחצן הביטול ולא על לחצן האישור. כך אפשר לוודא שהאישור נעשה בכוונה ולא בטעות.
סגירה באמצעות מקש Escape
חשוב להקפיד שיהיה קל לסגור את האלמנט הזה, שעלול להפריע. למזלכם, אלמנט תיבת הדו-שיח יטפל במקש Escape בשבילכם, כך שלא תצטרכו לדאוג לתיאום.
סגנונות
יש דרך קלה לעצב את רכיב תיבת הדו-שיח ודרך קשה. הדרך הקלה היא לא לשנות את מאפיין התצוגה של תיבת הדו-שיח ולעבוד עם המגבלות שלה. אני בוחר בדרך הקשה כדי לספק אנימציות מותאמות אישית לפתיחה ולסגירה של תיבת הדו-שיח, להשתלט על המאפיין display
ועוד.
עיצוב באמצעות Open Props
כדי להאיץ את ההתאמה של הצבעים ואת העיצוב הכולל, השתמשתי בספריית משתני ה-CSS שלי Open Props. בנוסף למשתנים החינמיים שסופקו, אני מייבא גם קובץ normalize וכמה buttons, שניהם מסופקים על ידי Open Props כייבוא אופציונלי. הייבוא הזה עוזר לי להתמקד בהתאמה אישית של הדיאלוג וההדגמה, בלי שאצטרך הרבה סגנונות כדי לתמוך בהם ולגרום להם להיראות טוב.
עיצוב הרכיב <dialog>
בעלות על מאפיין התצוגה
התנהגות ברירת המחדל של הצגה והסתרה של רכיב בתיבת דו-שיח היא החלפת הערך של מאפיין התצוגה מ-block
ל-none
. לצערנו, המשמעות היא שאי אפשר להוסיף אנימציה לכניסה וליציאה של הרכיב, אלא רק לכניסה. אני רוצה להנפיש את הכניסה והיציאה, והשלב הראשון הוא להגדיר מאפיין display משלי:
dialog {
display: grid;
}
כדי לשנות את הערך של מאפיין התצוגה, וכך להיות הבעלים שלו, כמו שמוצג בקטע ה-CSS שלמעלה, צריך לנהל כמות משמעותית של סגנונות כדי לאפשר חוויית משתמש תקינה. קודם כל, מצב ברירת המחדל של תיבת דו-שיח הוא סגור. אפשר לייצג את המצב הזה באופן חזותי ולמנוע את האינטראקציות עם תיבת הדו-שיח באמצעות הסגנונות הבאים:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
עכשיו תיבת הדו-שיח לא גלויה ואי אפשר לבצע בה פעולות כשהיא לא פתוחה. בהמשך
אני אוסיף קוד JavaScript כדי לנהל את המאפיין inert
בתיבת הדו-שיח, כדי לוודא
שמשתמשים במקלדת ובקורא מסך לא יוכלו להגיע לתיבת הדו-שיח המוסתרת.
הגדרת עיצוב של צבע דינמי לתיבת הדו-שיח
התג color-scheme
מאפשר להגדיר את המסמך כך שיוצג בדפדפן עם עיצוב צבעים דינמי שמותאם להעדפות המערכת לגבי עיצוב בהיר או כהה, אבל רציתי להתאים אישית את רכיב תיבת הדו-שיח יותר מזה. Open Props מספק כמה צבעי רקע שמותאמים אוטומטית להעדפות המערכת לגבי מצב בהיר או כהה, בדומה לשימוש ב-color-scheme
. הם מצוינים ליצירת שכבות בעיצוב, ואני אוהב להשתמש בצבע כדי לתמוך חזותית במראה הזה של משטחי שכבות. צבע הרקע הוא
var(--surface-1)
; כדי להוסיף שכבה מעל, משתמשים ב-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);
}
}
בהמשך נוסיף עוד צבעים דינמיים לרכיבים משניים, כמו הכותרת והכותרת התחתונה. אני רואה בהם תוספת לאלמנט של תיבת דו-שיח, אבל הם חשובים מאוד ליצירת עיצוב משכנע ומוצלח של תיבת דו-שיח.
שינוי גודל רספונסיבי של תיבת דו-שיח
ברירת המחדל של תיבת הדו-שיח היא להקצות את הגודל שלה לתוכן שלה, וזה בדרך כלל מצוין. המטרה שלי היא להגביל את max-inline-size
לגודל קריא (--size-content-3
= 60ch
) או ל-90% מרוחב אזור התצוגה. כך מוודאים שהתיבת הדו-שיח לא תתפרס מקצה לקצה במכשיר נייד, ושלא תהיה רחבה מדי במסך מחשב כך שיהיה קשה לקרוא אותה. אחר כך אני מוסיף את התג
max-block-size
כדי שגובה תיבת הדו-שיח לא יעלה על גובה הדף. זה גם אומר שנצטרך לציין איפה נמצא האזור שאפשר לגלול בו בתיבת הדו-שיח, במקרה שמדובר ברכיב גבוה של תיבת דו-שיח.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
שימו לב שהשתמשתי ב-max-block-size
פעמיים. השימוש הראשון הוא ב-80vh
, יחידת אזור תצוגה פיזי. מה שאני באמת רוצה זה לשמור על תיבת הדו-שיח בתוך זרימה יחסית, למשתמשים בינלאומיים, אז אני משתמש ביחידה הלוגית, החדשה, שנתמכת רק באופן חלקי dvb
בהצהרה השנייה, כשהיא תהיה יציבה יותר.
מיקום של תיבת דו-שיח גדולה
כדי לעזור במיקום של רכיב תיבת הדו-שיח, כדאי לפרק אותו לשני חלקים: הרקע במסך מלא והמאגר של תיבת הדו-שיח. תמונת הרקע צריכה לכסות את הכול, וליצור אפקט הצללה שיעזור להראות שהתיבת הדו-שיח נמצאת בחזית ואי אפשר לגשת לתוכן שמאחוריה. הקונטיינר של תיבת הדו-שיח יכול להתמקם במרכז הרקע ולקבל את הצורה שנדרשת בהתאם לתוכן שלו.
הסגנונות הבאים מקבעים את רכיב תיבת הדו-שיח לחלון, מותחים אותו לכל פינה ומשתמשים ב-margin: auto
כדי למרכז את התוכן:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
סגנונות של תיבות דו-שיח גדולות בנייד
בחלונות תצוגה קטנים, אני מעצב את המגה-מודל הזה שמוצג על כל המסך בצורה קצת שונה. הגדרתי את השוליים התחתונים ל-0
, וכך התוכן של תיבת הדו-שיח מופיע בתחתית אזור התצוגה. בעזרת כמה שינויים בסגנון, אפשר להפוך את תיבת הדו-שיח לגיליון פעולות, קרוב יותר לאגודלים של המשתמש:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
מיקום של תיבת דו-שיח קטנה
כשמשתמשים באזור תצוגה גדול יותר, כמו במחשב שולחני, בחרתי למקם את תיבות הדו-שיח הקטנות מעל הרכיב שקרא להן. כדי לעשות את זה, אני צריך JavaScript. אפשר לקרוא על הטכניקה שבה אני משתמש כאן, אבל לדעתי היא לא רלוונטית למאמר הזה. בלי ה-JavaScript, תיבת הדו-שיח הקטנה מופיעה במרכז המסך, בדיוק כמו תיבת הדו-שיח הגדולה.
איך מוסיפים עניין
לבסוף, מוסיפים קצת סגנון לתיבת הדו-שיח כדי שהיא תיראה כמו משטח רך שמרחף מעל הדף. כדי להשיג את המראה הרך, הפינות של תיבת הדו-שיח מעוגלות. העומק מושג באמצעות אחד ממאפייני הצל של Open Props, שנוצרו בקפידה:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
התאמה אישית של פסאודו-אלמנט הרקע
בחרתי להשתמש ברקע בצורה מינימלית, והוספתי רק אפקט טשטוש באמצעות
backdrop-filter
בתיבת הדו-שיח הגדולה:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
בחרתי גם להוסיף מעבר ל-backdrop-filter
, בתקווה שבעתיד דפדפנים יאפשרו מעבר של רכיב הרקע:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
תוספות לעיצוב
אני קורא לקטע הזה 'תוספות' כי הוא קשור יותר להדגמה של רכיב הדו-שיח שלי מאשר לרכיב הדו-שיח באופן כללי.
הגבלת גלילה
כשתיבת הדו-שיח מוצגת, המשתמש עדיין יכול לגלול בדף שמאחוריה, וזה לא מה שאני רוצה:
בדרך כלל,
overscroll-behavior
היה הפתרון הרגיל שלי, אבל לפי
המפרט,
אין לו השפעה על תיבת הדו-שיח כי הוא לא אזור גלילה, כלומר הוא לא
רכיב גלילה ולכן אין מה למנוע. אפשר להשתמש ב-JavaScript כדי לעקוב אחרי האירועים החדשים מהמדריך הזה, כמו 'נסגר' ו'נפתח', ולהפעיל או להשבית את overflow: hidden
במסמך, או לחכות עד ש-:has()
יהיה יציב בכל הדפדפנים:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
מעכשיו, כשתיבת דו-שיח גדולה פתוחה, במסמך ה-HTML מופיע overflow: hidden
.
הפריסה <form>
בנוסף להיותו רכיב חשוב מאוד לאיסוף מידע על האינטראקציה של המשתמש, אני משתמש בו כאן כדי להגדיר את רכיבי הכותרת, הכותרת התחתונה והמאמר. בפריסה הזו אני רוצה להגדיר את רכיב ה-article כשטח שאפשר לגלול בו. אני עושה את זה באמצעות grid-template-rows
.
לרכיב article מוקצה הערך 1fr
ולטופס עצמו יש אותו גובה מקסימלי כמו לרכיב dialog. הגדרת הגובה הקבוע וגודל השורה הקבוע מאפשרת להגביל את רכיב המאמר ולגלול אותו כשהוא חורג מהגבולות:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
עיצוב תיבת הדו-שיח <header>
התפקיד של הרכיב הזה הוא לספק כותרת לתוכן של תיבת הדו-שיח ולהציע כפתור סגירה שקל למצוא. הוא מקבל גם צבע של משטח כדי ליצור את הרושם שהוא נמצא מאחורי התוכן של מאמר תיבת הדו-שיח. הדרישות האלה מובילות לקונטיינר flexbox, לפריטים עם יישור אנכי שמוצבים במרווחים שווים עד לקצוות, ולריווח מסוים כדי לתת מקום לכותרת וללחצני הסגירה:
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);
}
}
עיצוב כפתור הסגירה של הכותרת
מכיוון שההדגמה משתמשת בלחצני Open Props, לחצן הסגירה מותאם אישית ללחצן עגול עם סמל במרכז, כך:
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;
}
עיצוב תיבת הדו-שיח <article>
לרכיב article יש תפקיד מיוחד בתיבת הדו-שיח הזו: זהו אזור שנועד לגלילה במקרה של תיבת דו-שיח גבוהה או ארוכה.
כדי להשיג את זה, רכיב הטופס ברמת ההורה הגדיר לעצמו כמה ערכים מקסימליים, שמגבילים את רכיב המאמר הזה אם הוא גבוה מדי. מגדירים את overflow-y: auto
כך שסרגלי הגלילה יוצגו רק כשצריך, מגדירים את overscroll-behavior: contain
כך שהגלילה תהיה בתוך הרכיב, ואת השאר מגדירים כסגנונות הצגה בהתאמה אישית:
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);
}
}
עיצוב תיבת הדו-שיח <footer>
התפקיד של הכותרת התחתונה הוא להכיל תפריטים של לחצני פעולה. משתמשים ב-Flexbox כדי ליישר את התוכן לסוף של ציר השורה של הכותרת התחתונה, ואז מוסיפים קצת ריווח כדי לתת ללחצנים קצת מקום.
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);
}
}
עיצוב תפריט הכותרת התחתונה של תיבת הדו-שיח
רכיב menu
משמש להכלת לחצני הפעולה של תיבת הדו-שיח. הוא משתמש בפריסת flexbox עם גלישת שורות ועם gap
כדי לספק רווח בין הכפתורים. רכיבי התפריט
כוללים ריווח פנימי, כמו <ul>
. אני גם מסיר את הסגנון הזה כי הוא לא נחוץ לי.
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;
}
Animation
אלמנטים של תיבת דו-שיח מונפשים לעיתים קרובות כי הם נכנסים לחלון ויוצאים ממנו. הוספת תנועה תומכת לתיבות דו-שיח בכניסה וביציאה עוזרת למשתמשים להתמצא בתהליך.
בדרך כלל אפשר להנפיש את רכיב תיבת הדו-שיח רק בכניסה, ולא ביציאה. הסיבה לכך היא שהדפדפן מחליף את הערך של המאפיין display
ברכיב. קודם לכן, התצוגה של המדריך הוגדרה לרשת, והיא אף פעם לא הוגדרה ל'ללא'. כך תוכלו להוסיף אנימציה לכניסה וליציאה של הרכיבים.
Open Props כולל הרבה אנימציות של מסגרות מפתח שאפשר להשתמש בהן, ולכן קל להבין ולנהל את האנימציות. אלה היעדים של האנימציה והגישה השכבתית שבה השתמשתי:
- האפשרות 'תנועה מופחתת' היא ברירת המחדל למעבר, והיא כוללת שינוי פשוט של אטימות התמונה.
- אם התנועה תקינה, יתווספו אנימציות של החלקה ושינוי גודל.
- הפריסה הרספונסיבית לניידים של תיבת הדו-שיח הגדולה מותאמת להצגה בהזזה.
מעבר בטוח ומשמעותי לברירת מחדל
למרות ש-Open Props מגיע עם מסגרות מפתח למעבר הדרגתי פנימה והחוצה, אני מעדיף את הגישה השכבתית הזו של מעברים כברירת מחדל, עם אנימציות של מסגרות מפתח כשדרוגים פוטנציאליים. קודם כבר הגדרנו את הנראות של תיבת הדו-שיח באמצעות opacity, וקבענו את הערך 1
או 0
בהתאם למאפיין [open]
. כדי לבצע מעבר בין 0% ל-100%, צריך לציין לדפדפן את משך המעבר ואת סוג ההאצה הרצוי:
dialog {
transition: opacity .5s var(--ease-3);
}
הוספת תנועה למעבר
אם המשתמש לא מתנגד לתנועה, גם הדיאלוג הגדול וגם הדיאלוג הקטן צריכים להחליק כלפי מעלה בכניסה ולגדול ביציאה. אפשר לעשות את זה באמצעות שאילתת המדיה prefers-reduced-motion
וכמה מאפיינים של 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;
}
}
התאמת אנימציית היציאה לנייד
בקטע הקודם בנושא סגנון, סגנון תיבת הדו-שיח הגדולה מותאם למכשירים ניידים כדי שייראה יותר כמו גיליון פעולות, כאילו פיסת נייר קטנה החליקה כלפי מעלה מתחתית המסך ועדיין מחוברת לתחתית. אנימציית היציאה של ההתרחבות לא מתאימה לעיצוב החדש, ואפשר להתאים אותה באמצעות כמה שאילתות מדיה וכמה מאפיינים של 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
יש הרבה דברים שאפשר להוסיף באמצעות 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
}
השינויים האלה נובעים מהרצון לאפשר סגירה קלה (לחיצה על הרקע של תיבת הדו-שיח), אנימציה וכמה אירועים נוספים לשיפור התזמון של קבלת נתוני הטופס.
הוספת סגירה קלה
זו משימה פשוטה, והיא תוספת מצוינת לרכיב של תיבת דו-שיח שלא מונפשת. האינטראקציה מתבצעת על ידי מעקב אחרי קליקים ברכיב הדו-שיח ושימוש בהעברת אירועים כדי להעריך מה נלחץ, והיא תתבצע רק close()
אם זה הרכיב העליון ביותר:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
הודעה dialog.close('dismiss')
. האירוע נקרא ומסופקת מחרוזת.
אפשר לאחזר את המחרוזת הזו באמצעות JavaScript אחר כדי לקבל תובנות לגבי אופן הסגירה של תיבת הדו-שיח. בנוסף, בכל פעם שאני קורא לפונקציה מכפתורים שונים, אני מספק מחרוזות קרובות כדי לספק לאפליקציה שלי הקשר לגבי האינטראקציה של המשתמש.
הוספת אירועים שמתחילים או מסתיימים
רכיב תיבת הדו-שיח מגיע עם אירוע סגירה: הוא מופעל מיד כשקוראים לפונקציה close()
של תיבת הדו-שיח. מכיוון שאנחנו מפעילים אנימציה לרכיב הזה, כדאי שיהיו אירועים לפני האנימציה ואחריה, כדי שיהיה אפשר לאחזר את הנתונים או לאפס את הטופס של תיבת הדו-שיח. אני משתמש בו כאן כדי לנהל את ההוספה של המאפיין inert
בתיבת הדו-שיח הסגורה, ובסרטון ההדגמה אני משתמש בו כדי לשנות את רשימת הדמויות אם המשתמש שלח תמונה חדשה.
כדי לעשות את זה, יוצרים שני אירועים חדשים בשם closing
ו-closed
. לאחר מכן
מאזינים לאירוע הסגירה המובנה בתיבת הדו-שיח. מכאן, מגדירים את תיבת הדו-שיח ל-inert
ושולחים את האירוע closing
. המשימה הבאה היא להמתין עד שהאנימציות והמעברים יסיימו לפעול בתיבת הדו-שיח, ואז לשלוח את האירוע 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))
הפונקציה animationsComplete
, שמשמשת גם במאמר יצירת רכיב של הודעת טוסט, מחזירה הבטחה על סמך השלמת ההבטחות של האנימציה והמעבר. זו הסיבה לכך ש-dialogClose
היא פונקציה אסינכרונית. היא יכולה await
את ההבטחה שהוחזרה ולהמשיך בבטחה לאירוע הסגור.
הוספת אירועים של פתיחה ושל אירועים פתוחים
יותר קשה להוסיף את האירועים האלה כי אלמנט תיבת הדו-שיח המובנה לא מספק אירוע פתוח כמו שהוא מספק אירוע סגור. אני משתמש ב-MutationObserver כדי לקבל תובנות לגבי שינויים במאפיינים של תיבת הדו-שיח. ב-observer הזה, אעקוב אחרי שינויים במאפיין open ואנהל את האירועים המותאמים אישית בהתאם.
בדומה לאופן שבו התחלנו את האירועים של סגירה וסגרנו אותם, יוצרים שני אירועים חדשים בשם opening
וopened
. במקום להאזין לאירוע סגירת תיבת הדו-שיח כמו שעשינו קודם, הפעם נשתמש ב-mutation observer שנוצר כדי לעקוב אחרי המאפיינים של תיבת הדו-שיח.
…
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)
}
})
})
המערכת תקרא לפונקציית הקריאה החוזרת של MutationObserver כשהמאפיינים של תיבת הדו-שיח ישתנו, ותספק את רשימת השינויים כמערך. חוזרים על השינויים במאפיינים ומחפשים את attributeName
כדי לפתוח אותו. לאחר מכן בודקים אם לרכיב יש את המאפיין או לא: כך אפשר לדעת אם תיבת הדו-שיח נפתחה או לא. אם הוא פתוח, מסירים את מאפיין inert
, מעבירים את המיקוד לאלמנט שמבקש autofocus
או לאלמנט button
הראשון שנמצא בתיבת הדו-שיח. לבסוף, בדומה לאירוע הסגירה ולאירוע הסגור, שולחים את אירוע הפתיחה מיד, מחכים שהאנימציות יסתיימו ואז שולחים את אירוע הפתיחה.
הוספה של אירוע שהוסר
באפליקציות של דף יחיד, תיבות דו-שיח מתווספות ומוסרות לעיתים קרובות על סמך נתיבים או צרכים ומצבים אחרים של האפליקציה. יכול להיות שימושי לנקות אירועים או נתונים כשמסירים תיבת דו-שיח.
אפשר לעשות את זה באמצעות MutationObserver אחר. הפעם, במקום להתבונן במאפיינים של רכיב dialog, נתבונן ברכיבי הצאצא של רכיב body ונחפש רכיבי dialog שמוסרים.
…
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)
}
})
})
})
הקריאה החוזרת של MutationObserver מתבצעת בכל פעם שמוסיפים או מסירים צאצאים מגוף המסמך. המוטציות הספציפיות שנבדקות הן של removedNodes
עם nodeName
של דו-שיח. אם תיבת דו-שיח הוסרה, האירועים של הלחיצה והסגירה מוסרים כדי לפנות זיכרון, והאירוע המותאם אישית של ההסרה נשלח.
הסרת מאפיין הטעינה
כדי למנוע את הפעלת אנימציית היציאה של תיבת הדו-שיח כשהיא מתווספת לדף או בטעינת הדף, נוסף לתיבת הדו-שיח מאפיין loading. הסקריפט הבא ממתין לסיום האנימציות של תיבת הדו-שיח, ואז מסיר את המאפיין. עכשיו תיבת הדו-שיח יכולה להיכנס ולצאת עם אנימציה, והסתרנו אנימציה שמסיחה את הדעת.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
מידע נוסף על הבעיה של מניעת אנימציות של פריים מרכזי בזמן טעינת הדף
כולם ביחד
אחרי שהסברנו כל חלק בנפרד, הנה dialog.js
במלואו:
// 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')
}
שימוש במודול dialog.js
הפונקציה המיוצאת מהמודול מצפה לקבל קריאה ולהעביר רכיב של תיבת דו-שיח שרוצה להוסיף את האירועים והפונקציונליות החדשים האלה:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
שני תיבות הדו-שיח משודרגות עם סגירה קלה, תיקונים בטעינת האנימציה ועוד אירועים שאפשר לעבוד איתם.
האזנה לאירועים המותאמים אישית החדשים
כל רכיב משודרג של תיבת דו-שיח יכול עכשיו להאזין לחמישה אירועים חדשים, כמו בדוגמה הבאה:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
הנה שתי דוגמאות לטיפול באירועים האלה:
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()
}
}
בהדגמה שיצרתי עם רכיב תיבת הדו-שיח, השתמשתי באירוע הסגירה ובנתוני הטופס כדי להוסיף רכיב אווטאר חדש לרשימה. התזמון טוב כי האנימציה של היציאה מתיבת הדו-שיח הסתיימה, ואז כמה סקריפטים מציגים אנימציה של האווטאר החדש. האירועים החדשים מאפשרים לנהל את חוויית המשתמש בצורה חלקה יותר.
הערה dialog.returnValue
: הערך הזה מכיל את מחרוזת הסגירה שמועברת כשקוראים לאירוע close()
של תיבת הדו-שיח. חשוב מאוד לדעת באירוע dialogClosed
אם תיבת הדו-שיח נסגרה, בוטלה או אושרה. אם האימות מצליח, הסקריפט שולף את ערכי הטופס ומאפס אותו. האיפוס שימושי כדי שכשתופיע שוב תיבת הדו-שיח, היא תהיה ריקה ומוכנה לשליחה חדשה.
סיכום
עכשיו כשאתה יודע איך עשיתי את זה, איך היית עושה את זה‽ 🙂
כדאי לגוון את הגישות שלנו וללמוד את כל הדרכים לבנות אתרים.
אפשר ליצור סרטון הדגמה, לצייץ לי קישורים, ואוסיף אותו לקטע של רמיקסים מהקהילה שבהמשך!
רמיקסים מהקהילה
- @GrimLink עם דיאלוג 3 ב-1.
- @mikemai2awesome עם רמיקס נחמד שלא משנה את המאפיין
display
. - @geoffrich_ עם Svelte וליטוש נחמד של Svelte FLIP.
משאבים
- קוד המקור ב-Github
- דמויות של שרבוטים