בניית רכיב של תיבת דו-שיח

סקירה כללית בסיסית של האופן שבו אפשר ליצור מודלים בגודל מיני ומגה-גובה שמתאימים לצבעים, רספונסיביים ונגישים באמצעות הרכיב <dialog>.

בפוסט הזה אני רוצה לשתף את המחשבות שלי לגבי יצירה של גרסה מותאמת לצבעים, מודולי מיני ומגה-מודיים נגישים ונגישים עם הרכיב <dialog>. כדאי לנסות את ההדגמה ולצפות source!

הדגמה של תיבות דו-שיח ענקיות בעיצוב כהה בעיצובם הבהיר והכהה.

אם אתם מעדיפים סרטון, הנה גרסה של YouTube לפוסט:

סקירה כללית

<dialog> שימושי מאוד ליצירת מידע או לפעולה בתוך הדף. כדאי להביא בחשבון את המקרים שבהם חוויית המשתמש יכולה להניב ביצועים טובים יותר באמצעות אותה פעולה בדף במקום רכישה של כמה דפים action: ייתכן שהסיבה לכך היא שהטופס קטן או הפעולה היחידה שנדרשת המשתמש מאשר או לבטל.

הרכיב <dialog> הפך ליציב לאחרונה בדפדפנים:

תמיכה בדפדפן

  • 37
  • 79
  • 98
  • 15.4

מקור

מצאתי שבאלמנט היו חסרים כמה דברים, אז בGUI הזה אתגר: אני מוסיף את חוויית המפתח הפריטים שציפיתי לראות: אירועים נוספים, ביטול אור, אנימציות בהתאמה אישית ודמויות וסוג מגה-סוג.

Markup

הרכיבים הבסיסיים של רכיב <dialog> הם צנועים. הרכיב יוסתר באופן אוטומטי וכולל סגנונות מובנים ליצירת שכבת-על לתוכן שלכם.

<dialog>
  …
</dialog>

אנחנו יכולים לשפר את הבסיס הזה.

באופן מסורתי, רכיב של תיבת דו-שיח משותף הרבה עם חלון, ולרוב השמות הם ניתנים להחלפה. נתנו כאן את החופשי להשתמש באלמנט תיבת הדו-שיח גם חלונות קופצים קטנים של תיבות דו-שיח (מיני) וגם תיבות דו-שיח בדפים מלאים (מגה). בשם אותם מגה-ומיניים, כששני תיבות הדו-שיח מותאמות מעט למקרי שימוש שונים. הוספתי מאפיין 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>

רכיב תיבת הדו-שיח מספק בסיס חזק לרכיב אזור תצוגה מלא יכול לאסוף נתונים ואינטראקציות של משתמשים. הבסיסים האלה יכולים ליצור לאינטראקציות מעניינות ומועילות באתר או באפליקציה.

נגישות

לרכיב תיבת הדו-שיח יש נגישות מובנית טובה מאוד. במקום להוסיף את התגים האלה כמו שאני עושה בדרך כלל, רבות כבר קיימות.

שחזור המיקוד מתבצע

כפי שעשינו ידנית ביצירת ניווט צדדי רכיב, חשוב פתיחה וסגירה נכונה של משהו שמה דגש על הפתיחה והסגירה הרלוונטיים הלחצנים. כשהניווט הצדי נפתח, המיקוד מועבר ללחצן הסגירה. כאשר לחצת על לחצן הסגירה, המיקוד יחזור ללחצן שפתח אותו.

באמצעות רכיב תיבת הדו-שיח, זוהי התנהגות ברירת מחדל מובנית:

לצערנו, אם רוצים להוסיף אנימציה לתיבת הדו-שיח כדי להוסיף אנימציה, הפונקציונליות הזו אבד. בקטע JavaScript, אשחזר אותו החדשה.

פוקוס פוקוס

רכיב תיבת הדו-שיח מנהל inert עבורך במסמך. לפני inert, נעשה שימוש ב-JavaScript כדי לעקוב אחר המיקוד ואז הוא מיירט ומחזיר אותו.

תמיכה בדפדפן

  • 102
  • 102
  • 112
  • 15.5

מקור

אחרי inert, כל החלקים מהמסמך יכולים להיות "קפואים" כדי להתחמק מהם כבר לא מטרות התמקדות או לבצע פעולות אינטראקטיביות עם העכבר. במקום מלכודות המיקוד יועבר לחלק האינטראקטיבי היחיד של המסמך.

פתיחת רכיב ומיקוד אוטומטי שלו

כברירת מחדל, רכיב תיבת הדו-שיח יקצה את המיקוד לרכיב הראשון שניתן להתמקד בו בתגי העיצוב של תיבת הדו-שיח. אם זה לא האלמנט הכי טוב שהמשתמש צריך להגדיר כברירת מחדל, להשתמש במאפיין autofocus. כפי שתיארתי קודם, לדעתי זו שיטה מומלצת להוסיף זאת על לחצן הביטול ולא על לחצן האישור. כך אפשר להבטיח אישור הוא מכוון ולא מקרי.

סגירה באמצעות מקש Escape

חשוב להקל על הסגירה של האלמנט הזה שעלול להפריע. למרבה המזל, רכיב תיבת הדו-שיח יטפל במקש Escape בשבילך, ויפנה אותך מנטל התזמור.

סגנונות

יש דרך קלה לעצב את רכיב תיבת הדו-שיח ודרך קשיחה. הדרך הקלה הנתיב נוצר אם לא משנים את מאפיין התצוגה של תיבת הדו-שיח למגבלות שלו. אני אמשיך בדרך הקשה כדי לספק אנימציות בהתאמה אישית פתיחה וסגירה של תיבת הדו-שיח, השתלטות על המאפיין display ועוד.

עיצוב עם אביזרים פתוחים

כדי להאיץ את הצבעים המותאמים ואת העקביות הכללית בעיצוב, עבדתי ללא בושה הוספתי את ספריית המשתנים של CSS בתור Open Props. לחשבון בנוסף למשתנים שניתנים בחינם, אני גם מייבאת לנרמל את הקובץ, לחצנים, ששניהם פתוחים ל-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 לתיבת הדו-שיח הענקית:

תמיכה בדפדפן

  • 76
  • 79
  • 103
  • 18

מקור

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

בחרתי גם לבצע מעבר ב-backdrop-filter, בתקווה שהדפדפנים תאפשר מעבר של רכיב הרקע בעתיד:

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

צילום מסך של תיבת הדו-שיח הענקית שמעל רקע מטושטש של דמויות צבעוניות.

תוספות לעיצוב

אני קורא לקטע הזה "תוספות" כי זה קשור יותר לרכיב תיבת הדו-שיח שלי מאשר ברכיב של תיבת הדו-שיח באופן כללי.

גבול ויזואלי של גלילה

כשתיבת הדו-שיח מופיעה, המשתמש עדיין יכול לגלול את הדף מאחוריה, שאני לא רוצה:

בדרך כלל, overscroll-behavior יהיה הפתרון הרגיל שלי, אבל לפי spec, אין לה השפעה על תיבת הדו-שיח כי זו לא יציאת גלילה, כלומר כך שאין מה למנוע. יכולתי להשתמש ב-JavaScript כדי לצפות האירועים החדשים מהמדריך הזה, כמו 'סגור' ו'פתוח', ומחליפים את המצב. overflow: hidden במסמך, או לחכות ליציבות של :has() כל הדפדפנים:

תמיכה בדפדפן

  • 105
  • 105
  • 121
  • 15.4

מקור

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

עכשיו כשנפתחת תיבת דו-שיח ענקית, במסמך ה-HTML יש overflow: hidden.

הפריסה של <form>

מעבר לרכיב חשוב מאוד לאיסוף האינטראקציות אני משתמש בו כאן כדי לפרוס את הכותרת העליונה, הכותרת התחתונה רכיבי מאמר. בפריסה הזו, בכוונתי להציג את המאמר כילד באזור שניתן לגלול. אני משיג/ה את זה grid-template-rows לרכיב המאמר ניתן 1fr ולטופס עצמו יש אותו ערך מקסימלי גובה כרכיב בתיבת הדו-שיח. הגדרה של גובה החברה הזה וגודל השורה באופן יציב היא מאפשרת להגביל את רכיב המאמר ולגלול כשהוא מגיע:

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

צילום מסך של כלי פיתוח שמופיעים בשכבת-על של מידע על פריסת הרשת מעל השורות.

עיצוב של תיבת הדו-שיח <header>

תפקידו של הרכיב הזה הוא לספק שם לתוכן ולהצעה של תיבת הדו-שיח לחצן סגירה שניתן למצוא בקלות. הוא גם קיבל צבע משטח כדי לגרום לו להיראות להיות מאחורי תוכן תיבת הדו-שיח. הדרישות האלה מובילות ל-Flexiblebox מיכל, פריטים במיושר אנכי עם ריווח בקצוות שלהם, וחלק מרווח פנימי ופערים כדי שיהיה מספיק מקום לכותרת וללחצנים לסגירה:

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);
  }
}

צילום מסך של פרטי פריסת Flexbox עם שכבת-על של Chrome Devtools בכותרת של תיבת הדו-שיח.

עיצוב לחצן סגירת הכותרת

מאחר שההדגמה משתמשת בלחצני 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;
}

צילום מסך של מידע על שכבות-על של שינוי גודל ומרווח פנימי בכלי הפיתוח ל-Chrome באמצעות לחצן סגירת הכותרת.

עיצוב של תיבת הדו-שיח <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);
  }
}

בכותרת התחתונה יש תפריטים של לחצני פעולות. 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);
  }
}

צילום מסך של מידע על פריסת Flexbox ב-Chrome Devtools ברכיב הכותרת התחתונה.

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;
}

צילום מסך של Chrome Devtools שיוצר שכבת-על של מידע על Flexbox ברכיבים בתפריט הכותרת התחתונה.

Animation

לרוב, רכיבי תיבת דו-שיח מונפשים כי הם נכנסים לחלון ויוצאים ממנו. הצגה של תנועה תומכת בכניסה וביציאה הזו כדי לעזור למשתמשים להתמצאות בתהליך.

בדרך כלל, ניתן להוסיף אנימציה רק לרכיב של תיבת הדו-שיח, ולא החוצה. הסיבה לכך היא הדפדפן משנה את המצב של המאפיין display ברכיב. מוקדם יותר, המדריך הגדרת התצוגה כרשת ואף פעם לא מגדירה אותה כללא. כך מתבטלת היכולת להוסיף אנימציה פנימה והחוצה.

Open Props מגיע עם הרבה תמונות מפתח אנימציות לשימוש, התזמור קל וקריא. אלו יעדי האנימציה והשכבות הגישה שבחרתי:

  1. תנועה מופחתת היא מעבר ברירת המחדל, עמעום פשוט של אטימות פנימה והחוצה.
  2. אם התנועה תקינה, נוספות אנימציות של שקפים ושינוי קנה מידה.
  3. הפריסה הרספונסיבית לנייד של תיבת הדו-שיח ענקית משתנה כך שלא תודפס.

מעבר בטוח ומשמעותי כברירת מחדל

אף על פי שב-Open Props מגיעים עם תמונות מפתח כדי להפוך את התמונה הזו לאטרקטיבית, אני מעדיפה להשתמש בה שכבת-על של מעברים כברירת מחדל עם אנימציות של תמונות מפתח, שדרוגים אפשריים. מוקדם יותר כבר עיצבנו את הנראות של תיבת הדו-שיח באמצעות אטימוּת, תזמור 1 או 0 בהתאם למאפיין [open]. שפת תרגום בין 0% ל-100%, ספרו לדפדפן כמה זמן ואיזה סוג של התאמה שמתאימה לך:

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

הוספת תנועה למעבר

אם אין למשתמש בעיה בתנועה, גם תיבת הדו-שיח הענקה וגם תיבת הדו-שיח הקצרה צריכות להיסחף גבוה יותר בתור הכניסה שלהם, והם יתרחבו בתור היציאה. אפשר לעשות זאת בעזרת שאילתת מדיה אחת (prefers-reduced-motion) וכמה תכונות פתוחות:

@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;
  }
}

התאמה של אנימציית היציאה לנייד

בשלב מוקדם יותר בקטע העיצוב, סגנון תיבת הדו-שיח ענקי הותאם לניידים כך שיהיה כמו גיליון פעולות, כאילו פיסת נייר קטנה החליטה. כלפי מעלה מתחתית המסך ועדיין מחובר לתחתית המסך. קנה המידה אנימציית היציאה לא מתאימה לעיצוב החדש הזה, ואנחנו יכולים להתאים אותה כמה שאילתות מדיה וכמה פרומפטים פתוחים:

@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 הוא אסינכרוני function; ואז הוא יכול await ההבטחה הוחזרה, וממשיכים בביטחון מלא לאירוע שנסגר.

הוספה של אירועי פתיחה ואירועי פתיחה

לא קל להוסיף את האירועים האלה, כי רכיב תיבת הדו-שיח המובנה אירוע פתוח כמו באירוע של סגירה. אני משתמש/ת MutationObserver כדי לספק תובנות לגבי השינויים במאפיינים של תיבת הדו-שיח. מהצופה הזה אעקוב אחר שינויים במאפיין הפתוח ואנהל את האירועים המותאמים אישית בהתאם.

בדומה לאופן שבו התחלנו את אירועי הסגירה והסגירה, צריך ליצור שני אירועים חדשים שנקראת opening ו-opened. המקומות שבהם האזנו בעבר לתיבת הדו-שיח הפעם, נשתמש במשתנה שנוצר באמצעות 'תצפית מוטציה' כדי לצפות .

…
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)
    }
  })
})

תתבצע קריאה לפונקציית הקריאה החוזרת של התצפית במוטציה כשתיבת הדו-שיח המאפיינים משתנים, ומספקים את רשימת השינויים כמערך. איטרציה מעל במאפיין משתנה, מחפשים את ה-attributeName פתוח. השלב הבא אם לרכיב יש את המאפיין או לא: מציין אם תיבת הדו-שיח נפתח. אם המכשיר נפתח, צריך להסיר את המאפיין inert ולהגדיר את המיקוד לרכיב שמבקש autofocus או רכיב button הראשון שנמצא בתיבת הדו-שיח. לבסוף, דומה לסגירה ואירוע נסגר, שולחים את אירוע הפתיחה מיד, מחכים לאנימציות. כדי לסיים ולאחר מכן לשלוח את האירוע הפתוח.

הוספה של אירוע שהוסר

באפליקציות עם דף יחיד, לעיתים קרובות נוספות ומוסרות תיבות דו-שיח על סמך נתיבים או צרכים ומצבים אחרים של האפליקציה. כדאי למחוק אירועים נתונים כשמסירים תיבת דו-שיח.

תוכלו להשיג זאת באמצעות כלי מעקב אחר מוטציה. הפעם, במקום ובוחנים מאפיינים באלמנט של תיבת דו-שיח, אז נחקור את הצאצאים של הגוף של תיבת דו-שיח.

…
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)
      }
    })
  })
})

מתבצעת קריאה חוזרת (callback) של צופה המוטציה בכל פעם שמוסיפים או מסירים ילדים בגוף המסמך. המוטציות הספציפיות שאנחנו עוקבים אחריהן מיועדות removedNodes שכוללים nodeName מתוך תיבת דו-שיח. אם הוסרה תיבת דו-שיח, אירועי הלחיצה ואירועי הסגירה יוסרו תפנו מקום בזיכרון, והאירוע בהתאמה אישית שהוסר יישלח.

הסרת מאפיין הטעינה

כדי למנוע מהאנימציה של תיבת הדו-שיח להפעיל את אנימציית היציאה שלה כשהיא נוספת אל בדף או בזמן טעינת הדף, הוספנו מאפיין טעינה לתיבת הדו-שיח. הסקריפט הבא ממתין לסיום ההפעלה של האנימציות בתיבת הדו-שיח, ואז מסיר את המאפיין. עכשיו תיבת הדו-שיח ניתנת לאנימציה פנימה והחוצה, הסתיר ביעילות אנימציה שמסיחה את הדעת.

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 כדי לדעת אם תיבת הדו-שיח נסגרה, בוטלה או אושרה. אם הוא יאושר, לאחר מכן, הסקריפט יאחזר את ערכי הטופס ומאפס את הטופס. האיפוס שימושי לכן כשתיבת הדו-שיח מוצגת שוב, היא תהיה ריקה ומוכן להגשה חדשה.

סיכום

עכשיו, אחרי שהסברתי איך עשיתי את זה, איך היית? 🙂

בואו לגוון את הגישות שלנו ונלמד את כל הדרכים לבניית אתרים באינטרנט.

אפשר ליצור הדגמה, לשלוח לי קישורים של שלחו לי ציוץ ואני אוסיף אותם לקטע 'רמיקסים' של הקהילה שבהמשך.

רמיקסים קהילתיים

משאבים