בניית רכיב של הודעה קופצת

סקירה כללית בסיסית של אופן הבנייה של רכיב טוסט גמיש וגמיש.

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

דמו

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

סקירה כללית

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

אינטראקציות

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

Markup

הרכיב <output> הוא בחירה טובה להודעת ה-Toast כי הוא מופיע בקריינות של קוראי המסך. HTML תקין מספק בסיס בטוח שאפשר לשפר באמצעות JavaScript ו-CSS, ואנחנו נשתמש בהרבה JavaScript.

הודעה קופצת

<output class="gui-toast">Item added to cart</output>

היא יכולה להיות יותר מכילה על ידי הוספה של role="status". כך אפשר להשתמש בחלופה אם הדפדפן לא מקצה לרכיבי <output> את התפקיד המשתמע בהתאם למפרט.

<output role="status" class="gui-toast">Item added to cart</output>

מאגר של טוסט

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

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

פריסות

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

קונטיינר של ממשק משתמש גרפי

מאגר ההודעות הקוליות מבצע את כל תהליך הפריסה להצגת הודעות קופצות. הוא fixed לחלון התצוגה, והוא משתמש במאפיין הלוגי inset כדי לציין לאילו קצוות להצמיד אותו, וגם קצת padding מאותו קצה block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

צילום מסך עם גודל התיבה של DevTools והמרווח הפנימי שלה, שמופיעים על גבי רכיב gui-toast-container.

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

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

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

הודעה קופצת בממשק משתמש גרפי

בתיעוד אירועים ספציפיים יש padding, פינות מעוגלות יותר עם border-radius ופונקציה min() שבעזרתה אפשר להתאים את הגודל לנייד ולמחשב. הגודל הרספונסיבי ב-CSS הבא מונע הגדלה של יותר מ-90% מאזור התצוגה או מ-25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

צילום מסך של רכיב gui-toast יחיד, עם רווחים ורדיוס של גבול.

סגנונות

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

מאגר של הודעות Toast

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

.gui-toast-group {
  pointer-events: none;
}

הודעה קופצת בממשק משתמש גרפי

אפשר להגדיר למודעות ה-Toast עיצוב בהיר או כהה ומתאים אישית עם מאפיינים מותאמים אישית, HSL ושאילתת מדיה לפי העדפה.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animation

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

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

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

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

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

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

import Toast from './toast.js'

Toast('My first toast')

יצירת קבוצת ההודעות הקופצות והודעות הקופצות

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

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

צילום מסך של קבוצת ההודעות הקוליות בין התגים head ו-body.

הפונקציה init() נקראת באופן פנימי בתוך המודול, ומטמינת את הרכיב בתור Toaster:

const Toaster = init()

יצירת רכיב HTML של Toast מתבצעת באמצעות הפונקציה createToast(). הפונקציה מקבלת טקסט ל-Toast, יוצרת רכיב <output>, מעטרת אותו בכמה כיתות ומאפיינים, מגדירה את הטקסט ומחזירה את הצומת.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

ניהול הודעה אחת או יותר

JavaScript מוסיף עכשיו למסמך מאגר לשימוש בהודעות הקטנות, ומאפשר להוסיף הודעות קטנות שנוצרו. הפונקציה addToast() מנהלת את הטיפול בהודעת Toast אחת או יותר. קודם בודקים את מספר ההתראות הקופצות ואת תנועת ההתראות, ואז משתמשים במידע הזה כדי להוסיף את ההתראה הקופצת או להוסיף אנימציה מיוחדת כדי שההתראות הקופצות האחרות ייראו כאילו הן "מפנות מקום" להודעה הקופצת החדשה.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

כשמוסיפים את ההודעה הראשונה, Toaster.appendChild(toast) מוסיף הודעה לדף שמפעילה את אנימציות ה-CSS: אנימציה של כניסה, המתנה 3s, אנימציה של יציאה. הפונקציה flipToast() נקראת כשיש הודעות קיימות, באמצעות טכניקה שנקראת FLIP של Paul Lewis. הרעיון הוא לחשב את ההבדל במיקומים של הקונטיינר, לפני ואחרי הוספת הטוסט החדש. אפשר לחשוב על זה כסימון המיקום הנוכחי של המכשיר, סימון המיקום הרצוי שלו ואז הוספת אנימציה מהמיקום הקודם למיקום הנוכחי.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

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

חיבור כל קטעי ה-JavaScript

כשמפעילים את Toast('my first toast'), נוצרת הודעת טוסט, היא מתווספת לדף (יכול להיות שגם הקונטיינר יתנועע כדי להתאים להודעת הטוסט החדשה), מוחזר promise ומופעל מעקב אחרי הודעת הטוסט שנוצרה כדי לבדוק אם האנימציה ב-CSS הושלמה (שלושת האנימציות של נקודות המפתח) לצורך פתרון ה-promise.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

לדעתי, החלק המבלבל בקוד הזה הוא בפונקציה Promise.allSettled() ובמיפוי toast.getAnimations(). מכיוון שהשתמשתי בכמה אנימציות של נקודות מפתח להודעת ה-Toast, כדי לוודא שהן הסתיימו צריך לבקש כל אחת מהן מ-JavaScript ולבדוק את השלמת כל אחת מהfinished שלהן. allSettled עושה את זה בשבילנו, והוא מסתיים כשהוא מקיים את כל ההבטחות שלו. השימוש ב-await Promise.allSettled() מאפשר לשורה הבאה של הקוד להסיר את הרכיב בביטחון ולהניח שהטוסט סיים את מחזור החיים שלו. לבסוף, הקריאה ל-resolve() ממלאת את ההתחייבות ברמה הגבוהה של Toast, כך שמפתחים יכולים לנקות או לבצע משימות אחרות אחרי שההודעה מופיעה.

export default Toast

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

שימוש ברכיב Toast

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

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

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

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

סיכום

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

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

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